diff --git a/.gitignore b/.gitignore index 123ae94..2ef8c34 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,29 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git -node_modules +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +testing diff --git a/LICENSE b/LICENSE index 788cdfc..7206f0f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,22 @@ -The MIT License (MIT) - -Copyright (c) 2015 choxnox - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +The MIT License (MIT) + +Copyright (c) 2015 choxnox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md index 77ec32e..e5470fa 100644 --- a/README.md +++ b/README.md @@ -1,221 +1,221 @@ -# CarbonJS Forms / `carbon-form` -The `carbon-form` module provides Zend-like forms in your projects. It is a complete solution which not only provides rendering but filtering and validation too. It packs all the logic behind HTML forms and it abstracts a lot of work so that you can focus on building awesome web applications and best of all it allows you to define the layout and style your forms any way you want them. - -If you have ever used `Zend_Form` before you're going to be familiar with the syntax and if not just keep reading. - -## Installation -``` -npm install carbon-form [--save] -``` - -## Usage -The usage is pretty simple. You create a form and then add elements to it. For each element you define a set of options such as name, label, filters, validators, HTML attributes etc. The following example should present most of `carbon-form` features. - -#### Defining the form (file: `signup-form.js`) -First you need to define your form and elements that it will contain. - -```js -var Form = require("carbon-form"); -var Filter = require("carbon-filter"); -var Validate = require("carbon-validate"); - -module.exports = exports = function(options) { - var form = new Form(options); - - form.setAction("/signup"); - form.setViewScriptFile("forms/signup.jade"); - - form.addElements([ - new Form.Element.Text("name", { - label: "Name", - attribs: { - class: "text-field" - }, - filters: [ - new Filter.StringTrim() - ], - validators: [ - new Validate.NotEmpty({ - messages: { - "is_empty": "Name is required" - }); - ] - }), - new Form.Element.Text("email_address", { - label: "Email address", - attribs: { - class: "text-field" - }, - filters: [ - new Filter.StringTrim() - ], - validators: [ - new Validate.NotEmpty({ - messages: { - "is_empty": "Email address is required" - } - }), - new Validate.StringLength({ - max: 255, - messages: { - "too_long": "Email address can't be longer than %max% characters" - } - }), - new Validate.EmailAddress(), - new Validate.DbNoRecordExists({ - adapter: "mongoose", - collection: "users", - field: "email_address", - messages: { - "record_found": "Email address is already in the database" - } - }) - ] - }), - new Form.Element.Password("password1", { - label: "Password", - attribs: { - class: "text-field" - }, - validators: [ - new Validate.NotEmpty({ - messages: { - "is_empty": "Password is required" - } - }), - new Validate.StringLength({ - min: 6, - messages: { - too_short: "Password must be at least %min% characters long" - } - }) - ] - }), - new Form.Element.Password("password2", { - label: "Repeat password", - attribs: { - class: "form-control" - }, - validators: [ - new Validate.NotEmpty({ - messages: { - "is_empty": "Repeated password is required" - } - }), - new Validate.Identical({ - token: "password1", - messages: { - "not_same": "Passwords do not match" - } - }) - ] - }), - new Form.Element.Button("submit", { - content: "Sign up", - attribs: { - class: "btn btn-red", - type: "submit" - } - }) - ]); - - return form; -} -``` - -#### Defining form layout (file: `signup-form.jade`) -Since `carbon-form` gives you freedom to style your own forms any way you wish, you can define form layout in the separate file and then tell to `carbon-form` where to look for this file. When the form renders it will use this layout as a template. - -```Jade -.form-group - div - label(for="#{elements.name.getName()}") - != elements.name.getLabel() - div - != elements.name.render() -.form-group - div - label(for="#{elements.email_address.getName()}") - != elements.email_address.getLabel() - div - != elements.email_address.render() -.form-group - div - label(for="#{elements.password1.getName()}") - != elements.password1.getLabel() - div - != elements.password1.render() -.form-group - div - label(for="#{elements.password2.getName()}") - != elements.password2.getLabel() - div - != elements.password2.render() -.form-group - div - != elements.submit.render() -``` - -#### Validation and rendering (using `carbon-framework`) -This example features `carbon-framework` just to make it easier for you to understand how `carbon-form` works in reality. Of course you can use `carbon-form` with any other Node.js framework or no framework at all. - -```js -module.exports = function() { - return { - signupAction: { - post: function(req, res) { - var postData = req.body; - - var form = require("./forms/signup-form")({ - viewiewScriptPaths: res.viewPaths - }); - - form.isValid(postData, function(err, values) { - if (err) - { - form.render(function(err) { - // Now in the view all you have to do is call `!= form.render()` - // (if you're using Jade engine) and it will return rendered form - // as HTML all together with all errored fields - - res.render("scripts/signup", { - formSignup: form - }); - }); - } - else - { - // Form validation is successful and argument `values` now contains all - // field values which are filtered and validated and therefor safe - // to be inserted into the database - - res.redirect("/signup-success"); - } - }); - - } - } - } -} -``` - -## Elements -The `carbon-form` currently supports 6 HTML form elements (`Button`, `Checkbox`, `Hidden`, `Password`, `Text`, `Textarea`) and 4 extended elements (`EmailAddress`, `Link`, `Recaptcha`, `Switch`). - -## Subforms -Depending on how you organize your forms you can nest one or more forms within a single form. - -```js -var Form = require("carbon-form"); - -var form = new Form(); -var buttons = new Form(); - -parentForm.addSubForm("buttons", buttons); -``` - -## Who is using it -The `carbon-form` is one of many that is running behind our web application: [Timelinity](https://www.timelinity.com) - -## Contributing -If you're willing to contribute to this project feel free to report issues, send pull request, write tests or simply contact me - [Amir Ahmetovic](https://github.com/choxnox) +# CarbonJS Forms / `carbon-form` +The `carbon-form` module provides Zend-like forms in your projects. It is a complete solution which not only provides rendering but filtering and validation too. It packs all the logic behind HTML forms and it abstracts a lot of work so that you can focus on building awesome web applications and best of all it allows you to define the layout and style your forms any way you want them. + +If you have ever used `Zend_Form` before you're going to be familiar with the syntax and if not just keep reading. + +## Installation +``` +npm install carbon-form [--save] +``` + +## Usage +The usage is pretty simple. You create a form and then add elements to it. For each element you define a set of options such as name, label, filters, validators, HTML attributes etc. The following example should present most of `carbon-form` features. + +#### Defining the form (file: `signup-form.js`) +First you need to define your form and elements that it will contain. + +```js +var Form = require("carbon-form"); +var Filter = require("carbon-filter"); +var Validate = require("carbon-validate"); + +module.exports = exports = function(options) { + var form = new Form(options); + + form.setAction("/signup"); + form.setViewScriptFile("forms/signup.jade"); + + form.addElements([ + new Form.Element.Text("name", { + label: "Name", + attribs: { + class: "text-field" + }, + filters: [ + new Filter.StringTrim() + ], + validators: [ + new Validate.NotEmpty({ + messages: { + "is_empty": "Name is required" + }); + ] + }), + new Form.Element.Text("email_address", { + label: "Email address", + attribs: { + class: "text-field" + }, + filters: [ + new Filter.StringTrim() + ], + validators: [ + new Validate.NotEmpty({ + messages: { + "is_empty": "Email address is required" + } + }), + new Validate.StringLength({ + max: 255, + messages: { + "too_long": "Email address can't be longer than %max% characters" + } + }), + new Validate.EmailAddress(), + new Validate.DbNoRecordExists({ + adapter: "mongoose", + collection: "users", + field: "email_address", + messages: { + "record_found": "Email address is already in the database" + } + }) + ] + }), + new Form.Element.Password("password1", { + label: "Password", + attribs: { + class: "text-field" + }, + validators: [ + new Validate.NotEmpty({ + messages: { + "is_empty": "Password is required" + } + }), + new Validate.StringLength({ + min: 6, + messages: { + too_short: "Password must be at least %min% characters long" + } + }) + ] + }), + new Form.Element.Password("password2", { + label: "Repeat password", + attribs: { + class: "form-control" + }, + validators: [ + new Validate.NotEmpty({ + messages: { + "is_empty": "Repeated password is required" + } + }), + new Validate.Identical({ + token: "password1", + messages: { + "not_same": "Passwords do not match" + } + }) + ] + }), + new Form.Element.Button("submit", { + content: "Sign up", + attribs: { + class: "btn btn-red", + type: "submit" + } + }) + ]); + + return form; +} +``` + +#### Defining form layout (file: `signup-form.jade`) +Since `carbon-form` gives you freedom to style your own forms any way you wish, you can define form layout in the separate file and then tell to `carbon-form` where to look for this file. When the form renders it will use this layout as a template. + +```Jade +.form-group + div + label(for="#{elements.name.getName()}") + != elements.name.getLabel() + div + != elements.name.render() +.form-group + div + label(for="#{elements.email_address.getName()}") + != elements.email_address.getLabel() + div + != elements.email_address.render() +.form-group + div + label(for="#{elements.password1.getName()}") + != elements.password1.getLabel() + div + != elements.password1.render() +.form-group + div + label(for="#{elements.password2.getName()}") + != elements.password2.getLabel() + div + != elements.password2.render() +.form-group + div + != elements.submit.render() +``` + +#### Validation and rendering (using `carbon-framework`) +This example features `carbon-framework` just to make it easier for you to understand how `carbon-form` works in reality. Of course you can use `carbon-form` with any other Node.js framework or no framework at all. + +```js +module.exports = function() { + return { + signupAction: { + post: function(req, res) { + var postData = req.body; + + var form = require("./forms/signup-form")({ + viewiewScriptPaths: res.viewPaths + }); + + form.isValid(postData, function(err, values) { + if (err) + { + form.render(function(err) { + // Now in the view all you have to do is call `!= form.render()` + // (if you're using Jade engine) and it will return rendered form + // as HTML all together with all errored fields + + res.render("scripts/signup", { + formSignup: form + }); + }); + } + else + { + // Form validation is successful and argument `values` now contains all + // field values which are filtered and validated and therefor safe + // to be inserted into the database + + res.redirect("/signup-success"); + } + }); + + } + } + } +} +``` + +## Elements +The `carbon-form` currently supports 6 HTML form elements (`Button`, `Checkbox`, `Hidden`, `Password`, `Text`, `Textarea`) and 4 extended elements (`EmailAddress`, `Link`, `Recaptcha`, `Switch`). + +## Subforms +Depending on how you organize your forms you can nest one or more forms within a single form. + +```js +var Form = require("carbon-form"); + +var form = new Form(); +var buttons = new Form(); + +parentForm.addSubForm("buttons", buttons); +``` + +## Who is using it +The `carbon-form` is one of many that is running behind our web application: [Timelinity](https://www.timelinity.com) + +## Contributing +If you're willing to contribute to this project feel free to report issues, send pull request, write tests or simply contact me - [Amir Ahmetovic](https://github.com/choxnox) diff --git a/index.js b/index.js index f648212..1c93b65 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require("./lib/"); +module.exports = require("./lib/"); diff --git a/lib/element.js b/lib/element.js index 7ea6392..8390d4f 100644 --- a/lib/element.js +++ b/lib/element.js @@ -49,8 +49,38 @@ Element.prototype.addValidator = function(validator) { this._options.validators.push(validator); }; +Element.prototype.getFilter = function(instance) { + var result = null; + + _.each(this.getFilters(), function(filter) { + if (_.isString(instance)) + { + if (filter instanceof eval(instance)) + { + result = validator; + return false; + } + } + else + { + if (filter instanceof instance) + { + result = filter; + return false; + } + } + }); + + return result; +}; + Element.prototype.getFullyQualifiedName = function(HTMLnotation) { - var name = (this.getBelongsTo() ? this.getBelongsTo() + "." : "") + this.getName(); + var name; + + if (this.getName().match(/^\d/)) + name = (this.getBelongsTo() ? this.getBelongsTo() + "[" + (HTMLnotation ? "" : "\"") : "") + this.getName() + (this.getBelongsTo() ? (HTMLnotation ? "" : "\"") + "]" : ""); + else + name = (this.getBelongsTo() ? this.getBelongsTo() + "." : "") + this.getName(); if (HTMLnotation) { @@ -94,15 +124,19 @@ Element.prototype.getValidator = function(instance) { Element.prototype.getValue = function() { var value = this._options.value; + var filters = []; _.forEach(this._options.filters, function(filter) { - if (_.isArray(value)) + if (filter.filter.length == 1) { - for (var index = 0; index < value.length; index++) - value[index] = filter.filter(value[index]); + if (_.isArray(value)) + { + for (var index = 0; index < value.length; index++) + value[index] = filter.filter(value[index]); + } + else + value = filter.filter(value); } - else - value = filter.filter(value); }); return value; @@ -222,7 +256,7 @@ Element.prototype.render = function(callback, options) { var attribs = _.extend({ name: this.getFullyQualifiedName(true), - id: this._options.attribs.id ? this._options.attribs.id : this._options.name + id: this._options.attribs.id ? this._options.attribs.id : this.getName() }, this._options.attribs); if (this._render.htmlTag) @@ -363,6 +397,7 @@ Element.prototype.getAttrib = function(name) { return this._options. Element.prototype.getAttribs = function() { return this._options.attribs; }; Element.prototype.getBelongsTo = function() { return this._options.belongsTo; }; Element.prototype.getError = function() { return this._error; }; +Element.prototype.getFilters = function() { return this._options.filters; }; Element.prototype.getGroup = function() { return this._options.group; }; Element.prototype.getId = function() { return this._options.attribs.id; }; Element.prototype.getIsArray = function() { return this._options.isArray; }; @@ -400,6 +435,7 @@ exports.Button = require("./elements/button"); exports.Checkbox = require("./elements/checkbox"); exports.EmailAddress = require("./elements/email-address"); exports.FileUpload = require("./elements/fileupload"); +exports.FileUploadX = require("./elements/fileupload-x/"); exports.Hidden = require("./elements/hidden"); exports.Link = require("./elements/link"); exports.Password = require("./elements/password"); diff --git a/lib/elements/fileupload-x/index.js b/lib/elements/fileupload-x/index.js new file mode 100644 index 0000000..b75a807 --- /dev/null +++ b/lib/elements/fileupload-x/index.js @@ -0,0 +1,280 @@ +var async = require("async"); +var request = require("request"); +var util = require("util"); +var _ = require("lodash"); + +var Filter = require("carbon-filter"); +var Validate = require("carbon-validate"); + +var Element = require("../../element"); + +var filesystem = require("./lib//utils/filesystem"); +var Helper = require("./lib/helper"); + +function FileUploadX(name, options) { + Element.call(this, name, options); + + var defaultOptions = { + button: { + icon: "", + text: "Select a file" + }, + maxFiles: 1, + mediaType: null, + mediaSubtype: null, + noFile: { + icon: "", + text: "You haven't uploaded a file yet" + }, + strategies: null, + uploadEndpoint: null + }; + + this._options = _.extend({}, this._options, defaultOptions, options); + + this.setId(this.getName() + "_" + Math.floor(Math.random() * (1000000 + 1))); + + this.setViewScriptFile("./views/element.jade"); + this.setViewScriptPaths([__dirname]); + + if (!this.getUploadEndpoint() && this.getStrategies()) + { + var typeStrategy = this.getStrategies()[this.getMediaType()]; + var subtypeStrategy = typeStrategy.types[this.getMediaSubtype()]; + + if (subtypeStrategy.uploadEndpoint) + this.setUploadEndpoint(subtypeStrategy.uploadEndpoint); + else if (typeStrategy.uploadEndpoint) + this.setUploadEndpoint(typeStrategy.uploadEndpoint); + } +} + +util.inherits(FileUploadX, Element); + +FileUploadX.prototype.deleteFileFromLivePath = function(value, callback) { + request + .post({ + url: this.getUploadEndpoint(), + json: true, + body: { + action: "deleteFileToLivePath", + value: value, + fileUpload: { + mediaType: this.getMediaType(), + mediaSubtype: this.getMediaSubtype() + } + } + }, function(err, response, body) { + callback(err, null); + }) + ; +}; + +FileUploadX.prototype.isValid = function(value, context, callback) { + var $this = this; + + if (!this.isRequired() && (value === null || (_.isArray(value) && (value.length === 0 || value[0].trim().length === 0)))) + { + return Element.prototype.isValid.call(this, value, context, function(err, value) { + callback(err, value); + }); + } + + if (!_.isArray(value)) + value = [value]; + + var valueEmpty = false; + + if (this.isRequired() && value.length === 0) + valueEmpty = true; + /*else + { + _.each(value, function(value) { + if (_.isEmpty(value)) + { + valueEmpty = true; + return false; + } + }); + }*/ + + if (valueEmpty) + return callback(this.getValidator("Validate.NotEmpty").getOptions().message.is_empty, value); + + var typeConfig = this.getStrategies()[this.getMediaType()]; + var subtypeConfig = typeConfig.types[this.getMediaSubtype()]; + + if (!typeConfig || !subtypeConfig) + return callback("Media is not defined", value); + + var func = []; + var foundFiles = 0; + + _.each(value, function(val) { + var path, filename, extension; + var uploadType = val.substr(0, 1); + + if (subtypeConfig.fileExtension) + extension = subtypeConfig.fileExtension; + else if (typeConfig.fileExtension) + extension = typeConfig.fileExtension; + else if ($this.getFilter("Filter.File.Move")) + { + var filter = $this.getFilter("Filter.File.Move"); + + if (filter.getOptions().extension) + extension = filter.getOptions().extension; + } + else + { + var filters = _.concat( + [], + (subtypeConfig.filters && _.isArray(subtypeConfig.filters)? subtypeConfig.filters : []), + (typeConfig.filters && _.isArray(typeConfig.filters)? typeConfig.filters : []) + ); + + _.each(filters, function(filter) { + if (filter instanceof Filter.File.Move) + { + if (filter.getOptions().extension) + extension = filter.getOptions().extension; + } + }); + } + + if (uploadType == "_") + { + filename = val.substr(1); + path = subtypeConfig.paths.public.temp; + } + else + { + filename = val; + path = subtypeConfig.paths.public.live; + } + + path = filesystem.appendSlash(path) + (extension ? filesystem.appendExtension(filename, extension) : filename); + + func.push(function(callback) { + var validator = new Validate.File.Exist(); + + validator.isValid(path, {}, callback); + }); + }); + + async.series(func, function(err) { + if (!err) + foundFiles++; + + if (foundFiles < value.length) + value = []; + + Element.prototype.isValid.call($this, value, context, function(err, value) { + callback(err, value); + }); + }); +}; + +FileUploadX.prototype.moveFileToLivePath = function(value, callback) { + request + .post({ + url: this.getUploadEndpoint(), + json: true, + body: { + action: "moveFileToLivePath", + value: value, + fileUpload: { + mediaType: this.getMediaType(), + mediaSubtype: this.getMediaSubtype() + } + } + }, function(err, response, body) { + callback(err, (body ? (body.value ? body.value : []) : [])); + }) + ; +}; + +FileUploadX.prototype.render = function(callback, options) { + if (!_.isObject(this.getStrategies())) + throw new Error("Upload strategies parameter is missing"); + + var typeConfig = this.getStrategies()[this.getMediaType()]; + var subtypeConfig = typeConfig.types[this.getMediaSubtype()]; + + if (!typeConfig || !subtypeConfig) + return console.log("Media is not defined"); + + var $this = this; + var value = this.getValue(); + var files = []; + + _.each(value, function(val) { + var path, filename, extension; + var uploadType = val.substr(0, 1); + + if (subtypeConfig.fileExtension) + extension = subtypeConfig.fileExtension; + else if (typeConfig.fileExtension) + extension = typeConfig.fileExtension; + else if ($this.getFilter("Filter.File.Move")) + { + var filter = $this.getFilter("Filter.File.Move"); + + if (filter.getOptions().extension) + extension = filter.getOptions().extension; + } + else + { + var filters = _.concat( + [], + (subtypeConfig.filters && _.isArray(subtypeConfig.filters)? subtypeConfig.filters : []), + (typeConfig.filters && _.isArray(typeConfig.filters)? typeConfig.filters : []) + ); + + _.each(filters, function(filter) { + if (filter instanceof Filter.File.Move) + { + if (filter.getOptions().extension) + extension = filter.getOptions().extension; + } + }); + } + + if (uploadType == "_") + { + filename = val.substr(1); + path = subtypeConfig.paths.public.temp; + } + else + { + filename = val; + path = subtypeConfig.paths.public.live; + } + + path = filesystem.appendSlash(path) + (extension ? filesystem.appendExtension(filename, extension) : filename); + + files.push({ + id: val, + previewUrl: ((!_.isUndefined(subtypeConfig.hasPreview) && subtypeConfig.hasPreview) || (typeConfig.hasPreview && _.isUndefined(subtypeConfig.hasPreview))) ? path : undefined + }); + }); + + this.setViewScriptData({ + files: files, + }); + + return Element.prototype.render.call(this, callback, options); +}; + +FileUploadX.prototype.getMaxFiles = function() { return this._options.maxFiles; }; +FileUploadX.prototype.getMediaSubtype = function() { return this._options.mediaSubtype; }; +FileUploadX.prototype.getMediaType = function() { return this._options.mediaType; }; +FileUploadX.prototype.getStrategies = function() { return this._options.strategies; }; +FileUploadX.prototype.getUploadEndpoint = function() { return this._options.uploadEndpoint; }; + +FileUploadX.prototype.setMaxFiles = function(maxFiles) { this._options.maxFiles = maxFiles; }; +FileUploadX.prototype.setUploadEndpoint = function(uploadEndpoint) { this._options.uploadEndpoint = uploadEndpoint; }; + +module.exports = exports = FileUploadX; + +exports.Helper = Helper; diff --git a/lib/elements/fileupload-x/lib/helper.js b/lib/elements/fileupload-x/lib/helper.js new file mode 100644 index 0000000..3f7e69a --- /dev/null +++ b/lib/elements/fileupload-x/lib/helper.js @@ -0,0 +1,209 @@ +var async = require("async"); +var bytes = require("bytes"); +var crypto = require("crypto"); +var fs = require("fs-extra"); +var gm = require("gm"); +var path = require("path"); +var Magic = require("mmmagic"); +var _ = require("lodash"); + +var filesystem = require("./utils/filesystem"); + +var Filter = require("carbon-filter"); + +function FileUploadHelper(config) +{ + this._config = config; +} + +FileUploadHelper.prototype.deleteFileFromLivePath = function(filename, mediaType, mediaSubtype, callback) { + if (!mediaType || + !mediaSubtype || + !this.getConfig()[mediaType] || + this.getConfig()[mediaType] && !this.getConfig()[mediaType].types[mediaSubtype]) + { + return callback("Media type and/or subtype not defined"); + } + else + { + if (!filename) + return callback("Invalid filename"); + + var typeConfig = this.getConfig()[mediaType]; + var subtypeConfig = typeConfig.types[mediaSubtype]; + + if (_.isArray(filename)) + filename = filename[0]; + + if (filename.length == 65) + filename = filename.substr(1); + else if (filename.length != 64) + return callback("Invalid filename"); + + var path = filesystem.appendSlash(subtypeConfig.paths.private.live) + (subtypeConfig.fileExtension ? filesystem.appendExtension(filename, subtypeConfig.fileExtension) : filename); + + fs.remove(path, function(err) { + if (err) + callback(err, false); + else + callback(null); + }); + } +}; + +FileUploadHelper.prototype.handleUpload = function(options, callback) { + var defaults = { + files: [], + mediaType: null, + mediaSubtype: null, + nonce: null + }; + + options = _.extend(defaults, options); + + if (!options.mediaType || + !options.mediaSubtype || + !this.getConfig()[options.mediaType] || + this.getConfig()[options.mediaType] && !this.getConfig()[options.mediaType].types[options.mediaSubtype]) + { + return callback("Media type and/or subtype not defined"); + } + else + { + var typeConfig = this.getConfig()[options.mediaType]; + var subtypeConfig = typeConfig.types[options.mediaSubtype]; + + _.each(options.files, function(file) { + var validators = []; + + _.each(subtypeConfig.validators, function(validator) { + validators.push(function(callback) { + validator.isValid(file.path, {}, callback); + }); + }); + + if (!validators.length) + { + validators.push(function(callback) { + callback(null, [file.path]); + }); + } + + async.series(validators, function(err, filePaths) { + if (err) + { + fs.remove(file.path, function() { + callback(err, null); + }); + } + else + { + var filters = []; + + _.each(subtypeConfig.filters, function(filter) { + filters.push(function(callback) { + filter.filter(file.path, {}, callback); + }); + }); + + if (!filters.length) + { + filters.push(function(callback) { + callback(null, [file.path]); + }); + } + + async.series(filters, function(err, filePaths) { + if (err) + { + fs.remove(file.path, function() { + callback(err, null); + }); + } + else + { + var filePath = filePaths[filePaths.length - 1]; + + var index = options.files.length - 1; + var newFileName = crypto.createHash("sha256").update(file.filename).digest("hex"); + + var response = { + success: true, + files: [{ + id: "_" + newFileName, + nonce: options.nonce + }] + }; + + var Move = new Filter.File.Move({ + path: subtypeConfig.paths.private.temp, + filename: newFileName + }); + + Move.filter(filePath, {}, function(err, filePath) { + if (err) + callback(err, null); + else + { + if (subtypeConfig.hasPreview) + { + var parsedPath = path.parse(filePath); + + response.files[index] = _.extend(response.files[index], { + previewUrl: subtypeConfig.paths.public.temp + "/" + parsedPath.base + }); + } + + callback(null, response); + } + }); + } + }); + } + }); + }); + } +}; + +FileUploadHelper.prototype.moveFileToLivePath = function(filename, mediaType, mediaSubtype, callback) { + if (!mediaType || + !mediaSubtype || + !this.getConfig()[mediaType] || + this.getConfig()[mediaType] && !this.getConfig()[mediaType].types[mediaSubtype]) + { + return callback("Media type and/or subtype not defined"); + } + else + { + if (!filename) + return callback("Invalid filename"); + + var files = filename; + var typeConfig = this.getConfig()[mediaType]; + var subtypeConfig = typeConfig.types[mediaSubtype]; + + if (_.isArray(filename)) + filename = filename[0]; + + if (filename.length == 65) + filename = filename.substr(1); + else if (filename.length != 64) + return callback("Invalid filename"); + + var newFilename = crypto.createHash("sha256").update(filename + Date.now()).digest("hex"); + + var sourcePath = filesystem.appendSlash(subtypeConfig.paths.private.temp) + (subtypeConfig.fileExtension ? filesystem.appendExtension(filename, subtypeConfig.fileExtension) : filename); + var destinationPath = filesystem.appendSlash(subtypeConfig.paths.private.live) + (subtypeConfig.fileExtension ? filesystem.appendExtension(newFilename, subtypeConfig.fileExtension) : newFilename); + + fs.move(sourcePath, destinationPath, function(err) { + if (err) + callback(err, false); + else + callback(null, _.isArray(files) ? [newFilename] : newFileName); + }); + } +}; + +FileUploadHelper.prototype.getConfig = function() { return this._config; }; + +module.exports = exports = FileUploadHelper; diff --git a/lib/elements/fileupload-x/lib/utils/filesystem.js b/lib/elements/fileupload-x/lib/utils/filesystem.js new file mode 100644 index 0000000..5e1717b --- /dev/null +++ b/lib/elements/fileupload-x/lib/utils/filesystem.js @@ -0,0 +1,46 @@ +var fs = require("fs-extra"); + +exports.appendExtension = function(path, extension) { + if (extension.substr(0, 1) != ".") + extension = "." + extension; + + if (path.substr(-extension.length, extension.length) != extension) + path += extension; + + return path; +}; + +exports.appendSlash = function(path) { + if (path.substr(-1, 1) != "/") + path += "/"; + + return path; +}; + +exports.isDirectory = function(path) +{ + var isDirectory = false; + + try + { + if (fs.lstatSync(path).isDirectory()) + isDirectory = true; + } + catch (err) { } + + return isDirectory; +}; + +exports.isFile = function(path) +{ + var isFile = false; + + try + { + if (fs.lstatSync(path).isFile()) + isFile = true; + } + catch (err) { } + + return isFile; +}; diff --git a/lib/elements/fileupload-x/views/element.jade b/lib/elements/fileupload-x/views/element.jade new file mode 100644 index 0000000..5d7b02b --- /dev/null +++ b/lib/elements/fileupload-x/views/element.jade @@ -0,0 +1,324 @@ +div.file-uploader(id="#{element.getId()}") + div.input-group + ul.form-control.no-file + span.input-group-btn + span.btn.btn-blue.fileinput-button + - if (element.getOptions().button.icon) { + i(class="#{element.getOptions().button.icon}") + - } + - if (element.getOptions().button.text) { + span #{element.getOptions().button.text} + - } + input(type="file", id="#{element.getId()}-file", name="#{element.getName()}-file") + +script(type="text/javascript"). + var initFileUpload_#{element.getId()} = function() { + var container = $("##{element.getId()}"); + var element = $("##{element.getId()}-file"); + var button = container.find(".fileinput-button"); + var formControl = container.find(".form-control"); + + var fileUploader = { + maxFiles: #{element.getOptions().maxFiles}, + files: [], + filesUploading: 0, + removeFile: function(options) { + defaults = { + index: null + }; + + fileUploader.filesCount--; + var nonce = formControl.find(".media").eq(options.index).data("id"); + + if (nonce) + { + var file = _.findWhere(fileUploader.files, {nonce: nonce.toString()}); + + if (file && file.jqXHR) + file.jqXHR.abort(); + } + + formControl.find(".media").eq(options.index).remove(); + + if (formControl.find(".media").length < fileUploader.maxFiles) + formControl.find(".fileinput-button").removeClass("disabled"); + + if (fileUploader.maxFiles > 1 && element.find("li.media").length > 1) + formControl.sortable("enable"); + else if (fileUploader.maxFiles > 1) + formControl.sortable("disable"); + + if (!formControl.find("li.media").length) + { + fileUploader.resetField({ + files: [] + }); + } + }, + resetField: function(options) { + var defaults = { + files: null + }; + + if (options.files && options.files.length) + { + $.each(options.files, function (index, file) { + fileUploader.updateFile({ + file: file + }); + }); + + formControl.removeClass("no-file"); + } + else + { + formControl.empty().addClass("no-file"); + formControl.html("#{element.getOptions().noFile.text}"); + + element.find(".fileinput-button").removeClass("disabled"); + } + + if (fileUploader.maxFiles > 1 && !formControl.hasClass("no-file")) + formControl.sortable("enable"); + else if (fileUploader.maxFiles > 1) + formControl.sortable("disable"); + }, + updateFile: function(options) { + var defaults = { + file: { + name: null + } + }; + + if (fileUploader.maxFiles == 1) + { + var fileItem = formControl.find("li.media"); + + if (fileItem.length && fileItem.find(".alert").length) + fileItem.empty(); + + if (!fileItem.length) + { + fileUploader.files.push(options.file); + fileItem = $("
  • ").appendTo(formControl); + $("
    ").appendTo(fileItem); + } + + fileItem.attr("data-id", options.file.nonce); + } + else + { + fileItem = formControl.find("li.media[data-id='" + options.file.nonce + "']"); + + if (!fileItem.length) + { + fileUploader.files.push(options.file); + fileItem = $("
  • ").attr("data-id", options.file.nonce).appendTo(formControl); + $("
    ").appendTo(fileItem); + + if (fileUploader.files.length > 1) + formControl.sortable("enable"); + else + formControl.sortable("disable"); + } + + if (element.find("li.media").length >= fileUploader.maxFiles) + element.find(".fileinput-button").addClass("disabled"); + } + + var fileItemLeft = fileItem.find(".media-left"); + + if (!fileItemLeft.length) + fileItemLeft = $("
    ").addClass("media-left").appendTo(fileItem); + + var image = fileItem.find(".image-preview"); + + if (!image.length) + { + fileItem.children(".media-left").empty(); + + image = $("
    ").addClass("image-preview").appendTo(fileItemLeft); + + //$("").addClass("icon icon-4x").addClass(fileUploader.icons.noFile).appendTo(image); + image.append("
    "); + } + + var fileItemBody = fileItem.find(".media-body"); + + if (!fileItemBody.length) + fileItemBody = $("
    ").addClass("media-body").appendTo(fileItem); + + var buttonRemove = fileItem.find("button.file-remove"); + + if (!buttonRemove.length) + buttonRemove = $("