From d4b7c028fa3f01beaaa06e56b6fda90ee24f85d2 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 14 Nov 2019 11:56:12 -0500 Subject: [PATCH] REFACTOR: Move upload utilities to their own file --- .../components/composer-editor.js.es6 | 14 +- .../controllers/avatar-selector.js.es6 | 7 +- .../discourse/controllers/composer.js.es6 | 13 +- .../controllers/upload-selector.js.es6 | 24 +- .../javascripts/discourse/lib/uploads.js.es6 | 285 +++++++++++++++++ .../discourse/lib/utilities.js.es6 | 286 ------------------ .../discourse/mixins/upload.js.es6 | 4 +- test/javascripts/lib/uploads-test.js.es6 | 223 ++++++++++++++ test/javascripts/lib/utilities-test.js.es6 | 218 ------------- 9 files changed, 546 insertions(+), 528 deletions(-) create mode 100644 app/assets/javascripts/discourse/lib/uploads.js.es6 create mode 100644 test/javascripts/lib/uploads-test.js.es6 diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 0a5345e490e..ce8634bfcf7 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -31,14 +31,17 @@ import { findRawTemplate } from "discourse/lib/raw-templates"; import { iconHTML } from "discourse-common/lib/icon-library"; import { tinyAvatar, - displayErrorForUpload, - getUploadMarkdown, - validateUploadedFiles, - authorizesOneOrMoreImageExtensions, formatUsername, clipboardData, safariHacksDisabled } from "discourse/lib/utilities"; +import { + validateUploadedFiles, + authorizesOneOrMoreImageExtensions, + getUploadMarkdown, + displayErrorForUpload +} from "discourse/lib/uploads"; + import { cacheShortUploadUrl, resolveAllShortUrls @@ -82,7 +85,7 @@ export default Component.extend({ if (requiredCategoryMissing) { return "composer.reply_placeholder_choose_category"; } else { - const key = authorizesOneOrMoreImageExtensions() + const key = authorizesOneOrMoreImageExtensions(this.currentUser.staff) ? "reply_placeholder" : "reply_placeholder_no_images"; return `composer.${key}`; @@ -700,6 +703,7 @@ export default Component.extend({ if (this._pasted) data.formData.pasted = true; const opts = { + user: this.currentUser, isPrivateMessage, allowStaffToUploadAnyFileInPm: this.siteSettings .allow_staff_to_upload_any_file_in_pm diff --git a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 index 59c28e6932a..959b296cecb 100644 --- a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 @@ -2,7 +2,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; -import { allowsImages } from "discourse/lib/utilities"; +import { allowsImages } from "discourse/lib/uploads"; import { popupAjaxError } from "discourse/lib/ajax-error"; export default Controller.extend(ModalFunctionality, { @@ -42,7 +42,10 @@ export default Controller.extend(ModalFunctionality, { @discourseComputed() allowAvatarUpload() { - return this.siteSettings.allow_uploaded_avatars && allowsImages(); + return ( + this.siteSettings.allow_uploaded_avatars && + allowsImages(this.currentUser.staff) + ); }, actions: { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 2289470cfa9..ae1393bd34e 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -14,12 +14,11 @@ import { on } from "discourse-common/utils/decorators"; import { getOwner } from "discourse-common/lib/get-owner"; +import { escapeExpression, safariHacksDisabled } from "discourse/lib/utilities"; import { - escapeExpression, - uploadIcon, authorizesOneOrMoreExtensions, - safariHacksDisabled -} from "discourse/lib/utilities"; + uploadIcon +} from "discourse/lib/uploads"; import { emojiUnescape } from "discourse/lib/text"; import { shortDate } from "discourse/lib/formatter"; import { SAVE_LABELS, SAVE_ICONS } from "discourse/models/composer"; @@ -322,11 +321,13 @@ export default Controller.extend({ @discourseComputed allowUpload() { - return authorizesOneOrMoreExtensions(); + return authorizesOneOrMoreExtensions(this.currentUser.staff); }, @discourseComputed() - uploadIcon: () => uploadIcon(), + uploadIcon() { + return uploadIcon(this.currentUser.staff); + }, actions: { togglePreview() { diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index 554fe6db2f7..64a433ecd98 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -7,13 +7,13 @@ import { } from "discourse-common/utils/decorators"; import { allowsAttachments, - authorizesAllExtensions, authorizedExtensions, + authorizesAllExtensions, uploadIcon -} from "discourse/lib/utilities"; +} from "discourse/lib/uploads"; -function uploadTranslate(key) { - if (allowsAttachments()) { +function uploadTranslate(key, user) { + if (allowsAttachments(user.staff)) { key += "_with_attachments"; } return `upload_selector.${key}`; @@ -28,17 +28,23 @@ export default Controller.extend(ModalFunctionality, { selection: "local", @discourseComputed() - uploadIcon: () => uploadIcon(), + uploadIcon() { + return uploadIcon(this.currentUser.staff); + }, @discourseComputed() - title: () => uploadTranslate("title"), + title() { + return uploadTranslate("title", this.currentUser); + }, @discourseComputed("selection") tip(selection) { - const authorized_extensions = authorizesAllExtensions() + const authorized_extensions = authorizesAllExtensions( + this.currentUser.staff + ) ? "" - : `(${authorizedExtensions()})`; - return I18n.t(uploadTranslate(`${selection}_tip`), { + : `(${authorizedExtensions(this.currentUser.staff)})`; + return I18n.t(uploadTranslate(`${selection}_tip`, this.currentUser), { authorized_extensions }); }, diff --git a/app/assets/javascripts/discourse/lib/uploads.js.es6 b/app/assets/javascripts/discourse/lib/uploads.js.es6 new file mode 100644 index 00000000000..4bfd4ac2c03 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/uploads.js.es6 @@ -0,0 +1,285 @@ +import { isAppleDevice } from "discourse/lib/utilities"; + +function isGUID(value) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( + value + ); +} + +function imageNameFromFileName(fileName) { + const split = fileName.split("."); + let name = split[split.length - 2]; + + if (isAppleDevice() && isGUID(name)) { + name = I18n.t("upload_selector.default_image_alt_text"); + } + + return encodeURIComponent(name); +} + +export function validateUploadedFiles(files, opts) { + if (!files || files.length === 0) { + return false; + } + + if (files.length > 1) { + bootbox.alert(I18n.t("post.errors.too_many_uploads")); + return false; + } + + const upload = files[0]; + + // CHROME ONLY: if the image was pasted, sets its name to a default one + if (typeof Blob !== "undefined" && typeof File !== "undefined") { + if ( + upload instanceof Blob && + !(upload instanceof File) && + upload.type === "image/png" + ) { + upload.name = "image.png"; + } + } + + opts = opts || {}; + opts.type = uploadTypeFromFileName(upload.name); + + return validateUploadedFile(upload, opts); +} + +function validateUploadedFile(file, opts) { + if (opts.skipValidation) return true; + + opts = opts || {}; + let user = opts.user; + let staff = user && user.staff; + + if (!authorizesOneOrMoreExtensions(staff)) return false; + + const name = file && file.name; + + if (!name) { + return false; + } + + // check that the uploaded file is authorized + if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) { + if (staff) { + return true; + } + } + + if (opts.imagesOnly) { + if (!isAnImage(name) && !isAuthorizedImage(name, staff)) { + bootbox.alert( + I18n.t("post.errors.upload_not_authorized", { + authorized_extensions: authorizedImagesExtensions(staff) + }) + ); + return false; + } + } else if (opts.csvOnly) { + if (!/\.csv$/i.test(name)) { + bootbox.alert(I18n.t("user.invited.bulk_invite.error")); + return false; + } + } else { + if (!authorizesAllExtensions(staff) && !isAuthorizedFile(name, staff)) { + bootbox.alert( + I18n.t("post.errors.upload_not_authorized", { + authorized_extensions: authorizedExtensions(staff) + }) + ); + return false; + } + } + + if (!opts.bypassNewUserRestriction) { + // ensures that new users can upload a file + if (user && !user.isAllowedToUploadAFile(opts.type)) { + bootbox.alert( + I18n.t(`post.errors.${opts.type}_upload_not_allowed_for_new_user`) + ); + return false; + } + } + + // everything went fine + return true; +} + +const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico)/i; + +function extensionsToArray(exts) { + return exts + .toLowerCase() + .replace(/[\s\.]+/g, "") + .split("|") + .filter(ext => ext.indexOf("*") === -1); +} + +function extensions() { + return extensionsToArray(Discourse.SiteSettings.authorized_extensions); +} + +function staffExtensions() { + return extensionsToArray( + Discourse.SiteSettings.authorized_extensions_for_staff + ); +} + +function imagesExtensions(staff) { + let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); + if (staff) { + const staffExts = staffExtensions().filter(ext => + IMAGES_EXTENSIONS_REGEX.test(ext) + ); + exts = _.union(exts, staffExts); + } + return exts; +} + +function extensionsRegex() { + return new RegExp("\\.(" + extensions().join("|") + ")$", "i"); +} + +function imagesExtensionsRegex(staff) { + return new RegExp("\\.(" + imagesExtensions(staff).join("|") + ")$", "i"); +} + +function staffExtensionsRegex() { + return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i"); +} + +function isAuthorizedFile(fileName, staff) { + if (staff && staffExtensionsRegex().test(fileName)) { + return true; + } + return extensionsRegex().test(fileName); +} + +function isAuthorizedImage(fileName, staff) { + return imagesExtensionsRegex(staff).test(fileName); +} + +export function authorizedExtensions(staff) { + const exts = staff ? [...extensions(), ...staffExtensions()] : extensions(); + return exts.filter(ext => ext.length > 0).join(", "); +} + +function authorizedImagesExtensions(staff) { + return authorizesAllExtensions(staff) + ? "png, jpg, jpeg, gif, svg, ico" + : imagesExtensions(staff).join(", "); +} + +export function authorizesAllExtensions(staff) { + return ( + Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 || + (Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 && + staff) + ); +} + +export function authorizesOneOrMoreExtensions(staff) { + if (authorizesAllExtensions(staff)) return true; + + return ( + Discourse.SiteSettings.authorized_extensions.split("|").filter(ext => ext) + .length > 0 + ); +} + +export function authorizesOneOrMoreImageExtensions(staff) { + if (authorizesAllExtensions(staff)) return true; + return imagesExtensions(staff).length > 0; +} + +export function isAnImage(path) { + return /\.(png|jpe?g|gif|svg|ico)$/i.test(path); +} + +function uploadTypeFromFileName(fileName) { + return isAnImage(fileName) ? "image" : "attachment"; +} + +export function allowsImages(staff) { + return ( + authorizesAllExtensions(staff) || + IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions(staff)) + ); +} + +export function allowsAttachments(staff) { + return ( + authorizesAllExtensions(staff) || + authorizedExtensions(staff).split(", ").length > + imagesExtensions(staff).length + ); +} + +export function uploadIcon(staff) { + return allowsAttachments(staff) ? "upload" : "far-image"; +} + +function uploadLocation(url) { + if (Discourse.CDN) { + url = Discourse.getURLWithCDN(url); + return /^\/\//.test(url) ? "http:" + url : url; + } else if (Discourse.S3BaseUrl) { + return "https:" + url; + } else { + var protocol = window.location.protocol + "//", + hostname = window.location.hostname, + port = window.location.port ? ":" + window.location.port : ""; + return protocol + hostname + port + url; + } +} + +export function getUploadMarkdown(upload) { + if (isAnImage(upload.original_filename)) { + const name = imageNameFromFileName(upload.original_filename); + return `![${name}|${upload.thumbnail_width}x${ + upload.thumbnail_height + }](${upload.short_url || upload.url})`; + } else if ( + !Discourse.SiteSettings.prevent_anons_from_downloading_files && + /\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i.test(upload.original_filename) + ) { + return uploadLocation(upload.url); + } else { + return `[${upload.original_filename}|attachment](${ + upload.short_url + }) (${I18n.toHumanSize(upload.filesize)})`; + } +} + +export function displayErrorForUpload(data) { + if (data.jqXHR) { + switch (data.jqXHR.status) { + // cancelled by the user + case 0: + return; + + // entity too large, usually returned from the web server + case 413: + const type = uploadTypeFromFileName(data.files[0].name); + const max_size_kb = Discourse.SiteSettings[`max_${type}_size_kb`]; + bootbox.alert(I18n.t("post.errors.file_too_large", { max_size_kb })); + return; + + // the error message is provided by the server + case 422: + if (data.jqXHR.responseJSON.message) { + bootbox.alert(data.jqXHR.responseJSON.message); + } else { + bootbox.alert(data.jqXHR.responseJSON.errors.join("\n")); + } + return; + } + } else if (data.errors && data.errors.length > 0) { + bootbox.alert(data.errors.join("\n")); + return; + } + // otherwise, display a generic error message + bootbox.alert(I18n.t("post.errors.upload")); +} diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 599c3a4965a..145674f3a57 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -195,292 +195,6 @@ export function setCaretPosition(ctrl, pos) { } } -export function validateUploadedFiles(files, opts) { - if (!files || files.length === 0) { - return false; - } - - if (files.length > 1) { - bootbox.alert(I18n.t("post.errors.too_many_uploads")); - return false; - } - - const upload = files[0]; - - // CHROME ONLY: if the image was pasted, sets its name to a default one - if (typeof Blob !== "undefined" && typeof File !== "undefined") { - if ( - upload instanceof Blob && - !(upload instanceof File) && - upload.type === "image/png" - ) { - upload.name = "image.png"; - } - } - - opts = opts || {}; - opts.type = uploadTypeFromFileName(upload.name); - - return validateUploadedFile(upload, opts); -} - -export function validateUploadedFile(file, opts) { - if (opts.skipValidation) return true; - if (!authorizesOneOrMoreExtensions()) return false; - - opts = opts || {}; - - const name = file && file.name; - - if (!name) { - return false; - } - - // check that the uploaded file is authorized - if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) { - if (Discourse.User.currentProp("staff")) { - return true; - } - } - - if (opts.imagesOnly) { - if (!isAnImage(name) && !isAuthorizedImage(name)) { - bootbox.alert( - I18n.t("post.errors.upload_not_authorized", { - authorized_extensions: authorizedImagesExtensions() - }) - ); - return false; - } - } else if (opts.csvOnly) { - if (!/\.csv$/i.test(name)) { - bootbox.alert(I18n.t("user.invited.bulk_invite.error")); - return false; - } - } else { - if (!authorizesAllExtensions() && !isAuthorizedFile(name)) { - bootbox.alert( - I18n.t("post.errors.upload_not_authorized", { - authorized_extensions: authorizedExtensions() - }) - ); - return false; - } - } - - if (!opts.bypassNewUserRestriction) { - // ensures that new users can upload a file - if (!Discourse.User.current().isAllowedToUploadAFile(opts.type)) { - bootbox.alert( - I18n.t(`post.errors.${opts.type}_upload_not_allowed_for_new_user`) - ); - return false; - } - } - - // everything went fine - return true; -} - -const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico)/i; - -function extensionsToArray(exts) { - return exts - .toLowerCase() - .replace(/[\s\.]+/g, "") - .split("|") - .filter(ext => ext.indexOf("*") === -1); -} - -function extensions() { - return extensionsToArray(Discourse.SiteSettings.authorized_extensions); -} - -function staffExtensions() { - return extensionsToArray( - Discourse.SiteSettings.authorized_extensions_for_staff - ); -} - -function imagesExtensions() { - let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext)); - if (Discourse.User.currentProp("staff")) { - const staffExts = staffExtensions().filter(ext => - IMAGES_EXTENSIONS_REGEX.test(ext) - ); - exts = _.union(exts, staffExts); - } - return exts; -} - -function extensionsRegex() { - return new RegExp("\\.(" + extensions().join("|") + ")$", "i"); -} - -function imagesExtensionsRegex() { - return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i"); -} - -function staffExtensionsRegex() { - return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i"); -} - -function isAuthorizedFile(fileName) { - if ( - Discourse.User.currentProp("staff") && - staffExtensionsRegex().test(fileName) - ) { - return true; - } - return extensionsRegex().test(fileName); -} - -function isAuthorizedImage(fileName) { - return imagesExtensionsRegex().test(fileName); -} - -export function authorizedExtensions() { - const exts = Discourse.User.currentProp("staff") - ? [...extensions(), ...staffExtensions()] - : extensions(); - return exts.filter(ext => ext.length > 0).join(", "); -} - -export function authorizedImagesExtensions() { - return authorizesAllExtensions() - ? "png, jpg, jpeg, gif, svg, ico" - : imagesExtensions().join(", "); -} - -export function authorizesAllExtensions() { - return ( - Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 || - (Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 && - Discourse.User.currentProp("staff")) - ); -} - -export function authorizesOneOrMoreExtensions() { - if (authorizesAllExtensions()) return true; - - return ( - Discourse.SiteSettings.authorized_extensions.split("|").filter(ext => ext) - .length > 0 - ); -} - -export function authorizesOneOrMoreImageExtensions() { - if (authorizesAllExtensions()) return true; - - return imagesExtensions().length > 0; -} - -export function isAnImage(path) { - return /\.(png|jpe?g|gif|svg|ico)$/i.test(path); -} - -function uploadTypeFromFileName(fileName) { - return isAnImage(fileName) ? "image" : "attachment"; -} - -function isGUID(value) { - return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( - value - ); -} - -function imageNameFromFileName(fileName) { - const split = fileName.split("."); - let name = split[split.length - 2]; - - if (exports.isAppleDevice() && isGUID(name)) { - name = I18n.t("upload_selector.default_image_alt_text"); - } - - return encodeURIComponent(name); -} - -export function allowsImages() { - return ( - authorizesAllExtensions() || - IMAGES_EXTENSIONS_REGEX.test(authorizedExtensions()) - ); -} - -export function allowsAttachments() { - return ( - authorizesAllExtensions() || - authorizedExtensions().split(", ").length > imagesExtensions().length - ); -} - -export function uploadIcon() { - return allowsAttachments() ? "upload" : "far-image"; -} - -export function uploadLocation(url) { - if (Discourse.CDN) { - url = Discourse.getURLWithCDN(url); - return /^\/\//.test(url) ? "http:" + url : url; - } else if (Discourse.S3BaseUrl) { - return "https:" + url; - } else { - var protocol = window.location.protocol + "//", - hostname = window.location.hostname, - port = window.location.port ? ":" + window.location.port : ""; - return protocol + hostname + port + url; - } -} - -export function getUploadMarkdown(upload) { - if (isAnImage(upload.original_filename)) { - const name = imageNameFromFileName(upload.original_filename); - return `![${name}|${upload.thumbnail_width}x${ - upload.thumbnail_height - }](${upload.short_url || upload.url})`; - } else if ( - !Discourse.SiteSettings.prevent_anons_from_downloading_files && - /\.(mov|mp4|webm|ogv|mp3|ogg|wav|m4a)$/i.test(upload.original_filename) - ) { - return uploadLocation(upload.url); - } else { - return `[${upload.original_filename}|attachment](${ - upload.short_url - }) (${I18n.toHumanSize(upload.filesize)})`; - } -} - -export function displayErrorForUpload(data) { - if (data.jqXHR) { - switch (data.jqXHR.status) { - // cancelled by the user - case 0: - return; - - // entity too large, usually returned from the web server - case 413: - const type = uploadTypeFromFileName(data.files[0].name); - const max_size_kb = Discourse.SiteSettings[`max_${type}_size_kb`]; - bootbox.alert(I18n.t("post.errors.file_too_large", { max_size_kb })); - return; - - // the error message is provided by the server - case 422: - if (data.jqXHR.responseJSON.message) { - bootbox.alert(data.jqXHR.responseJSON.message); - } else { - bootbox.alert(data.jqXHR.responseJSON.errors.join("\n")); - } - return; - } - } else if (data.errors && data.errors.length > 0) { - bootbox.alert(data.errors.join("\n")); - return; - } - // otherwise, display a generic error message - bootbox.alert(I18n.t("post.errors.upload")); -} - export function defaultHomepage() { let homepage = null; let elem = _.first($(homepageSelector)); diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6 index 071f749b75f..820e2e912a6 100644 --- a/app/assets/javascripts/discourse/mixins/upload.js.es6 +++ b/app/assets/javascripts/discourse/mixins/upload.js.es6 @@ -1,7 +1,7 @@ import { displayErrorForUpload, validateUploadedFiles -} from "discourse/lib/utilities"; +} from "discourse/lib/uploads"; import getUrl from "discourse-common/lib/get-url"; import { on } from "@ember/object/evented"; import Mixin from "@ember/object/mixin"; @@ -78,7 +78,7 @@ export default Mixin.create({ $upload.on("fileuploadsubmit", (e, data) => { const opts = _.merge( - { bypassNewUserRestriction: true }, + { bypassNewUserRestriction: true, user: this.currentUser }, this.validateUploadedFilesOptions() ); const isValid = validateUploadedFiles(data.files, opts); diff --git a/test/javascripts/lib/uploads-test.js.es6 b/test/javascripts/lib/uploads-test.js.es6 new file mode 100644 index 00000000000..76a074aa6dd --- /dev/null +++ b/test/javascripts/lib/uploads-test.js.es6 @@ -0,0 +1,223 @@ +import { + validateUploadedFiles, + authorizedExtensions, + isAnImage, + allowsImages, + allowsAttachments, + getUploadMarkdown +} from "discourse/lib/uploads"; +import * as Utilities from "discourse/lib/utilities"; +import User from "discourse/models/user"; + +QUnit.module("lib:uploads"); + +const validUpload = validateUploadedFiles; + +QUnit.test("validateUploadedFiles", assert => { + assert.not(validUpload(null), "no files are invalid"); + assert.not(validUpload(undefined), "undefined files are invalid"); + assert.not(validUpload([]), "empty array of files is invalid"); +}); + +QUnit.test("uploading one file", assert => { + sandbox.stub(bootbox, "alert"); + + assert.not(validUpload([1, 2])); + assert.ok(bootbox.alert.calledWith(I18n.t("post.errors.too_many_uploads"))); +}); + +QUnit.test("new user cannot upload images", assert => { + Discourse.SiteSettings.newuser_max_images = 0; + sandbox.stub(bootbox, "alert"); + + assert.not( + validUpload([{ name: "image.png" }], { user: User.create() }), + "the upload is not valid" + ); + assert.ok( + bootbox.alert.calledWith( + I18n.t("post.errors.image_upload_not_allowed_for_new_user") + ), + "the alert is called" + ); +}); + +QUnit.test("new user cannot upload attachments", assert => { + Discourse.SiteSettings.newuser_max_attachments = 0; + sandbox.stub(bootbox, "alert"); + + assert.not(validUpload([{ name: "roman.txt" }], { user: User.create() })); + assert.ok( + bootbox.alert.calledWith( + I18n.t("post.errors.attachment_upload_not_allowed_for_new_user") + ) + ); +}); + +QUnit.test("ensures an authorized upload", assert => { + sandbox.stub(bootbox, "alert"); + assert.not(validUpload([{ name: "unauthorized.html" }])); + assert.ok( + bootbox.alert.calledWith( + I18n.t("post.errors.upload_not_authorized", { + authorized_extensions: authorizedExtensions() + }) + ) + ); +}); + +QUnit.test("skipping validation works", assert => { + const files = [{ name: "backup.tar.gz" }]; + sandbox.stub(bootbox, "alert"); + + assert.not(validUpload(files, { skipValidation: false })); + assert.ok(validUpload(files, { skipValidation: true })); +}); + +QUnit.test("staff can upload anything in PM", assert => { + const files = [{ name: "some.docx" }]; + Discourse.SiteSettings.authorized_extensions = "jpeg"; + sandbox.stub(bootbox, "alert"); + + let user = User.create({ moderator: true }); + assert.not(validUpload(files, { user })); + assert.ok( + validUpload(files, { + isPrivateMessage: true, + allowStaffToUploadAnyFileInPm: true, + user + }) + ); +}); + +const imageSize = 10 * 1024; + +const dummyBlob = function() { + const BlobBuilder = + window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder; + if (BlobBuilder) { + let bb = new BlobBuilder(); + bb.append([new Int8Array(imageSize)]); + return bb.getBlob("image/png"); + } else { + return new Blob([new Int8Array(imageSize)], { type: "image/png" }); + } +}; + +QUnit.test("allows valid uploads to go through", assert => { + sandbox.stub(bootbox, "alert"); + + let user = User.create({ trust_level: 1 }); + + // image + let image = { name: "image.png", size: imageSize }; + assert.ok(validUpload([image], { user })); + // pasted image + let pastedImage = dummyBlob(); + assert.ok(validUpload([pastedImage], { user })); + + assert.not(bootbox.alert.calledOnce); +}); + +QUnit.test("isAnImage", assert => { + ["png", "jpg", "jpeg", "gif", "ico"].forEach(extension => { + var image = "image." + extension; + assert.ok(isAnImage(image), image + " is recognized as an image"); + assert.ok( + isAnImage("http://foo.bar/path/to/" + image), + image + " is recognized as an image" + ); + }); + assert.not(isAnImage("file.txt")); + assert.not(isAnImage("http://foo.bar/path/to/file.txt")); + assert.not(isAnImage("")); +}); + +QUnit.test("allowsImages", assert => { + Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif"; + assert.ok(allowsImages(), "works"); + + Discourse.SiteSettings.authorized_extensions = ".jpg|.jpeg|.gif"; + assert.ok(allowsImages(), "works with old extensions syntax"); + + Discourse.SiteSettings.authorized_extensions = "txt|pdf|*"; + assert.ok( + allowsImages(), + "images are allowed when all extensions are allowed" + ); + + Discourse.SiteSettings.authorized_extensions = "json|jpg|pdf|txt"; + assert.ok( + allowsImages(), + "images are allowed when at least one extension is an image extension" + ); +}); + +QUnit.test("allowsAttachments", assert => { + Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif"; + assert.not(allowsAttachments(), "no attachments allowed by default"); + + Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|*"; + assert.ok( + allowsAttachments(), + "attachments are allowed when all extensions are allowed" + ); + + Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|pdf"; + assert.ok( + allowsAttachments(), + "attachments are allowed when at least one extension is not an image extension" + ); + + Discourse.SiteSettings.authorized_extensions = ".jpg|.jpeg|.gif|.pdf"; + assert.ok(allowsAttachments(), "works with old extensions syntax"); +}); + +function testUploadMarkdown(filename, opts = {}) { + return getUploadMarkdown( + Object.assign( + { + original_filename: filename, + filesize: 42, + thumbnail_width: 100, + thumbnail_height: 200, + url: "/uploads/123/abcdef.ext" + }, + opts + ) + ); +} + +QUnit.test("getUploadMarkdown", assert => { + assert.equal( + testUploadMarkdown("lolcat.gif"), + "![lolcat|100x200](/uploads/123/abcdef.ext)" + ); + assert.equal( + testUploadMarkdown("[foo|bar].png"), + "![%5Bfoo%7Cbar%5D|100x200](/uploads/123/abcdef.ext)" + ); + + const short_url = "uploads://asdaasd.ext"; + + assert.equal( + testUploadMarkdown("important.txt", { short_url }), + `[important.txt|attachment](${short_url}) (42 Bytes)` + ); +}); + +QUnit.test("replaces GUID in image alt text on iOS", assert => { + assert.equal( + testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"), + "![8F2B469B-6B2C-4213-BC68-57B4876365A0|100x200](/uploads/123/abcdef.ext)" + ); + + sandbox.stub(Utilities, "isAppleDevice").returns(true); + assert.equal( + testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"), + "![image|100x200](/uploads/123/abcdef.ext)" + ); +}); diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index 350be246960..39c9b0635a5 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -2,23 +2,15 @@ import { emailValid, extractDomainFromUrl, - isAnImage, avatarUrl, - authorizedExtensions, - allowsImages, - allowsAttachments, getRawSize, avatarImg, defaultHomepage, setDefaultHomepage, - validateUploadedFiles, - getUploadMarkdown, caretRowCol, setCaretPosition, fillMissingDates } from "discourse/lib/utilities"; -import User from "discourse/models/user"; -import * as Utilities from "discourse/lib/utilities"; QUnit.module("lib:utilities"); @@ -56,176 +48,6 @@ QUnit.test("extractDomainFromUrl", assert => { ); }); -var validUpload = validateUploadedFiles; - -QUnit.test("validateUploadedFiles", assert => { - assert.not(validUpload(null), "no files are invalid"); - assert.not(validUpload(undefined), "undefined files are invalid"); - assert.not(validUpload([]), "empty array of files is invalid"); -}); - -QUnit.test("uploading one file", assert => { - sandbox.stub(bootbox, "alert"); - - assert.not(validUpload([1, 2])); - assert.ok(bootbox.alert.calledWith(I18n.t("post.errors.too_many_uploads"))); -}); - -QUnit.test("new user cannot upload images", assert => { - Discourse.SiteSettings.newuser_max_images = 0; - User.resetCurrent(User.create()); - sandbox.stub(bootbox, "alert"); - - assert.not(validUpload([{ name: "image.png" }]), "the upload is not valid"); - assert.ok( - bootbox.alert.calledWith( - I18n.t("post.errors.image_upload_not_allowed_for_new_user") - ), - "the alert is called" - ); -}); - -QUnit.test("new user cannot upload attachments", assert => { - Discourse.SiteSettings.newuser_max_attachments = 0; - sandbox.stub(bootbox, "alert"); - User.resetCurrent(User.create()); - - assert.not(validUpload([{ name: "roman.txt" }])); - assert.ok( - bootbox.alert.calledWith( - I18n.t("post.errors.attachment_upload_not_allowed_for_new_user") - ) - ); -}); - -QUnit.test("ensures an authorized upload", assert => { - sandbox.stub(bootbox, "alert"); - assert.not(validUpload([{ name: "unauthorized.html" }])); - assert.ok( - bootbox.alert.calledWith( - I18n.t("post.errors.upload_not_authorized", { - authorized_extensions: authorizedExtensions() - }) - ) - ); -}); - -QUnit.test("skipping validation works", assert => { - const files = [{ name: "backup.tar.gz" }]; - sandbox.stub(bootbox, "alert"); - - assert.not(validUpload(files, { skipValidation: false })); - assert.ok(validUpload(files, { skipValidation: true })); -}); - -QUnit.test("staff can upload anything in PM", assert => { - const files = [{ name: "some.docx" }]; - Discourse.SiteSettings.authorized_extensions = "jpeg"; - User.resetCurrent(User.create({ moderator: true })); - - sandbox.stub(bootbox, "alert"); - - assert.not(validUpload(files)); - assert.ok( - validUpload(files, { - isPrivateMessage: true, - allowStaffToUploadAnyFileInPm: true - }) - ); -}); - -var imageSize = 10 * 1024; - -var dummyBlob = function() { - var BlobBuilder = - window.BlobBuilder || - window.WebKitBlobBuilder || - window.MozBlobBuilder || - window.MSBlobBuilder; - if (BlobBuilder) { - var bb = new BlobBuilder(); - bb.append([new Int8Array(imageSize)]); - return bb.getBlob("image/png"); - } else { - return new Blob([new Int8Array(imageSize)], { type: "image/png" }); - } -}; - -QUnit.test("allows valid uploads to go through", assert => { - User.resetCurrent(User.create()); - User.currentProp("trust_level", 1); - sandbox.stub(bootbox, "alert"); - - // image - var image = { name: "image.png", size: imageSize }; - assert.ok(validUpload([image])); - // pasted image - var pastedImage = dummyBlob(); - assert.ok(validUpload([pastedImage])); - - assert.not(bootbox.alert.calledOnce); -}); - -var testUploadMarkdown = function(filename, opts = {}) { - return getUploadMarkdown( - Object.assign( - { - original_filename: filename, - filesize: 42, - thumbnail_width: 100, - thumbnail_height: 200, - url: "/uploads/123/abcdef.ext" - }, - opts - ) - ); -}; - -QUnit.test("getUploadMarkdown", assert => { - assert.equal( - testUploadMarkdown("lolcat.gif"), - "![lolcat|100x200](/uploads/123/abcdef.ext)" - ); - assert.equal( - testUploadMarkdown("[foo|bar].png"), - "![%5Bfoo%7Cbar%5D|100x200](/uploads/123/abcdef.ext)" - ); - - const short_url = "uploads://asdaasd.ext"; - - assert.equal( - testUploadMarkdown("important.txt", { short_url }), - `[important.txt|attachment](${short_url}) (42 Bytes)` - ); -}); - -QUnit.test("replaces GUID in image alt text on iOS", assert => { - assert.equal( - testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"), - "![8F2B469B-6B2C-4213-BC68-57B4876365A0|100x200](/uploads/123/abcdef.ext)" - ); - - sandbox.stub(Utilities, "isAppleDevice").returns(true); - assert.equal( - testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"), - "![image|100x200](/uploads/123/abcdef.ext)" - ); -}); - -QUnit.test("isAnImage", assert => { - ["png", "jpg", "jpeg", "gif", "ico"].forEach(extension => { - var image = "image." + extension; - assert.ok(isAnImage(image), image + " is recognized as an image"); - assert.ok( - isAnImage("http://foo.bar/path/to/" + image), - image + " is recognized as an image" - ); - }); - assert.not(isAnImage("file.txt")); - assert.not(isAnImage("http://foo.bar/path/to/file.txt")); - assert.not(isAnImage("")); -}); - QUnit.test("avatarUrl", assert => { var rawSize = getRawSize; assert.blank(avatarUrl("", "tiny"), "no template returns blank"); @@ -288,46 +110,6 @@ QUnit.test("avatarImg", assert => { setDevicePixelRatio(oldRatio); }); -QUnit.test("allowsImages", assert => { - Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif"; - assert.ok(allowsImages(), "works"); - - Discourse.SiteSettings.authorized_extensions = ".jpg|.jpeg|.gif"; - assert.ok(allowsImages(), "works with old extensions syntax"); - - Discourse.SiteSettings.authorized_extensions = "txt|pdf|*"; - assert.ok( - allowsImages(), - "images are allowed when all extensions are allowed" - ); - - Discourse.SiteSettings.authorized_extensions = "json|jpg|pdf|txt"; - assert.ok( - allowsImages(), - "images are allowed when at least one extension is an image extension" - ); -}); - -QUnit.test("allowsAttachments", assert => { - Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif"; - assert.not(allowsAttachments(), "no attachments allowed by default"); - - Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|*"; - assert.ok( - allowsAttachments(), - "attachments are allowed when all extensions are allowed" - ); - - Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|pdf"; - assert.ok( - allowsAttachments(), - "attachments are allowed when at least one extension is not an image extension" - ); - - Discourse.SiteSettings.authorized_extensions = ".jpg|.jpeg|.gif|.pdf"; - assert.ok(allowsAttachments(), "works with old extensions syntax"); -}); - QUnit.test("defaultHomepage", assert => { Discourse.SiteSettings.top_menu = "latest|top|hot"; assert.equal(