diff --git a/package.json b/package.json index 1395ce329d..c47921d71a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "connect-redis": "~3.1.0", "cookie-parser": "^1.3.3", "cron": "^1.0.5", + "cropperjs": "^0.8.1", "csurf": "^1.6.1", "daemon": "~1.1.0", "express": "^4.14.0", diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 6215486cb3..4013d5898c 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -68,6 +68,8 @@ "remove_uploaded_picture" : "Remove Uploaded Picture", "upload_cover_picture": "Upload cover picture", "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", "settings": "Settings", "show_email": "Show My Email", diff --git a/public/less/generics.less b/public/less/generics.less index 8533dd33b9..0725264501 100644 --- a/public/less/generics.less +++ b/public/less/generics.less @@ -129,4 +129,14 @@ .admin .ban-modal .units { line-height: 1.846; -} \ No newline at end of file +} + +#crop-picture-modal { + .cropped-image { + max-width: 100%; + } + + .cropper-container.cropper-bg { + max-width: 100%; + } +} diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index 4dbf805285..57eea04782 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -2,7 +2,7 @@ /* globals define, ajaxify, socket, app, config, templates, bootbox */ -define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', 'components'], function (header, uploader, translator, components) { +define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', 'components', 'cropper'], function (header, uploader, translator, components, cropper) { var AccountEdit = {}; AccountEdit.init = function () { @@ -210,6 +210,42 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', updateHeader(); } } + + function handleImageCrop(data) { + $('#crop-picture-modal').remove(); + templates.parse('modals/crop_picture', {url: data.url}, function (cropperHtml) { + translator.translate(cropperHtml, function (translated) { + var cropperModal = $(translated); + cropperModal.modal('show'); + + var img = document.getElementById('cropped-image'); + var cropperTool = new cropper.default(img, { + aspectRatio: 1 / 1, + viewMode: 1 + }); + + cropperModal.find('.crop-btn').on('click', function () { + $(this).addClass('disabled'); + var imageData = data.imageType ? cropperTool.getCroppedCanvas().toDataURL(data.imageType) : cropperTool.getCroppedCanvas().toDataURL(); + + cropperModal.find('#upload-progress-bar').css('width', '100%'); + cropperModal.find('#upload-progress-box').show().removeClass('hide'); + + socket.emit('user.uploadCroppedPicture', { + uid: ajaxify.data.theirid, + imageData: imageData + }, function (err, imageData) { + if (err) { + app.alertError(err.message); + } + + onUploadComplete(imageData.url); + cropperModal.modal('hide'); + }); + }); + }); + }); + } modal.find('[data-action="upload"]').on('click', function () { modal.modal('hide'); @@ -221,8 +257,8 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', title: '[[user:upload_picture]]', description: '[[user:upload_a_picture]]', accept: '.png,.jpg,.bmp' - }, function (imageUrlOnServer) { - onUploadComplete(imageUrlOnServer); + }, function (data) { + handleImageCrop(data); }); return false; @@ -240,15 +276,10 @@ define('forum/account/edit', ['forum/account/header', 'uploader', 'translator', if (!url) { return; } - socket.emit('user.uploadProfileImageFromUrl', {url: url, uid: ajaxify.data.theirid}, function (err, imageUrlOnServer) { - if (err) { - return app.alertError(err.message); - } - onUploadComplete(imageUrlOnServer); - - uploadModal.modal('hide'); - }); - + + uploadModal.modal('hide'); + handleImageCrop({url: url}); + return false; }); }); diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js index 106b82d67d..bae1a2ce00 100644 --- a/public/src/modules/uploader.js +++ b/public/src/modules/uploader.js @@ -1,8 +1,8 @@ 'use strict'; -/* globals define, templates */ +/* globals define, ajaxify, socket, app, templates */ -define('uploader', ['translator'], function (translator) { +define('uploader', ['translator', 'cropper'], function (translator, cropper) { var module = {}; @@ -61,46 +61,27 @@ define('uploader', ['translator'], function (translator) { uploadModal.find('#alert-' + type).translateText(message).removeClass('hide'); } - showAlert('status', '[[uploads:uploading-file]]'); - - uploadModal.find('#upload-progress-bar').css('width', '0%'); - uploadModal.find('#upload-progress-box').show().removeClass('hide'); - var fileInput = uploadModal.find('#fileInput'); if (!fileInput.val()) { return showAlert('error', '[[uploads:select-file-to-upload]]'); } - if (!hasValidFileSize(fileInput[0], fileSize)) { - return showAlert('error', '[[error:file-too-big, ' + fileSize + ']]'); + + var file = fileInput[0].files[0]; + var reader = new FileReader(); + var imageUrl; + var imageType = file.type; + + reader.addEventListener("load", function () { + imageUrl = reader.result; + + uploadModal.modal('hide'); + + callback({url: imageUrl, imageType: imageType}); + }, false); + + if (file) { + reader.readAsDataURL(file); } - - uploadModal.find('#uploadForm').ajaxSubmit({ - headers: { - 'x-csrf-token': config.csrf_token - }, - error: function (xhr) { - xhr = maybeParse(xhr); - showAlert('error', xhr.responseJSON ? (xhr.responseJSON.error || xhr.statusText) : 'error uploading, code : ' + xhr.status); - }, - uploadProgress: function (event, position, total, percent) { - uploadModal.find('#upload-progress-bar').css('width', percent + '%'); - }, - success: function (response) { - response = maybeParse(response); - - if (response.error) { - return showAlert('error', response.error); - } - - callback(response[0].url); - - showAlert('success', '[[uploads:upload-success]]'); - setTimeout(function () { - module.hideAlerts(uploadModal); - uploadModal.modal('hide'); - }, 750); - } - }); } function parseModal(tplVals, callback) { @@ -109,23 +90,5 @@ define('uploader', ['translator'], function (translator) { }); } - function maybeParse(response) { - if (typeof response === 'string') { - try { - return $.parseJSON(response); - } catch (e) { - return {error: '[[error:parse-error]]'}; - } - } - return response; - } - - function hasValidFileSize(fileElement, maxSize) { - if (window.FileReader && maxSize) { - return fileElement.files[0].size <= maxSize * 1000; - } - return true; - } - return module; }); diff --git a/src/meta/css.js b/src/meta/css.js index 0d63e09d6d..4bfb19ba47 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -70,6 +70,7 @@ module.exports = function (Meta) { source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/generics.less";'; source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/mixins.less";'; source += '\n@import "..' + path.sep + '..' + path.sep + 'public/less/global.less";'; + source += '\n@import (inline) "..' + path.sep + 'node_modules/cropperjs/dist/cropper.css";'; source = '@import "./theme";\n' + source; minify(source, paths, 'cache', callback); diff --git a/src/meta/js.js b/src/meta/js.js index 7f39235993..d53a2c1789 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -81,7 +81,8 @@ module.exports = function (Meta) { "Chart.js": './node_modules/chart.js/dist/Chart.min.js', "mousetrap.js": './node_modules/mousetrap/mousetrap.min.js', "jqueryui.js": 'public/vendor/jquery/js/jquery-ui.js', - "buzz.js": 'public/vendor/buzz/buzz.js' + "buzz.js": 'public/vendor/buzz/buzz.js', + "cropper.js": './node_modules/cropperjs/dist/cropper.min.js' } } }; diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index 8a59fbbff2..392c7559b7 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -37,6 +37,20 @@ module.exports = function (SocketUser) { } ], callback); }; + + SocketUser.uploadCroppedPicture = function (socket, data, callback) { + if (!socket.uid) { + return callback(new Error('[[error:no-privileges]]')); + } + async.waterfall([ + function (next) { + user.isAdminOrSelf(socket.uid, data.uid, next); + }, + function (next) { + user.uploadCroppedPicture(data, next); + } + ], callback); + }; SocketUser.removeCover = function (socket, data, callback) { if (!socket.uid) { diff --git a/src/user/picture.js b/src/user/picture.js index 0fd1d1a3bd..b790ef77c2 100644 --- a/src/user/picture.js +++ b/src/user/picture.js @@ -220,6 +220,84 @@ module.exports = function (User) { } }); }; + + User.uploadCroppedPicture = function (data, callback) { + var keepAllVersions = parseInt(meta.config['profile:keepAllUserImages'], 10) === 1; + var url, md5sum; + + if (!data.imageData) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + var size = data.file ? data.file.size : data.imageData.length; + var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256; + if (size > uploadSize * 1024) { + return next(new Error('[[error:file-too-big, ' + meta.config.maximumProfileImageSize + ']]')); + } + + md5sum = crypto.createHash('md5'); + md5sum.update(data.imageData); + md5sum = md5sum.digest('hex'); + + data.file = { + path: path.join(os.tmpdir(), md5sum) + }; + + var buffer = new Buffer(data.imageData.slice(data.imageData.indexOf('base64') + 7), 'base64'); + + fs.writeFile(data.file.path, buffer, { + encoding: 'base64' + }, next); + }, + function (next) { + var image = { + name: 'profileAvatar', + path: data.file.path, + uid: data.uid + }; + + if (plugins.hasListeners('filter:uploadImage')) { + return plugins.fireHook('filter:uploadImage', {image: image, uid: data.uid}, next); + } + + var filename = data.uid + '-profileavatar' + (keepAllVersions ? '-' + Date.now() : ''); + async.waterfall([ + function (next) { + file.isFileTypeAllowed(data.file.path, next); + }, + function (next) { + file.saveFileToLocal(filename, 'profile', image.path, next); + }, + function (upload, next) { + next(null, { + url: nconf.get('relative_path') + upload.url, + name: image.name + }); + } + ], next); + }, + function (uploadData, next) { + url = uploadData.url; + User.setUserFields(data.uid, {uploadedpicture: url, picture: url}, next); + }, + function (next) { + fs.unlink(data.file.path, function (err) { + if (err) { + winston.error(err); + } + next(); + }); + } + ], function (err) { + if (err) { + callback(err); // send back the original error + } + + callback(err, {url: url}); + }); + }; User.removeCoverPicture = function (data, callback) { db.deleteObjectFields('user:' + data.uid, ['cover:url', 'cover:position'], callback); diff --git a/src/views/modals/crop_picture.tpl b/src/views/modals/crop_picture.tpl new file mode 100644 index 0000000000..d3bf8e075b --- /dev/null +++ b/src/views/modals/crop_picture.tpl @@ -0,0 +1,22 @@ + \ No newline at end of file