summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/file.go9
-rw-r--r--api/file_test.go12
-rw-r--r--api4/file.go8
-rw-r--r--api4/file_test.go9
-rw-r--r--app/file.go2
-rw-r--r--config/config.json1
-rw-r--r--i18n/en.json6
-rw-r--r--model/config.go6
-rw-r--r--utils/config.go1
-rw-r--r--webapp/actions/websocket_actions.jsx3
-rw-r--r--webapp/components/admin_console/storage_settings.jsx20
-rw-r--r--webapp/components/create_post.jsx7
-rw-r--r--webapp/components/file_upload.jsx100
-rwxr-xr-xwebapp/i18n/en.json3
-rw-r--r--webapp/root.jsx14
-rw-r--r--webapp/sass/responsive/_tablet.scss16
-rw-r--r--webapp/stores/browser_store.jsx13
-rw-r--r--webapp/utils/async_client.jsx19
18 files changed, 180 insertions, 69 deletions
diff --git a/api/file.go b/api/file.go
index 0f2fd9319..9a5de5669 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 1e65c33e8..40534d724 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 6bd751a67..09132b9a1 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 9124e893b..e48aabffc 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 c5e2982d4..ad58de623 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 3111d3831..352fa0fbf 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 48491e3e9..209e40437 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 9d651035b..3015b3324 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 ea28cc912..25d222f3a 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 c6de42647..4d2b5a2b5 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 3b634dc53..1400b673c 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}
/>
+ <BooleanSetting
+ id='enableFileAttachments'
+ label={
+ <FormattedMessage
+ id='admin.file.enableFileAttachments'
+ defaultMessage='Enable File Attachments:'
+ />
+ }
+ helpText={
+ <FormattedMessage
+ id='admin.file.enableFileAttachmentsDesc'
+ defaultMessage='When false, disable file and image uploads on messages.'
+ />
+ }
+ value={this.state.enableFileAttachments}
+ onChange={this.handleChange}
+ />
<TextSetting
id='maxFileSize'
label={
@@ -216,6 +235,7 @@ export default class StorageSettings extends AdminSettings {
}
value={this.state.maxFileSize}
onChange={this.handleChange}
+ disabled={!this.state.enableFileAttachments}
/>
</SettingsGroup>
);
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index 390940914..6e59b88b1 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 (
<form
id='create_post'
@@ -638,7 +643,7 @@ export default class CreatePost extends React.Component {
className={centerClass}
onSubmit={this.handleSubmit}
>
- <div className='post-create'>
+ <div className={'post-create' + attachmentsDisabled}>
<div className='post-create-body'>
<div className='post-body__cell'>
<Textbox
diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx
index af5d76829..4ef8cd28c 100644
--- a/webapp/components/file_upload.jsx
+++ b/webapp/components/file_upload.jsx
@@ -132,6 +132,11 @@ class FileUpload extends React.Component {
}
handleDrop(e) {
+ if (global.window.mm_config.EnableFileAttachments === 'false') {
+ this.props.onUploadError(Utils.localizeMessage('file_upload.disabled', 'File attachments are disabled.'));
+ return;
+ }
+
this.props.onUploadError(null);
var files = e.originalEvent.dataTransfer.files;
@@ -163,36 +168,47 @@ class FileUpload extends React.Component {
}
});
- $(containerSelector).dragster({
- enter(dragsterEvent, e) {
- var files = e.originalEvent.dataTransfer;
-
- if (Utils.isFileTransfer(files)) {
- $(overlaySelector).removeClass('hidden');
+ let dragsterActions = {};
+ if (global.window.mm_config.EnableFileAttachments === 'true') {
+ dragsterActions = {
+ enter(dragsterEvent, e) {
+ var files = e.originalEvent.dataTransfer;
+
+ if (Utils.isFileTransfer(files)) {
+ $(overlaySelector).removeClass('hidden');
+ }
+ },
+ leave(dragsterEvent, e) {
+ var files = e.originalEvent.dataTransfer;
+
+ if (Utils.isFileTransfer(files) && !overlay.hasClass('hidden')) {
+ overlay.addClass('hidden');
+ }
+
+ dragTimeout.cancel();
+ },
+ over() {
+ dragTimeout.fireAfter(OverlayTimeout);
+ },
+ drop(dragsterEvent, e) {
+ if (!overlay.hasClass('hidden')) {
+ overlay.addClass('hidden');
+ }
+
+ dragTimeout.cancel();
+
+ self.handleDrop(e);
}
- },
- leave(dragsterEvent, e) {
- var files = e.originalEvent.dataTransfer;
-
- if (Utils.isFileTransfer(files) && !overlay.hasClass('hidden')) {
- overlay.addClass('hidden');
- }
-
- dragTimeout.cancel();
- },
- over() {
- dragTimeout.fireAfter(OverlayTimeout);
- },
- drop(dragsterEvent, e) {
- if (!overlay.hasClass('hidden')) {
- overlay.addClass('hidden');
+ };
+ } else {
+ dragsterActions = {
+ drop(dragsterEvent, e) {
+ self.handleDrop(e);
}
+ };
+ }
- dragTimeout.cancel();
-
- self.handleDrop(e);
- }
- });
+ $(containerSelector).dragster(dragsterActions);
this.props.onFileUploadChange();
}
@@ -247,7 +263,12 @@ class FileUpload extends React.Component {
// This looks redundant, but must be done this way due to
// setState being an asynchronous call
- if (items) {
+ if (items && items.length > 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 (
- <span
- ref='input'
- className={'btn btn-file' + (uploadsRemaining <= 0 ? ' btn-file__disabled' : '')}
- >
+ let fileDiv;
+ if (global.window.mm_config.EnableFileAttachments === 'true') {
+ fileDiv = (
<div className='icon--attachment'>
<span
className='icon'
@@ -380,6 +405,15 @@ class FileUpload extends React.Component {
accept={accept}
/>
</div>
+ );
+ }
+
+ return (
+ <span
+ ref='input'
+ className={'btn btn-file' + (uploadsRemaining <= 0 ? ' btn-file__disabled' : '')}
+ >
+ {fileDiv}
{emojiSpan}
</span>
);
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 1ac5854ee..cb9d9686c 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 6a63e6dad..03595f85c 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 1a0a27267..7bf1efe2f 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 b0c923594..18fcc452d 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 712d447e8..deb967e3a 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) {