Commit 49481caf authored by Joram Wilander's avatar Joram Wilander Committed by GitHub

PLT-6262 Add config setting to disable file attachments (#6301)

* Add config setting to disable file attachments

* Add unit tests

* Updating UI for no attachments (#6312)

* Update UI text on file upload System Console setting (#6313)

* Update storage_settings.jsx

* Update en.json
parent 44a8f76d
......@@ -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)
......
......@@ -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)
......
......@@ -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
}
......
......@@ -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) {
......
......@@ -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)
}
}
......
......@@ -97,6 +97,7 @@
"Symbol": false
},
"FileSettings": {
"EnableFileAttachments": true,
"MaxFileSize": 52428800,
"DriverName": "local",
"Directory": "./data/",
......
......@@ -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",
......
......@@ -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
......
......@@ -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)
......
......@@ -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) {
......
......@@ -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>
);
......
......@@ -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
......
......@@ -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,7 +168,9 @@ class FileUpload extends React.Component {
}
});
$(containerSelector).dragster({
let dragsterActions = {};
if (global.window.mm_config.EnableFileAttachments === 'true') {
dragsterActions = {
enter(dragsterEvent, e) {
var files = e.originalEvent.dataTransfer;
......@@ -192,7 +199,16 @@ class FileUpload extends React.Component {
self.handleDrop(e);
}
});
};
} else {
dragsterActions = {
drop(dragsterEvent, e) {
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>
);
......
......@@ -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}}",
......
......@@ -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);
......
......@@ -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;
}
}
}
......
......@@ -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);
}
......
// 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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment