From f784f3fd73515e5d673452f9c28e05d90ade5942 Mon Sep 17 00:00:00 2001 From: Eelco Wiersma Date: Mon, 29 Dec 2014 18:06:13 +0100 Subject: [PATCH] #32: File constraints now available on both client and server, added client side validation --- .jshintrc | 4 +- lib/directive.js | 134 ++++++++---------------------------------- lib/restrictions.js | 140 ++++++++++++++++++++++++++++++++++++++++++++ lib/upload.js | 37 ++++++++++-- package.js | 3 +- versions.json | 2 +- 6 files changed, 204 insertions(+), 116 deletions(-) create mode 100644 lib/restrictions.js diff --git a/.jshintrc b/.jshintrc index e96a07d..a2fb006 100644 --- a/.jshintrc +++ b/.jshintrc @@ -125,6 +125,8 @@ //Internals "S3Policy": false, - "Slingshot": true + "Slingshot": true, + "matchAllowedFileTypes": false, + "mixins": false } } diff --git a/lib/directive.js b/lib/directive.js index 5a5eefa..1985a1a 100644 --- a/lib/directive.js +++ b/lib/directive.js @@ -1,10 +1,3 @@ -/** - * @module meteor-slingshot - */ - -Slingshot = {}; - - /** * @callback Directive~authorize * @@ -80,9 +73,12 @@ Slingshot._directives = {}; Slingshot.createDirective = function (name, service, options) { if (_.has(Slingshot._directives, name)) throw new Error("Directive '" + name + "' already exists"); + + var restrictions = Slingshot.getRestrictions(name); + _.defaults(options, restrictions); return (Slingshot._directives[name] = - new Slingshot.Directive(service, options)); + new Slingshot.Directive(name, service, options)); }; /** @@ -95,18 +91,17 @@ Slingshot.getDirective = function (name) { return this._directives[name]; }; - -var matchAllowedFileTypes = Match.OneOf(String, [String], RegExp, null); - /** * @param {Object} service * @param {Directive} directive * @constructor */ -Slingshot.Directive = function (service, directive) { +Slingshot.Directive = function (name, service, directive) { check(this, Slingshot.Directive); - + + check(name, String); + //service does not have to be a plain-object, so checking fields individually check(service.directiveMatch, Object); check(service.upload, Function); @@ -122,7 +117,13 @@ Slingshot.Directive = function (service, directive) { cacheControl: Match.Optional(String), contentDisposition: Match.Optional(Match.OneOf(String, null)) }, service.directiveMatch)); - + + /** + * @public + * @property {String} The directive name + */ + this.name = name; + /** * @method storageService * @returns {Object} @@ -140,80 +141,7 @@ Slingshot.Directive = function (service, directive) { this._directive = directive; }; -_.extend(Slingshot.Directive.prototype, { - - /** - * - * @method requestAuthorization - * - * @throws Meteor.Error - * - * @param {FileInfo} file - * @param {Object} [meta] - * - * @returns {Boolean} - */ - - requestAuthorization: function (method, file, meta) { - return this.checkFileSize(file.size) && this.checkFileType(file.type) && - this._directive.authorize.call(method, file, meta); - }, - - /** - * @throws Meteor.Error - * - * @param {Number} size - Size of file in bytes. - * @returns {boolean} - */ - - checkFileSize: function (size) { - var maxSize = Math.min(this._directive.maxSize, - this.storageService().maxSize || Infinity); - - if (maxSize && size > maxSize) - throw new Meteor.Error("Upload denied", "File exceeds allowed size of " + - formatBytes(maxSize)); - - return true; - }, - - /** - * - * @throws Meteor.Error - * - * @param {String} type - Mime type - * @returns {boolean} - */ - - checkFileType: function (type) { - var allowed = this._directive.allowedFileTypes; - - if (allowed instanceof RegExp) { - - if (!allowed.test(type)) - throw new Meteor.Error("Upload denied", - type + " is not an allowed file type"); - - return true; - } - - if (_.isArray(allowed)) { - if (allowed.indexOf(type) < 0) { - throw new Meteor.Error("Upload denied", - type + " is not one of the followed allowed file types: " + - allowed.join(", ")); - } - - return true; - } - - if (allowed !== type) { - throw new Meteor.Error("Upload denied", "Only file of type " + allowed + - " can be uploaded"); - } - - return true; - }, +_.extend(Slingshot.Directive.prototype, mixins, { /** * @param {{userId: String}} method @@ -238,6 +166,16 @@ _.extend(Slingshot.Directive.prototype, { }); return instructions; + }, + + /** + * @param {String} restriction - Name of the restriction to retrieve + * @returns {mixed} The restriction configuration + */ + + getRestriction: function (restriction) { + return Slingshot.getRestrictions(this.name)[restriction] || + this._directive[restriction]; } }); @@ -284,26 +222,6 @@ Meteor.methods({ } }); - -/** Human readable data-size in bytes. - * - * @param size {Number} - * @returns {string} - */ - -function formatBytes(size) { - var units = ['Bytes', 'KB', 'MB', 'GB', 'TB'], - unit = units.shift(); - - while (size >= 0x400 && units.length) { - size /= 0x400; - unit = units.shift(); - } - - return (Math.round(size * 100) / 100) + " " + unit; -} - - function quoteString(string, quotes) { return quotes + string.replace(quotes, '\\' + quotes) + quotes; } diff --git a/lib/restrictions.js b/lib/restrictions.js new file mode 100644 index 0000000..6299f0d --- /dev/null +++ b/lib/restrictions.js @@ -0,0 +1,140 @@ +/** + * @module meteor-slingshot + */ + +Slingshot = {}; + +/* global matchAllowedFileTypes: true */ +matchAllowedFileTypes = Match.OneOf(String, [String], RegExp, null); + +/** + * List of configured restrictions by name. + * + * @type {Object.} + * @private + */ + +Slingshot._restrictions = {}; + +/** + * Creates file upload restrictions for a specific directive. + * + * @param {string} name - A unique identifier of the directive. + * @param {Object} restrictions - The file upload restrictions. + * @returns {Object} + */ + +Slingshot.fileRestrictions = function (name, restrictions) { + check(restrictions, { + authorize: Match.Optional(Function), + maxSize: Match.Optional(Match.OneOf(Number, null)), + allowedFileTypes: Match.Optional(matchAllowedFileTypes), + }); + + return (Slingshot._restrictions[name] = + _.extend(Slingshot._restrictions[name] || {}, restrictions)); +}; + +/** + * @param {string} name - The unique identifier of the directive to + * retrieve the restrictions for. + * @returns {Object} + */ + +Slingshot.getRestrictions = function (name) { + return this._restrictions[name] || {}; +}; + +/* global mixins: true */ +mixins = { + /** + * + * @method requestAuthorization + * + * @throws Meteor.Error + * + * @param {FileInfo} file + * @param {Object} [meta] + * + * @returns {Boolean} + */ + + requestAuthorization: function (method, file, meta) { + var authorize = this.getRestriction("authorize"); + return this.checkFileSize(file.size) && this.checkFileType(file.type) && + (typeof authorize !== 'function' || authorize.call(method, file, meta)); + }, + + /** + * @throws Meteor.Error + * + * @param {Number} size - Size of file in bytes. + * @returns {boolean} + */ + + checkFileSize: function (size) { + var maxSize = Math.min(this.getRestriction("maxSize"), Infinity); + + if (maxSize && size > maxSize) + throw new Meteor.Error("Upload denied", "File exceeds allowed size of " + + formatBytes(maxSize)); + + return true; + }, + + /** + * + * @throws Meteor.Error + * + * @param {String} type - Mime type + * @returns {boolean} + */ + + checkFileType: function (type) { + var allowed = this.getRestriction("allowedFileTypes"); + + if (allowed instanceof RegExp) { + + if (!allowed.test(type)) + throw new Meteor.Error("Upload denied", + type + " is not an allowed file type"); + + return true; + } + + if (_.isArray(allowed)) { + if (allowed.indexOf(type) < 0) { + throw new Meteor.Error("Upload denied", + type + " is not one of the followed allowed file types: " + + allowed.join(", ")); + } + + return true; + } + + if (allowed !== type) { + throw new Meteor.Error("Upload denied", "Only file of type " + allowed + + " can be uploaded"); + } + + return true; + } +}; + +/** Human readable data-size in bytes. + * + * @param size {Number} + * @returns {string} + */ + +function formatBytes(size) { + var units = ['Bytes', 'KB', 'MB', 'GB', 'TB'], + unit = units.shift(); + + while (size >= 0x400 && units.length) { + size /= 0x400; + unit = units.shift(); + } + + return (Math.round(size * 100) / 100) + " " + unit; +} \ No newline at end of file diff --git a/lib/upload.js b/lib/upload.js index c834fbd..bfa02ce 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -2,8 +2,6 @@ * @fileOverview Defines client side API in which files can be uploaded. */ -Slingshot = {}; - /** * * @param {string} directive - Name of server-directive to use. @@ -36,7 +34,7 @@ Slingshot.Upload = function (directive, metaData) { return formData; } - _.extend(self, { + _.extend(self, mixins, { /** * @returns {string} @@ -61,6 +59,18 @@ Slingshot.Upload = function (directive, metaData) { uploaded: function () { return loaded.get(); }, + + validate: function(file) { + var context = { + userId: Meteor.userId() + }; + try { + return self.requestAuthorization(context, file, metaData); + } catch(error) { + console.log(error); + return error; + } + }, /** * @param {File} file @@ -69,7 +79,7 @@ Slingshot.Upload = function (directive, metaData) { */ send: function (file, callback) { - if(window.chrome && window.navigator.vendor === "Google Inc.") { + if (window.chrome && window.navigator.vendor === "Google Inc.") { check(file, window.File); } else { // not Google chrome @@ -102,11 +112,19 @@ Slingshot.Upload = function (directive, metaData) { if (!self.file) { callback(new Error("No file to request upload for")); } + + var file = _.pick(self.file, "name", "size", "type"); status.set("authorizing"); + + var valid = this.validate(file); + if (valid !== true) { + callback(valid); + return true; + } Meteor.call("slingshot/uploadRequest", directive, - _.pick(self.file, "name", "size", "type"), metaData, function (error, + file, metaData, function (error, instructions) { status.set(error ? "failed" : "authorized"); @@ -245,6 +263,15 @@ Slingshot.Upload = function (directive, metaData) { field = data && _.findWhere(data, {name: name}); return field && field.value; + }, + + /** + * @param {String} restriction - Name of the restriction to retrieve + * @returns {mixed} The restriction configuration + */ + + getRestriction: function (restriction) { + return Slingshot.getRestrictions(directive)[restriction]; } }); }; diff --git a/package.js b/package.js index 5d09165..2f0facf 100644 --- a/package.js +++ b/package.js @@ -10,7 +10,8 @@ Package.on_use(function (api) { api.use(["underscore", "check"]); api.use(["tracker", "reactive-var"], "client"); - + + api.add_files("lib/restrictions.js", ["client", "server"]); api.add_files("lib/upload.js", "client"); api.add_files([ "lib/directive.js", diff --git a/versions.json b/versions.json index ade065e..c2a4a24 100644 --- a/versions.json +++ b/versions.json @@ -34,6 +34,6 @@ ] ], "pluginDependencies": [], - "toolVersion": "meteor-tool@1.0.35", + "toolVersion": "meteor-tool@1.0.36", "format": "1.0" } \ No newline at end of file