diff --git a/server/Makefile b/server/Makefile index 9fb237008c3..d45c8c0c939 100644 --- a/server/Makefile +++ b/server/Makefile @@ -179,7 +179,7 @@ PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.3.0 ifeq ($(FIPS_ENABLED),true) PLUGIN_PACKAGES = mattermost-plugin-playbooks-v2.8.0%2Bc4449ac-fips PLUGIN_PACKAGES += mattermost-plugin-agents-v1.7.2%2B866e2dd-fips - PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.2%2B4282c63-fips + PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.4%2B5855fe1-fips endif EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...) diff --git a/server/channels/app/email/notification_email.go b/server/channels/app/email/notification_email.go index d95cc452a56..8fa9d460170 100644 --- a/server/channels/app/email/notification_email.go +++ b/server/channels/app/email/notification_email.go @@ -8,7 +8,6 @@ import ( "html" "html/template" "net/url" - "path/filepath" "strings" "github.com/mattermost/mattermost/server/public/model" @@ -33,7 +32,7 @@ func (es *Service) GetMessageForNotification(post *model.Post, teamName, siteUrl return es.prepareNotificationMessageForEmail(post.Message, teamName, siteUrl) } - // extract the filenames from their paths and determine what type of files are attached + // normalize the filenames and determine what type of files are attached infos, err := es.store.FileInfo().GetForPost(post.Id, true, false, true) if err != nil { mlog.Warn("Encountered error when getting files for notification message", mlog.String("post_id", post.Id), mlog.Err(err)) @@ -42,12 +41,11 @@ func (es *Service) GetMessageForNotification(post *model.Post, teamName, siteUrl filenames := make([]string, len(infos)) onlyImages := true for i, info := range infos { - if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { - // this should never error since filepath was escaped using url.QueryEscape - filenames[i] = escaped - } else { - filenames[i] = info.Name + filename := info.Name + if unescaped, err := url.QueryUnescape(filename); err == nil { + filename = unescaped } + filenames[i] = filename onlyImages = onlyImages && info.IsImage() } @@ -55,9 +53,15 @@ func (es *Service) GetMessageForNotification(post *model.Post, teamName, siteUrl props := map[string]any{"Filenames": strings.Join(filenames, ", ")} if onlyImages { - return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) + return string(i18n.TranslateAsHTML(translateFunc, "api.post.get_message_for_notification.images_sent", map[string]any{ + "Count": len(filenames), + "Filenames": props["Filenames"], + })) } - return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) + return string(i18n.TranslateAsHTML(translateFunc, "api.post.get_message_for_notification.files_sent", map[string]any{ + "Count": len(filenames), + "Filenames": props["Filenames"], + })) } func ProcessMessageAttachments(post *model.Post, siteURL string) []*EmailMessageAttachment { diff --git a/server/channels/app/notification_email_test.go b/server/channels/app/notification_email_test.go index 841ea921373..5d399ac7438 100644 --- a/server/channels/app/notification_email_test.go +++ b/server/channels/app/notification_email_test.go @@ -6,7 +6,9 @@ package app import ( "bytes" "fmt" + "html" "html/template" + "net/url" "regexp" "testing" "time" @@ -746,6 +748,106 @@ func TestGetNotificationEmailEscapingChars(t *testing.T) { assert.NotContains(t, body, message) } +func TestGetNotificationEmailBodyFullNotificationFileOnlyPostEscapesFilenameHTML(t *testing.T) { + mainHelper.Parallel(t) + th := SetupWithStoreMock(t) + + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) + dangerousFilename := "owned.txt" + post := &model.Post{ + Id: "test-post-id", + Message: "", + FileIds: []string{"test-file-id"}, + } + channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", + DisplayName: "ChannelName", + Type: model.ChannelTypeOpen, + } + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") + + storeMock := th.App.Srv().Store().(*mocks.Store) + fileInfoStoreMock := mocks.FileInfoStore{} + fileInfoStoreMock.On("GetForPost", post.Id, true, false, true).Return([]*model.FileInfo{{ + Id: "test-file-id", + PostId: post.Id, + Name: dangerousFilename, + Extension: "txt", + MimeType: "text/plain", + }}, nil) + storeMock.On("FileInfo").Return(&fileInfoStoreMock) + + setupPreferenceMocks(th, recipient.Id, true) + th.App.Srv().EmailService.SetStore(storeMock) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "") + require.NoError(t, err) + + require.Contains(t, body, html.EscapeString(dangerousFilename)) + require.NotContains(t, body, dangerousFilename) +} + +func TestGetNotificationEmailBodyFullNotificationImageOnlyPostNormalizesAndEscapesFilenamesHTML(t *testing.T) { + mainHelper.Parallel(t) + th := SetupWithStoreMock(t) + + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) + rawDangerousFilename := ".png" + encodedDangerousFilename := url.QueryEscape(rawDangerousFilename) + secondFilename := "photo & notes.png" + post := &model.Post{ + Id: "test-post-id", + Message: "", + FileIds: []string{"test-file-id-1", "test-file-id-2"}, + } + channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", + DisplayName: "ChannelName", + Type: model.ChannelTypeOpen, + } + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") + + storeMock := th.App.Srv().Store().(*mocks.Store) + fileInfoStoreMock := mocks.FileInfoStore{} + fileInfoStoreMock.On("GetForPost", post.Id, true, false, true).Return([]*model.FileInfo{ + { + Id: "test-file-id-1", + PostId: post.Id, + Name: encodedDangerousFilename, + Extension: "png", + MimeType: "image/png", + }, + { + Id: "test-file-id-2", + PostId: post.Id, + Name: secondFilename, + Extension: "png", + MimeType: "image/png", + }, + }, nil) + storeMock.On("FileInfo").Return(&fileInfoStoreMock) + + setupPreferenceMocks(th, recipient.Id, true) + th.App.Srv().EmailService.SetStore(storeMock) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "") + require.NoError(t, err) + + expectedFilenames := fmt.Sprintf("%s, %s", html.EscapeString(rawDangerousFilename), html.EscapeString(secondFilename)) + require.Contains(t, body, "2 images sent:") + require.Contains(t, body, expectedFilenames) + require.NotContains(t, body, rawDangerousFilename) + require.NotContains(t, body, encodedDangerousFilename) +} + func TestGetNotificationEmailBodyPublicChannelMention(t *testing.T) { mainHelper.Parallel(t) th := SetupWithStoreMock(t) diff --git a/webapp/channels/src/components/channel_layout/channel_controller.tsx b/webapp/channels/src/components/channel_layout/channel_controller.tsx index 0de96244157..a430dc7a633 100644 --- a/webapp/channels/src/components/channel_layout/channel_controller.tsx +++ b/webapp/channels/src/components/channel_layout/channel_controller.tsx @@ -21,7 +21,6 @@ import UnreadsStatusHandler from 'components/unreads_status_handler'; import Pluggable from 'plugins/pluggable'; import {Constants} from 'utils/constants'; -import {isInternetExplorer, isEdge} from 'utils/user_agent'; const ProductNoticesModal = makeAsyncComponent('ProductNoticesModal', lazy(() => import('components/product_notices_modal'))); const ResetStatusModal = makeAsyncComponent('ResetStatusModal', lazy(() => import('components/reset_status_modal'))); @@ -39,12 +38,11 @@ export default function ChannelController(props: Props) { const dispatch = useDispatch(); useEffect(() => { - const isMsBrowser = isInternetExplorer() || isEdge(); const {navigator} = window; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'; - document.body.classList.add(...getClassnamesForBody(platform, isMsBrowser)); + document.body.classList.add(...getClassnamesForBody(platform)); return () => { document.body.classList.remove(...BODY_CLASS_FOR_CHANNEL); @@ -93,7 +91,7 @@ export default function ChannelController(props: Props) { ); } -export function getClassnamesForBody(platform: Window['navigator']['platform'], isMsBrowser = false) { +export function getClassnamesForBody(platform: Window['navigator']['platform']) { const bodyClass = [...BODY_CLASS_FOR_CHANNEL]; // OS Detection @@ -103,10 +101,5 @@ export function getClassnamesForBody(platform: Window['navigator']['platform'], bodyClass.push('os--mac'); } - // IE Detection - if (isMsBrowser) { - bodyClass.push('browser--ie'); - } - return bodyClass; } diff --git a/webapp/channels/src/images/icon_WS.png b/webapp/channels/src/images/icon_WS.png deleted file mode 100644 index 9d34b88c7b2..00000000000 Binary files a/webapp/channels/src/images/icon_WS.png and /dev/null differ diff --git a/webapp/channels/src/sass/components/_emoticons.scss b/webapp/channels/src/sass/components/_emoticons.scss index 22e49ca3d75..bdc20573964 100644 --- a/webapp/channels/src/sass/components/_emoticons.scss +++ b/webapp/channels/src/sass/components/_emoticons.scss @@ -100,10 +100,6 @@ @include mixins.clearfix; - .browser--ie & { - width: 325px; - } - .app__content & { margin-right: 0; } diff --git a/webapp/channels/src/sass/components/_modal.scss b/webapp/channels/src/sass/components/_modal.scss index 218fa87039b..7f4b1d965b6 100644 --- a/webapp/channels/src/sass/components/_modal.scss +++ b/webapp/channels/src/sass/components/_modal.scss @@ -4,14 +4,6 @@ @use 'sass:color'; -.browser--ie { - .modal { - .modal-dialog { - transform: translateY(0); - } - } -} - .modal-backdrop { &.in { opacity: 0.64; @@ -754,10 +746,6 @@ display: flex; flex-direction: column; - .browser--ie & { - flex: 20; - } - >div:not([data-simplebar]) { overflow: auto; min-height: 100%; diff --git a/webapp/channels/src/sass/components/_post.scss b/webapp/channels/src/sass/components/_post.scss index 08fe147268f..c6500b2a4c1 100644 --- a/webapp/channels/src/sass/components/_post.scss +++ b/webapp/channels/src/sass/components/_post.scss @@ -599,27 +599,6 @@ } } - .browser--ie & { - .post__header { - .col__reply { - .comment-icon__container { - flex: 0 1 auto; - align-items: center; - } - - > .open, - > div { - flex: 0 1 30px; - align-items: center; - - &:first-child { - flex: 0 1 25px; - } - } - } - } - } - &.post--system { .post__header { .post-menu { diff --git a/webapp/channels/src/sass/layout/_navigation.scss b/webapp/channels/src/sass/layout/_navigation.scss index 9604b7f898e..e3b06992fae 100644 --- a/webapp/channels/src/sass/layout/_navigation.scss +++ b/webapp/channels/src/sass/layout/_navigation.scss @@ -46,19 +46,6 @@ justify-content: center; } - .browser--ie & { - .navbar-default { - .navbar-brand { - overflow: visible; - padding: 1px; - - .heading { - max-width: 100px; - } - } - } - } - .navbar-default { position: absolute; min-height: 50px; diff --git a/webapp/channels/src/sass/responsive/_mobile.scss b/webapp/channels/src/sass/responsive/_mobile.scss index 7cb92cbb658..ea0089d6f0f 100644 --- a/webapp/channels/src/sass/responsive/_mobile.scss +++ b/webapp/channels/src/sass/responsive/_mobile.scss @@ -46,16 +46,6 @@ } } } - - .browser--ie & { - .navbar-default { - .dropdown-menu { - .close { - top: 70px; - } - } - } - } } .app__body { @@ -732,16 +722,6 @@ visibility: visible; } - .browser--ie & { - .post__header { - .post-menu { - .dropdown + div { - display: none; - } - } - } - } - .post-menu__item--reactions { display: none; } @@ -1394,15 +1374,6 @@ width: 100%; transform: translate3d(100%, 0, 0); - .browser--ie & { - display: none; - -webkit-transform: none !important; - -moz-transform: none !important; - -ms-transform: none !important; - -o-transform: none !important; - transform: none !important; - } - .sidebar--right__bg { display: none; } @@ -1586,12 +1557,6 @@ } @media screen and (max-width: 640px) { - body { - &.browser--ie { - min-width: 600px; - } - } - .section-min .d-flex { flex-direction: column; } diff --git a/webapp/channels/src/sass/responsive/_tablet.scss b/webapp/channels/src/sass/responsive/_tablet.scss index 8fc287b3a32..430e7beb8dd 100644 --- a/webapp/channels/src/sass/responsive/_tablet.scss +++ b/webapp/channels/src/sass/responsive/_tablet.scss @@ -431,15 +431,6 @@ -o-transform: translateX(0) !important; transform: translateX(0) !important; - .browser--ie & { - display: block; - -webkit-transform: none !important; - -moz-transform: none !important; - -ms-transform: none !important; - -o-transform: none !important; - transform: none !important; - } - .search-bar__container { z-index: 5; display: block !important; diff --git a/webapp/channels/src/sass/routes/_settings.scss b/webapp/channels/src/sass/routes/_settings.scss index 44a53703c1d..ffd92c20019 100644 --- a/webapp/channels/src/sass/routes/_settings.scss +++ b/webapp/channels/src/sass/routes/_settings.scss @@ -355,10 +355,6 @@ font: normal normal normal 14px/1 FontAwesome; pointer-events: none; text-rendering: auto; - - .browser--ie & { - display: none; - } } } diff --git a/webapp/channels/src/utils/notification_sounds.tsx b/webapp/channels/src/utils/notification_sounds.tsx index fbe6dd41e60..ac16066c748 100644 --- a/webapp/channels/src/utils/notification_sounds.tsx +++ b/webapp/channels/src/utils/notification_sounds.tsx @@ -19,7 +19,6 @@ import hello from 'sounds/hello.mp3'; import ripple from 'sounds/ripple.mp3'; import upstairs from 'sounds/upstairs.mp3'; import {DesktopSound} from 'utils/constants'; -import {isEdge} from 'utils/user_agent'; export const DesktopNotificationSounds = { DEFAULT: 'default', @@ -169,7 +168,7 @@ export function getValueOfIncomingCallSoundsSelect(soundName?: string) { let canDing = true; export function ding(name: string) { - if (hasSoundOptions() && canDing) { + if (canDing) { tryNotificationSound(name); canDing = false; setTimeout(() => { @@ -185,9 +184,6 @@ export function tryNotificationSound(name: string) { let currentRing: HTMLAudioElement | null = null; export function ring(name: string) { - if (!hasSoundOptions()) { - return; - } stopRing(); currentRing = loopNotificationRing(name); @@ -208,9 +204,6 @@ export function stopRing() { let currentTryRing: HTMLAudioElement | null = null; let currentTimer: NodeJS.Timeout; export function tryNotificationRing(name: string) { - if (!hasSoundOptions()) { - return; - } stopTryNotificationRing(); clearTimeout(currentTimer); @@ -240,10 +233,6 @@ export function loopNotificationRing(name: string) { return audio; } -export function hasSoundOptions() { - return (!isEdge()); -} - /** * This conversion is needed because User's preference for desktop sound is stored as either true or false. On the other hand, * Channel's specific desktop sound is stored as either On or Off. diff --git a/webapp/channels/src/utils/notifications.ts b/webapp/channels/src/utils/notifications.ts index 253a9081832..2cfef8a4c7a 100644 --- a/webapp/channels/src/utils/notifications.ts +++ b/webapp/channels/src/utils/notifications.ts @@ -2,8 +2,6 @@ // See LICENSE.txt for license information. import icon50 from 'images/icon50x50.png'; -import iconWS from 'images/icon_WS.png'; -import {isEdge} from 'utils/user_agent'; import type {ThunkActionFunc} from 'types/store'; @@ -45,11 +43,6 @@ export function showNotification( }, ): ThunkActionFunc void}>> { return async () => { - let icon = icon50; - if (isEdge()) { - icon = iconWS; - } - if (!isNotificationAPISupported()) { throw new Error('Notification API is not supported'); } @@ -79,7 +72,7 @@ export function showNotification( const notification = new Notification(title, { body, tag: body, - icon, + icon: icon50, requireInteraction, silent, }); diff --git a/webapp/channels/src/utils/user_agent.tsx b/webapp/channels/src/utils/user_agent.tsx deleted file mode 100644 index 69aab209236..00000000000 --- a/webapp/channels/src/utils/user_agent.tsx +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -export function isInternetExplorer(): boolean { - return window.navigator.userAgent.indexOf('Trident') !== -1; -} - -export function isEdge(): boolean { - return window.navigator.userAgent.indexOf('Edge') !== -1; -} diff --git a/webapp/channels/src/utils/utils.tsx b/webapp/channels/src/utils/utils.tsx index ff8df0126da..bcd0a5ea7ca 100644 --- a/webapp/channels/src/utils/utils.tsx +++ b/webapp/channels/src/utils/utils.tsx @@ -61,7 +61,6 @@ import DesktopApp from 'utils/desktop_api'; import {getIntl} from 'utils/i18n'; import * as Keyboard from 'utils/keyboard'; import {FOCUS_REPLY_POST, isPopoutWindow, sendToParent} from 'utils/popouts/popout_windows'; -import {isInternetExplorer, isEdge} from 'utils/user_agent'; import {joinPrivateChannelPrompt} from './channel_utils'; @@ -1192,15 +1191,11 @@ export function fillRecord(value: T, length: number): Record { // Checks if a data transfer contains files not text, folders, etc.. // Slightly modified from http://stackoverflow.com/questions/6848043/how-do-i-detect-a-file-is-being-dragged-rather-than-a-draggable-element-on-my-pa export function isFileTransfer(files: DataTransfer) { - if (isInternetExplorer() || isEdge()) { - return files.types != null && files.types.includes('Files'); - } - return files.types != null && (files.types.indexOf ? files.types.indexOf('Files') !== -1 : files.types.includes('application/x-moz-file')); } export function isUriDrop(dataTransfer: DataTransfer) { - if (isInternetExplorer() || isEdge() || isSafari()) { + if (isSafari()) { for (let i = 0; i < dataTransfer.items.length; i++) { if (dataTransfer.items[i].type === 'text/uri-list') { return true;