diff --git a/api/file.go b/api/file.go index 0f2fd9319299cbf1b10d7987ac7a59fbe634d50b..9a5de56691fa8256045e88e9b7e09159db27bcf4 100644 --- a/api/file.go +++ b/api/file.go @@ -31,6 +31,11 @@ func InitFile() { } func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.FileSettings.EnableFileAttachments { + c.Err = model.NewAppError("uploadFile", "api.file.attachments.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + if r.ContentLength > *utils.Cfg.FileSettings.MaxFileSize { c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "") c.Err.StatusCode = http.StatusRequestEntityTooLarge @@ -181,9 +186,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) { func getFileInfoForRequest(c *Context, r *http.Request, requireFileVisible bool) (*model.FileInfo, *model.AppError) { if len(utils.Cfg.FileSettings.DriverName) == 0 { - err := model.NewLocAppError("getFileInfoForRequest", "api.file.get_file_info_for_request.storage.app_error", nil, "") - err.StatusCode = http.StatusNotImplemented - return nil, err + return nil, model.NewAppError("getFileInfoForRequest", "api.file.get_info_for_request.storage.app_error", nil, "", http.StatusNotImplemented) } params := mux.Vars(r) diff --git a/api/file_test.go b/api/file_test.go index 1e65c33e86aa95822e080f70ad9fecac8f4d99b9..40534d724afa392680ed834d3c89b83aa466de96 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -99,6 +99,18 @@ func TestUploadFile(t *testing.T) { t.Fatalf("file preview should've been saved in %v", expectedPreviewPath) } + enableFileAttachments := *utils.Cfg.FileSettings.EnableFileAttachments + defer func() { + *utils.Cfg.FileSettings.EnableFileAttachments = enableFileAttachments + }() + *utils.Cfg.FileSettings.EnableFileAttachments = false + + if data, err := readTestFile("test.png"); err != nil { + t.Fatal(err) + } else if _, err = Client.UploadPostAttachment(data, channel.Id, "test.png"); err == nil { + t.Fatal("should have errored") + } + // Wait a bit for files to ready time.Sleep(2 * time.Second) diff --git a/api4/file.go b/api4/file.go index 6bd751a6798dd6c492f45a1c0ac370ab34880b4a..09132b9a1622967b7f8032c8d2564cf6fef3927e 100644 --- a/api4/file.go +++ b/api4/file.go @@ -33,9 +33,13 @@ func InitFile() { } func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.FileSettings.EnableFileAttachments { + c.Err = model.NewAppError("uploadFile", "api.file.attachments.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + if r.ContentLength > *utils.Cfg.FileSettings.MaxFileSize { - c.Err = model.NewLocAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "") - c.Err.StatusCode = http.StatusRequestEntityTooLarge + c.Err = model.NewAppError("uploadFile", "api.file.upload_file.too_large.app_error", nil, "", http.StatusRequestEntityTooLarge) return } diff --git a/api4/file_test.go b/api4/file_test.go index 9124e893b456825d8af8bf92457ef3b5841d1c58..e48aabffc754b52e1267364d1805790139bc2a5b 100644 --- a/api4/file_test.go +++ b/api4/file_test.go @@ -102,6 +102,15 @@ func TestUploadFile(t *testing.T) { _, resp = th.SystemAdminClient.UploadFile(data, channel.Id, "test.png") CheckNoError(t, resp) + + enableFileAttachments := *utils.Cfg.FileSettings.EnableFileAttachments + defer func() { + *utils.Cfg.FileSettings.EnableFileAttachments = enableFileAttachments + }() + *utils.Cfg.FileSettings.EnableFileAttachments = false + + _, resp = th.SystemAdminClient.UploadFile(data, channel.Id, "test.png") + CheckNotImplementedStatus(t, resp) } func TestGetFile(t *testing.T) { diff --git a/app/file.go b/app/file.go index c5e2982d46648989efcd5f4185fd4429fe4bdb70..ad58de623bd68b03d0d1878315e665be17fae4ac 100644 --- a/app/file.go +++ b/app/file.go @@ -84,7 +84,7 @@ func ReadFile(path string) ([]byte, *model.AppError) { return f, nil } } else { - return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "") + return nil, model.NewAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "", http.StatusNotImplemented) } } diff --git a/config/config.json b/config/config.json index 3111d3831dfee29b5e9ff7fa352b65c3ad3b8692..352fa0fbf669965c9b05aef372fed6390ef3d7ef 100644 --- a/config/config.json +++ b/config/config.json @@ -97,6 +97,7 @@ "Symbol": false }, "FileSettings": { + "EnableFileAttachments": true, "MaxFileSize": 52428800, "DriverName": "local", "Directory": "./data/", diff --git a/i18n/en.json b/i18n/en.json index 48491e3e9e95cce7f5c0d0ce6f3ad297791e0529..209e40437b49883b48936d6b3f9ad0b549056c8b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1051,7 +1051,11 @@ }, { "id": "api.file.get_info_for_request.storage.app_error", - "translation": "Unable to get info for file. Image storage is not configured." + "translation": "Unable to get info for file. File storage is not configured." + }, + { + "id": "api.file.attachments.disabled.app_error", + "translation": "File attachments have been disabled on this server." }, { "id": "api.file.get_public_file_old.storage.app_error", diff --git a/model/config.go b/model/config.go index 9d651035b36de2ecc949ac1999ec1246c9eeb02f..3015b332410207e32e9449dd9c2692997deee9e6 100644 --- a/model/config.go +++ b/model/config.go @@ -214,6 +214,7 @@ type PasswordSettings struct { } type FileSettings struct { + EnableFileAttachments *bool MaxFileSize *int64 DriverName string Directory string @@ -474,6 +475,11 @@ func (o *Config) SetDefaults() { *o.FileSettings.AmazonS3SSL = true // Secure by default. } + if o.FileSettings.EnableFileAttachments == nil { + o.FileSettings.EnableFileAttachments = new(bool) + *o.FileSettings.EnableFileAttachments = true + } + if o.FileSettings.MaxFileSize == nil { o.FileSettings.MaxFileSize = new(int64) *o.FileSettings.MaxFileSize = 52428800 // 50 MB diff --git a/utils/config.go b/utils/config.go index ea28cc912c685ed646cca9e995ce0e62bf1df8da..25d222f3a96195aa7872e676cc550fba0f46c00e 100644 --- a/utils/config.go +++ b/utils/config.go @@ -405,6 +405,7 @@ func getClientConfig(c *model.Config) map[string]string { props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink props["SupportEmail"] = *c.SupportSettings.SupportEmail + props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments) props["EnablePublicLink"] = strconv.FormatBool(c.FileSettings.EnablePublicLink) props["ProfileHeight"] = fmt.Sprintf("%v", c.FileSettings.ProfileHeight) props["ProfileWidth"] = fmt.Sprintf("%v", c.FileSettings.ProfileWidth) diff --git a/webapp/actions/websocket_actions.jsx b/webapp/actions/websocket_actions.jsx index c6de4264738d0417762c4808c860ad31baca629d..4d2b5a2b5ac3168f471521cdb7d8fdee0d8ffe80 100644 --- a/webapp/actions/websocket_actions.jsx +++ b/webapp/actions/websocket_actions.jsx @@ -35,6 +35,7 @@ import store from 'stores/redux_store.jsx'; const dispatch = store.dispatch; const getState = store.getState; import {viewChannel, getChannelAndMyMember, getChannelStats} from 'mattermost-redux/actions/channels'; +import {setServerVersion} from 'mattermost-redux/actions/general'; import {ChannelTypes} from 'mattermost-redux/action_types'; const MAX_WEBSOCKET_FAILS = 7; @@ -390,7 +391,7 @@ function handleStatusChangedEvent(msg) { function handleHelloEvent(msg) { Client.serverVersion = msg.data.server_version; - AsyncClient.checkVersion(); + setServerVersion(msg.data.server_version)(dispatch, getState); } function handleWebrtc(msg) { diff --git a/webapp/components/admin_console/storage_settings.jsx b/webapp/components/admin_console/storage_settings.jsx index 3b634dc53f48b1338ee05bc9a5e3a5e47f86ab9a..1400b673c729cc7be4db779787a8f0e834bbd08f 100644 --- a/webapp/components/admin_console/storage_settings.jsx +++ b/webapp/components/admin_console/storage_settings.jsx @@ -25,6 +25,7 @@ export default class StorageSettings extends AdminSettings { } getConfigFromState(config) { + config.FileSettings.EnableFileAttachments = this.state.enableFileAttachments; config.FileSettings.MaxFileSize = this.parseInt(this.state.maxFileSize) * 1024 * 1024; config.FileSettings.DriverName = this.state.driverName; config.FileSettings.Directory = this.state.directory; @@ -39,6 +40,7 @@ export default class StorageSettings extends AdminSettings { getStateFromConfig(config) { return { + enableFileAttachments: config.FileSettings.EnableFileAttachments, maxFileSize: config.FileSettings.MaxFileSize / 1024 / 1024, driverName: config.FileSettings.DriverName, directory: config.FileSettings.Directory, @@ -199,6 +201,23 @@ export default class StorageSettings extends AdminSettings { onChange={this.handleChange} disabled={this.state.driverName !== DRIVER_S3} /> + + } + helpText={ + + } + value={this.state.enableFileAttachments} + onChange={this.handleChange} + /> ); diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx index 39094091440035d8e72e039a53ad472a830e70e9..6e59b88b13d817e7c36b29b8fcc3d3ac60cf5fa3 100644 --- a/webapp/components/create_post.jsx +++ b/webapp/components/create_post.jsx @@ -630,6 +630,11 @@ export default class CreatePost extends React.Component { ); } + let attachmentsDisabled = ''; + if (global.window.mm_config.EnableFileAttachments === 'false') { + attachmentsDisabled = ' post-create--attachment-disabled'; + } + return (
-
+
0) { + if (global.window.mm_config.EnableFileAttachments === 'false') { + this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.')); + return; + } + var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(ChannelStore.getCurrentId()), items.length); if (items.length > numToUpload) { @@ -305,6 +326,12 @@ class FileUpload extends React.Component { keyUpload(e) { if (Utils.cmdOrCtrlPressed(e) && e.keyCode === Constants.KeyCodes.U) { e.preventDefault(); + + if (global.window.mm_config.EnableFileAttachments === 'false') { + this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.')); + return; + } + if ((this.props.postType === 'post' && document.activeElement.id === 'post_textbox') || (this.props.postType === 'comment' && document.activeElement.id === 'reply_textbox')) { $(this.refs.fileInput).focus().trigger('click'); @@ -361,11 +388,9 @@ class FileUpload extends React.Component { ); } - return ( - + let fileDiv; + if (global.window.mm_config.EnableFileAttachments === 'true') { + fileDiv = (
+ ); + } + + return ( + + {fileDiv} {emojiSpan} ); diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 1ac5854ee27d791b73f8514b0eb662260dd328a2..cb9d9686c3afa9391ec1021c2b509227495eb9e9 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -300,6 +300,8 @@ "admin.email.smtpUsernameTitle": "SMTP Server Username:", "admin.email.testing": "Testing...", "admin.false": "false", + "admin.file.enableFileAttachments": "Enable File Attachments:", + "admin.file.enableFileAttachmentsDesc": "When false, disable file and image uploads on messages.", "admin.file_upload.chooseFile": "Choose File", "admin.file_upload.noFile": "No file uploaded", "admin.file_upload.uploadFile": "Upload", @@ -1324,6 +1326,7 @@ "file_info_preview.type": "File type ", "file_upload.fileAbove": "File above {max}MB cannot be uploaded: {filename}", "file_upload.filesAbove": "Files above {max}MB cannot be uploaded: {filenames}", + "file_upload.disabled": "File attachments are disabled.", "file_upload.limited": "Uploads limited to {count} files maximum. Please use additional posts for more files.", "file_upload.pasted": "Image Pasted at ", "filtered_channels_list.count": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}}", diff --git a/webapp/root.jsx b/webapp/root.jsx index 6a63e6dad4737139ef6bc4db33f4980cb81644e5..03595f85c6054b6482eefd534b53a68c24e2cfdf 100644 --- a/webapp/root.jsx +++ b/webapp/root.jsx @@ -12,7 +12,6 @@ import PDFJS from 'pdfjs-dist'; import * as Websockets from 'actions/websocket_actions.jsx'; import {loadMeAndConfig} from 'actions/user_actions.jsx'; -import BrowserStore from 'stores/browser_store.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import * as I18n from 'i18n/i18n.jsx'; @@ -86,7 +85,6 @@ function preRenderSetup(callwhendone) { () => { // Turn off to prevent getting stuck in a loop $(window).off('beforeunload'); - BrowserStore.setLastServerVersion(''); if (UserStore.getCurrentUser()) { viewChannel('', ChannelStore.getCurrentId() || '')(dispatch, getState); } @@ -120,6 +118,18 @@ function renderRootComponent() { document.getElementById('root')); } +let serverVersion = ''; + +store.subscribe(() => { + const newServerVersion = getState().entities.general.serverVersion; + if (serverVersion && serverVersion !== newServerVersion) { + console.log('Detected version update refreshing the page'); //eslint-disable-line no-console + window.location.reload(true); + } + + serverVersion = newServerVersion; +}); + global.window.setup_root = () => { // Do the pre-render setup and call renderRootComponent when done preRenderSetup(renderRootComponent); diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss index 1a0a272671e7453b7c06a9851eee928f196810dc..7bf1efe2f319af9420b711d4c29cdf75f5ed4991 100644 --- a/webapp/sass/responsive/_tablet.scss +++ b/webapp/sass/responsive/_tablet.scss @@ -32,6 +32,18 @@ } .post-create__container { + .post-create { + &.post-create--attachment-disabled { + .post-body__cell { + padding-left: 1em; + } + + .post-create-footer { + padding-left: 1em; + } + } + } + form { padding: .5em 0 0; } @@ -71,6 +83,10 @@ padding: 0; top: auto; width: 25px; + + .icon--emoji-picker { + display: none; + } } } diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx index b0c923594f6c5c85b9a36360f72e58bd4a80efce..18fcc452dfa7126bfeca898db3f46882fe6d1a8f 100644 --- a/webapp/stores/browser_store.jsx +++ b/webapp/stores/browser_store.jsx @@ -75,14 +75,6 @@ class BrowserStoreClass { } } - getLastServerVersion() { - return this.getGlobalItem('last_server_version'); - } - - setLastServerVersion(version) { - this.setGlobalItem('last_server_version', version); - } - signalLogout() { if (this.isLocalStorageSupported()) { // PLT-1285 store an identifier in session storage so we can catch if the logout came from this tab on IE11 @@ -144,7 +136,6 @@ class BrowserStoreClass { clear() { // persist some values through logout since they're independent of which user is logged in const logoutId = sessionStorage.getItem('__logout__'); - const serverVersion = this.getLastServerVersion(); const landingPageSeen = this.hasSeenLandingPage(); const selectedTeams = this.getItem('selected_teams'); const recentEmojis = localStorage.getItem(Constants.RECENT_EMOJI_KEY); @@ -160,10 +151,6 @@ class BrowserStoreClass { sessionStorage.setItem('__logout__', logoutId); } - if (serverVersion) { - this.setLastServerVersion(serverVersion); - } - if (landingPageSeen) { this.setLandingPageSeen(landingPageSeen); } diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx index 712d447e8a945e96654782a2bde1fd8aab15732c..deb967e3a1fef7c5f57f91bca43bd5920c6309a7 100644 --- a/webapp/utils/async_client.jsx +++ b/webapp/utils/async_client.jsx @@ -1,7 +1,6 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import BrowserStore from 'stores/browser_store.jsx'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -21,6 +20,12 @@ const callTracker = {}; const ASYNC_CLIENT_TIMEOUT = 5000; +// Redux actions +import store from 'stores/redux_store.jsx'; +const dispatch = store.dispatch; +const getState = store.getState; +import {setServerVersion} from 'mattermost-redux/actions/general'; + export function dispatchError(err, method) { AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_ERROR, @@ -47,17 +52,7 @@ function isCallInProgress(callName) { } export function checkVersion() { - var serverVersion = Client.getServerVersion(); - - 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 - } - } + setServerVersion(Client.getServerVersion())(dispatch, getState); } export function getUser(userId, success, error) {