From 033a546d4bbffc1d0a0c2cd99f5b420f736acbf3 Mon Sep 17 00:00:00 2001 From: Rodrigo Corsi Date: Tue, 8 Mar 2016 11:12:48 -0300 Subject: addded missing translations --- i18n/pt.json | 26 +++++++++++++------------- web/static/i18n/pt.json | 15 +++++++++++---- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/i18n/pt.json b/i18n/pt.json index e01db6b22..61a88a890 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -2647,6 +2647,10 @@ "id": "store.sql_channel.update_member.app_error", "translation": "Encontramos um erro ao atualizar o membro do canal" }, + { + "id": "store.sql_command.analytics_command_count.app_error", + "translation": "Não foi possível contar os comandos" + }, { "id": "store.sql_command.save.delete.app_error", "translation": "Não foi possível deletar o comando" @@ -2675,10 +2679,6 @@ "id": "store.sql_command.save.update.app_error", "translation": "Não foi possível atualizar o comando" }, - { - "id": "store.sql_command.analytics_command_count.app_error", - "translation": "Não foi possível contar os comandos" - }, { "id": "store.sql_license.get.app_error", "translation": "Encontramos um erro ao obter a licença" @@ -2899,6 +2899,10 @@ "id": "store.sql_preference.update.app_error", "translation": "Não foi possível atualizar a preferência" }, + { + "id": "store.sql_session.analytics_session_count.app_error", + "translation": "Não foi possível contar a sessão" + }, { "id": "store.sql_session.cleanup_expired_sessions.app_error", "translation": "Encontramos um erro enquanto deletava a sessão expirada do usuário" @@ -2951,10 +2955,6 @@ "id": "store.sql_session.update_roles.app_error", "translation": "Não foi possível atualizar as funções" }, - { - "id": "store.sql_session.analytics_session_count.app_error", - "translation": "Não foi possível contar a sessão" - }, { "id": "store.sql_system.get.app_error", "translation": "Encontramos um erro ao procurar as propriedades de sistema" @@ -2967,6 +2967,10 @@ "id": "store.sql_system.update.app_error", "translation": "Encontramos um erro ao atualizar as propriedades de sistema" }, + { + "id": "store.sql_team.analytics_team_count.app_error", + "translation": "Não foi possível contar as equipes" + }, { "id": "store.sql_team.get.find.app_error", "translation": "Não foi possível encontrar a equipe existente" @@ -3035,10 +3039,6 @@ "id": "store.sql_team.update_display_name.app_error", "translation": "Não foi possível atualizar o nome da equipe" }, - { - "id": "store.sql_team.analytics_team_count.app_error", - "translation": "Não foi possível contar as equipes" - }, { "id": "store.sql_user.analytics_unique_user_count.app_error", "translation": "Não foi possível obter o número de usuários únicos" @@ -3587,4 +3587,4 @@ "id": "web.watcher_fail.error", "translation": "Falha ao adicionar diretório observador %v" } -] +] \ No newline at end of file diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index f4b998ded..d96d8a6da 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -277,6 +277,9 @@ "admin.service.attemptTitle": "Máxima Tentativas de Login:", "admin.service.cmdsDesc": "Quando verdadeiro, comandos slash criados por usuários serão permitidos.", "admin.service.cmdsTitle": "Ativar Comandos Slash: ", + "admin.service.corsDescription": "Ativar requisição de origem HTTP Cross dos domínios especificados (separados por espaço). Usar \"*\" se você quiser permitir CORS de qualquer domínio ou deixe em branco para desativar.", + "admin.service.corsEx": "http://example.com https://example.com", + "admin.service.corsTitle": "Permitir Requisição Cross-origin de:", "admin.service.developerDesc": "(Opção dos desenvolvedores) Quando verdadeira, a informação extra em torno dos erros será exibida na UI.", "admin.service.developerTitle": "Ativar o Modo Desenvolvedor: ", "admin.service.false": "falso", @@ -558,6 +561,7 @@ "channel_loader.wrote": " escreveu: ", "channel_members_modal.addNew": " Adicionar Novos Membros", "channel_members_modal.close": "Fechar", + "channel_members_modal.remove": "Remover", "channel_memebers_modal.members": " Membros", "channel_modal.cancel": "Cancelar", "channel_modal.channel": "Canal", @@ -665,8 +669,8 @@ "file_upload.filesAbove": "Arquivos acima {max}MB não podem ser enviados: {filenames}", "file_upload.limited": "Limite máximo de uploads de {count} arquivos. Por favor use um post adicional para mais arquivos.", "file_upload.pasted": "Imagem Colada em ", - "filtered_user_list.count": "{count, number} {count, plural, one {Membro} other {Membros}}", - "filtered_user_list.countTotal": "{count, number} {count, plural, one {Membro} other {Membros}} de {total} Total", + "filtered_user_list.count": "{count, number} {count, plural, one {membro} other {membros}}", + "filtered_user_list.countTotal": "{count, number} {count, plural, one {membro} other {membros}} de {total} Total", "filtered_user_list.search": "Procurar membros", "find_team.email": "E-mail", "find_team.findDescription": "Foi enviado um e-mail com links para todas as equipes do qual você é membro.", @@ -698,6 +702,7 @@ "get_link.copy": "Copiar Link", "get_post_link_modal.help": "O link abaixo permite que usuários autorizados possam ver seus posts.", "get_post_link_modal.title": "Copiar Permalink", + "get_team_invite_link_modal.help": "Enviar o link abaixo para sua equipe de trabalho para que eles se inscrevam no site da sua equipe. O Link de Convite de Equipe como ele não muda pode ser compartilhado com várias pessoas ao menos que seja re-gerado em Configurações de Equipe pelo Administrador de Equipe.", "get_team_invite_link_modal.helpDisabled": "Criação de usuários está desabilitada para sua equipe. Por favor peça ao administrador de equipe por detalhes.", "get_team_invite_link_modal.title": "Link para Convite de Equipe", "intro_messages.DM": "Este é o início do seu histórico de mensagens diretas com {teammate}.
Mensagens diretas e arquivos compartilhados aqui não são mostrados para pessoas de fora desta área.", @@ -766,6 +771,7 @@ "login_username.verifyEmailError": "Por favor verifique seu endereço de email. Verifique por um email na sua caixa de entrada.", "member_item.makeAdmin": "Tornar Admin", "member_item.member": "Membro", + "member_list.noUsersAdd": "Nenhum usuário para adicionar.", "members_popover.msg": "Mensagem", "members_popover.title": "Membros", "more_channels.close": "Fechar", @@ -960,6 +966,7 @@ "sso_signup.team_error": "Por favor entre o nome da equipe", "suggestion.mention.all": "Notificar todo mundo na equipe", "suggestion.mention.channel": "Notifica todos no canal", + "suggestion.search.private": "Grupos Privados", "suggestion.search.public": "Canais Públicos", "team_export_tab.download": "download", "team_export_tab.export": "Exportar", @@ -1052,9 +1059,9 @@ "textbox.help": "Ajuda", "textbox.inlinecode": "`código`", "textbox.italic": "_itálico_", - "textbox.preformatted": "```pre-formatado```", + "textbox.preformatted": "```pré-formatado```", "textbox.preview": "Pré-visualização", - "textbox.quote": ">citado", + "textbox.quote": ">citar", "textbox.strike": "tachado", "tutorial_intro.allSet": "Está tudo pronto", "tutorial_intro.end": "Clique em “Próximo” para entrar Town Square. Este é o primeiro canal que sua equipe de trabalho vê quando eles se inscrevem. Use para postar atualizações que todos precisam saber.", -- cgit v1.2.3-1-g7c22 From 01536aa5508721a0d23c12aad520f5f34ba8e75d Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 8 Mar 2016 10:56:38 -0500 Subject: Globally exported all stores when developer mode is enabled --- web/react/stores/admin_store.jsx | 4 ++++ web/react/stores/analytics_store.jsx | 4 ++++ web/react/stores/channel_store.jsx | 4 ++++ web/react/stores/file_store.jsx | 7 ++++++- web/react/stores/modal_store.jsx | 4 ++++ web/react/stores/post_store.jsx | 4 ++++ web/react/stores/search_store.jsx | 4 ++++ web/react/stores/suggestion_store.jsx | 7 ++++++- web/react/stores/team_store.jsx | 4 ++++ web/react/stores/user_store.jsx | 4 ++++ 10 files changed, 44 insertions(+), 2 deletions(-) diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index eb3254cfe..5c911e94b 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -156,3 +156,7 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { }); export default AdminStore; + +if (window.mm_config.EnableDeveloper === 'true') { + window.AdminStore = AdminStore; +} diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx index ec827f6d7..0ad989206 100644 --- a/web/react/stores/analytics_store.jsx +++ b/web/react/stores/analytics_store.jsx @@ -83,3 +83,7 @@ AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => { }); export default AnalyticsStore; + +if (window.mm_config.EnableDeveloper === 'true') { + window.AnalyticsStore = AnalyticsStore; +} diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index 60cb10de7..eac24b071 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -350,3 +350,7 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { }); export default ChannelStore; + +if (window.mm_config.EnableDeveloper === 'true') { + window.ChannelStore = ChannelStore; +} diff --git a/web/react/stores/file_store.jsx b/web/react/stores/file_store.jsx index 6d7e0f354..c1fd0ef74 100644 --- a/web/react/stores/file_store.jsx +++ b/web/react/stores/file_store.jsx @@ -57,4 +57,9 @@ class FileStore extends EventEmitter { } } -export default new FileStore(); +const instance = new FileStore(); +export default instance; + +if (window.mm_config.EnableDeveloper === 'true') { + window.FileStore = instance; +} diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx index 5ea38030b..1819fffc0 100644 --- a/web/react/stores/modal_store.jsx +++ b/web/react/stores/modal_store.jsx @@ -45,3 +45,7 @@ class ModalStoreClass extends EventEmitter { const ModalStore = new ModalStoreClass(); export default ModalStore; + +if (window.mm_config.EnableDeveloper === 'true') { + window.ModalStore = ModalStore; +} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index a6dfcd46f..5cc3f300d 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -608,3 +608,7 @@ function isPostListNull(pl) { return false; } + +if (window.mm_config.EnableDeveloper === 'true') { + window.PostStore = PostStore; +} diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx index 549f355ef..96071665c 100644 --- a/web/react/stores/search_store.jsx +++ b/web/react/stores/search_store.jsx @@ -135,3 +135,7 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { }); export default SearchStore; + +if (window.mm_config.EnableDeveloper === 'true') { + window.SearchStore = SearchStore; +} diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx index efd2b76ed..487bae843 100644 --- a/web/react/stores/suggestion_store.jsx +++ b/web/react/stores/suggestion_store.jsx @@ -258,4 +258,9 @@ class SuggestionStore extends EventEmitter { } } -export default new SuggestionStore(); +const instance = new SuggestionStore(); +export default instance; + +if (window.mm_config.EnableDeveloper === 'true') { + window.SuggestionStore = instance; +} diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 7a1a2ef42..493d6bc4d 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -126,3 +126,7 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { }); export default TeamStore; + +if (window.mm_config.EnableDeveloper === 'true') { + window.TeamStore = TeamStore; +} diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 75a87d424..9fcd2440e 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -325,3 +325,7 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { }); export {UserStore as default}; + +if (window.mm_config.EnableDeveloper === 'true') { + window.UserStore = UserStore; +} -- cgit v1.2.3-1-g7c22 From 7d69d4a3c70d3aaeab9318c689ed8998a7ca0088 Mon Sep 17 00:00:00 2001 From: Rodrigo Corsi Date: Tue, 8 Mar 2016 13:54:15 -0300 Subject: Added missing web and server portuguese locs --- i18n/pt.json | 4 ++++ web/static/i18n/pt.json | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/i18n/pt.json b/i18n/pt.json index 61a88a890..8e45d5eaf 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -2407,6 +2407,10 @@ "id": "store.sql.pinging.info", "translation": "Pingando banco de dados sql %v" }, + { + "id": "store.sql.read_replicas_not_licensed.critical", + "translation": "A funcionalidade de mais de uma replica está desabilitada pela licença atual. Entre em contato com o administrador do sistema sobre como atualizar sua licença de empresa." + }, { "id": "store.sql.remove_index.critical", "translation": "Falha ao remover o índice %v" diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index d96d8a6da..76cb5a356 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -593,6 +593,7 @@ "choose_auth_page.find": "Encontrar minhas equipes", "choose_auth_page.gitlabCreate": "Criar uma equipe com uma conta GitLab", "choose_auth_page.googleCreate": "Criar nova equipe com a Conta do Google Apps", + "choose_auth_page.ldapCreate": "Criar uma nova equipe com uma conta LDAP", "choose_auth_page.noSignup": "Nenhum método de inscrição configurado, por favor contate seu administrador do sistema.", "claim.account.noEmail": "Nenhum email específicado", "claim.email_to_sso.enterPwd": "Entre a senha para o sua conta {team} {site}", @@ -736,6 +737,11 @@ "invite_member.send2": "Enviar Convites", "invite_member.sending": " Enviando", "invite_member.teamInviteLink": "Você também pode convidar pessoas usando o {link}", + "ldap_signup.find": "Encontrar minhas equipes", + "ldap_signup.ldap": "Criar uma equipe com uma conta LDAP", + "ldap_signup.length_error": "O nome deve ser de 3 ou mais caracteres até um máximo de 15", + "ldap_signup.teamName": "Entre o nome da nova equipe", + "ldap_signup.team_error": "Por favor entre o nome da equipe", "loading_screen.loading": "Carregando", "login.changed": " Método de login alterada com sucesso", "login.create": "Crie um agora", -- cgit v1.2.3-1-g7c22 From 5c29d4f64935d0516e9d72d1d8cc734bcf295a87 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Tue, 8 Mar 2016 12:27:27 -0500 Subject: Deleting posts with files now renames the file so that public links to those files no longer work --- api/file.go | 35 +++++++++++++++++++++++++++++++++++ api/post.go | 18 ++++++++++++++++++ i18n/en.json | 20 ++++++++++++++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/api/file.go b/api/file.go index 0011afd5b..9150e4bfe 100644 --- a/api/file.go +++ b/api/file.go @@ -547,6 +547,41 @@ func writeFile(f []byte, path string) *model.AppError { return nil } +func moveFile(oldPath, newPath string) *model.AppError { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + fileData := make(chan []byte) + getFileAndForget(oldPath, fileData) + fileBytes := <-fileData + + if fileBytes == nil { + return model.NewLocAppError("moveFile", "api.file.move_file.get_from_s3.app_error", nil, "") + } + + var auth aws.Auth + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey + + s := s3.New(auth, awsRegion()) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) + + if err := bucket.Del(oldPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error()) + } + + if err := writeFile(fileBytes, newPath); err != nil { + return err + } + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error()) + } + } else { + return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "") + } + + return nil +} + func writeFileLocally(f []byte, path string) *model.AppError { if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil { return model.NewLocAppError("writeFile", "api.file.write_file_locally.create_dir.app_error", nil, err.Error()) diff --git a/api/post.go b/api/post.go index e6560a8e8..cd78b16f0 100644 --- a/api/post.go +++ b/api/post.go @@ -1094,6 +1094,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { message.Add("post", post.ToJson()) PublishAndForget(message) + DeletePostFilesAndForget(c.Session.TeamId, post) result := make(map[string]string) result["id"] = postId @@ -1101,6 +1102,23 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { } } +func DeletePostFilesAndForget(teamId string, post *model.Post) { + go func() { + if len(post.Filenames) == 0 { + return + } + + prefix := "teams/" + teamId + "/channels/" + post.ChannelId + "/users/" + post.UserId + "/" + for _, filename := range post.Filenames { + splitUrl := strings.Split(filename, "/") + oldPath := prefix + splitUrl[len(splitUrl)-2] + "/" + splitUrl[len(splitUrl)-1] + newPath := prefix + splitUrl[len(splitUrl)-2] + "/deleted_" + splitUrl[len(splitUrl)-1] + moveFile(oldPath, newPath) + } + + }() +} + func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) { getPostsBeforeOrAfter(c, w, r, true) } diff --git a/i18n/en.json b/i18n/en.json index 5b261a770..244de5d56 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -456,8 +456,24 @@ "translation": "S3 is not supported for local storage export." }, { - "id": "api.export.write_file.app_error", - "translation": "Unable to write to export file" + "id": "api.file.move_file.get_from_s3.app_error", + "translation": "Unable to get file from S3." + }, + { + "id": "api.file.move_file.delete_from_s3.app_error", + "translation": "Unable to delete file from S3." + }, + { + "id": "api.file.move_file.rename.app_error", + "translation": "Unable to move file locally." + }, + { + "id": "api.file.move_file.configured.app_error", + "translation": "File storage not configured properly. Please configure for either S3 or local server file storage." + }, + { + "id": "api.file.file_upload.exceeds", + "translation": "File exceeds max image size." }, { "id": "api.file.file_upload.exceeds", -- cgit v1.2.3-1-g7c22 From 0d302d3b7e9777c7a738eca5a8c447f86c9339ad Mon Sep 17 00:00:00 2001 From: Eric Sethna Date: Tue, 8 Mar 2016 12:33:07 -0500 Subject: Update BaseDN and email attribute default text --- web/react/components/admin_console/ldap_settings.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/react/components/admin_console/ldap_settings.jsx b/web/react/components/admin_console/ldap_settings.jsx index 535c264dd..4cd19c886 100644 --- a/web/react/components/admin_console/ldap_settings.jsx +++ b/web/react/components/admin_console/ldap_settings.jsx @@ -20,7 +20,7 @@ var holders = defineMessages({ }, baseEx: { id: 'admin.ldap.baseEx', - defaultMessage: 'Ex "dc=mydomain,dc=com"' + defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"' }, firstnameAttrEx: { id: 'admin.ldap.firstnameAttrEx', @@ -32,7 +32,7 @@ var holders = defineMessages({ }, emailAttrEx: { id: 'admin.ldap.emailAttrEx', - defaultMessage: 'Ex "mail"' + defaultMessage: 'Ex "mail" or "userPrincipalName"' }, usernameAttrEx: { id: 'admin.ldap.usernameAttrEx', @@ -581,4 +581,4 @@ LdapSettings.propTypes = { config: React.PropTypes.object }; -export default injectIntl(LdapSettings); \ No newline at end of file +export default injectIntl(LdapSettings); -- cgit v1.2.3-1-g7c22 From 8a24a91c20c8e8147f1dbb5dd6fb12726d0cad1a Mon Sep 17 00:00:00 2001 From: Eric Sethna Date: Tue, 8 Mar 2016 12:37:01 -0500 Subject: Update BaseDN and Email LDAP settings help text in pt.json --- web/static/i18n/pt.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index f4b998ded..b8c372bdd 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -152,14 +152,14 @@ "admin.ldap.bannerDesc": "Se um atributo de usuário de mudar no servidor LDAP, ele será atualizado na próxima vez que o usuário inserir suas credenciais para iniciar sessão no Mattermost. Isso inclui se um usuário estiver inativo ou removido de um servidor LDAP. Sincronização com servidores LDAP está prevista para um lançamento futuro.", "admin.ldap.bannerHeading": "Nota:", "admin.ldap.baseDesc": "Base DN é o nome distinto do local onde Mattermost deve começar sua busca para os usuários na árvore LDAP.", - "admin.ldap.baseEx": "Ex \"dc=mydomain,dc=com\"", + "admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "BaseDN:", "admin.ldap.bindPwdDesc": "Senha do usuário fornecido em \"Bind Username\".", "admin.ldap.bindPwdTitle": "Vincular Senha:", "admin.ldap.bindUserDesc": "O nome de usuário usado para realizar a pesquisa LDAP. Isso deve ser tipicamente uma conta criada especificamente para uso do Mattermost. Deve ter acesso limitado a ler a parte da árvore LDAP especificado no campo BaseDN.", "admin.ldap.bindUserTitle": "Bind Username:", "admin.ldap.emailAttrDesc": "O atributo no servidor LDAP que será usado para preencher os endereços de e-mail de usuários no Mattermost.", - "admin.ldap.emailAttrEx": "Ex \"mail\"", + "admin.ldap.emailAttrEx": "Ex \"mail\" ou \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Atributo de E-mail:", "admin.ldap.enableDesc": "Quando verdadeiro, Mattermost permite login utilizando LDAP", "admin.ldap.enableTitle": "Ativar Login With LDAP:", @@ -1288,4 +1288,4 @@ "view_image_popover.download": "Download", "view_image_popover.file": "Arquivo {count} de {total}", "view_image_popover.publicLink": "Obter O Link Público" -} \ No newline at end of file +} -- cgit v1.2.3-1-g7c22 From b4ba344b491dc30497106edd90cfd6ac8a39e4ed Mon Sep 17 00:00:00 2001 From: Eric Sethna Date: Tue, 8 Mar 2016 12:39:54 -0500 Subject: Update BaseDN and Email LDAP settings help text in es.json --- web/static/i18n/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index b14470872..89326bff3 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -152,14 +152,14 @@ "admin.ldap.bannerDesc": "Si el atributo de un usuario cambia en el servidor LDAP será actualizado la próxima vez que el usuario ingrese sus credenciales para iniciar sesión en Mattermost. Esto incluye si un usuario se inactiva o se remueve en el servidor LDAP. Sincronización con servidores LDAP está planificado para futuras versiones.", "admin.ldap.bannerHeading": "Nota:", "admin.ldap.baseDesc": "El DN Base es el Nombre Distinguido de la ubicación donde Mattermost debe comenzar a buscar a los usuarios en el árbol del LDAP.", - "admin.ldap.baseEx": "Ex \"dc=midominio,dc=com\"", + "admin.ldap.baseEx": "Ej \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "DN Base:", "admin.ldap.bindPwdDesc": "Contraseña del usuario asignado en \"Usuario de Enlace\".", "admin.ldap.bindPwdTitle": "Contraseña de Enlace:", "admin.ldap.bindUserDesc": "El usuario que realizará las busquedas LDAP. Normalmente este debería ser una cuenta específicamente creada para ser utilizada por Mattermost. Debería contat con acceso limitado para leer la porción del árbol LDAP especificada en el campo DN Base.", "admin.ldap.bindUserTitle": "Usuario de Enlace:", "admin.ldap.emailAttrDesc": "El atributo en el servidor LDAP que será utilizado para poblar la dirección de correo electrónico de los usuarios en Mattermost.", - "admin.ldap.emailAttrEx": "Ej \"mail\"", + "admin.ldap.emailAttrEx": "Ej \"mail\" o \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Atributo de Correo Electrónico:", "admin.ldap.enableDesc": "Cuando es verdadero, Mattermost permite realizar inicio de sesión utilizando LDAP", "admin.ldap.enableTitle": "Habilitar inicio de sesión con LDAP:", -- cgit v1.2.3-1-g7c22 From 3478368065dee24a343dc1ce3d5c6e9c90325064 Mon Sep 17 00:00:00 2001 From: Eric Sethna Date: Tue, 8 Mar 2016 12:41:36 -0500 Subject: Update BaseDN and Email LDAP settings help text in en.json --- web/static/i18n/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 4c6fa0eae..58a19ea1b 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -152,14 +152,14 @@ "admin.ldap.bannerDesc": "If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.", "admin.ldap.bannerHeading": "Note:", "admin.ldap.baseDesc": "The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.", - "admin.ldap.baseEx": "Ex \"dc=mydomain,dc=com\"", + "admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "BaseDN:", "admin.ldap.bindPwdDesc": "Password of the user given in \"Bind Username\".", "admin.ldap.bindPwdTitle": "Bind Password:", "admin.ldap.bindUserDesc": "The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.", "admin.ldap.bindUserTitle": "Bind Username:", "admin.ldap.emailAttrDesc": "The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.", - "admin.ldap.emailAttrEx": "Ex \"mail\"", + "admin.ldap.emailAttrEx": "Ex \"mail\" or \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Email Attribute:", "admin.ldap.enableDesc": "When true, Mattermost allows login using LDAP", "admin.ldap.enableTitle": "Enable Login With LDAP:", -- cgit v1.2.3-1-g7c22 From 507eda092ebb96550dc8e46cfcf570da37ce1d56 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 8 Mar 2016 14:40:47 -0500 Subject: Added target='_blank' to all attachment download links --- web/react/components/file_attachment.jsx | 2 ++ web/react/components/view_image_popover_bar.jsx | 1 + 2 files changed, 3 insertions(+) diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index c719c6c7d..2f6067b86 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -185,6 +185,7 @@ class FileAttachment extends React.Component { data-toggle='tooltip' title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'} className='post-image__name' + target='_blank' > {trimmedFilename} @@ -193,6 +194,7 @@ class FileAttachment extends React.Component { href={fileUrl} download={filenameString} className='post-image__download' + target='_blank' > Date: Tue, 8 Mar 2016 14:57:35 -0500 Subject: Improving the about dialog for licencing --- web/react/components/about_build_modal.jsx | 39 ++++++++++++++++++------------ web/static/i18n/en.json | 5 ++-- web/static/i18n/es.json | 5 ++-- web/static/i18n/pt.json | 7 +++--- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index fe48bb48e..86519e5db 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -21,29 +21,38 @@ export default class AboutBuildModal extends React.Component { let title = ( ); + let licensee; - if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') { + if (config.BuildEnterpriseReady === 'true') { title = ( ); - licensee = ( -
-
- + if (license.IsLicensed === 'true') { + title = ( + + ); + licensee = ( +
+
+ +
+
{license.Company}
-
{license.Company}
-
- ); + ); + } } return ( diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 4c6fa0eae..340464667 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -1,11 +1,12 @@ { "about.close": "Close", "about.date": "Build Date:", - "about.enterpriseEdition": "Enterprise Edition", + "about.enterpriseEditione1": "Enterprise Edition E1", "about.hash": "Build Hash:", "about.licensed": "Licensed by:", "about.number": "Build Number:", - "about.teamEdtion": "Team Edition", + "about.teamEdtiont0": "Team Edition T0", + "about.teamEdtiont1": "Team Edition T1", "about.title": "About Mattermost", "about.version": "Version:", "access_history.title": "Access History", diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index b14470872..2849578a5 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -1,11 +1,12 @@ { "about.close": "Cerrar", "about.date": "Fecha de compilación:", - "about.enterpriseEdition": "Edición Enterprise", + "about.enterpriseEditione1": "Edición Enterprise E1", "about.hash": "Hash de compilación:", "about.licensed": "Licenciado por:", "about.number": "Número de compilación:", - "about.teamEdtion": "Edición Team", + "about.teamEdtiont0": "Edición Team T0", + "about.teamEdtiont1": "Edición Team T1", "about.title": "Acerca de Mattermost", "about.version": "Versión:", "access_history.title": "Historial de Acceso", diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index f4b998ded..94e67a799 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -1,11 +1,12 @@ { "about.close": "Fechar", "about.date": "Data De Criação:", - "about.enterpriseEdition": "Enterprise Edition", + "about.enterpriseEditione1": "Enterprise Edition E1", "about.hash": "Hash de Compilação:", "about.licensed": "Licenciado pela:", "about.number": "O Número De Compilação:", - "about.teamEdtion": "Team Edition", + "about.teamEdtiont0": "Team Edition T0", + "about.teamEdtiont1": "Team Edition T1", "about.title": "Sobre o Mattermost", "about.version": "Versão:", "access_history.title": "Histórico de Acesso", @@ -1288,4 +1289,4 @@ "view_image_popover.download": "Download", "view_image_popover.file": "Arquivo {count} de {total}", "view_image_popover.publicLink": "Obter O Link Público" -} \ No newline at end of file +} -- cgit v1.2.3-1-g7c22 From e0529a627c97612d533a661016cd37c1603f4c32 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 8 Mar 2016 15:55:15 -0500 Subject: Added proper click handler to search result text --- web/react/components/search_results_item.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 05292b7b3..5ab864b7c 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -123,6 +123,7 @@ export default class SearchResultsItem extends React.Component {
-- cgit v1.2.3-1-g7c22 From b77a04009e8b670d704b8f1b862d6ed5bb42d4e8 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 8 Mar 2016 21:16:46 -0300 Subject: Add missing server spanish locs --- i18n/en.json | 32 ++++++++++++++++---------------- i18n/es.json | 20 ++++++++++++++++++-- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 6ac5ce5c2..bc33fc019 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -455,22 +455,6 @@ "id": "api.export.s3.app_error", "translation": "S3 is not supported for local storage export." }, - { - "id": "api.file.move_file.get_from_s3.app_error", - "translation": "Unable to get file from S3." - }, - { - "id": "api.file.move_file.delete_from_s3.app_error", - "translation": "Unable to delete file from S3." - }, - { - "id": "api.file.move_file.rename.app_error", - "translation": "Unable to move file locally." - }, - { - "id": "api.file.move_file.configured.app_error", - "translation": "File storage not configured properly. Please configure for either S3 or local server file storage." - }, { "id": "api.file.file_upload.exceeds", "translation": "File exceeds max image size." @@ -523,6 +507,22 @@ "id": "api.file.init.debug", "translation": "Initializing file api routes" }, + { + "id": "api.file.move_file.configured.app_error", + "translation": "File storage not configured properly. Please configure for either S3 or local server file storage." + }, + { + "id": "api.file.move_file.delete_from_s3.app_error", + "translation": "Unable to delete file from S3." + }, + { + "id": "api.file.move_file.get_from_s3.app_error", + "translation": "Unable to get file from S3." + }, + { + "id": "api.file.move_file.rename.app_error", + "translation": "Unable to move file locally." + }, { "id": "api.file.open_file_write_stream.configured.app_error", "translation": "File storage not configured properly. Please configure for either S3 or local server file storage." diff --git a/i18n/es.json b/i18n/es.json index 6443d18dc..4c0c1fd03 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -456,8 +456,8 @@ "translation": "S3 no está soportado para exportar al almacenamiento local." }, { - "id": "api.export.write_file.app_error", - "translation": "No se puede escribir al archivo a exportar" + "id": "api.file.file_upload.exceeds", + "translation": "El archivo excede el tamaño máximo para una imagen." }, { "id": "api.file.file_upload.exceeds", @@ -507,6 +507,22 @@ "id": "api.file.init.debug", "translation": "Inicializando rutas del API para los archivos" }, + { + "id": "api.file.move_file.configured.app_error", + "translation": "No ha sido configurado apropiadamente el almacenamiento. Por favor configuralo para utilizar ya sea S3 o almacenamiento local." + }, + { + "id": "api.file.move_file.delete_from_s3.app_error", + "translation": "No se pudo eliminar el archivo del S3." + }, + { + "id": "api.file.move_file.get_from_s3.app_error", + "translation": "No se pudo obtener el archivo desde el S3." + }, + { + "id": "api.file.move_file.rename.app_error", + "translation": "No se pudo mover el archivo localmente." + }, { "id": "api.file.open_file_write_stream.configured.app_error", "translation": "El almacenamiento de archivos no ha sido configurado apropiadamente. Por favor configuralo ya sea para S3 o para almacenamiento en el servidor local." -- cgit v1.2.3-1-g7c22 From 3d8c6af4d9cc04141c2af5d8163a640b3bb6a9a3 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Tue, 8 Mar 2016 23:09:33 -0500 Subject: Fix spelling --- web/react/components/about_build_modal.jsx | 4 ++-- web/static/i18n/en.json | 4 ++-- web/static/i18n/es.json | 4 ++-- web/static/i18n/pt.json | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index 86519e5db..34b1fdccf 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -21,7 +21,7 @@ export default class AboutBuildModal extends React.Component { let title = ( ); @@ -30,7 +30,7 @@ export default class AboutBuildModal extends React.Component { if (config.BuildEnterpriseReady === 'true') { title = ( ); diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 340464667..e3d331593 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -5,8 +5,8 @@ "about.hash": "Build Hash:", "about.licensed": "Licensed by:", "about.number": "Build Number:", - "about.teamEdtiont0": "Team Edition T0", - "about.teamEdtiont1": "Team Edition T1", + "about.teamEditiont0": "Team Edition T0", + "about.teamEditiont1": "Team Edition T1", "about.title": "About Mattermost", "about.version": "Version:", "access_history.title": "Access History", diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index 2849578a5..f9bb58ef3 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -5,8 +5,8 @@ "about.hash": "Hash de compilación:", "about.licensed": "Licenciado por:", "about.number": "Número de compilación:", - "about.teamEdtiont0": "Edición Team T0", - "about.teamEdtiont1": "Edición Team T1", + "about.teamEditiont0": "Edición Team T0", + "about.teamEditiont1": "Edición Team T1", "about.title": "Acerca de Mattermost", "about.version": "Versión:", "access_history.title": "Historial de Acceso", diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index 94e67a799..3bed12ebf 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -5,8 +5,8 @@ "about.hash": "Hash de Compilação:", "about.licensed": "Licenciado pela:", "about.number": "O Número De Compilação:", - "about.teamEdtiont0": "Team Edition T0", - "about.teamEdtiont1": "Team Edition T1", + "about.teamEditiont0": "Team Edition T0", + "about.teamEditiont1": "Team Edition T1", "about.title": "Sobre o Mattermost", "about.version": "Versão:", "access_history.title": "Histórico de Acesso", -- cgit v1.2.3-1-g7c22 From 1a38fbf681422a1b14fceeb3dc3f16e43e8e62a6 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 9 Mar 2016 09:00:36 -0500 Subject: Fix ... for usernames on first page load --- web/react/components/post.jsx | 6 ++---- web/react/components/post_body.jsx | 3 +-- web/react/components/posts_view.jsx | 1 - web/react/components/posts_view_container.jsx | 10 +++++++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 889d4311e..57e919e45 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -98,7 +98,7 @@ export default class Post extends React.Component { return true; } - if (nextProps.hasProfiles !== this.props.hasProfiles) { + if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) { return true; } @@ -226,7 +226,6 @@ export default class Post extends React.Component { posts={posts} handleCommentClick={this.handleCommentClick} retryPost={this.retryPost} - hasProfiles={this.props.hasProfiles} />
@@ -246,6 +245,5 @@ Post.propTypes = { hideProfilePic: React.PropTypes.bool, isLastComment: React.PropTypes.bool, shouldHighlight: React.PropTypes.bool, - displayNameType: React.PropTypes.string, - hasProfiles: React.PropTypes.bool + displayNameType: React.PropTypes.string }; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 70cf86748..854cb095a 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -215,8 +215,7 @@ PostBody.propTypes = { post: React.PropTypes.object.isRequired, parentPost: React.PropTypes.object, retryPost: React.PropTypes.func.isRequired, - handleCommentClick: React.PropTypes.func.isRequired, - hasProfiles: React.PropTypes.bool + handleCommentClick: React.PropTypes.func.isRequired }; export default injectIntl(PostBody); diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index 9a1673483..c2c739e9a 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -250,7 +250,6 @@ export default class PostsView extends React.Component { shouldHighlight={shouldHighlight} onClick={() => EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} - hasProfiles={profiles && Object.keys(profiles).length > 1} user={profile} /> ); diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 92d658b55..976e03fab 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -149,11 +149,15 @@ export default class PostsViewContainer extends React.Component { } } shouldComponentUpdate(nextProps, nextState) { - if (Utils.areObjectsEqual(this.state, nextState)) { - return false; + if (!Utils.areObjectsEqual(this.state, nextState)) { + return true; } - return true; + if (!Utils.areObjectsEqual(this.props, nextProps)) { + return true; + } + + return false; } render() { const postLists = this.state.postLists; -- cgit v1.2.3-1-g7c22 From d378812349d139367a1fca33d0cf45a6b678d64d Mon Sep 17 00:00:00 2001 From: Eric Sethna Date: Wed, 9 Mar 2016 10:26:14 -0500 Subject: Update test-search.md with more markdown hashtags --- tests/test-search.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-search.md b/tests/test-search.md index 0f0ba1153..828828c25 100644 --- a/tests/test-search.md +++ b/tests/test-search.md @@ -40,4 +40,5 @@ Click on the linked hashtags below, and confirm that the search results match th #### Markdown surrounding a hashtag: -*#markdown-hashtag* +*#markdown* **#markdown** ~~#markdown~~ +##### #markdown -- cgit v1.2.3-1-g7c22 From 29eb6a1d0147767d9aba980c14e7a3df9dd3d7c9 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Tue, 8 Mar 2016 18:05:49 +0500 Subject: Improvements to license page --- .../components/admin_console/license_settings.jsx | 25 ++++++++++++---------- web/sass-files/sass/partials/_admin-console.scss | 19 ++++++++++++++-- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx index d4dfa13f2..632ef5ad4 100644 --- a/web/react/components/admin_console/license_settings.jsx +++ b/web/react/components/admin_console/license_settings.jsx @@ -174,15 +174,17 @@ class LicenseSettings extends React.Component { licenseKey = (
- +
+ + +
-
-
+
+ {'No file uploaded'} +

{serverError} -

+

Date: Tue, 8 Mar 2016 18:10:15 +0500 Subject: Updating button class --- web/react/components/admin_console/license_settings.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx index 632ef5ad4..4f3d40d71 100644 --- a/web/react/components/admin_console/license_settings.jsx +++ b/web/react/components/admin_console/license_settings.jsx @@ -175,7 +175,7 @@ class LicenseSettings extends React.Component { licenseKey = (

- + Date: Wed, 9 Mar 2016 11:23:29 -0500 Subject: Show filename for license settings, add loc --- utils/license.go | 15 +++++----- .../components/admin_console/license_settings.jsx | 32 +++++++++++++++++----- web/static/i18n/en.json | 2 ++ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/utils/license.go b/utils/license.go index b773a163e..5c975aec2 100644 --- a/utils/license.go +++ b/utils/license.go @@ -22,15 +22,14 @@ var IsLicensed bool = false var License *model.License = &model.License{} var ClientLicense map[string]string = make(map[string]string) -// test public key var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3/k3Al9q1Xe+xngQ/yGn -0suaJopea3Cpf6NjIHdO8sYTwLlxqt0Mdb+qBR9LbCjZfcNmqc5mZONvsyCEoN/5 -VoLdlv1m9ao2BSAWphUxE2CPdUWdLOsDbQWliSc5//UhiYeR+67Xxon0Hg0LKXF6 -PumRIWQenRHJWqlUQZ147e7/1v9ySVRZksKpvlmMDzgq+kCH/uyM1uVP3z7YXhlN -K7vSSQYbt4cghvWQxDZFwpLlsChoY+mmzClgq+Yv6FLhj4/lk94twdOZau/AeZFJ -NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR -1wIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r +tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N +EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E +HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk +1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn +a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt +hwIDAQAB -----END PUBLIC KEY-----`) func LoadLicense(licenseBytes []byte) { diff --git a/web/react/components/admin_console/license_settings.jsx b/web/react/components/admin_console/license_settings.jsx index 4f3d40d71..9d2ec8030 100644 --- a/web/react/components/admin_console/license_settings.jsx +++ b/web/react/components/admin_console/license_settings.jsx @@ -27,6 +27,7 @@ class LicenseSettings extends React.Component { this.state = { fileSelected: false, + fileName: null, serverError: null }; } @@ -34,7 +35,7 @@ class LicenseSettings extends React.Component { handleChange() { const element = $(ReactDOM.findDOMNode(this.refs.fileInput)); if (element.prop('files').length > 0) { - this.setState({fileSelected: true}); + this.setState({fileSelected: true, fileName: element.prop('files')[0].name}); } } @@ -56,13 +57,13 @@ class LicenseSettings extends React.Component { () => { Utils.clearFileInput(element[0]); $('#upload-button').button('reset'); - this.setState({serverError: null}); + this.setState({fileSelected: false, fileName: null, serverError: null}); window.location.reload(true); }, (error) => { Utils.clearFileInput(element[0]); $('#upload-button').button('reset'); - this.setState({serverError: error.message}); + this.setState({fileSelected: false, fileName: null, serverError: error.message}); } ); } @@ -75,12 +76,12 @@ class LicenseSettings extends React.Component { Client.removeLicenseFile( () => { $('#remove-button').button('reset'); - this.setState({serverError: null}); + this.setState({fileSelected: false, fileName: null, serverError: null}); window.location.reload(true); }, (error) => { $('#remove-button').button('reset'); - this.setState({serverError: error.message}); + this.setState({fileSelected: false, fileName: null, serverError: error.message}); } ); } @@ -172,10 +173,27 @@ class LicenseSettings extends React.Component { /> ); + let fileName; + if (this.state.fileName) { + fileName = this.state.fileName; + } else { + fileName = ( + + ); + } + licenseKey = (
- +
- {'No file uploaded'} + {fileName}

{serverError} diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 58a19ea1b..090b8be05 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -191,6 +191,8 @@ "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Username Attribute:", "admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, disable all Enterprise Edition features on this server. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.", + "admin.license.noFile": "No file uploaded", + "admin.license.chooseFile": "Choose File", "admin.license.edition": "Edition: ", "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Designed for enterprise-scale communication.", "admin.license.enterpriseType": "

This compiled release of Mattermost platform is provided under a commercial license from Mattermost, Inc. based on your subscription level and is subject to the Terms of Service.

Your subscription details are as follows:

Name: {name}
Company or organization name: {company}
Number of users: {users}
License issued: {issued}
Start date of license: {start}
Expiry date of license: {expires}
LDAP: {ldap}
", -- cgit v1.2.3-1-g7c22 From 5a6cee088280f28a03bd706c613d4db8af4b2cee Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 9 Mar 2016 14:55:21 -0300 Subject: Add license locs in spanish --- web/static/i18n/en.json | 2 +- web/static/i18n/es.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index feea91aad..d2e340641 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -192,13 +192,13 @@ "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Username Attribute:", "admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, disable all Enterprise Edition features on this server. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.", - "admin.license.noFile": "No file uploaded", "admin.license.chooseFile": "Choose File", "admin.license.edition": "Edition: ", "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Designed for enterprise-scale communication.", "admin.license.enterpriseType": "

This compiled release of Mattermost platform is provided under a commercial license from Mattermost, Inc. based on your subscription level and is subject to the Terms of Service.

Your subscription details are as follows:

Name: {name}
Company or organization name: {company}
Number of users: {users}
License issued: {issued}
Start date of license: {start}
Expiry date of license: {expires}
LDAP: {ldap}
", "admin.license.key": "License Key: ", "admin.license.keyRemove": "Remove Enterprise License and Downgrade Server", + "admin.license.noFile": "No file uploaded", "admin.license.removing": "Removing License...", "admin.license.teamEdition": "Mattermost Team Edition. Designed for teams from 5 to 50 users.", "admin.license.teamType": "

This compiled release of Mattermost platform is offered under an MIT license.

See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.

", diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index 2a7eaae16..c6b16a293 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -192,11 +192,13 @@ "admin.ldap.usernameAttrEx": "Ej \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Atributo Usuario:", "admin.licence.keyMigration": "Si estás migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar, deshabilita todas las características de la Edición Enterprise de este servidor. Esta operación habilitará la opción para remover la licencia y degradar este servidor de la Edición Enterprise a la Edición Team.", + "admin.license.chooseFile": "Escoger Archivo", "admin.license.edition": "Edición: ", "admin.license.enterpriseEdition": "Mattermost Edición Enterprise. Diseñada para comunicación de escala empresarial.", "admin.license.enterpriseType": "

Esta versión compilada de la plataforma de Mattermost es provista bajo una licencia comercial de Mattermost, Inc. en función en su nivel de subscripción y bajo los Términos del Servicio.

Los detalles de tu subscripción son los siguientes:

Nombre: {name}
Nombre compañía u organización: {company}
Cantidad de usuarios: {users}
Licencia emitida: {issued}
Fecha de inicio: {start}
Fecha de expiración: {expires}
LDAP: {ldap}
", "admin.license.key": "Llave de la Licencia: ", "admin.license.keyRemove": "Remover la Licencia Enterprise y Degradar el Servidor", + "admin.license.noFile": "No se subió ningún archivo", "admin.license.removing": "Removiendo Licencia...", "admin.license.teamEdition": "Mattermost Edición Team. Diseñado para equipos desde 5 hasta 50 usuarios.", "admin.license.teamType": "

Esta versión compilada de la plataforma de Mattermost es proporcionada bajo la licencia MIT.

Lea MIT-COMPILED-LICENSE.txt en el directorio raíz de la instalación para más detalles. Lea NOTICES.txt para información sobre software libre utilizado en este sistema.

", -- cgit v1.2.3-1-g7c22 From f72db35781b809f4bae49917426a4860f50ef35f Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Wed, 9 Mar 2016 23:43:42 +0500 Subject: Disabling help stuff and moving preview out of it --- web/react/components/textbox.jsx | 24 +++++++++++++----------- web/sass-files/sass/partials/_post.scss | 31 ++++++++++++++++++------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index 46900e436..c8c0c071e 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -194,7 +194,6 @@ export default class Textbox extends React.Component { defaultMessage='>quote' /> - {previewLink}
); @@ -230,16 +229,19 @@ export default class Textbox extends React.Component { >
{helpText} - - - +
+ {previewLink} + + + +
); } diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 39cebe856..209f8e27f 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -58,23 +58,27 @@ body.ios { box-shadow: none; white-space: normal; } - .textbox-help-link { + .help__text { + right: 0; position: absolute; z-index: 3; bottom: -23px; font-size: 13px; cursor: pointer; } - .textbox-help-link { - right: 0; + .textbox-preview-link { + margin-right: 8px; } min-height:36px; } .help_format_text { + display: none !important; position: absolute; bottom: -23px; - right: 40px; + left: 0px; + overflow: hidden; + text-overflow: ellipsis; font-size: 0.85em; @include opacity(0); @include single-transition(all 0.2s ease); @@ -391,15 +395,16 @@ body.ios { top: -5px; position: relative; } - .msg-typing { - min-height: 25px; - display: block; - @include opacity(0.7); - white-space: nowrap; - width: 80%; - overflow: hidden; - text-overflow: ellipsis; - } + } + .msg-typing { + display: block; + @include opacity(0.7); + white-space: nowrap; + margin-bottom: 5px; + overflow: hidden; + font-size: 0.95em; + text-overflow: ellipsis; + height: 20px; } } -- cgit v1.2.3-1-g7c22 From 424097e0883984cf1fc0ad188637d8a7b4d0ae87 Mon Sep 17 00:00:00 2001 From: Eric Sethna Date: Wed, 9 Mar 2016 14:12:13 -0500 Subject: Update Changelog.md for v2.1 --- CHANGELOG.md | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8606fc72c..e0052918f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,144 @@ # Mattermost Changelog +## Release v2.1.0 + +Expected release date: 2016-03-16 + +### Highlights + +- New Android application now available. +- New desktop applications for Windows, Mac and Linux now in beta. +- Brazilian Portuguese translation added. + +### New Features + +Android Application + +- New [Mattermost Android App](https://github.com/mattermost/android) supporting push notifications available for devices running Android 4.4.2+. Requires Mattermost server 2.1 and higher. See [list of tested devices](https://github.com/mattermost/android/blob/master/DEVICES.md). + +Desktop Application + +- New [Desktop Application](https://github.com/mattermost/desktop) for Windows, Mac, and Linux now available as a beta release. + + +Languages + +- Added Portuguese language translation (Beta) available from **Account Settings** > **Display**. + +Active Directory/LDAP (Enterprise Edition) + +- Create new teams using Active Directory/LDAP. Team creation and Active Directory/LDAP must be enabled in the System Console from **Team Settings** > **Enable Team Creation** and **LDAP Settings** > **Enable LDAP**. +- Added Active Directory/LDAP synchronization to check for deactivated users on the LDAP server. + +### Improvements + +System Console + +- Removed unused “Disable File Storage” option from the System Console as it is no longer relevant. +- Added a warning message if a system admin demotes themselves. +- System Console statistics now use a client store instead of fetching data and storing it in state. + +Messaging + +- Custom slash commands now support temporary messages that appear only to the user that issued the command. +- Username autocomplete list no longer suggests inactive users. + +Mobile + +- Significant responsiveness and speed improvements using [fastclick](https://github.com/ftlabs/fastclick). +- Team name and username are now shown in the LHS header. +- Added a button to go back to the team URL page from the login page. + +Files and Images + +- Increased the maximum size of image uploads to 24 megapixels. + +User Interface + +- Custom theme color selectors are now organized into categories. +- Add Members and Manage Members dialogs can now be filtered using a search bar. +- Deactivated members no longer appear in the channel members list. +- Keyboard focus is set to the text input box in the RHS if a user clicks the reply icon. +- Permalinks are now displayed in a Copy Permalink dialog instead of a popover. +- Permalink option is now available from the [...] menu on messages and comments in the RHS. +- Reply icon now only appears on-hover for messages that don’t have replies. +- Scroll bar now appears in the center channel. + +Enterprise Edition + +- Access History now contains audits for adding and removing Enterprise licenses. + + +#### Bug Fixes + +- System console user management tab now shows username and email on different lines. +- Yellow text box error no longer appears when the system is connected. +- Wildcard search on MySQL databases is now fixed. +- Usernames in the center channel no longer appear as “...” on login. +- Deleted messages now delete in the RHS and center channel without requiring a page refresh. +- Contact us email address in the footer of notification emails now uses the SupportEmail config setting instead of FeedbackEmail. +- Email addresses are now required to have at least one letter before and after the @ sign. +- Firefox desktop notifications are now fixed for some users experiencing missed notifications. +- “User is typing” message containing long usernames no longer causes text wrapping. +- Usernames appearing as “...” in the RHS when performing a search is fixed. +- Links that end in image extensions but do not actually link to raw images no longer generate a blank image preview. +- Channel handle field in the Rename Channel dialog is now visible on themes with dark backgrounds. +- Autolinked images no longer persist after the post containing the link is deleted. +- Code theme selector on IE11 now only shows one dropdown arrow and clicking directly on the arrow opens the dropdown. +- Save/Cancel buttons for language selection in Account Settings are now formatted the same as other settings. +- Inconsistent field spacing in the Channel Info dialog is fixed. +- Recent mentions icon no longer jumps to the left of the search bar when the RHS is opened. +- Custom slash command hints now show up in the autocomplete list. +- GIF links inside code blocks no longer auto-post the GIFs. +- Changing usernames no longer adds the old username to “words that trigger mentions”. +- Notification email footer is now translated based on the sender’s language setting. +- Slash command `/me` now posts as the user instead of a webhook message. +- Logout slash command now forces logout. +- Public links to file attachments on deleted posts no longer work. +- Error message is now shown in IE11 when uploading more than 5 files or a file over 50 MB. + + +### Compatibility +Changes from v2.0 to v2.1: + +**Android** +Mattermost Android Application is for use with Mattermost platform v2.1 and higher. + +#### Known Issues + +- File name tooltip stays open after clicking to download. +- Unable to paste images into the text box on Firefox, Safari, and IE11. +- Archived channels are not removed from the "More" menu for the person that archived the channel until after refresh. +- First load of an empty channel does not display the introduction message. +- Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks. +- Searching for a username or hashtag containing a dot returns a search where the dot is replaced with the "or" operator. +- Hashtags containing a dash incorrectly highlight in the search results. +- Emoji smileys ending with a letter at the end of a message do not auto-complete as expected. +- Incorrect formatting when a new line is added directly after a list. +- Timestamps are displayed in 12-hour format when set to 24-hour format. +- Syntax highlighting code block is missing the label for Latex documents. +- Posts from webhooks do not fire notifications to the user who created the webhook. +- Theme color vector is not updated after making custom changes to a default theme. +- Search term highlighting doesn't update on IE11 when search terms change but return the same posts. +- Team creation via SSO fails when email domain is restricted. + + +#### Contributors + +Many thanks to all our external contributors. In no particular order: + +- [rodrigocorsi2](https://github.com/rodrigocorsi2) +- [enahum](https://github.com/enahum) +- [khoa-le](https://github.com/khoa-le) +- [alanmoo](https://github.com/alanmoo) +- [daizenberg](https://github.com/daizenberg) +- [GuillaumeAmat](https://github.com/GuillaumeAmat) +- [kernicPanel](https://github.com/kernicPanel) +- [timlyo](https://github.com/timlyo) +- [ttyniwa](https://github.com/ttyniwa) + + + ## Release v2.0.0 Expected Release date: 2016-02-16 -- cgit v1.2.3-1-g7c22 From 5ebf40895c7d568b365c937349cc35b5d9c966cf Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Wed, 9 Mar 2016 14:25:51 -0500 Subject: Fixing minor issue --- web/react/utils/markdown.jsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 493916058..2b1aed9c0 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -193,6 +193,16 @@ class MattermostMarkdownRenderer extends marked.Renderer { outHref = outHref.substring(1, outHref.length - 1); } + try { + const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase(); + + if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0) { // eslint-disable-line no-script-url + return ''; + } + } catch (e) { + return ''; + } + if (!(/[a-z+.-]+:/i).test(outHref)) { outHref = `http://${outHref}`; } @@ -548,3 +558,18 @@ export function format(text, options) { return new MattermostParser(markdownOptions).parse(tokens); } +// Marked helper functions that should probably just be exported + +function unescape(html) { + return html.replace(/&([#\w]+);/g, (_, m) => { + const n = m.toLowerCase(); + if (n === 'colon') { + return ':'; + } else if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' ? + String.fromCharCode(parseInt(n.substring(2), 16)) : + String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} -- cgit v1.2.3-1-g7c22 From 1ad314f79c7e34bad991c2fa9f15d61831d2ba29 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Wed, 9 Mar 2016 20:31:44 +0000 Subject: Docker updates for 2.1.0-rc1 --- docker/1.4/Dockerfile | 49 --------- docker/1.4/Dockerrun.aws.zip | Bin 842 -> 0 bytes .../Dockerrun.aws/.ebextensions/01_files.config | 14 --- docker/1.4/Dockerrun.aws/Dockerrun.aws.json | 13 --- docker/1.4/README.md | 23 ---- docker/1.4/config_docker.json | 111 -------------------- docker/1.4/docker-entry.sh | 111 -------------------- docker/2.1/Dockerfile | 49 +++++++++ docker/2.1/Dockerrun.aws.zip | Bin 0 -> 866 bytes .../Dockerrun.aws/.ebextensions/01_files.config | 14 +++ docker/2.1/Dockerrun.aws/Dockerrun.aws.json | 13 +++ docker/2.1/README.md | 23 ++++ docker/2.1/config_docker.json | 116 +++++++++++++++++++++ docker/2.1/docker-entry.sh | 111 ++++++++++++++++++++ 14 files changed, 326 insertions(+), 321 deletions(-) delete mode 100644 docker/1.4/Dockerfile delete mode 100644 docker/1.4/Dockerrun.aws.zip delete mode 100644 docker/1.4/Dockerrun.aws/.ebextensions/01_files.config delete mode 100755 docker/1.4/Dockerrun.aws/Dockerrun.aws.json delete mode 100644 docker/1.4/README.md delete mode 100644 docker/1.4/config_docker.json delete mode 100755 docker/1.4/docker-entry.sh create mode 100644 docker/2.1/Dockerfile create mode 100644 docker/2.1/Dockerrun.aws.zip create mode 100644 docker/2.1/Dockerrun.aws/.ebextensions/01_files.config create mode 100755 docker/2.1/Dockerrun.aws/Dockerrun.aws.json create mode 100644 docker/2.1/README.md create mode 100644 docker/2.1/config_docker.json create mode 100755 docker/2.1/docker-entry.sh diff --git a/docker/1.4/Dockerfile b/docker/1.4/Dockerfile deleted file mode 100644 index 8dcea3927..000000000 --- a/docker/1.4/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -# See License.txt for license information. -FROM ubuntu:14.04 - -# -# Install SQL -# - -ENV MYSQL_ROOT_PASSWORD=mostest -ENV MYSQL_USER=mmuser -ENV MYSQL_PASSWORD=mostest -ENV MYSQL_DATABASE=mattermost_test - -RUN groupadd -r mysql && useradd -r -g mysql mysql - -RUN apt-key adv --keyserver pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5 - -ENV MYSQL_MAJOR 5.6 -ENV MYSQL_VERSION 5.6.25 - -RUN echo "deb http://repo.mysql.com/apt/debian/ wheezy mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list - -RUN apt-get update \ - && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install perl wget mysql-server \ - && rm -rf /var/lib/apt/lists/* \ - && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql - -RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/mysql/my.cnf - -VOLUME /var/lib/mysql -# --------------------------------------------------------------------------------------------------------------------- - -WORKDIR /mattermost - -# Copy over files -ADD https://github.com/mattermost/platform/releases/download/v1.4.0/mattermost.tar.gz / -RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz -ADD config_docker.json / -ADD docker-entry.sh / - -RUN chmod +x /docker-entry.sh -ENTRYPOINT /docker-entry.sh - -# Create default storage directory -RUN mkdir /mattermost-data/ - -# Ports -EXPOSE 80 diff --git a/docker/1.4/Dockerrun.aws.zip b/docker/1.4/Dockerrun.aws.zip deleted file mode 100644 index 55bc98eea..000000000 Binary files a/docker/1.4/Dockerrun.aws.zip and /dev/null differ diff --git a/docker/1.4/Dockerrun.aws/.ebextensions/01_files.config b/docker/1.4/Dockerrun.aws/.ebextensions/01_files.config deleted file mode 100644 index 7f40a8b34..000000000 --- a/docker/1.4/Dockerrun.aws/.ebextensions/01_files.config +++ /dev/null @@ -1,14 +0,0 @@ -files: - "/etc/nginx/conf.d/proxy.conf": - mode: "000755" - owner: root - group: root - content: | - client_max_body_size 50M; - "/opt/elasticbeanstalk/hooks/appdeploy/post/init.sh": - mode: "000755" - owner: root - group: root - content: | - #!/usr/bin/env bash - gpasswd -a ec2-user docker diff --git a/docker/1.4/Dockerrun.aws/Dockerrun.aws.json b/docker/1.4/Dockerrun.aws/Dockerrun.aws.json deleted file mode 100755 index 654961589..000000000 --- a/docker/1.4/Dockerrun.aws/Dockerrun.aws.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "AWSEBDockerrunVersion": "1", - "Image": { - "Name": "mattermost/platform:1.4", - "Update": "true" - }, - "Ports": [ - { - "ContainerPort": "80" - } - ], - "Logging": "/var/log/" -} diff --git a/docker/1.4/README.md b/docker/1.4/README.md deleted file mode 100644 index f737a1554..000000000 --- a/docker/1.4/README.md +++ /dev/null @@ -1,23 +0,0 @@ -Mattermost -========== - -http:/mattermost.org - -Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search. - -Installing Mattermost -===================== - -To run an instance of the latest version of mattermost on your local machine you can run: - -`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform` - -To update this image to the latest version you can run: - -`docker pull mattermost/platform` - -To run an instance of the latest code from the master branch on GitHub you can run: - -`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev` - -Any questions, please visit http://forum.mattermost.org diff --git a/docker/1.4/config_docker.json b/docker/1.4/config_docker.json deleted file mode 100644 index 1aa2ee843..000000000 --- a/docker/1.4/config_docker.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "ServiceSettings": { - "ListenAddress": ":80", - "MaximumLoginAttempts": 10, - "SegmentDeveloperKey": "", - "GoogleDeveloperKey": "", - "EnableOAuthServiceProvider": false, - "EnableIncomingWebhooks": false, - "EnableOutgoingWebhooks": false, - "EnablePostUsernameOverride": false, - "EnablePostIconOverride": false, - "EnableTesting": false, - "EnableDeveloper": false, - "EnableSecurityFixAlert": true, - "SessionLengthWebInDays" : 30, - "SessionLengthMobileInDays" : 30, - "SessionLengthSSOInDays" : 30, - "SessionCacheInMinutes" : 10 - }, - "TeamSettings": { - "SiteName": "Mattermost", - "MaxUsersPerTeam": 50, - "EnableTeamCreation": true, - "EnableUserCreation": true, - "RestrictCreationToDomains": "", - "RestrictTeamNames": true, - "EnableTeamListing": false - }, - "SqlSettings": { - "DriverName": "mysql", - "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8", - "DataSourceReplicas": [], - "MaxIdleConns": 10, - "MaxOpenConns": 10, - "Trace": false, - "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg" - }, - "LogSettings": { - "EnableConsole": false, - "ConsoleLevel": "INFO", - "EnableFile": true, - "FileLevel": "INFO", - "FileFormat": "", - "FileLocation": "" - }, - "FileSettings": { - "DriverName": "local", - "Directory": "/mattermost/data/", - "EnablePublicLink": true, - "PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip", - "ThumbnailWidth": 120, - "ThumbnailHeight": 100, - "PreviewWidth": 1024, - "PreviewHeight": 0, - "ProfileWidth": 128, - "ProfileHeight": 128, - "InitialFont": "luximbi.ttf", - "AmazonS3AccessKeyId": "", - "AmazonS3SecretAccessKey": "", - "AmazonS3Bucket": "", - "AmazonS3Region": "", - "AmazonS3Endpoint": "", - "AmazonS3BucketEndpoint": "", - "AmazonS3LocationConstraint": false, - "AmazonS3LowercaseBucket": false - }, - "EmailSettings": { - "EnableSignUpWithEmail": true, - "SendEmailNotifications": false, - "RequireEmailVerification": false, - "FeedbackName": "", - "FeedbackEmail": "", - "SMTPUsername": "", - "SMTPPassword": "", - "SMTPServer": "", - "SMTPPort": "", - "ConnectionSecurity": "", - "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS", - "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL", - "SendPushNotifications": false, - "PushNotificationServer": "" - }, - "RateLimitSettings": { - "EnableRateLimiter": true, - "PerSec": 10, - "MemoryStoreSize": 10000, - "VaryByRemoteAddr": true, - "VaryByHeader": "" - }, - "PrivacySettings": { - "ShowEmailAddress": true, - "ShowFullName": true - }, - "SupportSettings": { - "TermsOfServiceLink": "/static/help/terms.html", - "PrivacyPolicyLink": "/static/help/privacy.html", - "AboutLink": "/static/help/about.html", - "HelpLink": "/static/help/help.html", - "ReportAProblemLink": "/static/help/report_problem.html", - "SupportEmail": "feedback@mattermost.com" - }, - "GitLabSettings": { - "Enable": false, - "Secret": "", - "Id": "", - "Scope": "", - "AuthEndpoint": "", - "TokenEndpoint": "", - "UserApiEndpoint": "" - } -} diff --git a/docker/1.4/docker-entry.sh b/docker/1.4/docker-entry.sh deleted file mode 100755 index 6bd2a1263..000000000 --- a/docker/1.4/docker-entry.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash -# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -# See License.txt for license information. - -mkdir -p web/static/js - -echo "127.0.0.1 dockerhost" >> /etc/hosts -/etc/init.d/networking restart - -echo configuring mysql - -# SQL!!! -set -e - -get_option () { - local section=$1 - local option=$2 - local default=$3 - ret=$(my_print_defaults $section | grep '^--'${option}'=' | cut -d= -f2-) - [ -z $ret ] && ret=$default - echo $ret -} - - -# Get config -DATADIR="$("mysqld" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')" -SOCKET=$(get_option mysqld socket "$DATADIR/mysql.sock") -PIDFILE=$(get_option mysqld pid-file "/var/run/mysqld/mysqld.pid") - -if [ ! -d "$DATADIR/mysql" ]; then - if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then - echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set' - echo >&2 ' Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?' - exit 1 - fi - - mkdir -p "$DATADIR" - chown -R mysql:mysql "$DATADIR" - - echo 'Running mysql_install_db' - mysql_install_db --user=mysql --datadir="$DATADIR" --rpm --keep-my-cnf - echo 'Finished mysql_install_db' - - mysqld --user=mysql --datadir="$DATADIR" --skip-networking & - for i in $(seq 30 -1 0); do - [ -S "$SOCKET" ] && break - echo 'MySQL init process in progress...' - sleep 1 - done - if [ $i = 0 ]; then - echo >&2 'MySQL init process failed.' - exit 1 - fi - - # These statements _must_ be on individual lines, and _must_ end with - # semicolons (no line breaks or comments are permitted). - # TODO proper SQL escaping on ALL the things D: - - tempSqlFile=$(mktemp /tmp/mysql-first-time.XXXXXX.sql) - cat > "$tempSqlFile" <<-EOSQL - -- What's done in this file shouldn't be replicated - -- or products like mysql-fabric won't work - SET @@SESSION.SQL_LOG_BIN=0; - - DELETE FROM mysql.user ; - CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; - GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ; - DROP DATABASE IF EXISTS test ; - EOSQL - - if [ "$MYSQL_DATABASE" ]; then - echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile" - fi - - if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then - echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" >> "$tempSqlFile" - - if [ "$MYSQL_DATABASE" ]; then - echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" >> "$tempSqlFile" - fi - fi - - echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile" - - mysql -uroot < "$tempSqlFile" - - rm -f "$tempSqlFile" - kill $(cat $PIDFILE) - for i in $(seq 30 -1 0); do - [ -f "$PIDFILE" ] || break - echo 'MySQL init process in progress...' - sleep 1 - done - if [ $i = 0 ]; then - echo >&2 'MySQL hangs during init process.' - exit 1 - fi - echo 'MySQL init process done. Ready for start up.' -fi - -chown -R mysql:mysql "$DATADIR" - -mysqld & - -sleep 5 - -# ------------------------ - -echo starting platform -cd /mattermost/bin -./platform -config=/config_docker.json diff --git a/docker/2.1/Dockerfile b/docker/2.1/Dockerfile new file mode 100644 index 000000000..71dcd602a --- /dev/null +++ b/docker/2.1/Dockerfile @@ -0,0 +1,49 @@ +# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +# See License.txt for license information. +FROM ubuntu:14.04 + +# +# Install SQL +# + +ENV MYSQL_ROOT_PASSWORD=mostest +ENV MYSQL_USER=mmuser +ENV MYSQL_PASSWORD=mostest +ENV MYSQL_DATABASE=mattermost_test + +RUN groupadd -r mysql && useradd -r -g mysql mysql + +RUN apt-key adv --keyserver pool.sks-keyservers.net --recv-keys A4A9406876FCBD3C456770C88C718D3B5072E1F5 + +ENV MYSQL_MAJOR 5.6 +ENV MYSQL_VERSION 5.6.25 + +RUN echo "deb http://repo.mysql.com/apt/debian/ wheezy mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list + +RUN apt-get update \ + && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install perl wget mysql-server \ + && rm -rf /var/lib/apt/lists/* \ + && rm -rf /var/lib/mysql && mkdir -p /var/lib/mysql + +RUN sed -Ei 's/^(bind-address|log)/#&/' /etc/mysql/my.cnf + +VOLUME /var/lib/mysql +# --------------------------------------------------------------------------------------------------------------------- + +WORKDIR /mattermost + +# Copy over files +ADD https://releases.mattermost.com/mattermost-team-2.1.0-rc1-linux-amd64.tar.gz +RUN tar -zxvf /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz --strip-components=1 && rm /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz +ADD config_docker.json / +ADD docker-entry.sh / + +RUN chmod +x /docker-entry.sh +ENTRYPOINT /docker-entry.sh + +# Create default storage directory +RUN mkdir /mattermost-data/ + +# Ports +EXPOSE 80 diff --git a/docker/2.1/Dockerrun.aws.zip b/docker/2.1/Dockerrun.aws.zip new file mode 100644 index 000000000..19bbc7403 Binary files /dev/null and b/docker/2.1/Dockerrun.aws.zip differ diff --git a/docker/2.1/Dockerrun.aws/.ebextensions/01_files.config b/docker/2.1/Dockerrun.aws/.ebextensions/01_files.config new file mode 100644 index 000000000..7f40a8b34 --- /dev/null +++ b/docker/2.1/Dockerrun.aws/.ebextensions/01_files.config @@ -0,0 +1,14 @@ +files: + "/etc/nginx/conf.d/proxy.conf": + mode: "000755" + owner: root + group: root + content: | + client_max_body_size 50M; + "/opt/elasticbeanstalk/hooks/appdeploy/post/init.sh": + mode: "000755" + owner: root + group: root + content: | + #!/usr/bin/env bash + gpasswd -a ec2-user docker diff --git a/docker/2.1/Dockerrun.aws/Dockerrun.aws.json b/docker/2.1/Dockerrun.aws/Dockerrun.aws.json new file mode 100755 index 000000000..a52309aad --- /dev/null +++ b/docker/2.1/Dockerrun.aws/Dockerrun.aws.json @@ -0,0 +1,13 @@ +{ + "AWSEBDockerrunVersion": "1", + "Image": { + "Name": "mattermost/platform:2.1", + "Update": "true" + }, + "Ports": [ + { + "ContainerPort": "80" + } + ], + "Logging": "/var/log/" +} diff --git a/docker/2.1/README.md b/docker/2.1/README.md new file mode 100644 index 000000000..f737a1554 --- /dev/null +++ b/docker/2.1/README.md @@ -0,0 +1,23 @@ +Mattermost +========== + +http:/mattermost.org + +Mattermost is an open-source team communication service. It brings team messaging and file sharing into one place, accessible across PCs and phones, with archiving and search. + +Installing Mattermost +===================== + +To run an instance of the latest version of mattermost on your local machine you can run: + +`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform` + +To update this image to the latest version you can run: + +`docker pull mattermost/platform` + +To run an instance of the latest code from the master branch on GitHub you can run: + +`docker run --name mattermost-dev -d --publish 8065:80 mattermost/platform:dev` + +Any questions, please visit http://forum.mattermost.org diff --git a/docker/2.1/config_docker.json b/docker/2.1/config_docker.json new file mode 100644 index 000000000..6a1290189 --- /dev/null +++ b/docker/2.1/config_docker.json @@ -0,0 +1,116 @@ +{ + "ServiceSettings": { + "ListenAddress": ":80", + "MaximumLoginAttempts": 10, + "SegmentDeveloperKey": "", + "GoogleDeveloperKey": "", + "EnableOAuthServiceProvider": false, + "EnableIncomingWebhooks": false, + "EnableOutgoingWebhooks": false, + "EnableCommands": false, + "EnableOnlyAdminIntegrations": true, + "EnablePostUsernameOverride": false, + "EnablePostIconOverride": false, + "EnableTesting": false, + "EnableDeveloper": false, + "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, + "SessionLengthWebInDays" : 30, + "SessionLengthMobileInDays" : 30, + "SessionLengthSSOInDays" : 30, + "SessionCacheInMinutes" : 10 + }, + "TeamSettings": { + "SiteName": "Mattermost", + "MaxUsersPerTeam": 50, + "EnableTeamCreation": true, + "EnableUserCreation": true, + "RestrictCreationToDomains": "", + "RestrictTeamNames": true, + "EnableTeamListing": false + }, + "SqlSettings": { + "DriverName": "mysql", + "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8", + "DataSourceReplicas": [], + "MaxIdleConns": 10, + "MaxOpenConns": 10, + "Trace": false, + "AtRestEncryptKey": "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg" + }, + "LogSettings": { + "EnableConsole": false, + "ConsoleLevel": "INFO", + "EnableFile": true, + "FileLevel": "INFO", + "FileFormat": "", + "FileLocation": "" + }, + "FileSettings": { + "DriverName": "local", + "Directory": "/mattermost/data/", + "EnablePublicLink": true, + "PublicLinkSalt": "A705AklYF8MFDOfcwh3I488G8vtLlVip", + "ThumbnailWidth": 120, + "ThumbnailHeight": 100, + "PreviewWidth": 1024, + "PreviewHeight": 0, + "ProfileWidth": 128, + "ProfileHeight": 128, + "InitialFont": "luximbi.ttf", + "AmazonS3AccessKeyId": "", + "AmazonS3SecretAccessKey": "", + "AmazonS3Bucket": "", + "AmazonS3Region": "", + "AmazonS3Endpoint": "", + "AmazonS3BucketEndpoint": "", + "AmazonS3LocationConstraint": false, + "AmazonS3LowercaseBucket": false + }, + "EmailSettings": { + "EnableSignUpWithEmail": true, + "EnableSignInWithEmail": true, + "EnableSignInWithUsername": false, + "SendEmailNotifications": false, + "RequireEmailVerification": false, + "FeedbackName": "", + "FeedbackEmail": "", + "SMTPUsername": "", + "SMTPPassword": "", + "SMTPServer": "", + "SMTPPort": "", + "ConnectionSecurity": "", + "InviteSalt": "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS", + "PasswordResetSalt": "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL", + "SendPushNotifications": false, + "PushNotificationServer": "" + }, + "RateLimitSettings": { + "EnableRateLimiter": true, + "PerSec": 10, + "MemoryStoreSize": 10000, + "VaryByRemoteAddr": true, + "VaryByHeader": "" + }, + "PrivacySettings": { + "ShowEmailAddress": true, + "ShowFullName": true + }, + "SupportSettings": { + "TermsOfServiceLink": "/static/help/terms.html", + "PrivacyPolicyLink": "/static/help/privacy.html", + "AboutLink": "/static/help/about.html", + "HelpLink": "/static/help/help.html", + "ReportAProblemLink": "/static/help/report_problem.html", + "SupportEmail": "feedback@mattermost.com" + }, + "GitLabSettings": { + "Enable": false, + "Secret": "", + "Id": "", + "Scope": "", + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserApiEndpoint": "" + } +} diff --git a/docker/2.1/docker-entry.sh b/docker/2.1/docker-entry.sh new file mode 100755 index 000000000..6bd2a1263 --- /dev/null +++ b/docker/2.1/docker-entry.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +# See License.txt for license information. + +mkdir -p web/static/js + +echo "127.0.0.1 dockerhost" >> /etc/hosts +/etc/init.d/networking restart + +echo configuring mysql + +# SQL!!! +set -e + +get_option () { + local section=$1 + local option=$2 + local default=$3 + ret=$(my_print_defaults $section | grep '^--'${option}'=' | cut -d= -f2-) + [ -z $ret ] && ret=$default + echo $ret +} + + +# Get config +DATADIR="$("mysqld" --verbose --help 2>/dev/null | awk '$1 == "datadir" { print $2; exit }')" +SOCKET=$(get_option mysqld socket "$DATADIR/mysql.sock") +PIDFILE=$(get_option mysqld pid-file "/var/run/mysqld/mysqld.pid") + +if [ ! -d "$DATADIR/mysql" ]; then + if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" ]; then + echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set' + echo >&2 ' Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?' + exit 1 + fi + + mkdir -p "$DATADIR" + chown -R mysql:mysql "$DATADIR" + + echo 'Running mysql_install_db' + mysql_install_db --user=mysql --datadir="$DATADIR" --rpm --keep-my-cnf + echo 'Finished mysql_install_db' + + mysqld --user=mysql --datadir="$DATADIR" --skip-networking & + for i in $(seq 30 -1 0); do + [ -S "$SOCKET" ] && break + echo 'MySQL init process in progress...' + sleep 1 + done + if [ $i = 0 ]; then + echo >&2 'MySQL init process failed.' + exit 1 + fi + + # These statements _must_ be on individual lines, and _must_ end with + # semicolons (no line breaks or comments are permitted). + # TODO proper SQL escaping on ALL the things D: + + tempSqlFile=$(mktemp /tmp/mysql-first-time.XXXXXX.sql) + cat > "$tempSqlFile" <<-EOSQL + -- What's done in this file shouldn't be replicated + -- or products like mysql-fabric won't work + SET @@SESSION.SQL_LOG_BIN=0; + + DELETE FROM mysql.user ; + CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ; + GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ; + DROP DATABASE IF EXISTS test ; + EOSQL + + if [ "$MYSQL_DATABASE" ]; then + echo "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` ;" >> "$tempSqlFile" + fi + + if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then + echo "CREATE USER '"$MYSQL_USER"'@'%' IDENTIFIED BY '"$MYSQL_PASSWORD"' ;" >> "$tempSqlFile" + + if [ "$MYSQL_DATABASE" ]; then + echo "GRANT ALL ON \`"$MYSQL_DATABASE"\`.* TO '"$MYSQL_USER"'@'%' ;" >> "$tempSqlFile" + fi + fi + + echo 'FLUSH PRIVILEGES ;' >> "$tempSqlFile" + + mysql -uroot < "$tempSqlFile" + + rm -f "$tempSqlFile" + kill $(cat $PIDFILE) + for i in $(seq 30 -1 0); do + [ -f "$PIDFILE" ] || break + echo 'MySQL init process in progress...' + sleep 1 + done + if [ $i = 0 ]; then + echo >&2 'MySQL hangs during init process.' + exit 1 + fi + echo 'MySQL init process done. Ready for start up.' +fi + +chown -R mysql:mysql "$DATADIR" + +mysqld & + +sleep 5 + +# ------------------------ + +echo starting platform +cd /mattermost/bin +./platform -config=/config_docker.json -- cgit v1.2.3-1-g7c22 From b305e03ab5428103b4990bcd723f54c4c779e7c2 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Wed, 9 Mar 2016 20:31:44 +0000 Subject: Bumping version to: 2.1.0-rc1 --- model/version.go | 1 + 1 file changed, 1 insertion(+) diff --git a/model/version.go b/model/version.go index 8fbd65d03..6011a9245 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maitained in chronological order with most current // release at the front of the list. var versions = []string{ + "2.1.0", "2.0.0", "1.4.0", "1.3.0", -- cgit v1.2.3-1-g7c22 From 528180ce28846bab2fc39346eb1560e98e665f49 Mon Sep 17 00:00:00 2001 From: Rodrigo Corsi Date: Thu, 10 Mar 2016 08:23:29 -0300 Subject: added missing translations --- i18n/pt.json | 20 ++++++++++++++++++-- web/static/i18n/pt.json | 4 +++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/i18n/pt.json b/i18n/pt.json index 8e45d5eaf..81e0cdaff 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -456,8 +456,8 @@ "translation": "S3 não é suportado para o armazenamento local de exportação." }, { - "id": "api.export.write_file.app_error", - "translation": "Não é possível gravar para exportar arquivo" + "id": "api.file.file_upload.exceeds", + "translation": "Arquivo excede o máximo de tamanho de imagem." }, { "id": "api.file.file_upload.exceeds", @@ -507,6 +507,22 @@ "id": "api.file.init.debug", "translation": "Inicializando file api routes" }, + { + "id": "api.file.move_file.configured.app_error", + "translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local." + }, + { + "id": "api.file.move_file.delete_from_s3.app_error", + "translation": "Não é possível deletar o arquivo a partir do S3." + }, + { + "id": "api.file.move_file.get_from_s3.app_error", + "translation": "Não é possível obter o arquivo a partir do S3." + }, + { + "id": "api.file.move_file.rename.app_error", + "translation": "Não foi possível mover o arquivo localmente." + }, { "id": "api.file.open_file_write_stream.configured.app_error", "translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local." diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index b9b8f4c07..463c0af37 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -192,11 +192,13 @@ "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Atributo do Usuário:", "admin.licence.keyMigration": "Se você estiver migrando seu servidor você pode precisar remover sua chave da licença deste servidor a pedido para instala-la em um novo servidor. Para iniciar, desativar todos os recursos Enterprise Edition deste servidor. Isto irá habilitar para remover a chave da licença e fazer downgrade deste servidor Enterprise Edition para Team Edition.", + "admin.license.chooseFile": "Escolha um Arquivo", "admin.license.edition": "Edição: ", "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Desenvolvido para escala empresarial de comunicação.", "admin.license.enterpriseType": "

Esta versão compilada da plataforma Mattermost é fornecida sob a licença comercial para Mattermost, Inc. com base em seu nível de subscrição e está sujeito a Termos de Serviço.

Os detalhes de sua assinatura, são como segue:

Nome: {name}
Nome da Empresa ou organização: {company}
Número de usuários: {users}
Licença emitida: {issued}
Data de Início da licença: {start}
Data de expiração da licença: {expires}
LDAP: {ldap}
", "admin.license.key": "Chave da Licença: ", "admin.license.keyRemove": "Remover a Licença Enterprise e fazer Downgrade do Servidor", + "admin.license.noFile": "Nenhum arquivo enviado", "admin.license.removing": "Removendo a Licença...", "admin.license.teamEdition": "Mattermost Team Edition. Desenvolvido para equipes de 5 a 50 usuários.", "admin.license.teamType": "

Esta versão compilada da plataforma Mattermost é oferecido sob uma licença MIT.

Ver MIT-COMPILED-LICENSE.txt no raiz do diretório de instalação para obter detalhes. Ver NOTICES.txt para obter informações sobre o software open source usados neste sistema.

", @@ -1302,4 +1304,4 @@ "view_image_popover.download": "Download", "view_image_popover.file": "Arquivo {count} de {total}", "view_image_popover.publicLink": "Obter O Link Público" -} +} \ No newline at end of file -- cgit v1.2.3-1-g7c22 From 26ad6d2c769615cd60b03140b17c19e5e19705a3 Mon Sep 17 00:00:00 2001 From: peasead Date: Thu, 10 Mar 2016 10:09:16 -0600 Subject: updated the 2.0 Dockerfile to include the proper location of mm gzip --- docker/2.0/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/2.0/Dockerfile b/docker/2.0/Dockerfile index 0f7a13e45..38cb1e197 100644 --- a/docker/2.0/Dockerfile +++ b/docker/2.0/Dockerfile @@ -34,7 +34,7 @@ VOLUME /var/lib/mysql WORKDIR /mattermost # Copy over files -ADD https://github.com/mattermost/platform/releases/download/v2.0.0-rc2/mattermost.tar.gz / +ADD https://github.com/mattermost/platform/releases/download/v2.0.0/mattermost.tar.gz / RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz ADD config_docker.json / ADD docker-entry.sh / -- cgit v1.2.3-1-g7c22 From 12434f8963b7e93836e2fa44ae1ce8806cea63cb Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Thu, 10 Mar 2016 11:14:40 -0500 Subject: Fixing docker automation issue --- docker/2.1/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/2.1/Dockerfile b/docker/2.1/Dockerfile index 71dcd602a..7ecf64116 100644 --- a/docker/2.1/Dockerfile +++ b/docker/2.1/Dockerfile @@ -34,7 +34,7 @@ VOLUME /var/lib/mysql WORKDIR /mattermost # Copy over files -ADD https://releases.mattermost.com/mattermost-team-2.1.0-rc1-linux-amd64.tar.gz +ADD https://releases.mattermost.com/2.1.0-rc1/mattermost-team-2.1.0-rc1-linux-amd64.tar.gz / RUN tar -zxvf /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz --strip-components=1 && rm /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz ADD config_docker.json / ADD docker-entry.sh / -- cgit v1.2.3-1-g7c22 From 461e8532a7b82b31964c47b2e69e8350b9c77474 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Thu, 10 Mar 2016 22:55:16 +0500 Subject: PLT-2138 - Fixing theme sharing box --- .../user_settings/custom_theme_chooser.jsx | 4 +++ web/react/utils/constants.jsx | 30 +++++++++++----------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 1e724bb6e..143eadccb 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -253,6 +253,9 @@ class CustomThemeChooser extends React.Component {
); + + colors += theme[element.id] + ','; + } else if (element.group === 'sidebarElements') { sidebarElements.push(
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index daea9f43e..4b1f73b79 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -370,21 +370,6 @@ export default { id: 'newMessageSeparator', uiName: 'New Message Separator' }, - { - group: 'linkAndButtonElements', - id: 'linkColor', - uiName: 'Link Color' - }, - { - group: 'linkAndButtonElements', - id: 'buttonBg', - uiName: 'Button BG' - }, - { - group: 'linkAndButtonElements', - id: 'buttonColor', - uiName: 'Button Text' - }, { group: 'centerChannelElements', id: 'mentionHighlightBg', @@ -417,6 +402,21 @@ export default { uiName: 'Monokai' } ] + }, + { + group: 'linkAndButtonElements', + id: 'linkColor', + uiName: 'Link Color' + }, + { + group: 'linkAndButtonElements', + id: 'buttonBg', + uiName: 'Button BG' + }, + { + group: 'linkAndButtonElements', + id: 'buttonColor', + uiName: 'Button Text' } ], DEFAULT_CODE_THEME: 'github', -- cgit v1.2.3-1-g7c22 From 3ee2d218130e01d8df729495e0d121615271e98b Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Thu, 10 Mar 2016 09:56:43 -0800 Subject: Fixing blue bar on iOS --- web/react/components/textbox.jsx | 2 +- web/react/stores/socket_store.jsx | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index c8c0c071e..d4eb60676 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -61,7 +61,7 @@ export default class Textbox extends React.Component { onRecievedError() { const errorCount = ErrorStore.getConnectionErrorCount(); - if (errorCount > 0) { + if (errorCount > 1) { this.setState({connection: 'bad-connection'}); } else { this.setState({connection: ''}); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 9b2b049b7..ec25255d9 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -31,6 +31,7 @@ class SocketStoreClass extends EventEmitter { this.close = this.close.bind(this); this.failCount = 0; + this.isInitialize = false; this.translations = this.getDefaultTranslations(); @@ -58,24 +59,23 @@ class SocketStoreClass extends EventEmitter { if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console - if (ErrorStore.getConnectionErrorCount() > 0) { - ErrorStore.setConnectionErrorCount(0); - ErrorStore.emitChange(); - } } + conn = new WebSocket(connUrl); conn.onopen = () => { if (this.failCount > 0) { console.log('websocket re-established connection'); //eslint-disable-line no-console + AsyncClient.getChannels(); + AsyncClient.getPosts(ChannelStore.getCurrentId()); + } + if (this.isInitialize) { ErrorStore.clearLastError(); ErrorStore.emitChange(); - - AsyncClient.getChannels(); - AsyncClient.getPosts(ChannelStore.getCurrentId()); } + this.isInitialize = true; this.failCount = 0; }; -- cgit v1.2.3-1-g7c22 From b9f468623cac8e7afd5552da100f39c22c44ec25 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Thu, 10 Mar 2016 23:03:06 +0500 Subject: Removing console --- web/react/components/user_settings/custom_theme_chooser.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 143eadccb..33191a9f0 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -306,7 +306,6 @@ class CustomThemeChooser extends React.Component { }); colors += theme.codeTheme; - console.log(colors); const pasteBox = (
-- cgit v1.2.3-1-g7c22 From 060cc2c825cd951c37a715af79b982f8aa99ebc4 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Thu, 10 Mar 2016 23:19:36 +0500 Subject: Fixing padding issue --- web/react/components/user_settings/custom_theme_chooser.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 33191a9f0..4ee9fd0e2 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -255,7 +255,6 @@ class CustomThemeChooser extends React.Component { ); colors += theme[element.id] + ','; - } else if (element.group === 'sidebarElements') { sidebarElements.push(
Date: Wed, 9 Mar 2016 10:00:16 -0500 Subject: Disabled embed toggle when it's disabled in user settings --- .../components/post_body_additional_content.jsx | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/web/react/components/post_body_additional_content.jsx b/web/react/components/post_body_additional_content.jsx index c2a928f3b..70b3c8dbf 100644 --- a/web/react/components/post_body_additional_content.jsx +++ b/web/react/components/post_body_additional_content.jsx @@ -112,24 +112,32 @@ export default class PostBodyAdditionalContent extends React.Component { } render() { - var generateEmbed = this.generateEmbed(); + const generateEmbed = this.generateEmbed(); + if (generateEmbed) { - return ( -
+ let toggle; + if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) { + toggle = ( - + /> + ); + } + + return ( +
+ {toggle}
- ); + ); } + return null; } } -- cgit v1.2.3-1-g7c22 From 5e9f707cb3368f6c91326007c52b8d8530739c2c Mon Sep 17 00:00:00 2001 From: Rodrigo Corsi Date: Thu, 10 Mar 2016 17:01:41 -0300 Subject: standardized terminology for sign in and sign up --- i18n/pt.json | 14 +++++++------- web/static/i18n/pt.json | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/i18n/pt.json b/i18n/pt.json index 81e0cdaff..29c8c58c7 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -1221,7 +1221,7 @@ }, { "id": "api.templates.signin_change_email.body.title", - "translation": "Você atualizou seu método de acesso" + "translation": "Você atualizou seu método de login" }, { "id": "api.templates.signup_team_body.button", @@ -1241,7 +1241,7 @@ }, { "id": "api.templates.singin_change_email.body.info", - "translation": "Você atualizou seu método de inscrição para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.
Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema." + "translation": "Você atualizou seu método de login para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.
Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema." }, { "id": "api.templates.singin_change_email.subject", @@ -1273,7 +1273,7 @@ }, { "id": "api.templates.welcome_body.info2", - "translation": "Você pode acessar sua nova equipe pelo endereço web:" + "translation": "Você pode fazer login sua nova equipe pelo endereço web:" }, { "id": "api.templates.welcome_body.info3", @@ -1349,7 +1349,7 @@ }, { "id": "api.user.create_oauth_user.already_used.app_error", - "translation": "Está conta {{.Service}} já foi utilizada para logar na equipe {{.DisplayName}}" + "translation": "Está conta {{.Service}} já foi utilizada para se inscrever na equipe {{.DisplayName}}" }, { "id": "api.user.create_oauth_user.create.app_error", @@ -1373,7 +1373,7 @@ }, { "id": "api.user.create_user.accepted_domain.app_error", - "translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou entre com um email diferente." + "translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou se inscreve com um email diferente." }, { "id": "api.user.create_user.joining.error", @@ -1389,11 +1389,11 @@ }, { "id": "api.user.create_user.signup_link_expired.app_error", - "translation": "O link de acesso expirou" + "translation": "O link de inscrição expirou" }, { "id": "api.user.create_user.signup_link_invalid.app_error", - "translation": "O link de acesso não parece ser válido" + "translation": "O link de inscrição não parece ser válido" }, { "id": "api.user.create_user.team_name.app_error", diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index 463c0af37..8f7908031 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -27,7 +27,7 @@ "admin.email.allowEmailSignInDescription": "Quando verdadeiro, Mattermost permite aos usuários fazer login usando o e-mail e senha.", "admin.email.allowEmailSignInTitle": "Permitir Login Com E-mail: ", "admin.email.allowSignupDescription": "Quando verdadeiro, Mattermost permite a criação de equipe e conta de inscrição através de e-mail e senha. Este valor deve ser falso somente quando você deseja limitar a entrada para o single-sign-on service como OAuth ou LDAP.", - "admin.email.allowSignupTitle": "Permitir Login com E-Mail: ", + "admin.email.allowSignupTitle": "Permitir Inscrição com E-Mail: ", "admin.email.allowUsernameSignInDescription": "Quando verdadeiro, Mattermost permite os usuários fazer login usando seu nome de usuário e senha. Esta configuração é normalmente utilizado apenas quando a verificação de e-mail está desativada.", "admin.email.allowUsernameSignInTitle": "Permitir Login Com Usuário: ", "admin.email.connectionSecurityNone": "Nenhum", @@ -44,7 +44,7 @@ "admin.email.false": "falso", "admin.email.inviteSaltDescription": "32-caracteres salt adicionados a assinatura de convites por e-mail. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.", "admin.email.inviteSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"", - "admin.email.inviteSaltTitle": "Convidar Salt:", + "admin.email.inviteSaltTitle": "Salt Convite:", "admin.email.notificationDisplayDescription": "Mostra o nome da conta de e-mail usada quando a notificação de e-mail é enviado do Mattermost.", "admin.email.notificationDisplayExample": "Ex: \"Mattermost Notificação\", \"Sistema\", \"Não-Responda\"", "admin.email.notificationDisplayTitle": "Notificação Nome de Exibição:", @@ -208,7 +208,7 @@ "admin.license.uploadDesc": "Enviar uma chave da licença para Mattermost Enterprise Edition para fazer upgrade deste servidor. Visite-nos online para saber mais sobre os beneficios da Enterprise Edition ou para comprar uma chave.", "admin.license.uploading": "Enviando Licença...", "admin.log.consoleDescription": "Normalmente definido como falso em produção. Os desenvolvedores podem definir este campo como verdadeiro para mensagens de log de saída no console baseado na opção de nível de console. Se verdadeiro, o servidor escreve mensagens para o fluxo de saída padrão (stdout).", - "admin.log.consoleTitle": "Log Do Console: ", + "admin.log.consoleTitle": "Log Para o Console: ", "admin.log.false": "falso", "admin.log.fileDescription": "Normalmente definido como verdadeiro em produção. Quando verdadeiro, arquivos de log são gravados no arquivo de log especificado no campo de localização abaixo.", "admin.log.fileLevelDescription": "Esta configuração determina o nível de detalhe que são gravados no log de eventos no console. ERROR: Saídas somente mensagens de erro. INFO: Saídas de mensagens de erro e informações em torno de inicialização. DEBUG: Impressões de alto detalhe para desenvolvedores que trabalham na depuração de problemas.", @@ -698,7 +698,7 @@ "general_tab.regenerate": "Re-Gerar", "general_tab.required": "Este campo é obrigatório", "general_tab.teamName": "Nome da Equipe", - "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela inicial e no topo na lateral esquerda.", + "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela de login e no topo na lateral esquerda.", "general_tab.title": "Definições Gerais", "general_tab.yes": "Sim", "get_link.clipboard": " Link copiado para a área de transferência.", @@ -756,7 +756,7 @@ "login.noAccount": "Não tem uma conta? ", "login.on": "no {siteName}", "login.or": "ou", - "login.signTo": "Entrar em:", + "login.signTo": "Login em:", "login.verified": " Email Verificado", "login_email.badTeam": "Nome ruim de equipe", "login_email.email": "E-mail", @@ -951,7 +951,7 @@ "signup_user_completed.chooseUser": "Escolha o seu nome de usuário", "signup_user_completed.create": "Criar Conta", "signup_user_completed.emailHelp": "Email valido necessário para inscrição", - "signup_user_completed.emailIs": "Seu endereço de e-mail é {email}. Você irá usar esse endereço para entrar em {siteName}.", + "signup_user_completed.emailIs": "Seu endereço de e-mail é {email}. Você irá usar esse endereço para logar no {siteName}.", "signup_user_completed.expired": "Você já concluiu o processo de inscrição para este convite ou este convite expirou.", "signup_user_completed.gitlab": "com GitLab", "signup_user_completed.google": "com Google", @@ -1015,7 +1015,7 @@ "team_signup_display_name.required": "Este campo é obrigatório", "team_signup_display_name.teamName": "Nome Da Equipe", "team_signup_email.address": "Endereço de E-mail", - "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado no login", + "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado na inscrição", "team_signup_email.validEmail": "Por favor entre um endereço de e-mail válido", "team_signup_password.agreement": "Ao prosseguir para criar sua conta e usar {siteName}, você concorda com nosso Termo de Serviço e Politica de Privacidade. Se você não concorda, você não pode usar {siteName}.", "team_signup_password.back": "Voltar para o passo anterior", @@ -1288,7 +1288,7 @@ "user.settings.security.logoutActiveSessions": "Ver e fazer Logout das Sessões Ativas", "user.settings.security.method": "Método de Login", "user.settings.security.newPassword": "Nova Senha", - "user.settings.security.oneSignin": "Você pode ter somente um método de inscrição por vez. Trocando o método de inscrição será enviado um email de notificação se você alterar com sucesso.", + "user.settings.security.oneSignin": "Você pode ter somente um método de login por vez. Trocando o método de login será enviado um email de notificação se você alterar com sucesso.", "user.settings.security.password": "Senha", "user.settings.security.passwordLengthError": "Novas senhas precisam ter pelo menos {chars} characters", "user.settings.security.passwordMatchError": "As novas senhas que você inseriu não correspondem", -- cgit v1.2.3-1-g7c22 From 601c4cd72db876a05d2352d8034c3481a4363ff2 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Fri, 11 Mar 2016 18:04:07 +0500 Subject: Small fix for search loading icon on mobile --- web/sass-files/sass/partials/_responsive.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index a9a572768..06ce17041 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -573,8 +573,7 @@ .glyphicon-refresh-animate { right: 33px; top: 15px; - color: #fff; - color: rgba(255,255,255,0.5); + color: #aaa; } .form-control { border: none; -- cgit v1.2.3-1-g7c22 From e4fed928d9365ba3e096a02e590cd741de4d2cb2 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Thu, 10 Mar 2016 13:27:34 +0500 Subject: Adding or separator between ldap, username, email, github Updating code theme name Updating conditions for login page updated the 2.0 Dockerfile to include the proper location of mm gzip Fixing search loading icon on mobile Adding or separator between ldap, username, email and github --- docker/2.0/Dockerfile | 2 +- web/react/components/login.jsx | 34 +++++++++++++++++++++ web/react/utils/constants.jsx | 6 ++-- .../images/themes/code_themes/solarized-dark.png | Bin 0 -> 81942 bytes .../images/themes/code_themes/solarized-light.png | Bin 0 -> 82868 bytes .../images/themes/code_themes/solarized_dark.png | Bin 81942 -> 0 bytes .../images/themes/code_themes/solarized_light.png | Bin 82868 -> 0 bytes 7 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 web/static/images/themes/code_themes/solarized-dark.png create mode 100644 web/static/images/themes/code_themes/solarized-light.png delete mode 100644 web/static/images/themes/code_themes/solarized_dark.png delete mode 100644 web/static/images/themes/code_themes/solarized_light.png diff --git a/docker/2.0/Dockerfile b/docker/2.0/Dockerfile index 0f7a13e45..38cb1e197 100644 --- a/docker/2.0/Dockerfile +++ b/docker/2.0/Dockerfile @@ -34,7 +34,7 @@ VOLUME /var/lib/mysql WORKDIR /mattermost # Copy over files -ADD https://github.com/mattermost/platform/releases/download/v2.0.0-rc2/mattermost.tar.gz / +ADD https://github.com/mattermost/platform/releases/download/v2.0.0/mattermost.tar.gz / RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz ADD config_docker.json / ADD docker-entry.sh / diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 581b8e0b5..1c393c679 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -19,6 +19,8 @@ export default class Login extends React.Component { render() { const teamDisplayName = this.props.teamDisplayName; const teamName = this.props.teamName; + const ldapEnabled = global.window.mm_config.EnableLdap === 'true'; + const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true'; let loginMessage = []; if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { @@ -173,6 +175,22 @@ export default class Login extends React.Component { ); } + if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) { + ldapLogin = ( +
+
+ +
+ +
+ ); + } + let findTeams = null; if (!Utils.isMobileApp()) { findTeams = ( @@ -197,6 +215,22 @@ export default class Login extends React.Component { ); } + if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) { + usernameLogin = ( +
+
+ +
+ +
+ ); + } + return (
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 4b1f73b79..3147bbd6a 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -267,7 +267,7 @@ export default { buttonColor: '#FFFFFF', mentionHighlightBg: '#984063', mentionHighlightLink: '#A4FFEB', - codeTheme: 'solarized_dark' + codeTheme: 'solarized-dark' }, windows10: { type: 'Windows Dark', @@ -386,11 +386,11 @@ export default { uiName: 'Code Theme', themes: [ { - id: 'solarized_dark', + id: 'solarized-dark', uiName: 'Solarized Dark' }, { - id: 'solarized_light', + id: 'solarized-light', uiName: 'Solarized Light' }, { diff --git a/web/static/images/themes/code_themes/solarized-dark.png b/web/static/images/themes/code_themes/solarized-dark.png new file mode 100644 index 000000000..582df48f9 Binary files /dev/null and b/web/static/images/themes/code_themes/solarized-dark.png differ diff --git a/web/static/images/themes/code_themes/solarized-light.png b/web/static/images/themes/code_themes/solarized-light.png new file mode 100644 index 000000000..d2c2702fb Binary files /dev/null and b/web/static/images/themes/code_themes/solarized-light.png differ diff --git a/web/static/images/themes/code_themes/solarized_dark.png b/web/static/images/themes/code_themes/solarized_dark.png deleted file mode 100644 index 582df48f9..000000000 Binary files a/web/static/images/themes/code_themes/solarized_dark.png and /dev/null differ diff --git a/web/static/images/themes/code_themes/solarized_light.png b/web/static/images/themes/code_themes/solarized_light.png deleted file mode 100644 index d2c2702fb..000000000 Binary files a/web/static/images/themes/code_themes/solarized_light.png and /dev/null differ -- cgit v1.2.3-1-g7c22 From 160e1a8bea6d8280ef3b41a4a9f80674b75f3eb0 Mon Sep 17 00:00:00 2001 From: it33 Date: Fri, 11 Mar 2016 21:06:49 -0800 Subject: Removing items not in T0 --- CHANGELOG.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0052918f..08b5ddbf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,16 +20,10 @@ Desktop Application - New [Desktop Application](https://github.com/mattermost/desktop) for Windows, Mac, and Linux now available as a beta release. - Languages - Added Portuguese language translation (Beta) available from **Account Settings** > **Display**. -Active Directory/LDAP (Enterprise Edition) - -- Create new teams using Active Directory/LDAP. Team creation and Active Directory/LDAP must be enabled in the System Console from **Team Settings** > **Enable Team Creation** and **LDAP Settings** > **Enable LDAP**. -- Added Active Directory/LDAP synchronization to check for deactivated users on the LDAP server. - ### Improvements System Console @@ -64,11 +58,6 @@ User Interface - Reply icon now only appears on-hover for messages that don’t have replies. - Scroll bar now appears in the center channel. -Enterprise Edition - -- Access History now contains audits for adding and removing Enterprise licenses. - - #### Bug Fixes - System console user management tab now shows username and email on different lines. @@ -122,7 +111,6 @@ Mattermost Android Application is for use with Mattermost platform v2.1 and high - Search term highlighting doesn't update on IE11 when search terms change but return the same posts. - Team creation via SSO fails when email domain is restricted. - #### Contributors Many thanks to all our external contributors. In no particular order: @@ -137,8 +125,6 @@ Many thanks to all our external contributors. In no particular order: - [timlyo](https://github.com/timlyo) - [ttyniwa](https://github.com/ttyniwa) - - ## Release v2.0.0 Expected Release date: 2016-02-16 -- cgit v1.2.3-1-g7c22 From d7cdcf082fab6c0cb7c2fe4bed821bd1a8000e69 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 8 Feb 2016 07:26:10 -0500 Subject: Convering client to react-router. --- Makefile | 25 +- api/api.go | 43 +- api/context.go | 137 +-- api/license.go | 20 + api/license_test.go | 22 + api/oauth.go | 343 +++++- api/post.go | 7 +- api/team.go | 114 +- api/team_test.go | 111 +- api/templates/email_change_body.html | 41 - api/templates/email_change_subject.html | 1 - api/templates/email_change_verify_body.html | 44 - api/templates/email_change_verify_subject.html | 1 - api/templates/email_footer.html | 13 - api/templates/email_info.html | 7 - api/templates/error.html | 37 - api/templates/find_teams_body.html | 52 - api/templates/find_teams_subject.html | 1 - api/templates/invite_body.html | 46 - api/templates/invite_subject.html | 1 - api/templates/password_change_body.html | 43 - api/templates/password_change_subject.html | 1 - api/templates/post_body.html | 45 - api/templates/post_subject.html | 1 - api/templates/reset_body.html | 46 - api/templates/reset_subject.html | 1 - api/templates/signin_change_body.html | 43 - api/templates/signin_change_subject.html | 1 - api/templates/signup_team_body.html | 44 - api/templates/signup_team_subject.html | 1 - api/templates/verify_body.html | 44 - api/templates/verify_subject.html | 1 - api/templates/welcome_body.html | 51 - api/templates/welcome_subject.html | 1 - api/user.go | 146 ++- api/user_test.go | 35 + i18n/en.json | 28 +- i18n/es.json | 20 - model/client.go | 67 +- model/session.go | 2 +- model/team.go | 7 + templates/authorize.html | 12 + templates/email_change_body.html | 41 + templates/email_change_subject.html | 1 + templates/email_change_verify_body.html | 44 + templates/email_change_verify_subject.html | 1 + templates/email_footer.html | 13 + templates/email_info.html | 7 + templates/error.html | 24 + templates/head.html | 92 ++ templates/invite_body.html | 46 + templates/invite_subject.html | 1 + templates/password_change_body.html | 43 + templates/password_change_subject.html | 1 + templates/post_body.html | 45 + templates/post_subject.html | 1 + templates/reset_body.html | 46 + templates/reset_subject.html | 1 + templates/root.html | 12 + templates/signin_change_body.html | 43 + templates/signin_change_subject.html | 1 + templates/signup_team_body.html | 44 + templates/signup_team_subject.html | 1 + templates/verify_body.html | 44 + templates/verify_subject.html | 1 + templates/welcome_body.html | 51 + templates/welcome_subject.html | 1 + utils/html.go | 97 ++ utils/license.go | 2 +- web/react/action_creators/global_actions.jsx | 252 +++++ web/react/components/activity_log_modal.jsx | 36 +- .../components/admin_console/admin_controller.jsx | 11 +- .../admin_console/admin_navbar_dropdown.jsx | 19 +- .../components/admin_console/admin_sidebar.jsx | 4 - .../admin_console/admin_sidebar_header.jsx | 5 +- web/react/components/admin_console/user_item.jsx | 2 +- web/react/components/audit_table.jsx | 21 +- web/react/components/center_panel.jsx | 47 +- web/react/components/channel_header.jsx | 32 +- web/react/components/channel_invite_modal.jsx | 37 +- web/react/components/channel_loader.jsx | 204 ---- .../components/channel_notifications_modal.jsx | 107 +- web/react/components/channel_view.jsx | 26 +- web/react/components/claim/claim_account.jsx | 87 +- web/react/components/claim/sso_to_email.jsx | 2 +- web/react/components/create_post.jsx | 6 +- web/react/components/delete_channel_modal.jsx | 4 +- web/react/components/do_verify_email.jsx | 82 ++ web/react/components/docs.jsx | 41 - web/react/components/edit_post_modal.jsx | 4 +- web/react/components/email_verify.jsx | 108 -- web/react/components/file_attachment.jsx | 4 +- web/react/components/find_team.jsx | 135 --- web/react/components/invite_member_modal.jsx | 4 +- web/react/components/logged_in.jsx | 224 ++++ web/react/components/login.jsx | 222 ++-- web/react/components/login_email.jsx | 11 +- web/react/components/navbar.jsx | 20 +- web/react/components/navbar_dropdown.jsx | 88 +- web/react/components/needs_team.jsx | 20 + web/react/components/not_logged_in.jsx | 70 ++ web/react/components/password_reset.jsx | 47 - web/react/components/password_reset_form.jsx | 105 +- web/react/components/password_reset_send_link.jsx | 186 ++-- web/react/components/popover_list_members.jsx | 2 +- web/react/components/post.jsx | 19 +- web/react/components/post_body.jsx | 10 +- web/react/components/post_focus_view.jsx | 22 +- web/react/components/post_header.jsx | 17 +- web/react/components/post_info.jsx | 56 +- web/react/components/posts_view.jsx | 17 +- web/react/components/posts_view_container.jsx | 19 +- web/react/components/rhs_comment.jsx | 8 +- web/react/components/rhs_root_post.jsx | 8 +- web/react/components/root.jsx | 90 ++ web/react/components/search_results_item.jsx | 9 +- web/react/components/should_verify_email.jsx | 111 ++ web/react/components/sidebar.jsx | 30 +- web/react/components/sidebar_header.jsx | 14 +- web/react/components/sidebar_right.jsx | 9 +- web/react/components/sidebar_right_menu.jsx | 39 +- web/react/components/signup_team.jsx | 159 +-- web/react/components/signup_team_complete.jsx | 121 -- .../components/signup_team_complete.jsx | 79 ++ .../components/team_signup_display_name_page.jsx | 136 +++ .../components/team_signup_email_item.jsx | 86 ++ .../components/team_signup_finished.jsx | 15 + .../components/team_signup_password_page.jsx | 215 ++++ .../components/team_signup_send_invites_page.jsx | 210 ++++ .../components/team_signup_url_page.jsx | 205 ++++ .../components/team_signup_username_page.jsx | 164 +++ .../components/team_signup_welcome_page.jsx | 234 ++++ web/react/components/signup_team_confirm.jsx | 47 +- web/react/components/signup_user_complete.jsx | 331 +++--- .../components/suggestion/at_mention_provider.jsx | 2 +- web/react/components/suggestion/suggestion_box.jsx | 12 +- .../components/suggestion/suggestion_list.jsx | 4 +- web/react/components/team_members_modal.jsx | 32 +- web/react/components/team_settings.jsx | 3 + .../components/team_signup_display_name_page.jsx | 136 --- web/react/components/team_signup_email_item.jsx | 86 -- web/react/components/team_signup_password_page.jsx | 214 ---- .../components/team_signup_send_invites_page.jsx | 210 ---- web/react/components/team_signup_url_page.jsx | 205 ---- web/react/components/team_signup_username_page.jsx | 164 --- web/react/components/team_signup_welcome_page.jsx | 232 ---- web/react/components/team_signup_with_email.jsx | 7 +- web/react/components/user_list_row.jsx | 2 +- web/react/components/user_profile.jsx | 29 +- .../components/user_settings/manage_languages.jsx | 3 +- .../user_settings/user_settings_developer.jsx | 4 +- .../user_settings/user_settings_general.jsx | 15 +- .../user_settings/user_settings_modal.jsx | 16 +- .../user_settings/user_settings_security.jsx | 29 +- web/react/dispatcher/event_helpers.jsx | 222 ---- web/react/package.json | 2 + web/react/pages/admin_console.jsx | 71 -- web/react/pages/channel.jsx | 97 -- web/react/pages/claim_account.jsx | 68 -- web/react/pages/docs.jsx | 64 -- web/react/pages/find_team.jsx | 62 -- web/react/pages/home.jsx | 16 - web/react/pages/login.jsx | 66 -- web/react/pages/password_reset.jsx | 68 -- web/react/pages/root.jsx | 290 +++++ web/react/pages/signup_team.jsx | 76 -- web/react/pages/signup_team_complete.jsx | 66 -- web/react/pages/signup_team_confirm.jsx | 64 -- web/react/pages/signup_user_complete.jsx | 69 -- web/react/pages/verify.jsx | 67 -- web/react/stores/admin_store.jsx | 10 +- web/react/stores/analytics_store.jsx | 4 - web/react/stores/browser_store.jsx | 6 +- web/react/stores/channel_store.jsx | 4 - web/react/stores/file_store.jsx | 7 +- web/react/stores/localization_store.jsx | 60 + web/react/stores/modal_store.jsx | 4 - web/react/stores/post_store.jsx | 4 - web/react/stores/search_store.jsx | 4 - web/react/stores/socket_store.jsx | 14 +- web/react/stores/suggestion_store.jsx | 7 +- web/react/stores/team_store.jsx | 42 +- web/react/stores/user_store.jsx | 66 +- web/react/utils/async_client.jsx | 55 +- web/react/utils/channel_intro_messages.jsx | 49 +- web/react/utils/client.jsx | 179 ++- web/react/utils/constants.jsx | 5 +- web/react/utils/utils.jsx | 33 +- web/sass-files/sass/partials/_sidebar--left.scss | 6 - web/static/help/Messaging_en.md | 47 - web/static/help/Messaging_es.md | 37 - web/static/i18n/en.json | 17 +- web/static/i18n/es.json | 14 +- web/static/i18n/pt.json | 5 +- web/templates/admin_console.html | 21 - web/templates/authorize.html | 12 - web/templates/channel.html | 21 - web/templates/claim_account.html | 30 - web/templates/docs.html | 27 - web/templates/find_team.html | 30 - web/templates/footer.html | 39 - web/templates/head.html | 191 ---- web/templates/home.html | 24 - web/templates/login.html | 27 - web/templates/password_reset.html | 30 - web/templates/signup_team.html | 29 - web/templates/signup_team_complete.html | 29 - web/templates/signup_team_confirm.html | 26 - web/templates/signup_user_complete.html | 29 - web/templates/verify.html | 30 - web/web.go | 1158 +------------------- 211 files changed, 5356 insertions(+), 7014 deletions(-) create mode 100644 api/license_test.go delete mode 100644 api/templates/email_change_body.html delete mode 100644 api/templates/email_change_subject.html delete mode 100644 api/templates/email_change_verify_body.html delete mode 100644 api/templates/email_change_verify_subject.html delete mode 100644 api/templates/email_footer.html delete mode 100644 api/templates/email_info.html delete mode 100644 api/templates/error.html delete mode 100644 api/templates/find_teams_body.html delete mode 100644 api/templates/find_teams_subject.html delete mode 100644 api/templates/invite_body.html delete mode 100644 api/templates/invite_subject.html delete mode 100644 api/templates/password_change_body.html delete mode 100644 api/templates/password_change_subject.html delete mode 100644 api/templates/post_body.html delete mode 100644 api/templates/post_subject.html delete mode 100644 api/templates/reset_body.html delete mode 100644 api/templates/reset_subject.html delete mode 100644 api/templates/signin_change_body.html delete mode 100644 api/templates/signin_change_subject.html delete mode 100644 api/templates/signup_team_body.html delete mode 100644 api/templates/signup_team_subject.html delete mode 100644 api/templates/verify_body.html delete mode 100644 api/templates/verify_subject.html delete mode 100644 api/templates/welcome_body.html delete mode 100644 api/templates/welcome_subject.html create mode 100644 templates/authorize.html create mode 100644 templates/email_change_body.html create mode 100644 templates/email_change_subject.html create mode 100644 templates/email_change_verify_body.html create mode 100644 templates/email_change_verify_subject.html create mode 100644 templates/email_footer.html create mode 100644 templates/email_info.html create mode 100644 templates/error.html create mode 100644 templates/head.html create mode 100644 templates/invite_body.html create mode 100644 templates/invite_subject.html create mode 100644 templates/password_change_body.html create mode 100644 templates/password_change_subject.html create mode 100644 templates/post_body.html create mode 100644 templates/post_subject.html create mode 100644 templates/reset_body.html create mode 100644 templates/reset_subject.html create mode 100644 templates/root.html create mode 100644 templates/signin_change_body.html create mode 100644 templates/signin_change_subject.html create mode 100644 templates/signup_team_body.html create mode 100644 templates/signup_team_subject.html create mode 100644 templates/verify_body.html create mode 100644 templates/verify_subject.html create mode 100644 templates/welcome_body.html create mode 100644 templates/welcome_subject.html create mode 100644 utils/html.go create mode 100644 web/react/action_creators/global_actions.jsx delete mode 100644 web/react/components/channel_loader.jsx create mode 100644 web/react/components/do_verify_email.jsx delete mode 100644 web/react/components/docs.jsx delete mode 100644 web/react/components/email_verify.jsx delete mode 100644 web/react/components/find_team.jsx create mode 100644 web/react/components/logged_in.jsx create mode 100644 web/react/components/needs_team.jsx create mode 100644 web/react/components/not_logged_in.jsx delete mode 100644 web/react/components/password_reset.jsx create mode 100644 web/react/components/root.jsx create mode 100644 web/react/components/should_verify_email.jsx delete mode 100644 web/react/components/signup_team_complete.jsx create mode 100644 web/react/components/signup_team_complete/components/signup_team_complete.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_display_name_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_email_item.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_finished.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_password_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_send_invites_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_url_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_username_page.jsx create mode 100644 web/react/components/signup_team_complete/components/team_signup_welcome_page.jsx delete mode 100644 web/react/components/team_signup_display_name_page.jsx delete mode 100644 web/react/components/team_signup_email_item.jsx delete mode 100644 web/react/components/team_signup_password_page.jsx delete mode 100644 web/react/components/team_signup_send_invites_page.jsx delete mode 100644 web/react/components/team_signup_url_page.jsx delete mode 100644 web/react/components/team_signup_username_page.jsx delete mode 100644 web/react/components/team_signup_welcome_page.jsx delete mode 100644 web/react/dispatcher/event_helpers.jsx delete mode 100644 web/react/pages/admin_console.jsx delete mode 100644 web/react/pages/channel.jsx delete mode 100644 web/react/pages/claim_account.jsx delete mode 100644 web/react/pages/docs.jsx delete mode 100644 web/react/pages/find_team.jsx delete mode 100644 web/react/pages/home.jsx delete mode 100644 web/react/pages/login.jsx delete mode 100644 web/react/pages/password_reset.jsx create mode 100644 web/react/pages/root.jsx delete mode 100644 web/react/pages/signup_team.jsx delete mode 100644 web/react/pages/signup_team_complete.jsx delete mode 100644 web/react/pages/signup_team_confirm.jsx delete mode 100644 web/react/pages/signup_user_complete.jsx delete mode 100644 web/react/pages/verify.jsx create mode 100644 web/react/stores/localization_store.jsx delete mode 100644 web/static/help/Messaging_en.md delete mode 100644 web/static/help/Messaging_es.md delete mode 100644 web/templates/admin_console.html delete mode 100644 web/templates/authorize.html delete mode 100644 web/templates/channel.html delete mode 100644 web/templates/claim_account.html delete mode 100644 web/templates/docs.html delete mode 100644 web/templates/find_team.html delete mode 100644 web/templates/footer.html delete mode 100644 web/templates/head.html delete mode 100644 web/templates/home.html delete mode 100644 web/templates/login.html delete mode 100644 web/templates/password_reset.html delete mode 100644 web/templates/signup_team.html delete mode 100644 web/templates/signup_team_complete.html delete mode 100644 web/templates/signup_team_confirm.html delete mode 100644 web/templates/signup_user_complete.html delete mode 100644 web/templates/verify.html diff --git a/Makefile b/Makefile index a7c277e4c..57a28bf3a 100644 --- a/Makefile +++ b/Makefile @@ -127,10 +127,9 @@ package: cp -RL web/static/help $(DIST_PATH)/web/static cp -RL web/static/images $(DIST_PATH)/web/static cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/ - cp -RL web/templates $(DIST_PATH)/web + cp -RL templates $(DIST_PATH) mkdir -p $(DIST_PATH)/api - cp -RL api/templates $(DIST_PATH)/api cp -RL i18n $(DIST_PATH) cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH) @@ -140,17 +139,17 @@ package: mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js - sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - rm $(DIST_PATH)/web/templates/*.bak + sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + rm $(DIST_PATH)/templates/*.bak sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv' diff --git a/api/api.go b/api/api.go index 4fecd3dd4..20f77e558 100644 --- a/api/api.go +++ b/api/api.go @@ -4,47 +4,15 @@ package api import ( - "bytes" - l4g "github.com/alecthomas/log4go" + "net/http" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "html/template" - "net/http" _ "github.com/cloudfoundry/jibber_jabber" _ "github.com/nicksnyder/go-i18n/i18n" ) -var ServerTemplates *template.Template - -type ServerTemplatePage Page - -func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage { - return &ServerTemplatePage{ - TemplateName: templateName, - Props: make(map[string]string), - Extra: make(map[string]string), - Html: make(map[string]template.HTML), - ClientCfg: utils.ClientCfg, - Locale: locale, - } -} - -func (me *ServerTemplatePage) Render() string { - var text bytes.Buffer - - T := utils.GetUserTranslations(me.Locale) - me.Props["Footer"] = T("api.templates.email_footer") - me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", - map[string]interface{}{"SupportEmail": me.ClientCfg["SupportEmail"], "SiteName": me.ClientCfg["SiteName"]})) - - if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil { - l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err) - } - - return text.String() -} - func InitApi() { r := Srv.Router.PathPrefix("/api/v1").Subrouter() InitUser(r) @@ -60,12 +28,7 @@ func InitApi() { InitPreference(r) InitLicense(r) - templatesDir := utils.FindDir("api/templates") - l4g.Debug(utils.T("api.api.init.parsing_templates.debug"), templatesDir) - var err error - if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("api.api.init.parsing_templates.error"), err) - } + utils.InitHTML() } func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool { diff --git a/api/context.go b/api/context.go index edcdcbfef..eed035daf 100644 --- a/api/context.go +++ b/api/context.go @@ -5,11 +5,9 @@ package api import ( "fmt" - "html/template" "net" "net/http" "net/url" - "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -31,33 +29,16 @@ var allowedMethods []string = []string{ } type Context struct { - Session model.Session - RequestId string - IpAddress string - Path string - Err *model.AppError - teamURLValid bool - teamURL string - siteURL string - SessionTokenIndex int64 - T goi18n.TranslateFunc - Locale string -} - -type Page struct { - TemplateName string - Props map[string]string - Extra map[string]string - Html map[string]template.HTML - ClientCfg map[string]string - ClientLicense map[string]string - User *model.User - Team *model.Team - Channel *model.Channel - Preferences *model.Preferences - PostID string - SessionTokenIndex int64 - Locale string + Session model.Session + RequestId string + IpAddress string + Path string + Err *model.AppError + teamURLValid bool + teamURL string + siteURL string + T goi18n.TranslateFunc + Locale string } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -121,37 +102,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Attempt to parse the token from the cookie if len(token) == 0 { - tokens := GetMultiSessionCookieTokens(r) - if len(tokens) > 0 { - // If there is only 1 token in the cookie then just use it like normal - if len(tokens) == 1 { - token = tokens[0] - } else { - // If it is a multi-session token then find the correct session - sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX) - sessionTokenIndex := int64(-1) - if len(sessionTokenIndexStr) > 0 { - if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { - sessionTokenIndex = index - } - } else { - sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX) - if len(sessionTokenIndexStr) > 0 { - if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { - sessionTokenIndex = index - } - } - } - - if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) { - token = tokens[sessionTokenIndex] - c.SessionTokenIndex = sessionTokenIndex - } else { - c.SessionTokenIndex = -1 - } - } - } else { - c.SessionTokenIndex = -1 + if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + token = cookie.Value } } @@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if session == nil || session.IsExpired() { c.RemoveSessionCookie(w, r) - c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) - c.Err.StatusCode = http.StatusUnauthorized + if h.requireUser || h.requireSystemAdmin { + c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } } else if !session.IsOAuth && isTokenFromQueryString { c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token) c.Err.StatusCode = http.StatusUnauthorized @@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool { } func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { - - // multiToken := "" - // if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - // multiToken = oldMultiCookie.Value - // } - - // multiCookie := &http.Cookie{ - // Name: model.SESSION_COOKIE_TOKEN, - // Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)), - // Path: "/", - // MaxAge: model.SESSION_TIME_WEB_IN_SECS, - // HttpOnly: true, - // } - - //http.SetCookie(w, multiCookie) - cookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, Value: "", @@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool { } func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { - props := make(map[string]string) - props["Message"] = err.Message - props["Details"] = err.DetailedError + T, locale := utils.GetTranslationsAndLocale(w, r) + page := utils.NewHTMLTemplate("error", locale) + page.Props["Message"] = err.Message + page.Props["Details"] = err.DetailedError pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) > 1 { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] } else { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host } - T, _ := utils.GetTranslationsAndLocale(w, r) - props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - props["Link"] = T("api.templates.error.link") + page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + page.Props["Link"] = T("api.templates.error.link") w.WriteHeader(err.StatusCode) - ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg}) + if rErr := page.RenderToWriter(w); rErr != nil { + l4g.Error("Failed to create error page: " + rErr.Error() + ", Original error: " + err.Error()) + } } func Handle404(w http.ResponseWriter, r *http.Request) { @@ -588,29 +528,6 @@ func GetSession(token string) *model.Session { return session } -func GetMultiSessionCookieTokens(r *http.Request) []string { - if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { - multiToken := multiCookie.Value - - if len(multiToken) > 0 { - return strings.Split(multiToken, " ") - } - } - - return []string{} -} - -func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) { - for index, token := range GetMultiSessionCookieTokens(r) { - s := GetSession(token) - if s != nil && !s.IsExpired() && s.TeamId == teamId { - return int64(index), s - } - } - - return -1, nil -} - func AddSessionToCache(session *model.Session) { sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60)) } diff --git a/api/license.go b/api/license.go index 23e7946c8..542b45e26 100644 --- a/api/license.go +++ b/api/license.go @@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) { sr := r.PathPrefix("/license").Subrouter() sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST") sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST") + sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET") } func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { @@ -130,3 +131,22 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) { rdata["status"] = "ok" w.Write([]byte(model.MapToJson(rdata))) } + +func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) { + config := utils.ClientLicense + + var etag string + if config["IsLicensed"] == "false" { + etag = model.Etag(config["IsLicensed"]) + } else { + etag = model.Etag(config["IsLicensed"], config["IssuedAt"]) + } + + if HandleEtag(etag, w, r) { + return + } + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + + w.Write([]byte(model.MapToJson(config))) +} diff --git a/api/license_test.go b/api/license_test.go new file mode 100644 index 000000000..b34aeb7a6 --- /dev/null +++ b/api/license_test.go @@ -0,0 +1,22 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "testing" +) + +func TestGetLicenceConfig(t *testing.T) { + Setup() + + if result, err := Client.GetClientLicenceConfig(); err != nil { + t.Fatal(err) + } else { + cfg := result.Data.(map[string]string) + + if _, ok := cfg["IsLicensed"]; !ok { + t.Fatal(cfg) + } + } +} diff --git a/api/oauth.go b/api/oauth.go index 1ae3dbf78..9b7f3699d 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -5,12 +5,15 @@ package api import ( "fmt" + "net/http" + "net/url" + "strconv" + "strings" + l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "net/http" - "net/url" ) func InitOAuth(r *mux.Router) { @@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) { sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") + sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + + // Also handle this a the old routes remove soon apiv2? + mr := Srv.Router + mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") } func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -163,3 +177,328 @@ func GetAuthData(code string) *model.AuthData { return result.Data.(*model.AuthData) } } + +func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + uri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" + + if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { + c.Err = err + return + } else { + action := props["action"] + switch action { + case model.OAUTH_ACTION_SIGNUP: + CreateOAuthUser(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_LOGIN: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_EMAIL_TO_SSO: + CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_SSO_TO_EMAIL: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + } + break + default: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + } + } +} + +func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + responseType := r.URL.Query().Get("response_type") + clientId := r.URL.Query().Get("client_id") + redirect := r.URL.Query().Get("redirect_uri") + scope := r.URL.Query().Get("scope") + state := r.URL.Query().Get("state") + + if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { + c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + } + + var team *model.Team + if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + page := utils.NewHTMLTemplate("authorize", c.Locale) + page.Props["Title"] = c.T("web.authorize_oauth.title") + page.Props["TeamName"] = team.Name + page.Props["AppName"] = app.Name + page.Props["ResponseType"] = responseType + page.Props["ClientId"] = clientId + page.Props["RedirectUri"] = redirect + page.Props["Scope"] = scope + page.Props["State"] = state + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) + } +} + +func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + c.LogAudit("attempt") + + r.ParseForm() + + grantType := r.FormValue("grant_type") + if grantType != model.ACCESS_TOKEN_GRANT_TYPE { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") + return + } + + clientId := r.FormValue("client_id") + if len(clientId) != 26 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") + return + } + + secret := r.FormValue("client_secret") + if len(secret) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") + return + } + + code := r.FormValue("code") + if len(code) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") + return + } + + redirectUri := r.FormValue("redirect_uri") + + achan := Srv.Store.OAuth().GetApp(clientId) + tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code) + + authData := GetAuthData(code) + + if authData == nil { + c.LogAudit("fail - invalid auth code") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + uchan := Srv.Store.User().Get(authData.UserId) + + if authData.IsExpired() { + c.LogAudit("fail - auth code expired") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + if authData.RedirectUri != redirectUri { + c.LogAudit("fail - redirect uri provided did not match previous redirect uri") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") + return + } + + if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { + c.LogAudit("fail - auth code is invalid") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-achan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !model.ComparePassword(app.ClientSecret, secret) { + c.LogAudit("fail - invalid client credentials") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } + + callback := redirectUri + if len(callback) == 0 { + callback = app.CallbackUrls[0] + } + + if result := <-tchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") + return + } else if result.Data != nil { + c.LogAudit("fail - auth code has been used previously") + accessData := result.Data.(*model.AccessData) + + // Revoke access token, related auth code, and session from DB as well as from cache + if err := RevokeAccessToken(accessData.Token); err != nil { + l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) + } + + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") + return + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") + return + } else { + user = result.Data.(*model.User) + } + + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + + if result := <-Srv.Store.Session().Save(session); result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") + return + } else { + session = result.Data.(*model.Session) + AddSessionToCache(session) + } + + accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} + + if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") + return + } + + accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + + c.LogAuditWithUserId(user.Id, "success") + + w.Write([]byte(accessRsp.ToJson())) +} + +func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + loginHint := r.URL.Query().Get("login_hint") + teamName := r.URL.Query().Get("team") + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} + +func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + teamName := r.URL.Query().Get("team") + + if !utils.Cfg.TeamSettings.EnableUserCreation { + c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + hash := r.URL.Query().Get("h") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if IsVerifyHashRequired(nil, team, hash) { + data := r.URL.Query().Get("d") + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") + return + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") + return + } + + if team.Id != props["id"] { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) + return + } + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SIGNUP + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} diff --git a/api/post.go b/api/post.go index cd78b16f0..d0ec5826a 100644 --- a/api/post.go +++ b/api/post.go @@ -419,7 +419,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team // copy the context and create a mock session for posting the message mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false} - newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0, c.T, c.Locale} + newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, c.T, c.Locale} if text, ok := respProps["text"]; ok { if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { @@ -604,12 +604,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * year := fmt.Sprintf("%d", tm.Year()) zone, _ := tm.Zone() - subjectPage := NewServerTemplatePage("post_subject", profileMap[id].Locale) + subjectPage := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale) subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, "Month": month[:3], "Day": day, "Year": year}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("post_body", profileMap[id].Locale) + bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name diff --git a/api/team.go b/api/team.go index 2f680dc76..255982522 100644 --- a/api/team.go +++ b/api/team.go @@ -29,13 +29,12 @@ func InitTeam(r *mux.Router) { sr.Handle("/create_with_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST") sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") - sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET") + sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET") sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") - sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") - sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST") sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") + sr.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST") // These should be moved to the global admain console sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET") @@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signup_team_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title") bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button") @@ -86,7 +85,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { } if !utils.Cfg.EmailSettings.RequireEmailVerification { - m["follow_link"] = bodyPage.Props["Link"] + m["follow_link"] = fmt.Sprintf("/signup_team_complete/?d=%s&h=%s", url.QueryEscape(data), url.QueryEscape(hash)) } w.Header().Set("Access-Control-Allow-Origin", " *") @@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { return } - data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service} + data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name} w.Write([]byte(model.MapToJson(data))) } @@ -391,10 +390,6 @@ func isTeamCreationAllowed(c *Context, email string) bool { } func getAll(c *Context, w http.ResponseWriter, r *http.Request) { - if !c.HasSystemAdminPermissions("getLogs") { - return - } - if result := <-Srv.Store.Team().GetAll(); result.Err != nil { c.Err = result.Err return @@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) { m := make(map[string]*model.Team) for _, v := range teams { m[v.Id] = v + if !c.IsSystemAdmin() { + m[v.Id].SanitizeForNotLoggedIn() + } } w.Write([]byte(model.TeamMapToJson(m))) @@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool { return false } -func findTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - m := make(map[string]*model.Team) - for _, v := range teams { - v.Sanitize() - m[v.Id] = v - } - - w.Write([]byte(model.TeamMapToJson(m))) - } -} - -func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - siteURL := c.GetSiteURL() - subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - - bodyPage := NewServerTemplatePage("find_teams_body", c.Locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title") - bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found") - bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found") - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - } else { - teams := result.Data.([]*model.Team) - - // the template expects Props to be a map with team names as the keys and the team url as the value - props := make(map[string]string) - for _, team := range teams { - props[team.Name] = c.GetTeamURLFromTeam(team) - } - bodyPage.Extra = props - - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { - l4g.Error(utils.T("api.team.email_teams.sending.error"), err) - } - - w.Write([]byte(model.MapToJson(m))) - } -} - func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { invites := model.InvitesFromJson(r.Body) if len(invites.Invites) == 0 { @@ -600,11 +530,11 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str senderRole = c.T("api.team.invite_members.member") } - subjectPage := NewServerTemplatePage("invite_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.invite_subject", map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("invite_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.invite_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info", @@ -813,3 +743,25 @@ func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } } + +func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { + m := model.MapFromJson(r.Body) + inviteId := m["invite_id"] + + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + c.Err = result.Err + return + } else { + team := result.Data.(*model.Team) + if !(team.Type == model.TEAM_OPEN) { + c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId) + return + } + + result := map[string]string{} + result["display_name"] = team.DisplayName + result["name"] = team.Name + result["id"] = team.Id + w.Write([]byte(model.MapToJson(result))) + } +} diff --git a/api/team_test.go b/api/team_test.go index c942e2e1f..bbbc8385d 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -108,49 +108,36 @@ func TestCreateTeam(t *testing.T) { } } -func TestFindTeamByEmail(t *testing.T) { +func TestGetAllTeams(t *testing.T) { Setup() - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user.Id)) - if r1, err := Client.FindTeams(user.Email); err != nil { + Client.LoginByEmail(team.Name, user.Email, "pwd") + + enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing + defer func() { + *utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks + }() + *utils.Cfg.TeamSettings.EnableTeamListing = true + + if r1, err := Client.GetAllTeams(); err != nil { t.Fatal(err) } else { teams := r1.Data.(map[string]*model.Team) if teams[team.Id].Name != team.Name { t.Fatal() } - if teams[team.Id].DisplayName != team.DisplayName { - t.Fatal() + if teams[team.Id].Email != "" { + t.Fatal("Non admin users shoudn't get full listings") } } - if _, err := Client.FindTeams("missing"); err != nil { - t.Fatal(err) - } -} - -func TestGetAllTeams(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.GetAllTeams(); err == nil { - t.Fatal("you shouldn't have permissions") - } - c := &Context{} c.RequestId = model.NewId() c.IpAddress = "cmd_line" @@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) { if teams[team.Id].Name != team.Name { t.Fatal() } + if teams[team.Id].Email != team.Email { + t.Fatal() + } } } @@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) { Client.ClearOAuthToken() } -/* - -XXXXXX investigate and fix failing test - -func TestFindTeamByDomain(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - if r1, err := Client.FindTeamByDomain(team.Name, false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain(team.Name, true); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if val { - t.Fatal("shouldn't be a valid domain") - } - } -} - -*/ - -func TestFindTeamByEmailSend(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.FindTeamsSendEmail(user.Email); err != nil { - t.Fatal(err) - } else { - } - - if _, err := Client.FindTeamsSendEmail("missing"); err != nil { - - // It should actually succeed at sending the email since it doesn't exist - if !strings.Contains(err.DetailedError, "Failed to add to email address") { - t.Fatal(err) - } - } -} - func TestInviteMembers(t *testing.T) { Setup() diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html deleted file mode 100644 index 41b1bcd7d..000000000 --- a/api/templates/email_change_body.html +++ /dev/null @@ -1,41 +0,0 @@ -{{define "email_change_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-
-
-
-
- -{{end}} diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html deleted file mode 100644 index afabc2191..000000000 --- a/api/templates/email_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_change_verify_body.html b/api/templates/email_change_verify_body.html deleted file mode 100644 index 0d0c0aaba..000000000 --- a/api/templates/email_change_verify_body.html +++ /dev/null @@ -1,44 +0,0 @@ -{{define "email_change_verify_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Props.Info}}

-

- {{.Props.VerifyButton}} -

-
-
-
-
- -{{end}} diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html deleted file mode 100644 index 4fc4f4846..000000000 --- a/api/templates/email_change_verify_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_footer.html b/api/templates/email_footer.html deleted file mode 100644 index 6dc7fa483..000000000 --- a/api/templates/email_footer.html +++ /dev/null @@ -1,13 +0,0 @@ -{{define "email_footer"}} - - -

- -

-

- (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- {{.Props.Footer}} -

- - -{{end}} diff --git a/api/templates/email_info.html b/api/templates/email_info.html deleted file mode 100644 index 0a34f18a0..000000000 --- a/api/templates/email_info.html +++ /dev/null @@ -1,7 +0,0 @@ -{{define "email_info"}} - - - {{.Html.EmailInfo}} - - -{{end}} diff --git a/api/templates/error.html b/api/templates/error.html deleted file mode 100644 index 2f588aead..000000000 --- a/api/templates/error.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - <span class='fa fa-chevron-left'></span>Back - Error - - - - - - - - - - - - - - - - - -
-
-
-

{{.Props.Title}}

-

{{ .Props.Message }}

- {{.Props.Link}} -
-
- - - diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html deleted file mode 100644 index 1324091aa..000000000 --- a/api/templates/find_teams_body.html +++ /dev/null @@ -1,52 +0,0 @@ -{{define "find_teams_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{ if .Extra }} - {{.Props.Found}}
- {{range $index, $element := .Extra}} - {{ $index }}
- {{ end }} - {{ else }} - {{.Props.NotFound}} - {{ end }} -

-
-
-
-
- -{{end}} - - - diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html deleted file mode 100644 index ebc339562..000000000 --- a/api/templates/find_teams_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "find_teams_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html deleted file mode 100644 index 2b6bde6d3..000000000 --- a/api/templates/invite_body.html +++ /dev/null @@ -1,46 +0,0 @@ -{{define "invite_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-

- {{.Props.Button}} -

-
-

{{.Html.ExtraInfo}}

-
-
-
-
- -{{end}} diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html deleted file mode 100644 index 504915d50..000000000 --- a/api/templates/invite_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "invite_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html deleted file mode 100644 index 2c4ba10ca..000000000 --- a/api/templates/password_change_body.html +++ /dev/null @@ -1,43 +0,0 @@ -{{define "password_change_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-
-
-
-
- -{{end}} - - diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html deleted file mode 100644 index 897f1210d..000000000 --- a/api/templates/password_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "password_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/post_body.html b/api/templates/post_body.html deleted file mode 100644 index 54f34d1dd..000000000 --- a/api/templates/post_body.html +++ /dev/null @@ -1,45 +0,0 @@ -{{define "post_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.BodyText}}

-

{{.Html.Info}}

{{.Props.PostMessage}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} - diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html deleted file mode 100644 index 60daaa432..000000000 --- a/api/templates/post_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html deleted file mode 100644 index 69cd44957..000000000 --- a/api/templates/reset_body.html +++ /dev/null @@ -1,46 +0,0 @@ -{{define "reset_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} - - diff --git a/api/templates/reset_subject.html b/api/templates/reset_subject.html deleted file mode 100644 index a2852d332..000000000 --- a/api/templates/reset_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "reset_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/signin_change_body.html b/api/templates/signin_change_body.html deleted file mode 100644 index af8577f0f..000000000 --- a/api/templates/signin_change_body.html +++ /dev/null @@ -1,43 +0,0 @@ -{{define "signin_change_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Html.Info}}

-
-
-
-
- -{{end}} - - diff --git a/api/templates/signin_change_subject.html b/api/templates/signin_change_subject.html deleted file mode 100644 index 606dc4df3..000000000 --- a/api/templates/signin_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "signin_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html deleted file mode 100644 index 683a9891e..000000000 --- a/api/templates/signup_team_body.html +++ /dev/null @@ -1,44 +0,0 @@ -{{define "signup_team_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

- {{.Props.Button}} -

- {{.Html.Info}}

-
-
-
-
- -{{end}} diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html deleted file mode 100644 index 413a5c8c1..000000000 --- a/api/templates/signup_team_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "signup_team_subject"}}{{.Props.Subject}}{{end}} \ No newline at end of file diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html deleted file mode 100644 index 2b0d25f94..000000000 --- a/api/templates/verify_body.html +++ /dev/null @@ -1,44 +0,0 @@ -{{define "verify_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{.Props.Info}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html deleted file mode 100644 index ad7fc2aaa..000000000 --- a/api/templates/verify_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "verify_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html deleted file mode 100644 index b5ca9beb3..000000000 --- a/api/templates/welcome_body.html +++ /dev/null @@ -1,51 +0,0 @@ -{{define "welcome_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - {{if .Props.VerifyUrl }} - - - - {{end}} - - - -
-

{{.Props.Title}}

-

{{.Props.Info}}

-

- {{.Props.Button}} -

-
-

{{.Props.Info2}}

- {{.Props.TeamURL}} -

{{.Props.Info3}}

-
-
-
-
- -{{end}} - diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html deleted file mode 100644 index 95189b900..000000000 --- a/api/templates/welcome_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "welcome_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/user.go b/api/user.go index b7e6220d8..0841c38aa 100644 --- a/api/user.go +++ b/api/user.go @@ -53,10 +53,13 @@ func InitUser(r *mux.Router) { sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") + sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") + sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET") + sr.Handle("/me_logged_in", ApiAppHandler(getMeLoggedIn)).Methods("GET") sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST") sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET") sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") @@ -315,10 +318,10 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { - subjectPage := NewServerTemplatePage("welcome_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) - bodyPage := NewServerTemplatePage("welcome_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info") @@ -328,7 +331,7 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN bodyPage.Props["TeamURL"] = teamURL if !verified { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) bodyPage.Props["VerifyUrl"] = link } @@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) { func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) - subjectPage := NewServerTemplatePage("verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.verify_body.info") @@ -621,31 +624,17 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, w.Header().Set(model.HEADER_TOKEN, session.Token) - tokens := GetMultiSessionCookieTokens(r) - multiToken := "" - seen := make(map[string]string) - seen[session.TeamId] = session.TeamId - for _, token := range tokens { - s := GetSession(token) - if s != nil && !s.IsExpired() && seen[s.TeamId] == "" { - multiToken += " " + token - seen[s.TeamId] = s.TeamId - } - } - - multiToken = strings.TrimSpace(multiToken + " " + session.Token) expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0) - - multiSessionCookie := &http.Cookie{ + sessionCookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, - Value: multiToken, + Value: session.Token, Path: "/", MaxAge: maxAge, Expires: expiresAt, HttpOnly: true, } - http.SetCookie(w, multiSessionCookie) + http.SetCookie(w, sessionCookie) c.Session = *session c.LogAuditWithUserId(user.Id, "success") @@ -902,6 +891,26 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getMeLoggedIn(c *Context, w http.ResponseWriter, r *http.Request) { + data := make(map[string]string) + data["logged_in"] = "false" + data["team_name"] = "" + + if len(c.Session.UserId) != 0 { + teamChan := Srv.Store.Team().Get(c.Session.TeamId) + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return + } else { + team = tr.Data.(*model.Team) + } + data["logged_in"] = "true" + data["team_name"] = team.Name + } + w.Write([]byte(model.MapToJson(data))) +} + func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] @@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { data := model.MapToJson(newProps) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) - link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) + link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) - subjectPage := NewServerTemplatePage("reset_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.reset_subject") - bodyPage := NewServerTemplatePage("reset_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.reset_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info")) @@ -1743,11 +1752,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("password_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("password_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info", @@ -1763,11 +1772,12 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("email_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info", @@ -1785,11 +1795,12 @@ func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamN link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) - subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title") bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info", @@ -1918,7 +1929,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string props["team"] = teamName state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) - redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) + redirectUri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) @@ -2216,11 +2227,11 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signin_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info", @@ -2232,3 +2243,68 @@ func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, }() } + +func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + userId := props["uid"] + if len(userId) != 26 { + c.SetInvalidParam("verifyEmail", "uid") + return + } + + hashedId := props["hid"] + if len(hashedId) == 0 { + c.SetInvalidParam("verifyEmail", "hid") + return + } + + if model.ComparePassword(hashedId, userId) { + if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { + return + } else { + c.LogAudit("Email Verified") + return + } + } + + c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden +} + +func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("resendVerification", "team_name") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("resendVerification", "email") + return + } + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = result.Err + return + } else { + user := result.Data.(*model.User) + + if user.LastActivityAt > 0 { + SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } else { + SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } + } +} diff --git a/api/user_test.go b/api/user_test.go index 1a1cf9634..27f00829f 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) { t.Fatal("should have failed - wrong user") } } + +func TestMeLoggedIn(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + Client.AuthToken = "invalid" + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" { + t.Fatal("Got: " + val) + } + } + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" { + t.Fatal("Got: " + val) + } + } +} diff --git a/i18n/en.json b/i18n/en.json index bc33fc019..d16de7dbb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1623,6 +1623,10 @@ "id": "api.user.upload_profile_user.upload_profile.app_error", "translation": "Couldn't upload profile image" }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Bad verify email link." + }, { "id": "api.web_conn.new_web_conn.last_activity.error", "translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v" @@ -3399,22 +3403,6 @@ "id": "web.find_team.title", "translation": "Find Team" }, - { - "id": "web.footer.about", - "translation": "About" - }, - { - "id": "web.footer.help", - "translation": "Help" - }, - { - "id": "web.footer.privacy", - "translation": "Privacy" - }, - { - "id": "web.footer.terms", - "translation": "Terms" - }, { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: Bad client_id" @@ -3547,10 +3535,6 @@ "id": "web.root.home_title", "translation": "Home" }, - { - "id": "web.root.singup_info", - "translation": "All team communication in one place, searchable and accessible anywhere" - }, { "id": "web.root.singup_title", "translation": "Signup" @@ -3606,5 +3590,9 @@ { "id": "web.watcher_fail.error", "translation": "Failed to add directory to watcher %v" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Invite is invalid because this is not an open team." } ] diff --git a/i18n/es.json b/i18n/es.json index 4c0c1fd03..93ffb2341 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -3399,22 +3399,6 @@ "id": "web.find_team.title", "translation": "Encontrar Equipo" }, - { - "id": "web.footer.about", - "translation": "Acerca" - }, - { - "id": "web.footer.help", - "translation": "Ayuda" - }, - { - "id": "web.footer.privacy", - "translation": "Privacidad" - }, - { - "id": "web.footer.terms", - "translation": "Términos" - }, { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: client_id malo" @@ -3547,10 +3531,6 @@ "id": "web.root.home_title", "translation": "Inicio" }, - { - "id": "web.root.singup_info", - "translation": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" - }, { "id": "web.root.singup_title", "translation": "Registrar" diff --git a/model/client.go b/model/client.go index 560e47b76..3adcb980d 100644 --- a/model/client.go +++ b/model/client.go @@ -16,19 +16,17 @@ import ( ) const ( - HEADER_REQUEST_ID = "X-Request-ID" - HEADER_VERSION_ID = "X-Version-ID" - HEADER_ETAG_SERVER = "ETag" - HEADER_ETAG_CLIENT = "If-None-Match" - HEADER_FORWARDED = "X-Forwarded-For" - HEADER_REAL_IP = "X-Real-IP" - HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" - HEADER_TOKEN = "token" - HEADER_BEARER = "BEARER" - HEADER_AUTH = "Authorization" - HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex" - SESSION_TOKEN_INDEX = "session_token_index" - API_URL_SUFFIX = "/api/v1" + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" + HEADER_AUTH = "Authorization" + API_URL_SUFFIX = "/api/v1" ) type Result struct { @@ -179,29 +177,6 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro } } -func (c *Client) FindTeams(email string) (*Result, *AppError) { - m := make(map[string]string) - m["email"] = email - if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil { - return nil, err - } else { - - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil - } -} - -func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { - m := make(map[string]string) - m["email"] = email - if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil { - return nil, err - } else { - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil - } -} - func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil { return nil, err @@ -938,7 +913,7 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (* } func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { - if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil { + if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken c.AuthType = HEADER_BEARER } + +func (c *Client) GetClientLicenceConfig() (*Result, *AppError) { + if r, err := c.DoApiGet("/license/client_config", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) GetMeLoggedIn() (*Result, *AppError) { + if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} diff --git a/model/session.go b/model/session.go index 5d9424d64..bf0d9531e 100644 --- a/model/session.go +++ b/model/session.go @@ -9,7 +9,7 @@ import ( ) const ( - SESSION_COOKIE_TOKEN = "MMTOKEN" + SESSION_COOKIE_TOKEN = "MMAUTHTOKEN" SESSION_CACHE_SIZE = 10000 SESSION_PROP_PLATFORM = "platform" SESSION_PROP_OS = "os" diff --git a/model/team.go b/model/team.go index 9e9eaa25f..bed7bbd8d 100644 --- a/model/team.go +++ b/model/team.go @@ -232,3 +232,10 @@ func (o *Team) Sanitize() { o.Email = "" o.AllowedDomains = "" } + +func (o *Team) SanitizeForNotLoggedIn() { + o.Email = "" + o.AllowedDomains = "" + o.CompanyName = "" + o.InviteId = "" +} diff --git a/templates/authorize.html b/templates/authorize.html new file mode 100644 index 000000000..0fa36b0ab --- /dev/null +++ b/templates/authorize.html @@ -0,0 +1,12 @@ +{{define "authorize"}} + +{{template "head" . }} + +
+
+ + + +{{end}} diff --git a/templates/email_change_body.html b/templates/email_change_body.html new file mode 100644 index 000000000..41b1bcd7d --- /dev/null +++ b/templates/email_change_body.html @@ -0,0 +1,41 @@ +{{define "email_change_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+
+
+
+
+ +{{end}} diff --git a/templates/email_change_subject.html b/templates/email_change_subject.html new file mode 100644 index 000000000..540bc6eab --- /dev/null +++ b/templates/email_change_subject.html @@ -0,0 +1 @@ +{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/templates/email_change_verify_body.html b/templates/email_change_verify_body.html new file mode 100644 index 000000000..0d0c0aaba --- /dev/null +++ b/templates/email_change_verify_body.html @@ -0,0 +1,44 @@ +{{define "email_change_verify_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Props.Info}}

+

+ {{.Props.VerifyButton}} +

+
+
+
+
+ +{{end}} diff --git a/templates/email_change_verify_subject.html b/templates/email_change_verify_subject.html new file mode 100644 index 000000000..04da7593c --- /dev/null +++ b/templates/email_change_verify_subject.html @@ -0,0 +1 @@ +{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/templates/email_footer.html b/templates/email_footer.html new file mode 100644 index 000000000..6dc7fa483 --- /dev/null +++ b/templates/email_footer.html @@ -0,0 +1,13 @@ +{{define "email_footer"}} + + +

+ +

+

+ (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
+ {{.Props.Footer}} +

+ + +{{end}} diff --git a/templates/email_info.html b/templates/email_info.html new file mode 100644 index 000000000..0a34f18a0 --- /dev/null +++ b/templates/email_info.html @@ -0,0 +1,7 @@ +{{define "email_info"}} + + + {{.Html.EmailInfo}} + + +{{end}} diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 000000000..b86039ca3 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,24 @@ +{{define "error"}} + + +{{template "head" . }} + +
+
+
+ +
+

{{.Props.Title}}

+

{{ .Props.Message }}

+ {{.Props.Link}} +
+
+ + + +{{end}} diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 000000000..a7eacc85f --- /dev/null +++ b/templates/head.html @@ -0,0 +1,92 @@ +{{define "head"}} + + + + + + {{ .Props.Title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/templates/invite_body.html b/templates/invite_body.html new file mode 100644 index 000000000..2b6bde6d3 --- /dev/null +++ b/templates/invite_body.html @@ -0,0 +1,46 @@ +{{define "invite_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+

+ {{.Props.Button}} +

+
+

{{.Html.ExtraInfo}}

+
+
+
+
+ +{{end}} diff --git a/templates/invite_subject.html b/templates/invite_subject.html new file mode 100644 index 000000000..504915d50 --- /dev/null +++ b/templates/invite_subject.html @@ -0,0 +1 @@ +{{define "invite_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/password_change_body.html b/templates/password_change_body.html new file mode 100644 index 000000000..2c4ba10ca --- /dev/null +++ b/templates/password_change_body.html @@ -0,0 +1,43 @@ +{{define "password_change_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+
+
+
+
+ +{{end}} + + diff --git a/templates/password_change_subject.html b/templates/password_change_subject.html new file mode 100644 index 000000000..897f1210d --- /dev/null +++ b/templates/password_change_subject.html @@ -0,0 +1 @@ +{{define "password_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/post_body.html b/templates/post_body.html new file mode 100644 index 000000000..54f34d1dd --- /dev/null +++ b/templates/post_body.html @@ -0,0 +1,45 @@ +{{define "post_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.BodyText}}

+

{{.Html.Info}}

{{.Props.PostMessage}}

+

+ {{.Props.Button}} +

+
+
+
+
+ +{{end}} + diff --git a/templates/post_subject.html b/templates/post_subject.html new file mode 100644 index 000000000..9789d4142 --- /dev/null +++ b/templates/post_subject.html @@ -0,0 +1 @@ +{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/templates/reset_body.html b/templates/reset_body.html new file mode 100644 index 000000000..69cd44957 --- /dev/null +++ b/templates/reset_body.html @@ -0,0 +1,46 @@ +{{define "reset_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+

+ {{.Props.Button}} +

+
+
+
+
+ +{{end}} + + diff --git a/templates/reset_subject.html b/templates/reset_subject.html new file mode 100644 index 000000000..a2852d332 --- /dev/null +++ b/templates/reset_subject.html @@ -0,0 +1 @@ +{{define "reset_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/root.html b/templates/root.html new file mode 100644 index 000000000..560c7a4b0 --- /dev/null +++ b/templates/root.html @@ -0,0 +1,12 @@ +{{define "root"}} + + +{{template "head" . }} + +
+ + + +{{end}} diff --git a/templates/signin_change_body.html b/templates/signin_change_body.html new file mode 100644 index 000000000..af8577f0f --- /dev/null +++ b/templates/signin_change_body.html @@ -0,0 +1,43 @@ +{{define "signin_change_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Html.Info}}

+
+
+
+
+ +{{end}} + + diff --git a/templates/signin_change_subject.html b/templates/signin_change_subject.html new file mode 100644 index 000000000..606dc4df3 --- /dev/null +++ b/templates/signin_change_subject.html @@ -0,0 +1 @@ +{{define "signin_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/signup_team_body.html b/templates/signup_team_body.html new file mode 100644 index 000000000..683a9891e --- /dev/null +++ b/templates/signup_team_body.html @@ -0,0 +1,44 @@ +{{define "signup_team_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

+ {{.Props.Button}} +

+ {{.Html.Info}}

+
+
+
+
+ +{{end}} diff --git a/templates/signup_team_subject.html b/templates/signup_team_subject.html new file mode 100644 index 000000000..413a5c8c1 --- /dev/null +++ b/templates/signup_team_subject.html @@ -0,0 +1 @@ +{{define "signup_team_subject"}}{{.Props.Subject}}{{end}} \ No newline at end of file diff --git a/templates/verify_body.html b/templates/verify_body.html new file mode 100644 index 000000000..2b0d25f94 --- /dev/null +++ b/templates/verify_body.html @@ -0,0 +1,44 @@ +{{define "verify_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Props.Info}}

+

+ {{.Props.Button}} +

+
+
+
+
+ +{{end}} diff --git a/templates/verify_subject.html b/templates/verify_subject.html new file mode 100644 index 000000000..ad7fc2aaa --- /dev/null +++ b/templates/verify_subject.html @@ -0,0 +1 @@ +{{define "verify_subject"}}{{.Props.Subject}}{{end}} diff --git a/templates/welcome_body.html b/templates/welcome_body.html new file mode 100644 index 000000000..b5ca9beb3 --- /dev/null +++ b/templates/welcome_body.html @@ -0,0 +1,51 @@ +{{define "welcome_body"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + {{if .Props.VerifyUrl }} + + + + {{end}} + + + +
+

{{.Props.Title}}

+

{{.Props.Info}}

+

+ {{.Props.Button}} +

+
+

{{.Props.Info2}}

+ {{.Props.TeamURL}} +

{{.Props.Info3}}

+
+
+
+
+ +{{end}} + diff --git a/templates/welcome_subject.html b/templates/welcome_subject.html new file mode 100644 index 000000000..95189b900 --- /dev/null +++ b/templates/welcome_subject.html @@ -0,0 +1 @@ +{{define "welcome_subject"}}{{.Props.Subject}}{{end}} diff --git a/utils/html.go b/utils/html.go new file mode 100644 index 000000000..4203160d5 --- /dev/null +++ b/utils/html.go @@ -0,0 +1,97 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "bytes" + "html/template" + "net/http" + + l4g "github.com/alecthomas/log4go" + "gopkg.in/fsnotify.v1" +) + +// Global storage for templates +var htmlTemplates *template.Template + +type HTMLTemplate struct { + TemplateName string + Props map[string]string + Html map[string]template.HTML + Locale string +} + +func InitHTML() { + templatesDir := FindDir("templates") + l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) + var err error + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("api.api.init.parsing_templates.error"), err) + } + + // Watch the templates folder for changes. + watcher, err := fsnotify.NewWatcher() + if err != nil { + l4g.Error(T("web.create_dir.error"), err) + } + + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + l4g.Info(T("web.reparse_templates.info"), event.Name) + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("web.parsing_templates.error"), err) + } + } + case err := <-watcher.Errors: + l4g.Error(T("web.dir_fail.error"), err) + } + } + }() + + err = watcher.Add(templatesDir) + if err != nil { + l4g.Error(T("web.watcher_fail.error"), err) + } +} + +func NewHTMLTemplate(templateName string, locale string) *HTMLTemplate { + return &HTMLTemplate{ + TemplateName: templateName, + Props: make(map[string]string), + Html: make(map[string]template.HTML), + Locale: locale, + } +} + +func (t *HTMLTemplate) addDefaultProps() { + T := GetUserTranslations(t.Locale) + t.Props["Footer"] = T("api.templates.email_footer") + t.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", + map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName})) +} + +func (t *HTMLTemplate) Render() string { + t.addDefaultProps() + + var text bytes.Buffer + + if err := htmlTemplates.ExecuteTemplate(&text, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + } + + return text.String() +} + +func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error { + t.addDefaultProps() + + if err := htmlTemplates.ExecuteTemplate(w, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + return err + } + return nil +} diff --git a/utils/license.go b/utils/license.go index 5c975aec2..b1f15ad92 100644 --- a/utils/license.go +++ b/utils/license.go @@ -20,7 +20,7 @@ import ( var IsLicensed bool = false var License *model.License = &model.License{} -var ClientLicense map[string]string = make(map[string]string) +var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"} var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r diff --git a/web/react/action_creators/global_actions.jsx b/web/react/action_creators/global_actions.jsx new file mode 100644 index 000000000..4375d6c87 --- /dev/null +++ b/web/react/action_creators/global_actions.jsx @@ -0,0 +1,252 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import PostStore from '../stores/post_store.jsx'; +import SearchStore from '../stores/search_store.jsx'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; +import * as AsyncClient from '../utils/async_client.jsx'; +import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; + +export function emitChannelClickEvent(channel) { + AsyncClient.getChannels(true); + AsyncClient.getChannelExtraInfo(channel.id); + AsyncClient.updateLastViewedAt(channel.id); + AsyncClient.getPosts(channel.id); + + AppDispatcher.handleViewAction({ + type: ActionTypes.CLICK_CHANNEL, + name: channel.name, + id: channel.id, + prev: ChannelStore.getCurrentId() + }); +} + +export function emitPostFocusEvent(postId) { + Client.getPostById( + postId, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_FOCUSED_POST, + postId, + post_list: data + }); + + AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); + } + ); +} + +export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { + Client.getPost( + post.channel_id, + post.id, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POSTS, + id: post.channel_id, + numRequested: 0, + post_list: data + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST_SELECTED, + postId: Utils.getRootId(post), + from_search: SearchStore.getSearchTerm() + }); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_SEARCH, + results: null, + is_mention_search: isMentionSearch + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getPost'); + } + ); +} + +export function emitLoadMorePostsEvent() { + const id = ChannelStore.getCurrentId(); + loadMorePostsTop(id); +} + +export function emitLoadMorePostsFocusedTopEvent() { + const id = PostStore.getFocusedPostId(); + loadMorePostsTop(id); +} + +export function loadMorePostsTop(id) { + const earliestPostId = PostStore.getEarliestPost(id).id; + if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { + AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE); + } +} + +export function emitLoadMorePostsFocusedBottomEvent() { + const id = PostStore.getFocusedPostId(); + const latestPostId = PostStore.getLatestPost(id).id; + AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); +} + +export function emitPostRecievedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_POST, + post + }); +} + +export function emitUserPostedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.CREATE_POST, + post + }); +} + +export function emitPostDeletedEvent(post) { + AppDispatcher.handleServerAction({ + type: ActionTypes.POST_DELETED, + post + }); +} + +export function showDeletePostModal(post, commentCount = 0) { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_DELETE_POST_MODAL, + value: true, + post, + commentCount + }); +} + +export function showGetPostLinkModal(post) { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_GET_POST_LINK_MODAL, + value: true, + post + }); +} + +export function showGetTeamInviteLinkModal() { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, + value: true + }); +} + +export function showInviteMemberModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, + value: true + }); +} + +export function showRegisterAppModal() { + AppDispatcher.handleViewAction({ + type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, + value: true + }); +} + +export function emitSuggestionPretextChanged(suggestionId, pretext) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, + id: suggestionId, + pretext + }); +} + +export function emitSelectNextSuggestion(suggestionId) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_NEXT, + id: suggestionId + }); +} + +export function emitSelectPreviousSuggestion(suggestionId) { + AppDispatcher.handleViewAction({ + type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, + id: suggestionId + }); +} + +export function emitCompleteWordSuggestion(suggestionId, term = '') { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD, + id: suggestionId, + term + }); +} + +export function emitClearSuggestions(suggestionId) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS, + id: suggestionId + }); +} + +export function emitPreferenceChangedEvent(preference) { + AppDispatcher.handleServerAction({ + type: Constants.ActionTypes.RECEIVED_PREFERENCE, + preference + }); +} + +export function emitRemovePost(post) { + AppDispatcher.handleViewAction({ + type: Constants.ActionTypes.REMOVE_POST, + post + }); +} + +export function sendEphemeralPost(message, channelId) { + const timestamp = Utils.getTimestamp(); + const post = { + id: Utils.generateId(), + user_id: '0', + channel_id: channelId || ChannelStore.getCurrentId(), + message, + type: Constants.POST_TYPE_EPHEMERAL, + create_at: timestamp, + update_at: timestamp, + filenames: [], + props: {} + }; + + emitPostRecievedEvent(post); +} + +export function loadTeamRequiredPage() { + AsyncClient.getAllTeams(); +} + +export function newLocalizationSelected(locale) { + Client.getTranslations( + locale, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_LOCALE, + locale, + translations: data + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getTranslations'); + } + ); +} + +export function viewLoggedIn() { + AsyncClient.getChannels(); + AsyncClient.getChannelExtraInfo(); + AsyncClient.getMyTeam(); + AsyncClient.getMe(); + + // Clear pending posts (shouldn't have pending posts if we are loading) + PostStore.clearPendingPosts(); +} diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 95b4caa12..db366f8ed 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal; import LoadingScreen from './loading_screen.jsx'; import * as Utils from '../utils/utils.jsx'; -import {FormattedMessage} from 'mm-intl'; +import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; export default class ActivityLogModal extends React.Component { constructor(props) { @@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.firstTime' defaultMessage='First time active: {date}, {time}' values={{ - date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }} />
@@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.lastActivity' defaultMessage='Last activity: {date}, {time}' values={{ - date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }} />
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 32ed70a99..4c4f21f08 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx'; import TeamStore from '../../stores/team_store.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import LoadingScreen from '../loading_screen.jsx'; -import * as Utils from '../../utils/utils.jsx'; import EmailSettingsTab from './email_settings.jsx'; import LogSettingsTab from './log_settings.jsx'; @@ -50,11 +49,6 @@ export default class AdminController extends React.Component { selected: props.tab || 'system_analytics', selectedTeam: props.teamId || null }; - - if (!props.tab) { - var tokenIndex = Utils.getUrlParameter('session_token_index'); - history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`); - } } componentDidMount() { @@ -63,6 +57,9 @@ export default class AdminController extends React.Component { AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange); AsyncClient.getAllTeams(); + + $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="popover"]').popover(); } componentWillUnmount() { @@ -175,7 +172,7 @@ export default class AdminController extends React.Component { } return ( -
+
+ ); } } @@ -37,4 +14,5 @@ ChannelView.defaultProps = { }; ChannelView.propTypes = { + params: React.PropTypes.object }; diff --git a/web/react/components/claim/claim_account.jsx b/web/react/components/claim/claim_account.jsx index 5b3b584ee..42fd8dafa 100644 --- a/web/react/components/claim/claim_account.jsx +++ b/web/react/components/claim/claim_account.jsx @@ -3,6 +3,7 @@ import EmailToSSO from './email_to_sso.jsx'; import SSOToEmail from './sso_to_email.jsx'; +import TeamStore from '../../stores/team_store.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component { constructor(props) { super(props); + this.onTeamChange = this.onTeamChange.bind(this); + this.updateStateFromStores = this.updateStateFromStores.bind(this); + this.state = {}; } + componentWillMount() { + this.setState({ + email: this.props.location.query.email, + newType: this.props.location.query.new_type, + oldType: this.props.location.query.old_type, + teamName: this.props.params.team, + teamDisplayName: '' + }); + this.updateStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + updateStateFromStores() { + const team = TeamStore.getByName(this.state.teamName); + let displayName = ''; + if (team) { + displayName = team.displayName; + } + this.setState({ + teamDisplayName: displayName + }); + } + onTeamChange() { + this.updateStateFromStores(); + } render() { + if (this.state.teamDisplayName === '') { + return (
); + } let content; - if (this.props.email === '') { + if (this.state.email === '') { content = (

); - } else if (this.props.currentType === '' && this.props.newType !== '') { + } else if (this.state.oldType === '' && this.state.newType !== '') { content = ( ); } else { content = ( ); } - return content; + return ( +
+ +
+
+ +
+ {content} +
+
+
+
+ ); } } ClaimAccount.defaultProps = { }; ClaimAccount.propTypes = { - currentType: React.PropTypes.string.isRequired, - newType: React.PropTypes.string.isRequired, - email: React.PropTypes.string.isRequired, - teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired }; diff --git a/web/react/components/claim/sso_to_email.jsx b/web/react/components/claim/sso_to_email.jsx index 74137082a..a16efb57b 100644 --- a/web/react/components/claim/sso_to_email.jsx +++ b/web/react/components/claim/sso_to_email.jsx @@ -159,7 +159,7 @@ SSOToEmail.propTypes = { currentType: React.PropTypes.string.isRequired, email: React.PropTypes.string.isRequired, teamName: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + teamDisplayName: React.PropTypes.string }; export default injectIntl(SSOToEmail); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 62319b1a7..69cc74842 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx'; import TutorialTip from './tutorial/tutorial_tip.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; import * as Utils from '../utils/utils.jsx'; @@ -165,7 +165,7 @@ class CreatePost extends React.Component { const channel = ChannelStore.get(this.state.channelId); - EventHelpers.emitUserPostedEvent(post); + GlobalActions.emitUserPostedEvent(post); this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); Client.createPost(post, channel, @@ -177,7 +177,7 @@ class CreatePost extends React.Component { member.last_viewed_at = Date.now(); ChannelStore.setChannelMember(member); - EventHelpers.emitPostRecievedEvent(data); + GlobalActions.emitPostRecievedEvent(data); }, (err) => { if (err.id === 'api.post.create_post.root_id.app_error') { diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index d9113bc9f..70e7a67a8 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx'; import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; + export default class DeleteChannelModal extends React.Component { constructor(props) { super(props); @@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component { return; } + browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square'); Client.deleteChannel( this.props.channel.id, () => { AsyncClient.getChannels(true); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; }, (err) => { AsyncClient.dispatchError(err, 'handleDelete'); diff --git a/web/react/components/do_verify_email.jsx b/web/react/components/do_verify_email.jsx new file mode 100644 index 000000000..df98bf463 --- /dev/null +++ b/web/react/components/do_verify_email.jsx @@ -0,0 +1,82 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; +import * as Client from '../utils/client.jsx'; +import LoadingScreen from './loading_screen.jsx'; + +import {browserHistory} from 'react-router'; + +export default class DoVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.state = { + verifyStatus: 'pending', + serverError: '' + }; + } + componentWillMount() { + const uid = this.props.location.query.uid; + const hid = this.props.location.query.hid; + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + Client.verifyEmail( + () => { + browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email); + }, + (err) => { + this.setState({verifyStatus: 'failure', serverError: err.message}); + }, + uid, + hid + ); + } + render() { + if (this.state.verifyStatus !== 'failure') { + return (); + } + + return ( +
+ +
+
+

+ +

+
+

+ +

+

+ + {this.state.serverError} +

+
+
+
+
+ ); + } +} + +DoVerifyEmail.defaultProps = { +}; +DoVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/docs.jsx b/web/react/components/docs.jsx deleted file mode 100644 index 6d3a109c2..000000000 --- a/web/react/components/docs.jsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as TextFormatting from '../utils/text_formatting.jsx'; -import UserStore from '../stores/user_store.jsx'; - -export default class Docs extends React.Component { - constructor(props) { - super(props); - UserStore.setCurrentUser(global.window.mm_user || {}); - - this.state = {text: ''}; - const errorState = {text: '## 404'}; - - if (props.site) { - $.get(`/static/help/${props.site}_${global.window.mm_locale}.md`).then((response) => { - this.setState({text: response}); - }, () => { - this.setState(errorState); - }); - } else { - this.setState(errorState); - } - } - - render() { - return ( -
-
- ); - } -} - -Docs.defaultProps = { - site: '' -}; -Docs.propTypes = { - site: React.PropTypes.string -}; diff --git a/web/react/components/edit_post_modal.jsx b/web/react/components/edit_post_modal.jsx index 380ca7bde..f02239fcf 100644 --- a/web/react/components/edit_post_modal.jsx +++ b/web/react/components/edit_post_modal.jsx @@ -3,7 +3,7 @@ import * as Client from '../utils/client.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Textbox from './textbox.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import PostStore from '../stores/post_store.jsx'; @@ -45,7 +45,7 @@ class EditPostModal extends React.Component { delete tempState.editText; BrowserStore.setItem('edit_state_transfer', tempState); $('#edit_post').modal('hide'); - EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); + GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments); return; } diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx deleted file mode 100644 index 702a20eba..000000000 --- a/web/react/components/email_verify.jsx +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -export default class EmailVerify extends React.Component { - constructor(props) { - super(props); - - this.handleResend = this.handleResend.bind(this); - - this.state = {}; - } - handleResend() { - const newAddress = window.location.href.replace('&resend_success=true', ''); - window.location.href = newAddress + '&resend=true'; - } - render() { - var title = ''; - var body = ''; - var resend = ''; - var resendConfirm = ''; - if (this.props.isVerified === 'true') { - title = ( - - ); - body = ( - - ); - body = ( -

- -

- ); - resend = ( - - ); - if (this.props.resendSuccess) { - resendConfirm = ( -

- -

); - } - } - - return ( -
-
-

{title}

-
- {body} - {resend} - {resendConfirm} -
-
-
- ); - } -} - -EmailVerify.defaultProps = { - isVerified: 'false', - teamURL: '', - userEmail: '', - resendSuccess: 'false' -}; -EmailVerify.propTypes = { - isVerified: React.PropTypes.string, - teamURL: React.PropTypes.string, - userEmail: React.PropTypes.string, - resendSuccess: React.PropTypes.string -}; diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 2f6067b86..8abcac8c3 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -43,7 +43,7 @@ class FileAttachment extends React.Component { if (type === 'image') { var self = this; // Need this reference since we use the given "this" - $('').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) { + $('').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { return function loader() { $(this).remove(); if (name in self.refs) { @@ -114,7 +114,7 @@ class FileAttachment extends React.Component { var re3 = new RegExp('\\)', 'g'); var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); } } removeBackgroundImage(name) { diff --git a/web/react/components/find_team.jsx b/web/react/components/find_team.jsx deleted file mode 100644 index 3ff9787ad..000000000 --- a/web/react/components/find_team.jsx +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as utils from '../utils/utils.jsx'; -import * as client from '../utils/client.jsx'; - -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -var holders = defineMessages({ - submitError: { - id: 'find_team.submitError', - defaultMessage: 'Please enter a valid email address' - }, - placeholder: { - id: 'find_team.placeholder', - defaultMessage: 'you@domain.com' - } -}); - -class FindTeam extends React.Component { - constructor(props) { - super(props); - this.state = {}; - - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - - var state = { }; - - var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); - if (!email || !utils.isEmail(email)) { - state.email_error = this.props.intl.formatMessage(holders.submitError); - this.setState(state); - return; - } - - state.email_error = ''; - - client.findTeamsSendEmail(email, - function success() { - state.sent = true; - this.setState(state); - }.bind(this), - function fail(err) { - state.email_error = err.message; - this.setState(state); - }.bind(this) - ); - } - - render() { - var emailError = null; - var emailErrorClass = 'form-group'; - - if (this.state.email_error) { - emailError = ; - emailErrorClass = 'form-group has-error'; - } - - if (this.state.sent) { - return ( -
-

- -

-

- -

-
- ); - } - - return ( -
-

- -

-
-

- -

-
- -
- - {emailError} -
-
- -
-
- ); - } -} - -FindTeam.propTypes = { - intl: intlShape.isRequired -}; - -export default injectIntl(FindTeam); \ No newline at end of file diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 184ba1357..71cd5b8b6 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -5,7 +5,7 @@ import * as utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; import * as Client from '../utils/client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ModalStore from '../stores/modal_store.jsx'; import UserStore from '../stores/user_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component { showGetTeamInviteLinkModal() { this.handleHide(false); - EventHelpers.showGetTeamInviteLinkModal(); + GlobalActions.showGetTeamInviteLinkModal(); } render() { diff --git a/web/react/components/logged_in.jsx b/web/react/components/logged_in.jsx new file mode 100644 index 000000000..1ed3694e9 --- /dev/null +++ b/web/react/components/logged_in.jsx @@ -0,0 +1,224 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as AsyncClient from '../utils/async_client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import UserStore from '../stores/user_store.jsx'; +import SocketStore from '../stores/socket_store.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; +import * as Utils from '../utils/utils.jsx'; +import Constants from '../utils/constants.jsx'; +import ErrorBar from '../components/error_bar.jsx'; + +import {browserHistory} from 'react-router'; + +import SidebarRight from '../components/sidebar_right.jsx'; +import SidebarRightMenu from '../components/sidebar_right_menu.jsx'; + +// Modals +import GetPostLinkModal from '../components/get_post_link_modal.jsx'; +import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; +import EditPostModal from '../components/edit_post_modal.jsx'; +import DeletePostModal from '../components/delete_post_modal.jsx'; +import MoreChannelsModal from '../components/more_channels.jsx'; +import TeamSettingsModal from '../components/team_settings_modal.jsx'; +import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; +import RegisterAppModal from '../components/register_app_modal.jsx'; +import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx'; +import InviteMemberModal from '../components/invite_member_modal.jsx'; +import SelectTeamModal from '../components/admin_console/select_team_modal.jsx'; + +const CLIENT_STATUS_INTERVAL = 30000; +const BACKSPACE_CHAR = 8; + +export default class LoggedIn extends React.Component { + constructor(params) { + super(params); + + this.onUserChanged = this.onUserChanged.bind(this); + } + onUserChanged() { + // Grab the current user + const user = UserStore.getCurrentUser(); + + // Update segment indentify + if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') { + global.window.analytics.identify(user.id, { + name: user.nickname, + email: user.email, + createdAt: user.create_at, + username: user.username, + team_id: user.team_id, + id: user.id + }); + } + + // Update CSS classes to match user theme + if (user) { + if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) { + Utils.applyTheme(user.theme_props); + } else { + Utils.applyTheme(Constants.THEMES.default); + } + } + } + onSocketChange(msg) { + if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) { + UserStore.setStatus(msg.user_id, 'online'); + } + } + componentWillMount() { + // Emit view action + GlobalActions.viewLoggedIn(); + + // Listen for user + UserStore.addChangeListener(this.onUserChanged); + + // Add listner for socker store + SocketStore.addChangeListener(this.onSocketChange); + + // Get all statuses regularally. (Soon to be switched to websocket) + this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL); + + // Force logout of all tabs if one tab is logged out + $(window).bind('storage', (e) => { + // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out + if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { + // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) + if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) { + return; + } + + console.log('detected logout from a different tab'); //eslint-disable-line no-console + browserHistory.push('/' + this.props.params.team); + } + + if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) { + // make sure it isn't this tab that is sending the logout signal (only necessary for IE11) + if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) { + return; + } + + console.log('detected login from a different tab'); //eslint-disable-line no-console + location.reload(); + } + }); + + // Because current CSS requires the root tag to have specific stuff + $('#root').attr('class', 'channel-view'); + + // ??? + $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before'); + } + }); + + $('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before'); + } else { + $(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after'); + $(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before'); + } + }); + + $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { + if (ev.type === 'mouseenter') { + $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment'); + } else { + $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); + $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); + } + }); + + // Device tracking setup + var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); + if (iOS) { + $('body').addClass('ios'); + } + + // Set up tracking for whether the window is active + window.isActive = true; + $(window).on('focus', () => { + AsyncClient.updateLastViewedAt(); + ChannelStore.resetCounts(ChannelStore.getCurrentId()); + ChannelStore.emitChange(); + window.isActive = true; + }); + $(window).on('blur', () => { + window.isActive = false; + }); + + // if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx + const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT); + Utils.applyFont(selectedFont); + + // Pervent backspace from navigating back a page + $(window).on('keydown.preventBackspace', (e) => { + if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) { + e.preventDefault(); + } + }); + } + componentWillUnmount() { + $('#root').attr('class', ''); + clearInterval(this.intervalId); + + $(window).off('focus'); + $(window).off('blur'); + + SocketStore.removeChangeListener(this.onSocketChange); + UserStore.removeChangeListener(this.onUserChanged); + + $('body').off('click.userpopover'); + $('body').off('mouseenter mouseleave', '.post'); + $('body').off('mouseenter mouseleave', '.post.post--comment.same--root'); + + $('.modal').off('show.bs.modal'); + + $(window).off('keydown.preventBackspace'); + } + render() { + return ( +
+ +
+ + + {this.props.sidebar} + {this.props.center} + + + + + + + + + + + + +
+
+ ); + } +} + +LoggedIn.defaultProps = { +}; + +LoggedIn.propTypes = { + children: React.PropTypes.object, + sidebar: React.PropTypes.object, + center: React.PropTypes.object, + params: React.PropTypes.object +}; diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 581b8e0b5..30c8ffe4f 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -6,82 +6,118 @@ import LoginUsername from './login_username.jsx'; import LoginLdap from './login_ldap.jsx'; import * as Utils from '../utils/utils.jsx'; +import * as Client from '../utils/client.jsx'; import Constants from '../utils/constants.jsx'; +import TeamStore from '../stores/team_store.jsx'; import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; export default class Login extends React.Component { constructor(props) { super(props); - this.state = {}; + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onTeamChange = this.onTeamChange.bind(this); + + this.state = this.getStateFromStores(); + } + componentDidMount() { + TeamStore.addChangeListener(this.onTeamChange); + Client.getMeLoggedIn((data) => { + if (data && data.logged_in !== 'false') { + browserHistory.push('/' + this.props.params.team + '/channels/town-square'); + } + }); + } + componentWillUnmount() { + TeamStore.removeChangeListener(this.onTeamChange); + } + getStateFromStores() { + return { + currentTeam: TeamStore.getByName(this.props.params.team) + }; + } + onTeamChange() { + this.setState(this.getStateFromStores()); } render() { - const teamDisplayName = this.props.teamDisplayName; - const teamName = this.props.teamName; + const currentTeam = this.state.currentTeam; + if (currentTeam == null) { + return
; + } + + const teamDisplayName = currentTeam.display_name; + const teamName = currentTeam.name; let loginMessage = []; if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { loginMessage.push( - - - - - - + + + + + + ); } if (global.window.mm_config.EnableSignUpWithGoogle === 'true') { loginMessage.push( - - - - - - - ); + + + + + + + ); } const extraParam = Utils.getUrlParameter('extra'); let extraBox = ''; if (extraParam) { - let msg; if (extraParam === Constants.SIGNIN_CHANGE) { - msg = ( - + extraBox = ( +
+ + +
); } else if (extraParam === Constants.SIGNIN_VERIFIED) { - msg = ( - - ); - } - - if (msg != null) { extraBox = (
- {msg} + +
+ ); + } else if (extraParam === Constants.SESSION_EXPIRED) { + extraBox = ( +
+ +
); } @@ -91,7 +127,7 @@ export default class Login extends React.Component { if (global.window.mm_config.EnableSignInWithEmail === 'true') { emailSignup = ( ); } @@ -125,7 +161,7 @@ export default class Login extends React.Component { } let userSignUp = null; - if (this.props.inviteId) { + if (currentTeam.allow_open_invite) { userSignUp = (
@@ -134,7 +170,7 @@ export default class Login extends React.Component { defaultMessage="Don't have an account? " /> ); } - let findTeams = null; - if (!Utils.isMobileApp()) { - findTeams = ( - - ); - } - let usernameLogin = null; if (global.window.mm_config.EnableSignInWithUsername === 'true') { usernameLogin = ( ); } return ( -
-
- -
-

{teamDisplayName}

-

- -

- {extraBox} - {loginMessage} - {emailSignup} - {usernameLogin} - {ldapLogin} - {userSignUp} - {findTeams} - {forgotPassword} - {teamSignUp} +
+ +
+
+
+ +
+

{teamDisplayName}

+

+ +

+ {extraBox} + {loginMessage} + {emailSignup} + {usernameLogin} + {ldapLogin} + {userSignUp} + {forgotPassword} + {teamSignUp} +
+
); } } Login.defaultProps = { - teamName: '', - teamDisplayName: '' }; Login.propTypes = { - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - inviteId: React.PropTypes.string + params: React.PropTypes.object.isRequired }; diff --git a/web/react/components/login_email.jsx b/web/react/components/login_email.jsx index cf1e1bc40..3e0d8919d 100644 --- a/web/react/components/login_email.jsx +++ b/web/react/components/login_email.jsx @@ -4,6 +4,7 @@ import * as Utils from '../utils/utils.jsx'; import * as Client from '../utils/client.jsx'; import UserStore from '../stores/user_store.jsx'; +import {browserHistory} from 'react-router'; import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; @@ -72,13 +73,7 @@ class LoginEmail extends React.Component { Client.loginByEmail(name, email, password, () => { UserStore.setLastEmail(email); - - const redirect = Utils.getUrlParameter('redirect'); - if (redirect) { - window.location.href = decodeURIComponent(redirect); - } else { - window.location.href = '/' + name + '/channels/town-square'; - } + browserHistory.push('/' + name + '/channels/town-square'); }, (err) => { if (err.id === 'api.user.login.not_verified.app_error') { @@ -167,4 +162,4 @@ LoginEmail.propTypes = { teamName: React.PropTypes.string.isRequired }; -export default injectIntl(LoginEmail); \ No newline at end of file +export default injectIntl(LoginEmail); diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 93fe6c05a..974f026d0 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -56,9 +56,13 @@ export default class Navbar extends React.Component { return { channel: ChannelStore.getCurrent(), member: ChannelStore.getCurrentMember(), - users: ChannelStore.getCurrentExtraInfo().members + users: ChannelStore.getCurrentExtraInfo().members, + currentUser: UserStore.getCurrentUser() }; } + stateValid() { + return this.state.channel && this.state.member && this.state.users && this.state.currentUser; + } componentDidMount() { ChannelStore.addChangeListener(this.onChange); ChannelStore.addExtraInfoChangeListener(this.onChange); @@ -201,7 +205,7 @@ export default class Navbar extends React.Component { { $('.sidebar--left .dropdown-menu').scrollTop(0); this.blockToggle = true; @@ -67,24 +43,15 @@ export default class NavbarDropdown extends React.Component { }); } componentWillUnmount() { - UserStore.removeTeamsChangeListener(this.onListenerChange); - TeamStore.removeChangeListener(this.onListenerChange); - $(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown'); } - onListenerChange() { - var newState = getStateFromStores(); - if (!Utils.areObjectsEqual(newState, this.state)) { - this.setState(newState); - } - } render() { var teamLink = ''; var inviteLink = ''; var manageLink = ''; var sysAdminLink = ''; var adminDivider = ''; - var currentUser = UserStore.getCurrentUser(); + var currentUser = this.props.currentUser; var isAdmin = false; var isSystemAdmin = false; var teamSettings = null; @@ -97,7 +64,7 @@ export default class NavbarDropdown extends React.Component {
  • 1) { - teams.push( -
  • -
  • - ); - - this.state.teams.forEach((team) => { - if (team.name !== this.props.teamName) { - teams.push( -
  • - -
  • ); - } - }); - } - if (global.window.mm_config.EnableTeamCreation === 'true') { teams.push(
  • @@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component { {inviteLink} {teamLink}
  • - + - +
  • {adminDivider} {teamSettings} @@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = { NavbarDropdown.propTypes = { teamType: React.PropTypes.string, teamDisplayName: React.PropTypes.string, - teamName: React.PropTypes.string + teamName: React.PropTypes.string, + currentUser: React.PropTypes.object }; diff --git a/web/react/components/needs_team.jsx b/web/react/components/needs_team.jsx new file mode 100644 index 000000000..33b9cd37e --- /dev/null +++ b/web/react/components/needs_team.jsx @@ -0,0 +1,20 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as GlobalActions from '../action_creators/global_actions.jsx'; + +export default class NeedsTeam extends React.Component { + componentWillMount() { + GlobalActions.loadTeamRequiredPage(); + } + render() { + return this.props.children; + } +} + +NeedsTeam.defaultProps = { +}; + +NeedsTeam.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/not_logged_in.jsx b/web/react/components/not_logged_in.jsx new file mode 100644 index 000000000..7af293e77 --- /dev/null +++ b/web/react/components/not_logged_in.jsx @@ -0,0 +1,70 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; + +export default class NotLoggedIn extends React.Component { + componentDidMount() { + $('body').attr('class', 'white'); + $('#root').attr('class', 'container-fluid'); + } + componentWillUnmount() { + $('body').attr('class', ''); + $('#root').attr('class', ''); + } + render() { + return ( +
    +
    + {this.props.children} +
    +
    +
    +
    +
    + {global.window.mm_config.SiteName} +
    +
    + {'© 2015 Mattermost, Inc.'} + + + + + + + + + + + + +
    +
    +
    +
    + ); + } +} + +NotLoggedIn.defaultProps = { +}; + +NotLoggedIn.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/password_reset.jsx b/web/react/components/password_reset.jsx deleted file mode 100644 index 4c9bb6310..000000000 --- a/web/react/components/password_reset.jsx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PasswordResetSendLink from './password_reset_send_link.jsx'; -import PasswordResetForm from './password_reset_form.jsx'; - -export default class PasswordReset extends React.Component { - constructor(props) { - super(props); - - this.state = {}; - } - render() { - if (this.props.isReset === 'false') { - return ( - - ); - } - - return ( - - ); - } -} - -PasswordReset.defaultProps = { - isReset: '', - teamName: '', - teamDisplayName: '', - hash: '', - data: '' -}; -PasswordReset.propTypes = { - isReset: React.PropTypes.string, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - hash: React.PropTypes.string, - data: React.PropTypes.string -}; diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx index 380dbe973..cfd39e440 100644 --- a/web/react/components/password_reset_form.jsx +++ b/web/react/components/password_reset_form.jsx @@ -2,24 +2,11 @@ // See License.txt for license information. import * as Client from '../utils/client.jsx'; +import * as Utils from '../utils/utils.jsx'; import Constants from '../utils/constants.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; - -const holders = defineMessages({ - error: { - id: 'password_form.error', - defaultMessage: 'Please enter at least {chars} characters.' - }, - update: { - id: 'password_form.update', - defaultMessage: 'Your password has been updated successfully.' - }, - pwd: { - id: 'password_form.pwd', - defaultMessage: 'Password' - } -}); +import {FormattedMessage} from 'mm-intl'; +import {browserHistory} from 'react-router'; class PasswordResetForm extends React.Component { constructor(props) { @@ -32,51 +19,50 @@ class PasswordResetForm extends React.Component { handlePasswordReset(e) { e.preventDefault(); - const {formatMessage} = this.props.intl; - var state = {}; - - var password = ReactDOM.findDOMNode(this.refs.password).value.trim(); + const password = ReactDOM.findDOMNode(this.refs.password).value.trim(); if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) { - state.error = formatMessage(holders.error, {chars: Constants.MIN_PASSWORD_LENGTH}); - this.setState(state); + this.setState({ + error: ( + + ) + }); return; } - state.error = null; - this.setState(state); + this.setState({ + error: null + }); - var data = {}; + const data = {}; data.new_password = password; - data.hash = this.props.hash; - data.data = this.props.data; - data.name = this.props.teamName; + data.hash = this.props.location.query.h; + data.data = this.props.location.query.d; + data.name = this.props.params.team; Client.resetPassword(data, - function resetSuccess() { - this.setState({error: null, updateText: formatMessage(holders.update)}); - }.bind(this), - function resetFailure(err) { - this.setState({error: err.message, updateText: null}); - }.bind(this) + () => { + this.setState({error: null}); + browserHistory.push('/' + this.props.params.team + '/login'); + }, + (err) => { + this.setState({error: err.message}); + } ); } render() { - var updateText = null; - if (this.state.updateText) { - updateText = (

    ); - } - var error = null; if (this.state.error) { - error =
    ; + error = ( +
    + +
    + ); } var formClass = 'form-group'; @@ -84,7 +70,6 @@ class PasswordResetForm extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return (
    @@ -98,9 +83,8 @@ class PasswordResetForm extends React.Component {

    @@ -111,7 +95,10 @@ class PasswordResetForm extends React.Component { className='form-control' name='password' ref='password' - placeholder={formatMessage(holders.pwd)} + placeholder={Utils.localizeMessage( + 'password_form.pwd', + 'Password' + )} spellCheck='false' />

    @@ -125,7 +112,6 @@ class PasswordResetForm extends React.Component { defaultMessage='Change my password' /> - {updateText}
    @@ -134,17 +120,10 @@ class PasswordResetForm extends React.Component { } PasswordResetForm.defaultProps = { - teamName: '', - teamDisplayName: '', - hash: '', - data: '' }; PasswordResetForm.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - hash: React.PropTypes.string, - data: React.PropTypes.string + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetForm); \ No newline at end of file +export default PasswordResetForm; diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx index 8cc8a050d..ce6253e16 100644 --- a/web/react/components/password_reset_send_link.jsx +++ b/web/react/components/password_reset_send_link.jsx @@ -4,26 +4,7 @@ import * as Utils from '../utils/utils.jsx'; import * as client from '../utils/client.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -const holders = defineMessages({ - error: { - id: 'password_send.error', - defaultMessage: 'Please enter a valid email address.' - }, - link: { - id: 'password_send.link', - defaultMessage: '

    A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

    ' - }, - checkInbox: { - id: 'password_send.checkInbox', - defaultMessage: 'Please check your inbox.' - }, - email: { - id: 'password_send.email', - defaultMessage: 'Email' - } -}); +import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; class PasswordResetSendLink extends React.Component { constructor(props) { @@ -31,48 +12,64 @@ class PasswordResetSendLink extends React.Component { this.handleSendLink = this.handleSendLink.bind(this); - this.state = {}; + this.state = { + error: '', + updateText: '' + }; } handleSendLink(e) { e.preventDefault(); - var state = {}; - const {formatMessage} = this.props.intl; var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); if (!email || !Utils.isEmail(email)) { - state.error = formatMessage(holders.error); - this.setState(state); + this.setState({ + error: ( + + ) + }); return; } - state.error = null; - this.setState(state); + // End of error checking clear error + this.setState({ + error: '' + }); var data = {}; data.email = email; - data.name = this.props.teamName; - + data.name = this.props.params.team; client.sendPasswordReset(data, - function passwordResetSent() { - this.setState({error: null, updateText: formatMessage(holders.link, {email: email, teamDisplayName: this.props.teamDisplayName, hostname: window.location.hostname}), moreUpdateText: formatMessage(holders.checkInbox)}); - $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); - }.bind(this), - function passwordResetFailedToSend(err) { - this.setState({error: err.message, update_text: null, moreUpdateText: null}); - }.bind(this) - ); + () => { + this.setState({ + error: null, + updateText: ( +
    + + +
    + ) + }); + $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); + }, + (err) => { + this.setState({ + error: err.message, + update_text: null + }); + } + ); } render() { - var updateText = null; - if (this.state.updateText) { - updateText = ( -
    -
    - ); - } - var error = null; if (this.state.error) { error =
    ; @@ -83,51 +80,60 @@ class PasswordResetSendLink extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return ( -
    - +
    +
    +

    -

    -
    - -
    - {error} - - +

    + +

    +
    + +
    + {error} + + +

    ); @@ -135,13 +141,9 @@ class PasswordResetSendLink extends React.Component { } PasswordResetSendLink.defaultProps = { - teamName: '', - teamDisplayName: '' }; PasswordResetSendLink.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + params: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetSendLink); \ No newline at end of file +export default PasswordResetSendLink; diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index afff78bae..1943fb409 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -118,7 +118,7 @@ export default class PopoverListMembers extends React.Component { className='profile-img rounded pull-left' width='26px' height='26px' - src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`} + src={`/api/v1/users/${m.id}/image?time=${m.update_at}`} />
    ); diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 2803fe387..966775dad 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -14,16 +14,15 @@ export default class PostHeader extends React.Component { } render() { const post = this.props.post; - const user = this.props.user; - let userProfile = ; + let userProfile = ; let botIndicator; if (post.props && post.props.from_webhook) { if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( @@ -54,6 +53,7 @@ export default class PostHeader extends React.Component { allowReply='true' isLastComment={this.props.isLastComment} sameUser={this.props.sameUser} + currentUser={this.props.currentUser} /> @@ -68,10 +68,11 @@ PostHeader.defaultProps = { sameUser: false }; PostHeader.propTypes = { - post: React.PropTypes.object, + post: React.PropTypes.object.isRequired, user: React.PropTypes.object, - commentCount: React.PropTypes.number, - isLastComment: React.PropTypes.bool, - handleCommentClick: React.PropTypes.func, - sameUser: React.PropTypes.bool + currentUser: React.PropTypes.object.isRequired, + commentCount: React.PropTypes.number.isRequired, + isLastComment: React.PropTypes.bool.isRequired, + handleCommentClick: React.PropTypes.func.isRequired, + sameUser: React.PropTypes.bool.isRequired }; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index ffac6eaef..d0a4c828e 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -1,10 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import TimeSince from './time_since.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -27,8 +26,8 @@ export default class PostInfo extends React.Component { } createDropdown() { var post = this.props.post; - var isOwner = UserStore.getCurrentId() === post.user_id; - var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); + var isOwner = this.props.currentUser.id === post.user_id; + var isAdmin = Utils.isAdmin(this.props.currentUser.roles); if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) { return ''; @@ -47,21 +46,21 @@ export default class PostInfo extends React.Component { if (this.props.allowReply === 'true') { dropdownContents.push( -
  • - - - -
  • +
  • + + + +
  • ); } @@ -93,7 +92,7 @@ export default class PostInfo extends React.Component { EventHelpers.showDeletePostModal(post, dataComments)} + onClick={() => GlobalActions.showDeletePostModal(post, dataComments)} > EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} user={profile} + currentUser={this.props.currentUser} /> ); @@ -525,7 +525,7 @@ PostsView.defaultProps = { PostsView.propTypes = { isActive: React.PropTypes.bool, postList: React.PropTypes.object, - profiles: React.PropTypes.object, + profiles: React.PropTypes.object.isRequired, scrollPostId: React.PropTypes.string, scrollType: React.PropTypes.number, postViewScrolled: React.PropTypes.func.isRequired, @@ -535,7 +535,8 @@ PostsView.propTypes = { showMoreMessagesBottom: React.PropTypes.bool, introText: React.PropTypes.element, messageSeparatorTime: React.PropTypes.number, - postsToHighlight: React.PropTypes.object + postsToHighlight: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired }; function FloatingTimestamp({isScrolling, post}) { diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 976e03fab..b361779d2 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -6,9 +6,10 @@ import LoadingScreen from './loading_screen.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; +import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -21,6 +22,7 @@ export default class PostsViewContainer extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onChannelLeave = this.onChannelLeave.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); @@ -28,7 +30,8 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const state = { scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null + scrollPost: null, + currentUser: UserStore.getCurrentUser() }; if (currentChannelId) { Object.assign(state, { @@ -54,12 +57,17 @@ export default class PostsViewContainer extends React.Component { ChannelStore.addLeaveListener(this.onChannelLeave); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeLeaveListener(this.onChannelLeave); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.removeChangeListener(this.onUserChange); + } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser()}); } handlePostsViewJumpRequest(type, post) { switch (type) { @@ -139,7 +147,7 @@ export default class PostsViewContainer extends React.Component { return PostStore.getVisiblePosts(id); } loadMorePostsTop() { - EventHelpers.emitLoadMorePostsEvent(); + GlobalActions.emitLoadMorePostsEvent(); } handlePostsViewScroll(atBottom) { if (atBottom) { @@ -165,6 +173,10 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = channels[this.state.currentChannelIndex]; const channel = ChannelStore.get(currentChannelId); + if (!this.state.currentUser || !channel) { + return null; + } + const postListCtls = []; for (let i = 0; i < channels.length; i++) { const isActive = (channels[i] === currentChannelId); @@ -185,6 +197,7 @@ export default class PostsViewContainer extends React.Component { introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} profiles={this.props.profiles} + currentUser={this.state.currentUser} /> ); if (!postLists[i] && isActive) { diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 9588809eb..9183b761f 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../utils/async_client.jsx'; var ActionTypes = Constants.ActionTypes; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; @@ -70,7 +70,7 @@ class RhsComment extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -151,7 +151,7 @@ class RhsComment extends React.Component { EventHelpers.showDeletePostModal(post, 0)} + onClick={() => GlobalActions.showDeletePostModal(post, 0)} >
    diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 023f3dd2d..fc1cd0b41 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -10,7 +10,7 @@ import * as Emoji from '../utils/emoticons.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -34,7 +34,7 @@ export default class RhsRootPost extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -142,7 +142,7 @@ export default class RhsRootPost extends React.Component { EventHelpers.showDeletePostModal(post, this.props.commentCount)} + onClick={() => GlobalActions.showDeletePostModal(post, this.props.commentCount)} > { + var l = {}; + l.level = 'ERROR'; + l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url; + + $.ajax({ + url: '/api/v1/admin/log_client', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(l) + }); + + if (window.mm_config.EnableDeveloper === 'true') { + window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'}); + window.ErrorStore.emitChange(); + } + }; + + // Ya.... + /*eslint-disable */ + if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") { + !function(){var analytics=global.window.analytics=global.window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t; + } + + return ( + + {this.props.children} + + ); + } +} +Root.defaultProps = { +}; + +Root.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 5ab864b7c..3a091bdd1 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -3,8 +3,7 @@ import UserStore from '../stores/user_store.jsx'; import UserProfile from './user_profile.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; -import * as utils from '../utils/utils.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx'; import Constants from '../utils/constants.jsx'; @@ -22,7 +21,7 @@ export default class SearchResultsItem extends React.Component { handleClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusEvent(this.props.post.id); + GlobalActions.emitPostFocusEvent(this.props.post.id); if ($(window).width() < 768) { $('.sidebar--right').removeClass('move--left'); @@ -32,7 +31,7 @@ export default class SearchResultsItem extends React.Component { handleFocusRHSClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); + GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); } render() { @@ -78,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
    diff --git a/web/react/components/should_verify_email.jsx b/web/react/components/should_verify_email.jsx new file mode 100644 index 000000000..c473fe366 --- /dev/null +++ b/web/react/components/should_verify_email.jsx @@ -0,0 +1,111 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; +import * as Client from '../utils/client.jsx'; + +export default class ShouldVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.handleResend = this.handleResend.bind(this); + + this.state = { + resendStatus: 'none' + }; + } + handleResend() { + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + this.setState({resendStatus: 'sending'}); + + Client.resendVerification(() => { + this.setState({resendStatus: 'success'}); + }, + () => { + this.setState({resendStatus: 'failure'}); + }, + teamName, + email); + } + render() { + let resendConfirm = ''; + if (this.state.resendStatus === 'success') { + resendConfirm = ( +
    +
    +

    + + +

    +
    + ); + } + + if (this.state.resendStatus === 'failure') { + resendConfirm = ( +
    +
    +

    + + +

    +
    + ); + } + + return ( +
    + +
    +
    +

    + +

    +
    +

    + +

    + + {resendConfirm} +
    +
    +
    +
    + ); + } +} + +ShouldVerifyEmail.defaultProps = { +}; +ShouldVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index c7dba306b..5c682d64b 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -129,7 +129,9 @@ export default class Sidebar extends React.Component { directChannels, hiddenDirectChannelCount, unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), - showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER + showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER, + currentTeam: TeamStore.getCurrent(), + currentUser: UserStore.getCurrentUser() }; } @@ -179,7 +181,7 @@ export default class Sidebar extends React.Component { } updateTitle() { const channel = ChannelStore.getCurrent(); - if (channel) { + if (channel && this.state.currentTeam) { let currentSiteName = ''; if (global.window.mm_config.SiteName != null) { currentSiteName = global.window.mm_config.SiteName; @@ -196,7 +198,7 @@ export default class Sidebar extends React.Component { const unread = this.getTotalUnreadCount(); const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; const unreadTitle = unread.msgs > 0 ? '* ' : ''; - document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName; + document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.state.currentTeam.display_name + ' ' + currentSiteName; } } onScroll() { @@ -401,7 +403,6 @@ export default class Sidebar extends React.Component { // set up click handler to switch channels (or create a new channel for non-existant ones) var handleClick = null; var href = '#'; - var teamURL = TeamStore.getCurrentTeamUrl(); if (!channel.fake) { handleClick = function clickHandler(e) { @@ -413,7 +414,7 @@ export default class Sidebar extends React.Component { e.preventDefault(); }; - } else if (channel.fake && teamURL) { + } else if (channel.fake) { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = Utils.getUserIdFromChannelName(channel); @@ -434,7 +435,7 @@ export default class Sidebar extends React.Component { }, () => { this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + window.location.href = '/' + this.state.currentTeam.name; } ); } @@ -497,6 +498,11 @@ export default class Sidebar extends React.Component { ); } render() { + // Check if we have all info needed to render + if (this.state.currentTeam == null || this.state.currentUser == null) { + return (
    ); + } + this.badgesActive = false; // keep track of the first and last unread channels so we can use them to set the unread indicators @@ -586,7 +592,10 @@ export default class Sidebar extends React.Component { ); return ( -
    + ); @@ -114,7 +119,7 @@ UserProfile.defaultProps = { disablePopover: false }; UserProfile.propTypes = { - user: React.PropTypes.object.isRequired, + user: React.PropTypes.object, overwriteName: React.PropTypes.string, overwriteImage: React.PropTypes.string, disablePopover: React.PropTypes.bool diff --git a/web/react/components/user_settings/manage_languages.jsx b/web/react/components/user_settings/manage_languages.jsx index 2d1c74717..6b00a65c7 100644 --- a/web/react/components/user_settings/manage_languages.jsx +++ b/web/react/components/user_settings/manage_languages.jsx @@ -5,6 +5,7 @@ import SettingItemMax from '../setting_item_max.jsx'; import * as Client from '../../utils/client.jsx'; import * as Utils from '../../utils/utils.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -41,7 +42,7 @@ export default class ManageLanguage extends React.Component { submitUser(user) { Client.updateUser(user, () => { - window.location.reload(true); + GlobalActions.newLocalizationSelected(user.locale); }, (err) => { let serverError; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 0acfd4a16..1dd564c8d 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -3,7 +3,7 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; -import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -28,7 +28,7 @@ class DeveloperTab extends React.Component { } register() { this.props.closeModal(); - EventHelpers.showRegisterAppModal(); + GlobalActions.showRegisterAppModal(); } render() { var appSection; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index b0b1c414e..235892819 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -13,7 +13,7 @@ import Constants from '../../utils/constants.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; const holders = defineMessages({ usernameReserved: { @@ -712,7 +712,7 @@ class UserSettingsGeneralTab extends React.Component { + ) }); } pictureSection = ( @@ -805,4 +812,4 @@ UserSettingsGeneralTab.propTypes = { collapseModal: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsGeneralTab); \ No newline at end of file +export default injectIntl(UserSettingsGeneralTab); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index fa3415988..0c4a3d526 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -73,27 +73,35 @@ class UserSettingsModal extends React.Component { this.updateTab = this.updateTab.bind(this); this.updateSection = this.updateSection.bind(this); + this.onUserChanged = this.onUserChanged.bind(this); this.state = { active_tab: 'general', active_section: '', showConfirmModal: false, - enforceFocus: true + enforceFocus: true, + currentUser: UserStore.getCurrentUser() }; this.requireConfirm = false; } + onUserChanged() { + this.setState({currentUser: UserStore.getCurrentUser()}); + } + componentDidMount() { if (this.props.show) { this.handleShow(); } + UserStore.addChangeListener(this.onUserChanged); } componentDidUpdate(prevProps) { if (this.props.show && !prevProps.show) { this.handleShow(); } + UserStore.removeChangeListener(this.onUserChanged); } handleShow() { @@ -235,8 +243,10 @@ class UserSettingsModal extends React.Component { render() { const {formatMessage} = this.props.intl; - var currentUser = UserStore.getCurrentUser(); - var isAdmin = Utils.isAdmin(currentUser.roles); + if (this.state.currentUser == null) { + return (
    ); + } + var isAdmin = Utils.isAdmin(this.state.currentUser.roles); var tabs = []; tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'}); diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index cba7ffdea..0b6b6c398 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; const holders = defineMessages({ currentPasswordError: { @@ -218,11 +218,24 @@ class SecurityTab extends React.Component { var describe; var d = new Date(this.props.user.last_password_update); - const locale = global.window.mm_locale; const hours12 = !Utils.isMilitaryTime(); describe = formatMessage(holders.lastUpdated, { - date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }); updateSectionStatus = function updateSection() { @@ -251,7 +264,7 @@ class SecurityTab extends React.Component {
    { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_FOCUSED_POST, - postId, - post_list: data - }); - - AsyncClient.getPostsBefore(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); - AsyncClient.getPostsAfter(postId, 0, Constants.POST_FOCUS_CONTEXT_RADIUS); - } - ); -} - -export function emitPostFocusRightHandSideFromSearch(post, isMentionSearch) { - Client.getPost( - post.channel_id, - post.id, - (data) => { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POSTS, - id: post.channel_id, - numRequested: 0, - post_list: data - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST_SELECTED, - postId: Utils.getRootId(post), - from_search: SearchStore.getSearchTerm() - }); - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_SEARCH, - results: null, - is_mention_search: isMentionSearch - }); - }, - (err) => { - AsyncClient.dispatchError(err, 'getPost'); - } - ); -} - -export function emitLoadMorePostsEvent() { - const id = ChannelStore.getCurrentId(); - loadMorePostsTop(id); -} - -export function emitLoadMorePostsFocusedTopEvent() { - const id = PostStore.getFocusedPostId(); - loadMorePostsTop(id); -} - -export function loadMorePostsTop(id) { - const earliestPostId = PostStore.getEarliestPost(id).id; - if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) { - AsyncClient.getPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE); - } -} - -export function emitLoadMorePostsFocusedBottomEvent() { - const id = PostStore.getFocusedPostId(); - const latestPostId = PostStore.getLatestPost(id).id; - AsyncClient.getPostsAfter(latestPostId, 0, Constants.POST_CHUNK_SIZE); -} - -export function emitPostRecievedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_POST, - post - }); -} - -export function emitUserPostedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.CREATE_POST, - post - }); -} - -export function emitPostDeletedEvent(post) { - AppDispatcher.handleServerAction({ - type: ActionTypes.POST_DELETED, - post - }); -} - -export function showDeletePostModal(post, commentCount = 0) { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_DELETE_POST_MODAL, - value: true, - post, - commentCount - }); -} - -export function showGetPostLinkModal(post) { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_GET_POST_LINK_MODAL, - value: true, - post - }); -} - -export function showGetTeamInviteLinkModal() { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, - value: true - }); -} - -export function showInviteMemberModal() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, - value: true - }); -} - -export function showRegisterAppModal() { - AppDispatcher.handleViewAction({ - type: ActionTypes.TOGGLE_REGISTER_APP_MODAL, - value: true - }); -} - -export function emitSuggestionPretextChanged(suggestionId, pretext) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_PRETEXT_CHANGED, - id: suggestionId, - pretext - }); -} - -export function emitSelectNextSuggestion(suggestionId) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_SELECT_NEXT, - id: suggestionId - }); -} - -export function emitSelectPreviousSuggestion(suggestionId) { - AppDispatcher.handleViewAction({ - type: ActionTypes.SUGGESTION_SELECT_PREVIOUS, - id: suggestionId - }); -} - -export function emitCompleteWordSuggestion(suggestionId, term = '') { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.SUGGESTION_COMPLETE_WORD, - id: suggestionId, - term - }); -} - -export function emitClearSuggestions(suggestionId) { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS, - id: suggestionId - }); -} - -export function emitPreferenceChangedEvent(preference) { - AppDispatcher.handleServerAction({ - type: Constants.ActionTypes.RECEIVED_PREFERENCE, - preference - }); -} - -export function emitRemovePost(post) { - AppDispatcher.handleViewAction({ - type: Constants.ActionTypes.REMOVE_POST, - post - }); -} - -export function sendEphemeralPost(message, channelId) { - const timestamp = Utils.getTimestamp(); - const post = { - id: Utils.generateId(), - user_id: '0', - channel_id: channelId || ChannelStore.getCurrentId(), - message, - type: Constants.POST_TYPE_EPHEMERAL, - create_at: timestamp, - update_at: timestamp, - filenames: [], - props: {} - }; - - emitPostRecievedEvent(post); -} diff --git a/web/react/package.json b/web/react/package.json index 07ffa0cdf..509c9967b 100644 --- a/web/react/package.json +++ b/web/react/package.json @@ -11,6 +11,8 @@ "marked": "mattermost/marked#cb85e5cc81bc7937dbb73c3c53d9532b1b97e3ca", "mm-intl": "mattermost/mm-intl#805442fd474fa40cd586ddeda404dbbe8e60626d", "object-assign": "4.0.1", + "react": "0.14.3", + "react-router": "2.0.0", "twemoji": "1.4.1" }, "devDependencies": { diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx deleted file mode 100644 index 989936d9e..000000000 --- a/web/react/pages/admin_console.jsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ErrorBar from '../components/error_bar.jsx'; -import SelectTeamModal from '../components/admin_console/select_team_modal.jsx'; -import AdminController from '../components/admin_console/admin_controller.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - -
    - - - -
    -
    - ); - } -} - -global.window.setup_admin_console_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('admin_controller') - ); -}; diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx deleted file mode 100644 index bc78c049c..000000000 --- a/web/react/pages/channel.jsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChannelView from '../components/channel_view.jsx'; -import ChannelLoader from '../components/channel_loader.jsx'; -import ErrorBar from '../components/error_bar.jsx'; -import * as Client from '../utils/client.jsx'; - -import GetPostLinkModal from '../components/get_post_link_modal.jsx'; -import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; -import EditPostModal from '../components/edit_post_modal.jsx'; -import DeletePostModal from '../components/delete_post_modal.jsx'; -import MoreChannelsModal from '../components/more_channels.jsx'; -import TeamSettingsModal from '../components/team_settings_modal.jsx'; -import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; -import RegisterAppModal from '../components/register_app_modal.jsx'; -import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx'; -import InviteMemberModal from '../components/invite_member_modal.jsx'; - -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - -
    - - - - - - - - - - - - - -
    -
    - ); - } -} - -global.window.setup_channel_page = function setup(props, team, channel) { - if (props.PostId === '') { - EventHelpers.emitChannelClickEvent(channel); - } else { - EventHelpers.emitPostFocusEvent(props.PostId); - } - - ReactDOM.render( - , - document.getElementById('channel_view') - ); -}; diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx deleted file mode 100644 index abbf72ea3..000000000 --- a/web/react/pages/claim_account.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ClaimAccount from '../components/claim/claim_account.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_claim_account_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('claim') - ); -}; \ No newline at end of file diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx deleted file mode 100644 index 2e47e3e6a..000000000 --- a/web/react/pages/docs.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import Docs from '../components/docs.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.mm_user = global.window.mm_user || {}; - -global.window.setup_documentation_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('docs') - ); -}; diff --git a/web/react/pages/find_team.jsx b/web/react/pages/find_team.jsx deleted file mode 100644 index 93394fcde..000000000 --- a/web/react/pages/find_team.jsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import FindTeam from '../components/find_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_find_team_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('find-team') - ); -}; \ No newline at end of file diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx deleted file mode 100644 index ff81c4994..000000000 --- a/web/react/pages/home.jsx +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import TeamStore from '../stores/team_store.jsx'; -import Constants from '../utils/constants.jsx'; - -function setupHomePage() { - var last = null; - if (last == null || last.length === 0) { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; - } else { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last; - } -} - -global.window.setup_home_page = setupHomePage; diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx deleted file mode 100644 index ec9080945..000000000 --- a/web/react/pages/login.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Client from '../utils/client.jsx'; -import Login from '../components/login.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_login_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('login') - ); -}; \ No newline at end of file diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx deleted file mode 100644 index 7caff5034..000000000 --- a/web/react/pages/password_reset.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PasswordReset from '../components/password_reset.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_password_reset_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('reset') - ); -}; diff --git a/web/react/pages/root.jsx b/web/react/pages/root.jsx new file mode 100644 index 000000000..d0b06e32e --- /dev/null +++ b/web/react/pages/root.jsx @@ -0,0 +1,290 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router'; +import Root from '../components/root.jsx'; +import Login from '../components/login.jsx'; +import LoggedIn from '../components/logged_in.jsx'; +import NotLoggedIn from '../components/not_logged_in.jsx'; +import NeedsTeam from '../components/needs_team.jsx'; +import PasswordResetSendLink from '../components/password_reset_send_link.jsx'; +import PasswordResetForm from '../components/password_reset_form.jsx'; +import ChannelView from '../components/channel_view.jsx'; +import Sidebar from '../components/sidebar.jsx'; +import * as AsyncClient from '../utils/async_client.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import ErrorStore from '../stores/error_store.jsx'; +import BrowserStore from '../stores/browser_store.jsx'; +import SignupTeam from '../components/signup_team.jsx'; +import * as Client from '../utils/client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; +import SignupUserComplete from '../components/signup_user_complete.jsx'; +import ShouldVerifyEmail from '../components/should_verify_email.jsx'; +import DoVerifyEmail from '../components/do_verify_email.jsx'; +import AdminConsole from '../components/admin_console/admin_controller.jsx'; +import ClaimAccount from '../components/claim/claim_account.jsx'; + +import SignupTeamComplete from '../components/signup_team_complete/components/signup_team_complete.jsx'; +import WelcomePage from '../components/signup_team_complete/components/team_signup_welcome_page.jsx'; +import TeamDisplayNamePage from '../components/signup_team_complete/components/team_signup_display_name_page.jsx'; +import TeamURLPage from '../components/signup_team_complete/components/team_signup_url_page.jsx'; +import SendInivtesPage from '../components/signup_team_complete/components/team_signup_send_invites_page.jsx'; +import UsernamePage from '../components/signup_team_complete/components/team_signup_username_page.jsx'; +import PasswordPage from '../components/signup_team_complete/components/team_signup_password_page.jsx'; +import FinishedPage from '../components/signup_team_complete/components/team_signup_finished.jsx'; + +// This is for anything that needs to be done for ALL react components. +// This runs before we start to render anything. +function preRenderSetup(callwhendone) { + const d1 = Client.getClientConfig( + (data, textStatus, xhr) => { + if (!data) { + return; + } + + global.window.mm_config = data; + + var serverVersion = xhr.getResponseHeader('X-Version-ID'); + + if (serverVersion !== BrowserStore.getLastServerVersion()) { + if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') { + BrowserStore.setLastServerVersion(serverVersion); + } else { + BrowserStore.setLastServerVersion(serverVersion); + window.location.reload(true); + console.log('Detected version update refreshing the page'); //eslint-disable-line no-console + } + } + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientConfig'); + } + ); + + const d2 = Client.getClientLicenceConfig( + (data) => { + if (!data) { + return; + } + + global.window.mm_license = data; + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientLicenceConfig'); + } + ); + + // Set these here so they don't fail in client.jsx track + global.window.analytics = {}; + global.window.analytics.page = () => { + // Do Nothing + }; + global.window.analytics.track = () => { + // Do Nothing + }; + + $.when(d1, d2).done(callwhendone); +} + +function preLoggedIn(nextState, replace, callback) { + const d1 = Client.getAllPreferences( + (data) => { + if (!data) { + return; + } + + PreferenceStore.setPreferences(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'getAllPreferences'); + } + ); + + const d2 = AsyncClient.getChannels(); + + $.when(d1, d2).done(() => callback()); +} + +function onChannelChange(nextState) { + const channelName = nextState.params.channel; + + // Make sure we have all the channels + AsyncClient.getChannels(true); + + // Get our channel's ID + const channel = ChannelStore.getByName(channelName); + + // User clicked channel + GlobalActions.emitChannelClickEvent(channel); +} + +function onRootEnter(nextState, replace, callback) { + if (nextState.location.pathname === '/') { + Client.getMeLoggedIn((data) => { + if (!data || data.logged_in === 'false') { + replace({pathname: '/signup_team'}); + callback(); + } else { + replace({pathname: '/' + data.team_name + '/channels/town-square'}); + callback(); + } + }); + return; + } + + callback(); +} + +function onPermalinkEnter(nextState) { + const postId = nextState.params.postid; + + GlobalActions.emitPostFocusEvent(postId); +} + +function onLoggedOut(nextState) { + const teamName = nextState.params.team; + Client.logout( + () => { + browserHistory.push('/' + teamName + '/login'); + BrowserStore.signalLogout(); + BrowserStore.clear(); + ErrorStore.clearLastError(); + }, + () => { + browserHistory.push('/' + teamName + '/login'); + } + ); +} + +function renderRootComponent() { + ReactDOM.render(( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + document.getElementById('root')); +} + +global.window.setup_root = () => { + // Do the pre-render setup and call renderRootComponent when done + preRenderSetup(renderRootComponent); +}; diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx deleted file mode 100644 index f276c3ff7..000000000 --- a/web/react/pages/signup_team.jsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeam from '../components/signup_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired, - teams: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_page = function setup(props) { - var teams = []; - - for (var prop in props) { - if (props.hasOwnProperty(prop)) { - if (prop !== 'Title' && prop !== 'Locale' && prop !== 'Info') { - teams.push({name: prop, display_name: props[prop]}); - } - } - } - - ReactDOM.render( - , - document.getElementById('signup-team') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx deleted file mode 100644 index 8c237f698..000000000 --- a/web/react/pages/signup_team_complete.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamComplete from '../components/signup_team_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_complete_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-team-complete') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_team_confirm.jsx b/web/react/pages/signup_team_confirm.jsx deleted file mode 100644 index 13c8f3fd0..000000000 --- a/web/react/pages/signup_team_confirm.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_confirm_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-team-confirm') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx deleted file mode 100644 index a14f2140b..000000000 --- a/web/react/pages/signup_user_complete.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupUserComplete from '../components/signup_user_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_user_complete_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-user-complete') - ); -}; \ No newline at end of file diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx deleted file mode 100644 index 6b336daa1..000000000 --- a/web/react/pages/verify.jsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import EmailVerify from '../components/email_verify.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setupVerifyPage = function setup(props) { - ReactDOM.render( - , - document.getElementById('verify') - ); -}; diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index 5c911e94b..9f7f6e7ff 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -121,7 +121,11 @@ class AdminStoreClass extends EventEmitter { } getSelectedTeams() { - return BrowserStore.getItem('seleted_teams'); + const result = BrowserStore.getItem('seleted_teams'); + if (!result) { + return {}; + } + return result; } saveSelectedTeams(teams) { @@ -156,7 +160,3 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => { }); export default AdminStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AdminStore = AdminStore; -} diff --git a/web/react/stores/analytics_store.jsx b/web/react/stores/analytics_store.jsx index 0ad989206..ec827f6d7 100644 --- a/web/react/stores/analytics_store.jsx +++ b/web/react/stores/analytics_store.jsx @@ -83,7 +83,3 @@ AnalyticsStore.dispatchToken = AppDispatcher.register((payload) => { }); export default AnalyticsStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.AnalyticsStore = AnalyticsStore; -} diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 3417faaaf..3b35916b3 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -4,8 +4,8 @@ import {generateId} from '../utils/utils.jsx'; function getPrefix() { - if (global.window.mm_user) { - return global.window.mm_user.id + '_'; + if (global.window.mm_current_user_id) { + return global.window.mm_current_user_id + '_'; } return 'unknown_'; @@ -31,7 +31,9 @@ class BrowserStoreClass { this.isSignallingLogout = this.isSignallingLogout.bind(this); this.signalLogin = this.signalLogin.bind(this); this.isSignallingLogin = this.isSignallingLogin.bind(this); + } + checkVersion() { var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { sessionStorage.clear(); diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index eac24b071..60cb10de7 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -350,7 +350,3 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => { }); export default ChannelStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ChannelStore = ChannelStore; -} diff --git a/web/react/stores/file_store.jsx b/web/react/stores/file_store.jsx index c1fd0ef74..6d7e0f354 100644 --- a/web/react/stores/file_store.jsx +++ b/web/react/stores/file_store.jsx @@ -57,9 +57,4 @@ class FileStore extends EventEmitter { } } -const instance = new FileStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.FileStore = instance; -} +export default new FileStore(); diff --git a/web/react/stores/localization_store.jsx b/web/react/stores/localization_store.jsx new file mode 100644 index 000000000..0e3a63724 --- /dev/null +++ b/web/react/stores/localization_store.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class LocalizationStoreClass extends EventEmitter { + constructor() { + super(); + + this.currentLocale = 'en'; + this.currentTranslations = null; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + setCurrentLocale(locale, translations) { + this.currentLocale = locale; + this.currentTranslations = translations; + } + + getLocale() { + return this.currentLocale; + } + + getTranslations() { + return this.currentTranslations; + } +} + +var LocalizationStore = new LocalizationStoreClass(); +LocalizationStore.setMaxListeners(0); + +LocalizationStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_LOCALE: + LocalizationStore.setCurrentLocale(action.locale, action.translations); + LocalizationStore.emitChange(); + break; + default: + } +}); + +export default LocalizationStore; diff --git a/web/react/stores/modal_store.jsx b/web/react/stores/modal_store.jsx index 1819fffc0..5ea38030b 100644 --- a/web/react/stores/modal_store.jsx +++ b/web/react/stores/modal_store.jsx @@ -45,7 +45,3 @@ class ModalStoreClass extends EventEmitter { const ModalStore = new ModalStoreClass(); export default ModalStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.ModalStore = ModalStore; -} diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 5cc3f300d..a6dfcd46f 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -608,7 +608,3 @@ function isPostListNull(pl) { return false; } - -if (window.mm_config.EnableDeveloper === 'true') { - window.PostStore = PostStore; -} diff --git a/web/react/stores/search_store.jsx b/web/react/stores/search_store.jsx index 96071665c..549f355ef 100644 --- a/web/react/stores/search_store.jsx +++ b/web/react/stores/search_store.jsx @@ -135,7 +135,3 @@ SearchStore.dispatchToken = AppDispatcher.register((payload) => { }); export default SearchStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SearchStore = SearchStore; -} diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 9b2b049b7..ad24a04cd 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -10,7 +10,7 @@ import EventEmitter from 'events'; import * as Utils from '../utils/utils.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; const SocketEvents = Constants.SocketEvents; @@ -42,10 +42,6 @@ class SocketStoreClass extends EventEmitter { return; } - if (!global.window.hasOwnProperty('mm_session_token_index')) { - return; - } - this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -54,7 +50,7 @@ class SocketStoreClass extends EventEmitter { protocol = 'wss://'; } - var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex(); + var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console @@ -204,7 +200,7 @@ class SocketStoreClass extends EventEmitter { function handleNewPostEvent(msg, translations) { // Store post const post = JSON.parse(msg.props.post); - EventHelpers.emitPostRecievedEvent(post); + GlobalActions.emitPostRecievedEvent(post); // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { @@ -291,7 +287,7 @@ function handlePostEditEvent(msg) { function handlePostDeleteEvent(msg) { const post = JSON.parse(msg.props.post); - EventHelpers.emitPostDeletedEvent(post); + GlobalActions.emitPostDeletedEvent(post); } function handleNewUserEvent() { @@ -337,7 +333,7 @@ function handleChannelViewedEvent(msg) { function handlePreferenceChangedEvent(msg) { const preference = JSON.parse(msg.props.preference); - EventHelpers.emitPreferenceChangedEvent(preference); + GlobalActions.emitPreferenceChangedEvent(preference); } var SocketStore = new SocketStoreClass(); diff --git a/web/react/stores/suggestion_store.jsx b/web/react/stores/suggestion_store.jsx index 487bae843..efd2b76ed 100644 --- a/web/react/stores/suggestion_store.jsx +++ b/web/react/stores/suggestion_store.jsx @@ -258,9 +258,4 @@ class SuggestionStore extends EventEmitter { } } -const instance = new SuggestionStore(); -export default instance; - -if (window.mm_config.EnableDeveloper === 'true') { - window.SuggestionStore = instance; -} +export default new SuggestionStore(); diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 493d6bc4d..354a07b72 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -6,7 +6,6 @@ import EventEmitter from 'events'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import BrowserStore from '../stores/browser_store.jsx'; const CHANGE_EVENT = 'change'; @@ -33,6 +32,9 @@ class TeamStoreClass extends EventEmitter { this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this); this.saveTeam = this.saveTeam.bind(this); + + this.teams = {}; + this.currentTeamId = ''; } emitChange() { @@ -65,11 +67,11 @@ class TeamStoreClass extends EventEmitter { } getAll() { - return BrowserStore.getItem('user_teams', {}); + return this.teams; } getCurrentId() { - var team = global.window.mm_team; + var team = this.get(this.currentTeamId); if (team) { return team.id; @@ -79,11 +81,13 @@ class TeamStoreClass extends EventEmitter { } getCurrent() { - if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) { - this.saveTeam(global.window.mm_team); + const team = this.teams[this.currentTeamId]; + + if (team) { + return team; } - return global.window.mm_team; + return null; } getCurrentTeamUrl() { @@ -104,9 +108,16 @@ class TeamStoreClass extends EventEmitter { } saveTeam(team) { - var teams = this.getAll(); - teams[team.id] = team; - BrowserStore.setItem('user_teams', teams); + this.teams[team.id] = team; + } + + saveTeams(teams) { + this.teams = teams; + } + + saveMyTeam(team) { + this.saveTeam(team); + this.currentTeamId = team.id; } } @@ -116,17 +127,16 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECEIVED_TEAM: - TeamStore.saveTeam(action.team); + case ActionTypes.RECEIVED_MY_TEAM: + TeamStore.saveMyTeam(action.team); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAMS: + TeamStore.saveTeams(action.teams); TeamStore.emitChange(); break; - default: } }); export default TeamStore; - -if (window.mm_config.EnableDeveloper === 'true') { - window.TeamStore = TeamStore; -} diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 9fcd2440e..c1e5c75dc 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -11,13 +11,13 @@ import BrowserStore from './browser_store.jsx'; const CHANGE_EVENT = 'change'; const CHANGE_EVENT_SESSIONS = 'change_sessions'; const CHANGE_EVENT_AUDITS = 'change_audits'; -const CHANGE_EVENT_TEAMS = 'change_teams'; const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); this.profileCache = null; + this.currentUserId = ''; } emitChange(userId) { @@ -56,18 +56,6 @@ class UserStoreClass extends EventEmitter { this.removeListener(CHANGE_EVENT_AUDITS, callback); } - emitTeamsChange() { - this.emit(CHANGE_EVENT_TEAMS); - } - - addTeamsChangeListener(callback) { - this.on(CHANGE_EVENT_TEAMS, callback); - } - - removeTeamsChangeListener(callback) { - this.removeListener(CHANGE_EVENT_TEAMS, callback); - } - emitStatusesChange() { this.emit(CHANGE_EVENT_STATUSES); } @@ -81,26 +69,17 @@ class UserStoreClass extends EventEmitter { } getCurrentUser() { - if (this.getProfiles()[global.window.mm_user.id] == null) { - this.saveProfile(global.window.mm_user); - } - - return global.window.mm_user; + return this.getProfiles()[this.currentUserId]; } setCurrentUser(user) { - var oldUser = global.window.mm_user; - - if (oldUser.id === user.id) { - global.window.mm_user = user; - this.saveProfile(user); - } else { - throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id); - } + this.saveProfile(user); + this.currentUserId = user.id; + global.window.mm_current_user_id = this.currentUserId; } getCurrentId() { - var user = global.window.mm_user; + var user = this.getCurrentUser(); if (user) { return user.id; @@ -200,11 +179,22 @@ class UserStoreClass extends EventEmitter { saveProfiles(profiles) { const currentId = this.getCurrentId(); - if (currentId in profiles) { - delete profiles[currentId]; + if (this.profileCache) { + const currentUser = this.profileCache[currentId]; + if (currentUser) { + if (currentId in profiles) { + delete profiles[currentId]; + } + + this.profileCache = profiles; + this.profileCache[currentId] = currentUser; + } else { + this.profileCache = profiles; + } + } else { + this.profileCache = profiles; } - this.profileCache = profiles; BrowserStore.setItem('profiles', profiles); } @@ -224,14 +214,6 @@ class UserStoreClass extends EventEmitter { return BrowserStore.getItem('audits', {loading: true}); } - setTeams(teams) { - BrowserStore.setItem('teams', teams); - } - - getTeams() { - return BrowserStore.getItem('teams', []); - } - getCurrentMentionKeys() { return this.getMentionKeys(this.getCurrentId()); } @@ -312,10 +294,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { UserStore.setAudits(action.audits); UserStore.emitAuditsChange(); break; - case ActionTypes.RECEIVED_TEAMS: - UserStore.setTeams(action.teams); - UserStore.emitTeamsChange(); - break; case ActionTypes.RECEIVED_STATUSES: UserStore.pSetStatuses(action.statuses); UserStore.emitStatusesChange(); @@ -325,7 +303,3 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { }); export {UserStore as default}; - -if (window.mm_config.EnableDeveloper === 'true') { - window.UserStore = UserStore; -} diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 7d5e1bd0f..b9770a6e9 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import * as client from './client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -44,15 +45,19 @@ function isCallInProgress(callName) { export function getChannels(checkVersion) { if (isCallInProgress('getChannels')) { - return; + return null; } callTracker.getChannels = utils.getTimestamp(); - client.getChannels( + return client.getChannels( (data, textStatus, xhr) => { callTracker.getChannels = 0; + if (xhr.status === 304 || !data) { + return; + } + if (checkVersion) { var serverVersion = xhr.getResponseHeader('X-Version-ID'); @@ -67,10 +72,6 @@ export function getChannels(checkVersion) { } } - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNELS, channels: data.channels, @@ -392,36 +393,6 @@ export function getAllTeams() { ); } -export function findTeams(email) { - if (isCallInProgress('findTeams_' + email)) { - return; - } - - var user = UserStore.getCurrentUser(); - if (user) { - callTracker['findTeams_' + email] = utils.getTimestamp(); - client.findTeams( - user.email, - function findTeamsSuccess(data, textStatus, xhr) { - callTracker['findTeams_' + email] = 0; - - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAMS, - teams: data - }); - }, - function findTeamsFailure(err) { - callTracker['findTeams_' + email] = 0; - dispatchError(err, 'findTeams'); - } - ); - } -} - export function search(terms) { if (isCallInProgress('search_' + String(terms))) { return; @@ -645,11 +616,11 @@ export function getPostsAfter(postId, offset, numPost) { export function getMe() { if (isCallInProgress('getMe')) { - return; + return null; } callTracker.getMe = utils.getTimestamp(); - client.getMe( + return client.getMe( (data, textStatus, xhr) => { callTracker.getMe = 0; @@ -661,6 +632,8 @@ export function getMe() { type: ActionTypes.RECEIVED_ME, me: data }); + + GlobalActions.newLocalizationSelected(data.locale); }, (err) => { callTracker.getMe = 0; @@ -706,11 +679,11 @@ export function getStatuses() { export function getMyTeam() { if (isCallInProgress('getMyTeam')) { - return; + return null; } callTracker.getMyTeam = utils.getTimestamp(); - client.getMyTeam( + return client.getMyTeam( function getMyTeamSuccess(data, textStatus, xhr) { callTracker.getMyTeam = 0; @@ -719,7 +692,7 @@ export function getMyTeam() { } AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAM, + type: ActionTypes.RECEIVED_MY_TEAM, team: data }); }, diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx index ed94f94b8..94f3f0ce0 100644 --- a/web/react/utils/channel_intro_messages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -8,8 +8,7 @@ import ToggleModalButton from '../components/toggle_modal_button.jsx'; import UserProfile from '../components/user_profile.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import Constants from '../utils/constants.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'mm-intl'; @@ -40,7 +39,7 @@ export function createDMIntroMessage(channel) {
    @@ -93,37 +92,19 @@ export function createOffTopicIntroMessage(channel) { } export function createDefaultIntroMessage(channel) { - const team = TeamStore.getCurrent(); - let inviteModalLink; - if (team.type === Constants.INVITE_TEAM) { - inviteModalLink = ( - - - - - ); - } else { - inviteModalLink = ( - - - - - ); - } + const inviteModalLink = ( + + + + + ); return (
    diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 76d42137a..e00f28a14 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1,8 +1,8 @@ // See License.txt for license information. import BrowserStore from '../stores/browser_store.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import ErrorStore from '../stores/error_store.jsx'; + +import {browserHistory} from 'react-router'; let translations = { connectionError: 'There appears to be a problem with your internet connection.', @@ -50,10 +50,10 @@ function handleError(methodName, xhr, status, err) { if (xhr.status === 401) { if (window.location.href.indexOf('/channels') === 0) { - window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } else { - var teamURL = window.location.href.split('/channels')[0]; - window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + var teamURL = window.location.pathname.split('/channels')[0]; + browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } } @@ -289,13 +289,17 @@ export function switchToEmail(data, success, error) { track('api', 'api_users_switch_to_email'); } -export function logout() { +export function logout(success, error) { track('api', 'api_users_logout'); - var currentTeamUrl = TeamStore.getCurrentTeamUrl(); - BrowserStore.signalLogout(); - BrowserStore.clear(); - ErrorStore.clearLastError(); - window.location.href = currentTeamUrl + '/logout'; + $.ajax({ + url: '/api/v1/users/logout', + type: 'POST', + success, + error: function onError(xhr, status, err) { + var e = handleError('logout', xhr, status, err); + error(e); + } + }); } export function loginByEmail(name, email, password, success, error) { @@ -437,7 +441,7 @@ export function getServerAudits(success, error) { } export function getConfig(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/admin/config', dataType: 'json', contentType: 'application/json', @@ -457,7 +461,6 @@ export function getAnalytics(name, teamId, success, error) { } else { url += teamId + '/' + name; } - $.ajax({ url, dataType: 'json', @@ -471,6 +474,34 @@ export function getAnalytics(name, teamId, success, error) { }); } +export function getClientConfig(success, error) { + return $.ajax({ + url: '/api/v1/admin/client_props', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientConfig', xhr, status, err); + error(e); + } + }); +} + +export function getTeamAnalytics(teamId, name, success, error) { + $.ajax({ + url: '/api/v1/admin/analytics/' + teamId + '/' + name, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getTeamAnalytics', xhr, status, err); + error(e); + } + }); +} + export function saveConfig(config, success, error) { $.ajax({ url: '/api/v1/admin/save_config', @@ -529,6 +560,21 @@ export function getAllTeams(success, error) { }); } +export function getMeLoggedIn(success, error) { + return $.ajax({ + cache: false, + url: '/api/v1/users/me_logged_in', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getMeLoggedIn', xhr, status, err); + error(e); + } + }); +} + export function getMe(success, error) { var currentUser = null; $.ajax({ @@ -635,38 +681,6 @@ export function findTeamByName(teamName, success, error) { }); } -export function findTeamsSendEmail(email, success, error) { - $.ajax({ - url: '/api/v1/teams/email_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeamsSendEmail', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_email_teams'); -} - -export function findTeams(email, success, error) { - $.ajax({ - url: '/api/v1/teams/find_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeams', xhr, status, err); - error(e); - } - }); -} - export function createChannel(channel, success, error) { $.ajax({ url: '/api/v1/channels/create', @@ -835,7 +849,7 @@ export function updateLastViewedAt(channelId, success, error) { } export function getChannels(success, error) { - $.ajax({ + return $.ajax({ cache: false, url: '/api/v1/channels/', dataType: 'json', @@ -901,7 +915,7 @@ export function getChannelExtraInfo(id, memberLimit, success, error) { url += '/' + memberLimit; } - $.ajax({ + return $.ajax({ url, dataType: 'json', contentType: 'application/json', @@ -1018,7 +1032,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete) } export function getPosts(channelId, since, success, error, complete) { - $.ajax({ + return $.ajax({ url: '/api/v1/channels/' + channelId + '/posts/' + since, dataType: 'json', type: 'GET', @@ -1347,7 +1361,7 @@ export function getStatuses(ids, success, error) { } export function getMyTeam(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/teams/me', dataType: 'json', type: 'GET', @@ -1437,7 +1451,7 @@ export function listIncomingHooks(success, error) { } export function getAllPreferences(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/preferences/', dataType: 'json', type: 'GET', @@ -1569,3 +1583,68 @@ export function removeLicenseFile(success, error) { track('api', 'api_license_upload'); } + +export function getClientLicenceConfig(success, error) { + return $.ajax({ + url: '/api/v1/license/client_config', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientLicenceConfig', xhr, status, err); + error(e); + } + }); +} + +export function getInviteInfo(success, error, id) { + $.ajax({ + url: '/api/v1/teams/get_invite_info', + type: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({invite_id: id}), + success, + error: function onError(xhr, status, err) { + var e = handleError('getInviteInfo', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function verifyEmail(success, error, uid, hid) { + $.ajax({ + url: '/api/v1/users/verify_email', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({uid, hid}), + success, + error: function onError(xhr, status, err) { + var e = handleError('verifyEmail', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function resendVerification(success, error, teamName, email) { + $.ajax({ + url: '/api/v1/users/resend_verification', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({team_name: teamName, email}), + success, + error: function onError(xhr, status, err) { + var e = handleError('resendVerification', xhr, status, err); + if (error) { + error(e); + } + } + }); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index daea9f43e..2cff4dbed 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -42,13 +42,15 @@ export default { RECEIVED_MSG: null, - RECEIVED_TEAM: null, + RECEIVED_MY_TEAM: null, RECEIVED_CONFIG: null, RECEIVED_LOGS: null, RECEIVED_SERVER_AUDITS: null, RECEIVED_ALL_TEAMS: null, + RECEIVED_LOCALE: null, + SHOW_SEARCH: null, TOGGLE_IMPORT_THEME_MODAL: null, @@ -143,6 +145,7 @@ export default { EMAIL_SERVICE: 'email', SIGNIN_CHANGE: 'signin_change', SIGNIN_VERIFIED: 'verified', + SESSION_EXPIRED: 'expired', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_FOCUS_CONTEXT_RADIUS: 10, diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 6942a8e08..88777164b 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -2,9 +2,10 @@ // See License.txt for license information. import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import UserStore from '../stores/user_store.jsx'; +import LocalizationStore from '../stores/localization_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; import TeamStore from '../stores/team_store.jsx'; import Constants from '../utils/constants.jsx'; @@ -941,7 +942,7 @@ export function updateAddressBar(channelName) { } export function switchChannel(channel) { - EventHelpers.emitChannelClickEvent(channel); + GlobalActions.emitChannelClickEvent(channel); updateAddressBar(channel.name); @@ -1130,8 +1131,8 @@ export function fileSizeToString(bytes) { // Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server. export function getFileUrl(filename, isDownload) { - const downloadParam = isDownload ? '&download=1' : ''; - return getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + getSessionIndex() + downloadParam; + const downloadParam = isDownload ? '?download=1' : ''; + return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam; } // Gets the name of a file (including extension) from a given url or file path. @@ -1151,14 +1152,6 @@ export function getWebsocketPort(protocol) { return ''; } -export function getSessionIndex() { - if (global.window.mm_session_token_index >= 0) { - return 'session_token_index=' + global.window.mm_session_token_index; - } - - return ''; -} - // Generates a RFC-4122 version 4 compliant globally unique identifier. export function generateId() { // implementation taken from http://stackoverflow.com/a/2117523 @@ -1405,3 +1398,19 @@ export function isPostEphemeral(post) { export function getRootId(post) { return post.root_id === '' ? post.id : post.root_id; } + +export function localizeMessage(id, defaultMessage) { + const translations = LocalizationStore.getTranslations(); + if (translations) { + const value = translations[id]; + if (value) { + return value; + } + } + + if (defaultMessage) { + return defaultMessage; + } + + return id; +} diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 5e7f04724..44681291c 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -24,12 +24,6 @@ padding: 1em 1em 0; display: none; } - > div { - height: 100%; - position: absolute; - padding-bottom: 70px; - width: 100%; - } .badge { background-color: $primary-color; position: absolute; diff --git a/web/static/help/Messaging_en.md b/web/static/help/Messaging_en.md deleted file mode 100644 index 2063ad41c..000000000 --- a/web/static/help/Messaging_en.md +++ /dev/null @@ -1,47 +0,0 @@ -# Messaging - -### Writing Messages - -You can write messages using the input box with the text "Write a message..." at the bottom of Mattermost. - -Press **ENTER** to send a message. Use **Shift+ENTER** to create a new line without sending a message. - -### Formatting Messages - -Mattermost messages are formatted using a standard called "markdown". Here are examples: - -| Text Entered | How it appears | -|:---------------|:---------------| -|`**bold**`| **bold** | -| `_italic_`|_italic_| -|`[hyperlink](http://mattermost.org)`|[hyperlink](http://mattermost.org)| -|`![embedded image](https://travis-ci.org/mattermost/platform.svg)`|![embedded image](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Emojis provided free from [Emoji One](http://emojione.com/). Check out a full list of Emojis [here](http://emoji.codes/). - - -### Mentioning Teammates - -You can mention a teammate by using the `@` symbol plus their username to send them a special notification to draw their attention. - -For example, you might write: - -``` -@alice how did your interview go with the new candidate? -``` - -Which sends a special mention notification to **alice** to check your message. - -To mention a teammate, press `@` and you should see a list of team members who can be messaged. You can either type their username or use the **Up** and **Down** arrow keys and then **ENTER** to select them to be mentioned. - -You can configure how you'd like to be alerted about mentions of your username, your first name, your nickname, or other keywords from **Account Settings** > **Notifications** and you can set channel-specific preferences from **[Channel Name]** > **Notification Preferences** - -### Messages Dropdown Menu - -To get to the Messages Dropdown Menu, hover over a message and click on the [...] menu. This shows a dropdown list containing additional actions you can perform on a message: - -- **Reply:** Opens up the sidebar so you can reply to a message in a comment thread. -- **Permalink:** Creates a link to the message. Sharing this link with other users in the channel lets them view the linked message in the Message Archives. -- **Delete:** Deletes the message so it is no longer visible. Team Administrators and System Administrators can also delete another user's message. -- **Edit:** Lets you edit your own message. diff --git a/web/static/help/Messaging_es.md b/web/static/help/Messaging_es.md deleted file mode 100644 index d3947f36a..000000000 --- a/web/static/help/Messaging_es.md +++ /dev/null @@ -1,37 +0,0 @@ -# Mensajes - -## Escribiendo Mensajes - -Puedes escribir mensajes utilizando el cuadro de texto que dice "Escribe un mensaje..." al final de Mattermost. - -Presiona **RETORNO** para enviar un mensaje. Utiliza **Shift+RETORNO** para crear una nueva linea sin enviar el mensaje. - -## Darle formato a los Mensajes - -Los mensajes de Mattermost se les asigna formato utilizando un estándard que se llama "markdown". Aquí algunos ejemplos: - -| Texto escrito | Como aparece | -|:--------------|:-------------| -|`**negrita**`| **negrita** | -| `_italica_`|_italica_| -|`[hipervinculo](http://mattermost.org)`|[hipervinculo](http://mattermost.org)| -|`![imagen embebida](https://travis-ci.org/mattermost/platform.svg)`|![imagen embebida](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Revisa la lista completa de Emojis [aquí](http://www.emoji-cheat-sheet.com/). - -## Mencionando a compañeros - -Puedes mencionar a un compañero al utilizar el simbolo `@` más el nombre de usuario para enviarles una notificación especial que llame su atención. - -Por ejemplo, podrías escribir: - -``` -@alicia como te fue con la entrevista del nuevo candidato? -``` - -Lo cual enviará una notificación especial de mención a **alicia** para que lea tu mensaje. - -Para mencionar un compañero, presiona `@` y podrás ver una lista de los miembros de equipo a quienes puedes mandarles un mensaje. Puedes escribir su nombre de usuario o utilizar las flechas de **Arriba** y **Abajo** y presionar **RETORNO** para seleccionarlos. - -Puedes configurar como te gustaría ser notificado cuando alguien te menciona por nombre de usuario, tu primer nombre, sobrenombre o cualquier otra palabra clave en **Configurar Cuenta** > **Notificaciones** y puedes asignar preferencias especificas para un canal en **[Nombre del Canal]** > **Preferencias de Notificación** diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index d2e340641..2a536925c 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -661,8 +661,10 @@ "email_signup.find": "Find my teams", "email_verify.almost": "{siteName}: You are almost done", "email_verify.notVerifiedBody": "Please verify your email address. Check your inbox for an email.", + "email_verify.verifyFailed": "Failed to verify your email.", "email_verify.resend": "Resend Email", "email_verify.sent": " Verification email sent.", + "email_verify.failed": " Failed to send verification email.", "email_verify.verified": "{siteName} Email Verified", "email_verify.verifiedBody": "

    Your email has been verified! Click here to log in.

    ", "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured", @@ -758,6 +760,7 @@ "login.or": "or", "login.signTo": "Sign in to:", "login.verified": " Email Verified", + "login.session_expired": " Your session has expired. Please login again.", "login_email.badTeam": "Bad team name", "login_email.email": "Email", "login_email.emailReq": "An email is required", @@ -822,16 +825,16 @@ "navbar_dropdown.teamSettings": "Team Settings", "password_form.change": "Change my password", "password_form.click": "Click here to log in.", - "password_form.enter": "Enter a new password for your {teamDisplayName} {siteName} account.", + "password_form.enter": "Enter a new password for your {siteName} account.", "password_form.error": "Please enter at least {chars} characters.", "password_form.pwd": "Password", "password_form.title": "Password Reset", "password_form.update": "Your password has been updated successfully.", "password_send.checkInbox": "Please check your inbox.", - "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.", + "password_send.description": "To reset your password, enter the email address you used to sign up.", "password_send.email": "Email", "password_send.error": "Please enter a valid email address.", - "password_send.link": "

    A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

    ", + "password_send.link": "

    A password reset link has been sent to {email}

    ", "password_send.reset": "Reset my password", "password_send.title": "Password Reset", "post_attachment.collapse": "▲ collapse text", @@ -1303,5 +1306,11 @@ "view_image.loading": "Loading ", "view_image_popover.download": "Download", "view_image_popover.file": "File {count} of {total}", - "view_image_popover.publicLink": "Get Public Link" + "view_image_popover.publicLink": "Get Public Link", + "web.footer.about": "About", + "web.footer.help": "Help", + "web.footer.privacy": "Privacy", + "web.footer.terms": "Terms", + "web.header.back": "Back", + "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere" } diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index c6b16a293..f42dc879a 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -822,16 +822,16 @@ "navbar_dropdown.teamSettings": "Configurar Equipo", "password_form.change": "Cambiar mi contraseña", "password_form.click": " Pincha aquí para iniciar sesión.", - "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {teamDisplayName} {siteName}.", + "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {siteName}.", "password_form.error": "Por favor ingresa al menos {chars} caracteres.", "password_form.pwd": "Contraseña", "password_form.title": "Restablecer Contraseña", "password_form.update": "Tu contraseña ha sido actualizada satisfactoriamente.", "password_send.checkInbox": "Por favor revisa tu bandeja de entrada.", - "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte en {teamName}.", + "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte.", "password_send.email": "Correo electrónico", "password_send.error": "Por favor ingresa una dirección correo electrónico válida.", - "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email} para tu equipo {teamDisplayName} en {hostname}.

    ", + "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email}

    ", "password_send.reset": "Restablecer mi contraseña", "password_send.title": "Restablecer Contraseña", "post_attachment.collapse": "▲ colapsar texto", @@ -1303,5 +1303,11 @@ "view_image.loading": "Cargando ", "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", - "view_image_popover.publicLink": "Obtener Enlace Público" + "view_image_popover.publicLink": "Obtener Enlace Público", + "web.footer.about": "Acerca", + "web.footer.help": "Ayuda", + "web.footer.privacy": "Privacidad", + "web.footer.terms": "Términos", + "web.header.back": "Atrás", + "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" } diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index b9b8f4c07..d276e339a 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -820,16 +820,15 @@ "navbar_dropdown.teamSettings": "Configurações da Equipe", "password_form.change": "Alterar minha senha", "password_form.click": "Clique aqui para logar.", - "password_form.enter": "Entre uma nova senha para sua conta {teamDisplayName} {siteName}.", + "password_form.enter": "Entre uma nova senha para sua conta {siteName}.", "password_form.error": "Por favor, insira pelo menos {chars} caracteres.", "password_form.pwd": "Senha", "password_form.title": "Resetar Senha", "password_form.update": "Sua senha foi atualizada com sucesso.", "password_send.checkInbox": "Por favor verifique sua caixa de entrada.", - "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever em {teamName}.", + "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever.", "password_send.email": "E-mail", "password_send.error": "Por favor entre um endereço de e-mail válido.", - "password_send.link": "

    Um link para resetar a sua senha na equipe {teamDisplayName} em {hostname} foi enviado para {email}.

    ", "password_send.reset": "Resetar minha senha", "password_send.title": "Resetar Senha", "post_attachment.collapse": "▲ recolher texto", diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html deleted file mode 100644 index 08c90493e..000000000 --- a/web/templates/admin_console.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "admin_console"}} - - -{{template "head" . }} - - - -
    - - - - -{{end}} diff --git a/web/templates/authorize.html b/web/templates/authorize.html deleted file mode 100644 index 0fa36b0ab..000000000 --- a/web/templates/authorize.html +++ /dev/null @@ -1,12 +0,0 @@ -{{define "authorize"}} - -{{template "head" . }} - -
    -
    - - - -{{end}} diff --git a/web/templates/channel.html b/web/templates/channel.html deleted file mode 100644 index 94d79a022..000000000 --- a/web/templates/channel.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "channel"}} - - -{{template "head" . }} - -
    - - - -{{end}} diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html deleted file mode 100644 index 2a9126d1b..000000000 --- a/web/templates/claim_account.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "claim_account"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/docs.html b/web/templates/docs.html deleted file mode 100644 index dc18e5cb6..000000000 --- a/web/templates/docs.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "docs"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/find_team.html b/web/templates/find_team.html deleted file mode 100644 index b7e1d7eca..000000000 --- a/web/templates/find_team.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "find_team"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/footer.html b/web/templates/footer.html deleted file mode 100644 index 5b11328fb..000000000 --- a/web/templates/footer.html +++ /dev/null @@ -1,39 +0,0 @@ -{{define "footer"}} - - -{{end}} diff --git a/web/templates/head.html b/web/templates/head.html deleted file mode 100644 index 61b1aa12b..000000000 --- a/web/templates/head.html +++ /dev/null @@ -1,191 +0,0 @@ -{{define "head"}} - - - - - - {{ .Props.Title }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{{end}} diff --git a/web/templates/home.html b/web/templates/home.html deleted file mode 100644 index 08876d41d..000000000 --- a/web/templates/home.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "home"}} - - -{{template "head" . }} - -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    - - - -{{end}} diff --git a/web/templates/login.html b/web/templates/login.html deleted file mode 100644 index 88540a906..000000000 --- a/web/templates/login.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "login"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html deleted file mode 100644 index e68f8b693..000000000 --- a/web/templates/password_reset.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "password_reset"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html deleted file mode 100644 index afba58066..000000000 --- a/web/templates/signup_team.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team"}} - - -{{template "head" . }} - -
    -
    -
    -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html deleted file mode 100644 index 3873d8978..000000000 --- a/web/templates/signup_team_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html deleted file mode 100644 index 31f1ba95b..000000000 --- a/web/templates/signup_team_confirm.html +++ /dev/null @@ -1,26 +0,0 @@ -{{define "signup_team_confirm"}} - - -{{template "head" . }} - -
    -
    - - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html deleted file mode 100644 index 937a89dd2..000000000 --- a/web/templates/signup_user_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_user_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/verify.html b/web/templates/verify.html deleted file mode 100644 index 2e5496d7a..000000000 --- a/web/templates/verify.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "verify"}} - - - {{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/web.go b/web/web.go index 09450b976..2a44ece00 100644 --- a/web/web.go +++ b/web/web.go @@ -4,67 +4,16 @@ package web import ( - "fmt" + "net/http" + "strings" + l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" - "gopkg.in/fsnotify.v1" - "html/template" - "net/http" - "net/url" - "strconv" - "strings" ) -var Templates *template.Template - -type HtmlTemplatePage api.Page - -func NewHtmlTemplatePage(templateName string, title string, locale string) *HtmlTemplatePage { - - if len(title) > 0 { - title = utils.Cfg.TeamSettings.SiteName + " - " + title - } - - props := make(map[string]string) - props["Title"] = title - return &HtmlTemplatePage{ - TemplateName: templateName, - Props: props, - ClientCfg: utils.ClientCfg, - ClientLicense: utils.ClientLicense, - Locale: locale, - } -} - -func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { - if me.Team != nil { - me.Team.Sanitize() - } - - if me.User != nil { - me.User.Sanitize(map[string]bool{}) - me.Locale = me.User.Locale - } - - me.Props["Locale"] = me.Locale - me.SessionTokenIndex = c.SessionTokenIndex - - me.ClientCfg["HeaderBack"] = c.T("web.header.back") - me.ClientCfg["FooterHelp"] = c.T("web.footer.help") - me.ClientCfg["FooterTerms"] = c.T("web.footer.terms") - me.ClientCfg["FooterPrivacy"] = c.T("web.footer.privacy") - me.ClientCfg["FooterAbout"] = c.T("web.footer.about") - - if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil { - c.SetUnknownError(me.TemplateName, err.Error()) - } -} - func InitWeb() { l4g.Debug(utils.T("web.init.debug")) @@ -74,81 +23,7 @@ func InitWeb() { l4g.Debug("Using static directory at %v", staticDir) mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) - mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") - mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET") - mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST") - - mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") - mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") - mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") - mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") - mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") - - mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET") - - mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST") - - mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET") - - // ---------------------------------------------------------------------------------------------- - // *ANYTHING* team specific should go below this line - // ---------------------------------------------------------------------------------------------- - - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET") - mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - - watchAndParseTemplates() -} - -func watchAndParseTemplates() { - - templatesDir := utils.FindDir("web/templates") - l4g.Debug(utils.T("web.parsing_templates.debug"), templatesDir) - var err error - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - l4g.Error(utils.T("web.create_dir.error"), err) - } - - go func() { - for { - select { - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write { - l4g.Info(utils.T("web.reparse_templates.info"), event.Name) - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - } - case err := <-watcher.Errors: - l4g.Error(utils.T("web.dir_fail.error"), err) - } - } - }() - - err = watcher.Add(templatesDir) - if err != nil { - l4g.Error(utils.T("web.watcher_fail.error"), err) - } + mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET") } var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" @@ -177,1026 +52,9 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - if len(c.Session.UserId) == 0 { - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Props["Info"] = c.T("web.root.singup_info") - - if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - for _, team := range teams { - page.Props[team.Name] = team.DisplayName - } - - if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation { - http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect) - return - } - } - - page.Render(c, w) - } else { - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - page := NewHtmlTemplatePage("home", c.T("web.root.home_title"), c.Locale) - page.Team = team - page.User = user - page.Render(c, w) - } -} - -func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !CheckBrowserCompatability(c, r) { - return - } - - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Render(c, w) -} - -func login(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - params := mux.Vars(r) - teamName := params["team"] - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.login.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - // We still might be able to switch to this team because we've logged in before - _, session := api.FindMultiSessionForTeamId(r, team.Id) - if session != nil { - w.Header().Set(model.HEADER_TOKEN, session.Token) - lastViewChannelName := "town-square" - if lastViewResult := <-api.Srv.Store.Preference().Get(session.UserId, model.PREFERENCE_CATEGORY_LAST, model.PREFERENCE_NAME_LAST_CHANNEL); lastViewResult.Err == nil { - if lastViewChannelResult := <-api.Srv.Store.Channel().Get(lastViewResult.Data.(model.Preference).Value); lastViewChannelResult.Err == nil { - lastViewChannelName = lastViewChannelResult.Data.(*model.Channel).Name - } - } - - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/"+lastViewChannelName, http.StatusTemporaryRedirect) - return - } - - page := NewHtmlTemplatePage("login", c.T("web.login.login_title"), c.Locale) - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - if team.AllowOpenInvite { - page.Props["InviteId"] = team.InviteId - } - - page.Render(c, w) -} - -func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) { - email := r.FormValue("email") - - page := NewHtmlTemplatePage("signup_team_confirm", c.T("web.signup_team_confirm.title"), c.Locale) - page.Props["Email"] = email - page.Render(c, w) -} - -func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - data := r.FormValue("d") - hash := r.FormValue("h") - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*24*30 { // 30 days - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.link_expired.app_error", nil, "") - return - } - - page := NewHtmlTemplatePage("signup_team_complete", c.T("web.signup_team_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - - id := r.FormValue("id") - data := r.FormValue("d") - hash := r.FormValue("h") - var props map[string]string - - if len(id) > 0 { - props = make(map[string]string) - - if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil { - c.Err = result.Err - return - } else { - team := result.Data.(*model.Team) - if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) { - c.Err = model.NewLocAppError("signupUserComplete", "web.signup_user_complete.no_invites.app_error", nil, "id="+id) - return - } - - props["email"] = "" - props["display_name"] = team.DisplayName - props["name"] = team.Name - props["id"] = team.Id - data = model.MapToJson(props) - hash = "" - } - } else { - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_invalid.app_error", nil, "") - return - } - - props = model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_expired.app_error", nil, "") - return - } - } - - page := NewHtmlTemplatePage("signup_user_complete", c.T("web.signup_user_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["TeamDisplayName"] = props["display_name"] - page.Props["TeamName"] = props["name"] - page.Props["TeamId"] = props["id"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { - api.Logout(c, w, r) - http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect) -} - -func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - teamName := params["team"] - postId := params["postid"] - - if len(postId) != 26 { - c.Err = model.NewLocAppError("postPermalink", "web.post_permalink.app_error", nil, "id="+postId) - return - } - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var post *model.Post - if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil { - c.Err = result.Err - return - } else { - postlist := result.Data.(*model.PostList) - post = postlist.Posts[postlist.Order[0]] - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - if result.Data.(int64) == 0 { - if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, post.Id) -} - -func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - name := params["channelname"] - teamName := params["team"] - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - channelId := result.Data.(string) - if len(channelId) == 0 { - if channel = autoJoinChannelName(c, w, r, name); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, "") -} - -func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel { - if strings.Index(channelName, "__") > 0 { - // It's a direct message channel that doesn't exist yet so let's create it - ids := strings.Split(channelName, "__") - otherUserId := "" - if ids[0] == c.Session.UserId { - otherUserId = ids[1] - } else { - otherUserId = ids[0] - } - - if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil { - api.Handle404(w, r) - return nil - } else { - return sc - } - } else { - // We will attempt to auto-join open channels - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName)) - } - - return nil -} - -func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel { - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId)) -} - -func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel { - if cr := <-channel; cr.Err != nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } else { - channel := cr.Data.(*model.Channel) - if channel.Type == model.CHANNEL_OPEN { - api.JoinChannel(c, channel.Id, "") - if c.Err != nil { - return nil - } - } else { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } - return channel - } -} - -func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team { - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return nil - } else { - team = result.Data.(*model.Team) - } - - // We are logged into a different team. Lets see if we have another - // session in the cookie that will give us access. - if c.Session.TeamId != team.Id { - index, session := api.FindMultiSessionForTeamId(r, team.Id) - if session == nil { - // redirect to login - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) - } else { - c.Session = *session - c.SessionTokenIndex = index - } - } - - return team -} - -func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - prefChan := api.Srv.Store.Preference().GetAll(c.Session.UserId) - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - c.RemoveSessionCookie(w, r) - l4g.Error(utils.T("web.do_load_channel.error"), c.Session.UserId) - return - } else { - user = ur.Data.(*model.User) - } - - var preferences model.Preferences - if result := <-prefChan; result.Err != nil { - l4g.Error("Error in getting preferences for id=%v", c.Session.UserId) - } else { - preferences = result.Data.(model.Preferences) - } - - page := NewHtmlTemplatePage("channel", "", c.Locale) - page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["ChannelName"] = channel.Name - page.Props["ChannelId"] = channel.Id - page.Props["PostId"] = postid - page.Team = team - page.User = user - page.Channel = channel - page.Preferences = &preferences - page.Render(c, w) -} - -func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { - resend := r.URL.Query().Get("resend") - resendSuccess := r.URL.Query().Get("resend_success") - name := r.URL.Query().Get("teamname") - email := r.URL.Query().Get("email") - hashedId := r.URL.Query().Get("hid") - userId := r.URL.Query().Get("uid") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if resend == "true" { - if result := <-api.Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = result.Err - return - } else { - user := result.Data.(*model.User) - - if user.LastActivityAt > 0 { - api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } else { - api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } - - newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) - http.Redirect(w, r, newAddress, http.StatusFound) - return - } - } - - if len(userId) == 26 && len(hashedId) != 0 && model.ComparePassword(hashedId, userId) { - if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { - return - } else { - c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) - return - } - } - - page := NewHtmlTemplatePage("verify", c.T("web.email_verified.title"), c.Locale) - page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) - page.Props["UserEmail"] = email - page.Props["ResendSuccess"] = resendSuccess - page.Render(c, w) -} - -func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) { - page := NewHtmlTemplatePage("find_team", c.T("web.find_team.title"), c.Locale) - page.Render(c, w) -} - -func docs(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - doc := params["doc"] - - var user *model.User - if len(c.Session.UserId) != 0 { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - if userChan := <-userChan; userChan.Err == nil { - user = userChan.Data.(*model.User) - } - } - - page := NewHtmlTemplatePage("docs", c.T("web.doc.title"), c.Locale) - page.Props["Site"] = doc - page.User = user - page.Render(c, w) -} - -func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { - isResetLink := true - hash := r.URL.Query().Get("h") - data := r.URL.Query().Get("d") - params := mux.Vars(r) - teamName := params["team"] - - if len(hash) == 0 || len(data) == 0 { - isResetLink = false - } else { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) { - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.expired_link.app_error", nil, "") - return - } - } - - teamDisplayName := "Developer/Beta" - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } - - if team != nil { - teamDisplayName = team.DisplayName - } - - page := NewHtmlTemplatePage("password_reset", "", c.Locale) - page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = teamDisplayName - page.Props["TeamName"] = teamName - page.Props["Hash"] = hash - page.Props["Data"] = data - page.Props["TeamName"] = teamName - page.Props["IsReset"] = strconv.FormatBool(isResetLink) - page.Render(c, w) -} - -func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - - if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - hash := r.URL.Query().Get("h") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if api.IsVerifyHashRequired(nil, team, hash) { - data := r.URL.Query().Get("d") - props := model.MapFromJson(strings.NewReader(data)) - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") - return - } - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") - return - } - - if team.Id != props["id"] { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) - return - } - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SIGNUP - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - - uri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) - - if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { - c.Err = err - return - } else { - action := props["action"] - switch action { - case model.OAUTH_ACTION_SIGNUP: - api.CreateOAuthUser(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_LOGIN: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_EMAIL_TO_SSO: - api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_SSO_TO_EMAIL: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) - } - break - default: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - } - } -} - -func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - loginHint := r.URL.Query().Get("login_hint") - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - // Make sure team exists - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_LOGIN - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !c.HasSystemAdminPermissions("adminConsole") { - return - } - - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) - userChan := api.Srv.Store.User().Get(c.Session.UserId) - - var team *model.Team - if tr := <-teamChan; tr.Err != nil { - c.Err = tr.Err - return - } else { - team = tr.Data.(*model.Team) - - } - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - return - } else { - user = ur.Data.(*model.User) - } - - params := mux.Vars(r) - activeTab := params["tab"] - teamId := params["team"] - - page := NewHtmlTemplatePage("admin_console", c.T("web.admin_console.title"), c.Locale) - page.User = user - page.Team = team - page.Props["ActiveTab"] = activeTab - page.Props["TeamId"] = teamId - page.Render(c, w) -} - -func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if !CheckBrowserCompatability(c, r) { - return - } - - responseType := r.URL.Query().Get("response_type") - clientId := r.URL.Query().Get("client_id") - redirect := r.URL.Query().Get("redirect_uri") - scope := r.URL.Query().Get("scope") - state := r.URL.Query().Get("state") - - if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { - c.Err = result.Err - return - } else { - app = result.Data.(*model.OAuthApp) - } - - var team *model.Team - if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) + page := utils.NewHTMLTemplate("root", c.Locale) + page.Props["Title"] = c.T("web.root.home_title") + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) } - - page := NewHtmlTemplatePage("authorize", c.T("web.authorize_oauth.title"), c.Locale) - page.Props["TeamName"] = team.Name - page.Props["AppName"] = app.Name - page.Props["ResponseType"] = responseType - page.Props["ClientId"] = clientId - page.Props["RedirectUri"] = redirect - page.Props["Scope"] = scope - page.Props["State"] = state - page.Render(c, w) -} - -func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - c.LogAudit("attempt") - - r.ParseForm() - - grantType := r.FormValue("grant_type") - if grantType != model.ACCESS_TOKEN_GRANT_TYPE { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") - return - } - - clientId := r.FormValue("client_id") - if len(clientId) != 26 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") - return - } - - secret := r.FormValue("client_secret") - if len(secret) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") - return - } - - code := r.FormValue("code") - if len(code) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") - return - } - - redirectUri := r.FormValue("redirect_uri") - - achan := api.Srv.Store.OAuth().GetApp(clientId) - tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code) - - authData := api.GetAuthData(code) - - if authData == nil { - c.LogAudit("fail - invalid auth code") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - uchan := api.Srv.Store.User().Get(authData.UserId) - - if authData.IsExpired() { - c.LogAudit("fail - auth code expired") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - if authData.RedirectUri != redirectUri { - c.LogAudit("fail - redirect uri provided did not match previous redirect uri") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") - return - } - - if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { - c.LogAudit("fail - auth code is invalid") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-achan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } else { - app = result.Data.(*model.OAuthApp) - } - - if !model.ComparePassword(app.ClientSecret, secret) { - c.LogAudit("fail - invalid client credentials") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } - - callback := redirectUri - if len(callback) == 0 { - callback = app.CallbackUrls[0] - } - - if result := <-tchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") - return - } else if result.Data != nil { - c.LogAudit("fail - auth code has been used previously") - accessData := result.Data.(*model.AccessData) - - // Revoke access token, related auth code, and session from DB as well as from cache - if err := api.RevokeAccessToken(accessData.Token); err != nil { - l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) - } - - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") - return - } - - var user *model.User - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") - return - } else { - user = result.Data.(*model.User) - } - - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} - - if result := <-api.Srv.Store.Session().Save(session); result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") - return - } else { - session = result.Data.(*model.Session) - api.AddSessionToCache(session) - } - - accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} - - if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { - l4g.Error(result.Err) - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") - return - } - - accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("Pragma", "no-cache") - - c.LogAuditWithUserId(user.Id, "success") - - w.Write([]byte(accessRsp.ToJson())) -} - -func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - params := mux.Vars(r) - id := params["id"] - - hchan := api.Srv.Store.Webhook().GetIncoming(id) - - r.ParseForm() - - var parsedRequest *model.IncomingWebhookRequest - contentType := r.Header.Get("Content-Type") - if strings.Split(contentType, "; ")[0] == "application/json" { - parsedRequest = model.IncomingWebhookRequestFromJson(r.Body) - } else { - parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload"))) - } - - if parsedRequest == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "") - return - } - - text := parsedRequest.Text - if len(text) == 0 && parsedRequest.Attachments == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "") - return - } - - channelName := parsedRequest.ChannelName - webhookType := parsedRequest.Type - - //attachments is in here for slack compatibility - if parsedRequest.Attachments != nil { - if len(parsedRequest.Props) == 0 { - parsedRequest.Props = make(model.StringInterface) - } - parsedRequest.Props["attachments"] = parsedRequest.Attachments - webhookType = model.POST_SLACK_ATTACHMENT - } - - var hook *model.IncomingWebhook - if result := <-hchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message) - return - } else { - hook = result.Data.(*model.IncomingWebhook) - } - - var channel *model.Channel - var cchan store.StoreChannel - - if len(channelName) != 0 { - if channelName[0] == '@' { - if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message) - return - } else { - channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId) - } - } else if channelName[0] == '#' { - channelName = channelName[1:] - } - - cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName) - } else { - cchan = api.Srv.Store.Channel().Get(hook.ChannelId) - } - - overrideUsername := parsedRequest.Username - overrideIconUrl := parsedRequest.IconURL - - if result := <-cchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message) - return - } else { - channel = result.Data.(*model.Channel) - } - - pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) - - // create a mock session - c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} - - if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "") - return - } - - if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { - c.Err = err - return - } - - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte("ok")) -} - -func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - - params := mux.Vars(r) - teamName := params["team"] - email := r.URL.Query().Get("email") - newType := r.URL.Query().Get("new_type") - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.claim_account.team.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - authType := "" - if len(email) != 0 { - if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil { - l4g.Error(utils.T("web.claim_account.user.error"), team.Id, email, uResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - user := uResult.Data.(*model.User) - authType = user.AuthService - - // if user is not logged in to their SSO account, ask them to log in - if len(authType) != 0 && user.Id != c.Session.UserId { - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL - stateProps["email"] = email - - if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } - } - } - } - - page := NewHtmlTemplatePage("claim_account", c.T("web.claim_account.title"), c.Locale) - page.Props["Email"] = email - page.Props["CurrentType"] = authType - page.Props["NewType"] = newType - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - page.Render(c, w) } -- cgit v1.2.3-1-g7c22