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;