diff options
author | Mike Macgirvin <mike@macgirvin.com> | 2018-10-31 15:56:08 +1100 |
---|---|---|
committer | Mike Macgirvin <mike@macgirvin.com> | 2018-10-31 15:56:08 +1100 |
commit | 7e1f431eca7a8aa68fc0badfaa88e88de3ba094c (patch) | |
tree | 16beba352cd4ace4aa6eb13c7f9c1c82c92013b4 /vendor/blueimp | |
parent | 70c55da1df69d90dcbeb5a78c994b23a8456bfc9 (diff) | |
download | volse-hubzilla-7e1f431eca7a8aa68fc0badfaa88e88de3ba094c.tar.gz volse-hubzilla-7e1f431eca7a8aa68fc0badfaa88e88de3ba094c.tar.bz2 volse-hubzilla-7e1f431eca7a8aa68fc0badfaa88e88de3ba094c.zip |
yet another blueimp vulnerability. Move to composer.
Diffstat (limited to 'vendor/blueimp')
57 files changed, 10679 insertions, 0 deletions
diff --git a/vendor/blueimp/jquery-file-upload/.gitignore b/vendor/blueimp/jquery-file-upload/.gitignore new file mode 100644 index 000000000..29a41a8c4 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.pyc +node_modules diff --git a/vendor/blueimp/jquery-file-upload/.jshintrc b/vendor/blueimp/jquery-file-upload/.jshintrc new file mode 100644 index 000000000..4ad82e664 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/.jshintrc @@ -0,0 +1,81 @@ +{ + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : true, // true: Identifiers must be in camelCase + "curly" : true, // true: Require {} for every new block or scope + "eqeqeq" : true, // true: Require triple equals (===) for comparison + "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() + "immed" : true, // true: Require immediate invocations to be wrapped in parens + // e.g. `(function () { } ());` + "indent" : 4, // {int} Number of spaces to use for indentation + "latedef" : true, // true: Require variables/functions to be defined before being used + "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : "single", // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : true, // true: Require all defined variables be used + "strict" : true, // true: Requires all functions run in ES5 Strict Mode + "trailing" : true, // true: Prohibit trailing whitespaces + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + + // Relaxing + "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : false, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : false, // true: Tolerate use of `== null` + "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) + "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) + "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : false, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements" + "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : false, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : false, // Web Browser (window, document, etc) + "couch" : false, // CouchDB + "devel" : false, // Development/debugging (alert, confirm, etc) + "dojo" : false, // Dojo Toolkit + "jquery" : false, // jQuery + "mootools" : false, // MooTools + "node" : false, // Node.js + "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) + "prototypejs" : false, // Prototype and Scriptaculous + "rhino" : false, // Rhino + "worker" : false, // Web Workers + "wsh" : false, // Windows Scripting Host + "yui" : false, // Yahoo User Interface + + // Legacy + "nomen" : true, // true: Prohibit dangling `_` in variables + "onevar" : true, // true: Allow only one `var` statement per function + "passfail" : false, // true: Stop on first error + "white" : true, // true: Check against strict whitespace and indentation rules + + // Custom Globals + "globals" : {} // additional predefined global variables +} diff --git a/vendor/blueimp/jquery-file-upload/.npmignore b/vendor/blueimp/jquery-file-upload/.npmignore new file mode 100644 index 000000000..0530f5dbd --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/.npmignore @@ -0,0 +1,20 @@ +* +!css/jquery.fileupload-noscript.css +!css/jquery.fileupload-ui-noscript.css +!css/jquery.fileupload-ui.css +!css/jquery.fileupload.css +!img/loading.gif +!img/progressbar.gif +!js/cors/jquery.postmessage-transport.js +!js/cors/jquery.xdr-transport.js +!js/vendor/jquery.ui.widget.js +!js/jquery.fileupload-angular.js +!js/jquery.fileupload-audio.js +!js/jquery.fileupload-image.js +!js/jquery.fileupload-jquery-ui.js +!js/jquery.fileupload-process.js +!js/jquery.fileupload-ui.js +!js/jquery.fileupload-validate.js +!js/jquery.fileupload-video.js +!js/jquery.fileupload.js +!js/jquery.iframe-transport.js diff --git a/vendor/blueimp/jquery-file-upload/CONTRIBUTING.md b/vendor/blueimp/jquery-file-upload/CONTRIBUTING.md new file mode 100644 index 000000000..e182f9b37 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/CONTRIBUTING.md @@ -0,0 +1,15 @@ +Please follow these pull request guidelines: + +1. Update your fork to the latest upstream version. + +2. Follow the coding conventions of the original source files (indentation, spaces, brackets layout). + +3. Code changes must pass JSHint validation with the `.jshintrc` settings of this project. + +4. Code changes must pass the QUnit tests defined in the `test` folder. + +5. New features should be covered by accompanying QUnit tests. + +6. Keep your commits as atomic as possible, i.e. create a new commit for every single bug fix or feature added. + +7. Always add meaningful commit messages. diff --git a/vendor/blueimp/jquery-file-upload/LICENSE.txt b/vendor/blueimp/jquery-file-upload/LICENSE.txt new file mode 100644 index 000000000..87a644638 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2010 Sebastian Tschan, https://blueimp.net + +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/vendor/blueimp/jquery-file-upload/README.md b/vendor/blueimp/jquery-file-upload/README.md new file mode 100644 index 000000000..d2f104d5c --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/README.md @@ -0,0 +1,117 @@ +# jQuery File Upload Plugin + +## Description +File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery. +Supports cross-domain, chunked and resumable file uploads and client-side image resizing. Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads. + +## Demo +[Demo File Upload](https://blueimp.github.io/jQuery-File-Upload/) + +## ⚠️ Security Notice +Security related releases: + +* [v9.25.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.25.1) Mitigates some [Potential vulnerabilities with PHP+ImageMagick](VULNERABILITIES.md#potential-vulnerabilities-with-php+imagemagick). +* [v9.24.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.24.1) Fixes a [Remote code execution vulnerability in the PHP component](VULNERABILITIES.md#remote-code-execution-vulnerability-in-the-php-component). +* v[9.10.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/9.10.1) Fixes an [Open redirect vulnerability in the GAE components](VULNERABILITIES.md#open-redirect-vulnerability-in-the-gae-components). +* Commit [4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a) (*fixed in all tagged releases*) Fixes a [Cross-site scripting vulnerability in the Iframe Transport](VULNERABILITIES.md#cross-site-scripting-vulnerability-in-the-iframe-transport). + +Please read the [SECURITY](SECURITY.md) document for instructions on how to securely configure your Webserver for file uploads. + +## Setup +* [How to setup the plugin on your website](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) +* [How to use only the basic plugin (minimal setup guide).](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin) + +## Features +* **Multiple file upload:** + Allows to select multiple files at once and upload them simultaneously. +* **Drag & Drop support:** + Allows to upload files by dragging them from your desktop or file manager and dropping them on your browser window. +* **Upload progress bar:** + Shows a progress bar indicating the upload progress for individual files and for all uploads combined. +* **Cancelable uploads:** + Individual file uploads can be canceled to stop the upload progress. +* **Resumable uploads:** + Aborted uploads can be resumed with browsers supporting the Blob API. +* **Chunked uploads:** + Large files can be uploaded in smaller chunks with browsers supporting the Blob API. +* **Client-side image resizing:** + Images can be automatically resized on client-side with browsers supporting the required JS APIs. +* **Preview images, audio and video:** + A preview of image, audio and video files can be displayed before uploading with browsers supporting the required APIs. +* **No browser plugins (e.g. Adobe Flash) required:** + The implementation is based on open standards like HTML5 and JavaScript and requires no additional browser plugins. +* **Graceful fallback for legacy browsers:** + Uploads files via XMLHttpRequests if supported and uses iframes as fallback for legacy browsers. +* **HTML file upload form fallback:** + Allows progressive enhancement by using a standard HTML file upload form as widget element. +* **Cross-site file uploads:** + Supports uploading files to a different domain with cross-site XMLHttpRequests or iframe redirects. +* **Multiple plugin instances:** + Allows to use multiple plugin instances on the same webpage. +* **Customizable and extensible:** + Provides an API to set individual options and define callback methods for various upload events. +* **Multipart and file contents stream uploads:** + Files can be uploaded as standard "multipart/form-data" or file contents stream (HTTP PUT file upload). +* **Compatible with any server-side application platform:** + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads. + +## Requirements + +### Mandatory requirements +* [jQuery](https://jquery.com/) v. 1.6+ +* [jQuery UI widget factory](https://api.jqueryui.com/jQuery.widget/) v. 1.9+ (included): Required for the basic File Upload plugin, but very lightweight without any other dependencies from the jQuery UI suite. +* [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) (included): Required for [browsers without XHR file upload support](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +### Optional requirements +* [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates) v. 2.5.4+: Used to render the selected and uploaded files for the Basic Plus UI and jQuery UI versions. +* [JavaScript Load Image library](https://github.com/blueimp/JavaScript-Load-Image) v. 1.13.0+: Required for the image previews and resizing functionality. +* [JavaScript Canvas to Blob polyfill](https://github.com/blueimp/JavaScript-Canvas-to-Blob) v. 2.1.1+:Required for the image previews and resizing functionality. +* [blueimp Gallery](https://github.com/blueimp/Gallery) v. 2.15.1+: Used to display the uploaded images in a lightbox. +* [Bootstrap](http://getbootstrap.com/) v. 3.2.0+ +* [Glyphicons](http://glyphicons.com/) + +The user interface of all versions, except the jQuery UI version, is built with [Bootstrap](http://getbootstrap.com/) and icons from [Glyphicons](http://glyphicons.com/). + +### Cross-domain requirements +[Cross-domain File Uploads](https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads) using the [Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) require a redirect back to the origin server to retrieve the upload results. The [example implementation](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/main.js) makes use of [result.html](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/result.html) as a static redirect page for the origin server. + +The repository also includes the [jQuery XDomainRequest Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/cors/jquery.xdr-transport.js), which enables limited cross-domain AJAX requests in Microsoft Internet Explorer 8 and 9 (IE 10 supports cross-domain XHR requests). +The XDomainRequest object allows GET and POST requests only and doesn't support file uploads. It is used on the [Demo](https://blueimp.github.io/jQuery-File-Upload/) to delete uploaded files from the cross-domain demo file upload service. + +### Custom Backends + +You can add support for various backends by adhering to the specification [outlined here](https://github.com/blueimp/jQuery-File-Upload/wiki/JSON-Response). + +## Browsers + +### Desktop browsers +The File Upload plugin is regularly tested with the latest browser versions and supports the following minimal versions: + +* Google Chrome +* Apple Safari 4.0+ +* Mozilla Firefox 3.0+ +* Opera 11.0+ +* Microsoft Internet Explorer 6.0+ + +### Mobile browsers +The File Upload plugin has been tested with and supports the following mobile browsers: + +* Apple Safari on iOS 6.0+ +* Google Chrome on iOS 6.0+ +* Google Chrome on Android 4.0+ +* Default Browser on Android 2.3+ +* Opera Mobile 12.0+ + +### Supported features +For a detailed overview of the features supported by each browser version, please have a look at the [Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +## Contributing +**Bug fixes** and **new features** can be proposed using [pull requests](https://github.com/blueimp/jQuery-File-Upload/pulls). +Please read the [contribution guidelines](https://github.com/blueimp/jQuery-File-Upload/blob/master/CONTRIBUTING.md) before submitting a pull request. + +## Support +This project is actively maintained, but there is no official support channel. +If you have a question that another developer might help you with, please post to [Stack Overflow](http://stackoverflow.com/questions/tagged/blueimp+jquery+file-upload) and tag your question with `blueimp jquery file upload`. + +## License +Released under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/vendor/blueimp/jquery-file-upload/SECURITY.md b/vendor/blueimp/jquery-file-upload/SECURITY.md new file mode 100644 index 000000000..db58f7dd7 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/SECURITY.md @@ -0,0 +1,133 @@ +# File Upload Security +For an in-depth understanding of the potential security risks of providing file uploads and possible mitigations, please refer to the [OWASP - Unrestricted File Upload](https://www.owasp.org/index.php/Unrestricted_File_Upload) documentation. + +To securely setup the project to serve uploaded files, please refer to the sample [Secure file upload serving configurations](#secure-file-upload-serving-configurations). + +To mitigate potential vulnerabilities in image processing libraries, please refer to the [Secure image processing configurations](#secure-image-processing-configurations). + +By default, all sample upload handlers allow only upload of image files, which mitigates some attack vectors, but should not be relied on as the only protection. + +Please also have a look at the [list of fixed vulnerabilities](VULNERABILITIES.md) in jQuery File Upload, which relates mostly to the sample server-side upload handlers and how they have been configured. + +## Purpose of this project +Please note that this project is not a complete file management product, but foremost a client-side file upload library for [jQuery](https://jquery.com/). +The server-side sample upload handlers are just examples to demonstrate the client-side file upload functionality. + +To make this very clear, there is **no user authentication** by default: +* **everyone can upload files** +* **everyone can delete uploaded files** + +In some cases this can be acceptable, but for most projects you will want to extend the sample upload handlers to integrate user authentication, or implement your own. + +It is also up to you to configure your Webserver to securely serve the uploaded files, e.g. using the [sample server configurations](#secure-file-upload-serving-configurations). + +## Mitigations against file upload risks + +### Prevent code execution on the server +To prevent execution of scripts or binaries on server-side, the upload directory must be configured to not execute files in the upload directory (e.g. `server/php/files` as the default for the PHP upload handler) and only treat uploaded files as static content. + +The recommended way to do this is to configure the upload directory path to point outside of the web application root. +Then the Webserver can be configured to serve files from the upload directory with their default static files handler only. + +Limiting file uploads to a whitelist of safe file types (e.g. image files) also mitigates this issue, but should not be the only protection. + +### Prevent code execution in the browser +To prevent execution of scripts on client-side, the following headers must +be sent when delivering generic uploaded files to the client: + +``` +Content-Type: application/octet-stream +X-Content-Type-Options: nosniff +``` + +The `Content-Type: application/octet-stream` header instructs browsers to display a download dialog instead of parsing it and possibly executing script content e.g. in HTML files. + +The `X-Content-Type-Options: nosniff` header prevents browsers to try to detect the file mime type despite the given content-type header. + +For known safe files, the content-type header can be adjusted using a **whitelist**, e.g. sending `Content-Type: image/png` for PNG files. + +### Prevent distribution of malware +To prevent attackers from uploading and distributing malware (e.g. computer viruses), it is recommended to limit file uploads only to a whitelist of safe file types. + +Please note that the detection of file types in the sample file upload handlers is based on the file extension and not the actual file content. This makes it still possible for attackers to upload malware by giving their files an image file extension, but should prevent automatic execution on client computers when opening those files. + +It does not protect at all from exploiting vulnerabilities in image display programs, nor from users renaming file extensions to inadvertently execute the contained malicious code. + +## Secure file upload serving configurations +The following configurations serve uploaded files as static files with the proper headers as [mitigation against file upload risks](#mitigations-against-file-upload-risks). +Please do not simply copy&paste these configurations, but make sure you understand what they are doing and that you have implemented them correctly. + +> Always test your own setup and make sure that it is secure! + +e.g. try uploading PHP scripts (as "example.php", "example.php.png" and "example.png") to see if they get executed by your Webserver. + +### Apache config +Add the following directive to the Apache config, replacing the directory path with the absolute path to the upload directory: + +```ApacheConf +<Directory "/path/to/project/server/php/files"> + # To enable the Headers module, execute the following command and reload Apache: + # sudo a2enmod headers + + # The following directives prevent the execution of script files + # in the context of the website. + # They also force the content-type application/octet-stream and + # force browsers to display a download dialog for non-image files. + SetHandler default-handler + ForceType application/octet-stream + Header set Content-Disposition attachment + + # The following unsets the forced type and Content-Disposition headers + # for known image files: + <FilesMatch "(?i)\.(gif|jpe?g|png)$"> + ForceType none + Header unset Content-Disposition + </FilesMatch> + + # The following directive prevents browsers from MIME-sniffing the content-type. + # This is an important complement to the ForceType directive above: + Header set X-Content-Type-Options nosniff +</Directory> +``` + +### NGINX config +Add the following directive to the NGINX config, replacing the directory path with the absolute path to the upload directory: + +```Nginx +location ^~ /path/to/project/server/php/files { + root html; + default_type application/octet-stream; + types { + image/gif gif; + image/jpeg jpg; + image/png png; + } + add_header X-Content-Type-Options 'nosniff'; + if ($request_filename ~ /(((?!\.(jpg)|(png)|(gif)$)[^/])+$)) { + add_header Content-Disposition 'attachment; filename="$1"'; + # Add X-Content-Type-Options again, as using add_header in a new context + # dismisses all previous add_header calls: + add_header X-Content-Type-Options 'nosniff'; + } +} +``` + +## Secure image processing configurations +The following configuration mitigates [potential image processing vulnerabilities with ImageMagick](VULNERABILITIES.md#potential-vulnerabilities-with-php+imagemagick) by limiting the attack vectors to a small subset of image types (`GIF/JPEG/PNG`). + +Please also consider using alternative, safer image processing libraries like [libvips](https://github.com/libvips/libvips) or [imageflow](https://github.com/imazen/imageflow). + +## ImageMagick config +It is recommended to disable all non-required ImageMagick coders via [policy.xml](https://wiki.debian.org/imagemagick/security). +To do so, locate the ImageMagick `policy.xml` configuration file and add the following policies: + +```xml +<?xml version="1.0" encoding="UTF-8"?> +<!-- ... --> +<policymap> + <!-- ... --> + <policy domain="delegate" rights="none" pattern="*" /> + <policy domain="coder" rights="none" pattern="*" /> + <policy domain="coder" rights="read | write" pattern="{GIF,JPEG,PNG}" /> +</policymap> +``` diff --git a/vendor/blueimp/jquery-file-upload/VULNERABILITIES.md b/vendor/blueimp/jquery-file-upload/VULNERABILITIES.md new file mode 100644 index 000000000..1561a1401 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/VULNERABILITIES.md @@ -0,0 +1,65 @@ +# ⚠️ List of fixed vulnerabilities + +## Potential vulnerabilities with PHP+ImageMagick +> Mitigated: 2018-10-25 (GMT) + +The sample [PHP upload handler](server/php/UploadHandler.php) before [v9.25.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.25.1) did not validate file signatures before invoking [ImageMagick](https://www.imagemagick.org/) (via [Imagick](http://php.net/manual/en/book.imagick.php)). +Verifying those [magic bytes](https://en.wikipedia.org/wiki/List_of_file_signatures) mitigates potential vulnerabilities when handling input files other than `GIF/JPEG/PNG`. + +Please also configure ImageMagick to only enable the coders required for `GIF/JPEG/PNG` processing, e.g. with the sample [ImageMagick config](SECURITY.md#imagemagick-config). + +**Further information:** +* Commit containing the mitigation: [fe44d34](https://github.com/blueimp/jQuery-File-Upload/commit/fe44d34be43be32c6b8d507932f318dababb25dd) +* [ImageTragick](https://imagetragick.com/) +* [CERT Vulnerability Note VU#332928](https://www.kb.cert.org/vuls/id/332928) +* [ImageMagick CVE entries](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=imagemagick) + +## Remote code execution vulnerability in the PHP component +> Fixed: 2018-10-23 (GMT) + +The sample [PHP upload handler](server/php/UploadHandler.php) before [v9.24.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.24.1) allowed to upload all file types by default. +This opens up a remote code execution vulnerability, unless the server is configured to not execute (PHP) files in the upload directory (`server/php/files`). + +The provided [.htaccess](server/php/files/.htaccess) file includes instructions for Apache to disable script execution, however [.htaccess support](https://httpd.apache.org/docs/current/howto/htaccess.html) is disabled by default since Apache `v2.3.9` via [AllowOverride Directive](https://httpd.apache.org/docs/current/mod/core.html#allowoverride). + +**You are affected if you:** +1. A) Uploaded jQuery File Upload < `v9.24.1` on a Webserver that executes files with `.php` as part of the file extension (e.g. "example.php.png"), e.g. Apache with `mod_php` enabled and the following directive (*not a recommended configuration*): + ```ApacheConf + AddHandler php5-script .php + ``` + B) Uploaded jQuery File Upload < `v9.22.1` on a Webserver that executes files with the file extension `.php`, e.g. Apache with `mod_php` enabled and the following directive: + ```ApacheConf + <FilesMatch \.php$> + SetHandler application/x-httpd-php + </FilesMatch> + ``` +2. Did not actively configure your Webserver to not execute files in the upload directory (`server/php/files`). +3. Are running Apache `v2.3.9+` with the default `AllowOverride` Directive set to `None` or another Webserver with no `.htaccess` support. + +**How to fix it:** +1. Upgrade to the latest version of jQuery File Upload. +2. Configure your Webserver to not execute files in the upload directory, e.g. with the [sample Apache configuration](SECURITY.md#apache-config) + +**Further information:** +* Commits containing the security fix: [aeb47e5](https://github.com/blueimp/jQuery-File-Upload/commit/aeb47e51c67df8a504b7726595576c1c66b5dc2f), [ad4aefd](https://github.com/blueimp/jQuery-File-Upload/commit/ad4aefd96e4056deab6fea2690f0d8cf56bb2d7d) +* [Full disclosure post on Hacker News](https://news.ycombinator.com/item?id=18267309). +* [CVE-2018-9206](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-9206) +* [OWASP - Unrestricted File Upload](https://www.owasp.org/index.php/Unrestricted_File_Upload) + +## Open redirect vulnerability in the GAE components +> Fixed: 2015-06-12 (GMT) + +The sample Google App Engine upload handlers before v[9.10.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/9.10.1) accepted any URL as redirect target, making it possible to use the Webserver's domain for phishing attacks. + +**Further information:** +* Commit containing the security fix: [f74d2a8](https://github.com/blueimp/jQuery-File-Upload/commit/f74d2a8c3e3b1e8e336678d2899facd5bcdb589f) +* [OWASP - Unvalidated Redirects and Forwards Cheat Sheet](https://www.owasp.org/index.php/Unvalidated_Redirects_and_Forwards_Cheat_Sheet) + +## Cross-site scripting vulnerability in the Iframe Transport +> Fixed: 2012-08-09 (GMT) + +The [redirect page](cors/result.html) for the [Iframe Transport](js/jquery.iframe-transport.js) before commit [4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a) (*fixed in all tagged releases*) allowed executing arbitrary JavaScript in the context of the Webserver. + +**Further information:** +* Commit containing the security fix: [4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a) +* [OWASP - Cross-site Scripting (XSS)](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)) diff --git a/vendor/blueimp/jquery-file-upload/angularjs.html b/vendor/blueimp/jquery-file-upload/angularjs.html new file mode 100644 index 000000000..2051bbf79 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/angularjs.html @@ -0,0 +1,211 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery File Upload Plugin AngularJS Demo + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<!-- Force latest IE rendering engine or ChromeFrame if installed --> +<!--[if IE]> +<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> +<![endif]--> +<meta charset="utf-8"> +<title>jQuery File Upload Demo - AngularJS version</title> +<meta name="description" content="File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for AngularJS. Supports cross-domain, chunked and resumable file uploads and client-side image resizing. Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads."> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<!-- Bootstrap styles --> +<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> +<!-- Generic page styles --> +<link rel="stylesheet" href="css/style.css"> +<!-- blueimp Gallery styles --> +<link rel="stylesheet" href="https://blueimp.github.io/Gallery/css/blueimp-gallery.min.css"> +<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars --> +<link rel="stylesheet" href="css/jquery.fileupload.css"> +<link rel="stylesheet" href="css/jquery.fileupload-ui.css"> +<!-- CSS adjustments for browsers with JavaScript disabled --> +<noscript><link rel="stylesheet" href="css/jquery.fileupload-noscript.css"></noscript> +<noscript><link rel="stylesheet" href="css/jquery.fileupload-ui-noscript.css"></noscript> +<style> +/* Hide Angular JS elements before initializing */ +.ng-cloak { + display: none; +} +</style> +</head> +<body> +<div class="navbar navbar-default navbar-fixed-top"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-fixed-top .navbar-collapse"> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="https://github.com/blueimp/jQuery-File-Upload">jQuery File Upload</a> + </div> + <div class="navbar-collapse collapse"> + <ul class="nav navbar-nav"> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/tags">Download</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload">Source Code</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/wiki">Documentation</a></li> + <li><a href="https://blueimp.net">© Sebastian Tschan</a></li> + </ul> + </div> + </div> +</div> +<div class="container"> + <h1>jQuery File Upload Demo</h1> + <h2 class="lead">AngularJS version</h2> + <ul class="nav nav-tabs"> + <li><a href="basic.html">Basic</a></li> + <li><a href="basic-plus.html">Basic Plus</a></li> + <li><a href="index.html">Basic Plus UI</a></li> + <li class="active"><a href="angularjs.html">AngularJS</a></li> + <li><a href="jquery-ui.html">jQuery UI</a></li> + </ul> + <br> + <blockquote> + <p>File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for AngularJS.<br> + Supports cross-domain, chunked and resumable file uploads and client-side image resizing.<br> + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.</p> + </blockquote> + <br> + <!-- The file upload form used as target for the file upload widget --> + <form id="fileupload" action="https://jquery-file-upload.appspot.com/" method="POST" enctype="multipart/form-data" data-ng-app="demo" data-ng-controller="DemoFileUploadController" data-file-upload="options" data-ng-class="{'fileupload-processing': processing() || loadingFiles}"> + <!-- Redirect browsers with JavaScript disabled to the origin page --> + <noscript><input type="hidden" name="redirect" value="https://blueimp.github.io/jQuery-File-Upload/"></noscript> + <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload --> + <div class="row fileupload-buttonbar"> + <div class="col-lg-7"> + <!-- The fileinput-button span is used to style the file input field as button --> + <span class="btn btn-success fileinput-button" ng-class="{disabled: disabled}"> + <i class="glyphicon glyphicon-plus"></i> + <span>Add files...</span> + <input type="file" name="files[]" multiple ng-disabled="disabled"> + </span> + <button type="button" class="btn btn-primary start" data-ng-click="submit()"> + <i class="glyphicon glyphicon-upload"></i> + <span>Start upload</span> + </button> + <button type="button" class="btn btn-warning cancel" data-ng-click="cancel()"> + <i class="glyphicon glyphicon-ban-circle"></i> + <span>Cancel upload</span> + </button> + <!-- The global file processing state --> + <span class="fileupload-process"></span> + </div> + <!-- The global progress state --> + <div class="col-lg-5 fade" data-ng-class="{in: active()}"> + <!-- The global progress bar --> + <div class="progress progress-striped active" data-file-upload-progress="progress()"><div class="progress-bar progress-bar-success" data-ng-style="{width: num + '%'}"></div></div> + <!-- The extended global progress state --> + <div class="progress-extended"> </div> + </div> + </div> + <!-- The table listing the files available for upload/download --> + <table class="table table-striped files ng-cloak"> + <tr data-ng-repeat="file in queue" data-ng-class="{'processing': file.$processing()}"> + <td data-ng-switch data-on="!!file.thumbnailUrl"> + <div class="preview" data-ng-switch-when="true"> + <a data-ng-href="{{file.url}}" title="{{file.name}}" download="{{file.name}}" data-gallery><img data-ng-src="{{file.thumbnailUrl}}" alt=""></a> + </div> + <div class="preview" data-ng-switch-default data-file-upload-preview="file"></div> + </td> + <td> + <p class="name" data-ng-switch data-on="!!file.url"> + <span data-ng-switch-when="true" data-ng-switch data-on="!!file.thumbnailUrl"> + <a data-ng-switch-when="true" data-ng-href="{{file.url}}" title="{{file.name}}" download="{{file.name}}" data-gallery>{{file.name}}</a> + <a data-ng-switch-default data-ng-href="{{file.url}}" title="{{file.name}}" download="{{file.name}}">{{file.name}}</a> + </span> + <span data-ng-switch-default>{{file.name}}</span> + </p> + <strong data-ng-show="file.error" class="error text-danger">{{file.error}}</strong> + </td> + <td> + <p class="size">{{file.size | formatFileSize}}</p> + <div class="progress progress-striped active fade" data-ng-class="{pending: 'in'}[file.$state()]" data-file-upload-progress="file.$progress()"><div class="progress-bar progress-bar-success" data-ng-style="{width: num + '%'}"></div></div> + </td> + <td> + <button type="button" class="btn btn-primary start" data-ng-click="file.$submit()" data-ng-hide="!file.$submit || options.autoUpload" data-ng-disabled="file.$state() == 'pending' || file.$state() == 'rejected'"> + <i class="glyphicon glyphicon-upload"></i> + <span>Start</span> + </button> + <button type="button" class="btn btn-warning cancel" data-ng-click="file.$cancel()" data-ng-hide="!file.$cancel"> + <i class="glyphicon glyphicon-ban-circle"></i> + <span>Cancel</span> + </button> + <button data-ng-controller="FileDestroyController" type="button" class="btn btn-danger destroy" data-ng-click="file.$destroy()" data-ng-hide="!file.$destroy"> + <i class="glyphicon glyphicon-trash"></i> + <span>Delete</span> + </button> + </td> + </tr> + </table> + </form> + <br> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">Demo Notes</h3> + </div> + <div class="panel-body"> + <ul> + <li>The maximum file size for uploads in this demo is <strong>999 KB</strong> (default file size is unlimited).</li> + <li>Only image files (<strong>JPG, GIF, PNG</strong>) are allowed in this demo (by default there is no file type restriction).</li> + <li>Uploaded files will be deleted automatically after <strong>5 minutes or less</strong> (demo files are stored in memory).</li> + <li>You can <strong>drag & drop</strong> files from your desktop on this webpage (see <a href="https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support">Browser support</a>).</li> + <li>Please refer to the <a href="https://github.com/blueimp/jQuery-File-Upload">project website</a> and <a href="https://github.com/blueimp/jQuery-File-Upload/wiki">documentation</a> for more information.</li> + <li>Built with the <a href="http://getbootstrap.com/">Bootstrap</a> CSS framework and Icons from <a href="http://glyphicons.com/">Glyphicons</a>.</li> + </ul> + </div> + </div> +</div> +<!-- The blueimp Gallery widget --> +<div id="blueimp-gallery" class="blueimp-gallery blueimp-gallery-controls" data-filter=":even"> + <div class="slides"></div> + <h3 class="title"></h3> + <a class="prev">‹</a> + <a class="next">›</a> + <a class="close">×</a> + <a class="play-pause"></a> + <ol class="indicator"></ol> +</div> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> +<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script> +<!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included --> +<script src="js/vendor/jquery.ui.widget.js"></script> +<!-- The Load Image plugin is included for the preview images and image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script> +<!-- The Canvas to Blob plugin is included for image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script> +<!-- Bootstrap JS is not required, but included for the responsive demo navigation --> +<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> +<!-- blueimp Gallery script --> +<script src="https://blueimp.github.io/Gallery/js/jquery.blueimp-gallery.min.js"></script> +<!-- The Iframe Transport is required for browsers without support for XHR file uploads --> +<script src="js/jquery.iframe-transport.js"></script> +<!-- The basic File Upload plugin --> +<script src="js/jquery.fileupload.js"></script> +<!-- The File Upload processing plugin --> +<script src="js/jquery.fileupload-process.js"></script> +<!-- The File Upload image preview & resize plugin --> +<script src="js/jquery.fileupload-image.js"></script> +<!-- The File Upload audio preview plugin --> +<script src="js/jquery.fileupload-audio.js"></script> +<!-- The File Upload video preview plugin --> +<script src="js/jquery.fileupload-video.js"></script> +<!-- The File Upload validation plugin --> +<script src="js/jquery.fileupload-validate.js"></script> +<!-- The File Upload Angular JS module --> +<script src="js/jquery.fileupload-angular.js"></script> +<!-- The main application script --> +<script src="js/app.js"></script> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/basic-plus.html b/vendor/blueimp/jquery-file-upload/basic-plus.html new file mode 100644 index 000000000..acee24843 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/basic-plus.html @@ -0,0 +1,226 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery File Upload Plugin Basic Plus Demo + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<!-- Force latest IE rendering engine or ChromeFrame if installed --> +<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><![endif]--> +<meta charset="utf-8"> +<title>jQuery File Upload Demo - Basic Plus version</title> +<meta name="description" content="File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (Google App Engine, PHP, Python, Ruby on Rails, Java, etc.) that supports standard HTML form file uploads."> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<!-- Bootstrap styles --> +<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> +<!-- Generic page styles --> +<link rel="stylesheet" href="css/style.css"> +<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars --> +<link rel="stylesheet" href="css/jquery.fileupload.css"> +</head> +<body> +<div class="navbar navbar-default navbar-fixed-top"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-fixed-top .navbar-collapse"> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="https://github.com/blueimp/jQuery-File-Upload">jQuery File Upload</a> + </div> + <div class="navbar-collapse collapse"> + <ul class="nav navbar-nav"> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/tags">Download</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload">Source Code</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/wiki">Documentation</a></li> + <li><a href="https://blueimp.net">© Sebastian Tschan</a></li> + </ul> + </div> + </div> +</div> +<div class="container"> + <h1>jQuery File Upload Demo</h1> + <h2 class="lead">Basic Plus version</h2> + <ul class="nav nav-tabs"> + <li><a href="basic.html">Basic</a></li> + <li class="active"><a href="basic-plus.html">Basic Plus</a></li> + <li><a href="index.html">Basic Plus UI</a></li> + <li><a href="angularjs.html">AngularJS</a></li> + <li><a href="jquery-ui.html">jQuery UI</a></li> + </ul> + <br> + <blockquote> + <p>File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery.<br> + Supports cross-domain, chunked and resumable file uploads and client-side image resizing.<br> + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.</p> + </blockquote> + <br> + <!-- The fileinput-button span is used to style the file input field as button --> + <span class="btn btn-success fileinput-button"> + <i class="glyphicon glyphicon-plus"></i> + <span>Add files...</span> + <!-- The file input field used as target for the file upload widget --> + <input id="fileupload" type="file" name="files[]" multiple> + </span> + <br> + <br> + <!-- The global progress bar --> + <div id="progress" class="progress"> + <div class="progress-bar progress-bar-success"></div> + </div> + <!-- The container for the uploaded files --> + <div id="files" class="files"></div> + <br> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">Demo Notes</h3> + </div> + <div class="panel-body"> + <ul> + <li>The maximum file size for uploads in this demo is <strong>999 KB</strong> (default file size is unlimited).</li> + <li>Only image files (<strong>JPG, GIF, PNG</strong>) are allowed in this demo (by default there is no file type restriction).</li> + <li>Uploaded files will be deleted automatically after <strong>5 minutes or less</strong> (demo files are stored in memory).</li> + <li>You can <strong>drag & drop</strong> files from your desktop on this webpage (see <a href="https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support">Browser support</a>).</li> + <li>Please refer to the <a href="https://github.com/blueimp/jQuery-File-Upload">project website</a> and <a href="https://github.com/blueimp/jQuery-File-Upload/wiki">documentation</a> for more information.</li> + <li>Built with the <a href="http://getbootstrap.com/">Bootstrap</a> CSS framework and Icons from <a href="http://glyphicons.com/">Glyphicons</a>.</li> + </ul> + </div> + </div> +</div> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> +<!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included --> +<script src="js/vendor/jquery.ui.widget.js"></script> +<!-- The Load Image plugin is included for the preview images and image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script> +<!-- The Canvas to Blob plugin is included for image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script> +<!-- Bootstrap JS is not required, but included for the responsive demo navigation --> +<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> +<!-- The Iframe Transport is required for browsers without support for XHR file uploads --> +<script src="js/jquery.iframe-transport.js"></script> +<!-- The basic File Upload plugin --> +<script src="js/jquery.fileupload.js"></script> +<!-- The File Upload processing plugin --> +<script src="js/jquery.fileupload-process.js"></script> +<!-- The File Upload image preview & resize plugin --> +<script src="js/jquery.fileupload-image.js"></script> +<!-- The File Upload audio preview plugin --> +<script src="js/jquery.fileupload-audio.js"></script> +<!-- The File Upload video preview plugin --> +<script src="js/jquery.fileupload-video.js"></script> +<!-- The File Upload validation plugin --> +<script src="js/jquery.fileupload-validate.js"></script> +<script> +/*jslint unparam: true, regexp: true */ +/*global window, $ */ +$(function () { + 'use strict'; + // Change this to the location of your server-side upload handler: + var url = window.location.hostname === 'blueimp.github.io' ? + '//jquery-file-upload.appspot.com/' : 'server/php/', + uploadButton = $('<button/>') + .addClass('btn btn-primary') + .prop('disabled', true) + .text('Processing...') + .on('click', function () { + var $this = $(this), + data = $this.data(); + $this + .off('click') + .text('Abort') + .on('click', function () { + $this.remove(); + data.abort(); + }); + data.submit().always(function () { + $this.remove(); + }); + }); + $('#fileupload').fileupload({ + url: url, + dataType: 'json', + autoUpload: false, + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + maxFileSize: 999000, + // Enable image resizing, except for Android and Opera, + // which actually support image resizing, but fail to + // send Blob objects via XHR requests: + disableImageResize: /Android(?!.*Chrome)|Opera/ + .test(window.navigator.userAgent), + previewMaxWidth: 100, + previewMaxHeight: 100, + previewCrop: true + }).on('fileuploadadd', function (e, data) { + data.context = $('<div/>').appendTo('#files'); + $.each(data.files, function (index, file) { + var node = $('<p/>') + .append($('<span/>').text(file.name)); + if (!index) { + node + .append('<br>') + .append(uploadButton.clone(true).data(data)); + } + node.appendTo(data.context); + }); + }).on('fileuploadprocessalways', function (e, data) { + var index = data.index, + file = data.files[index], + node = $(data.context.children()[index]); + if (file.preview) { + node + .prepend('<br>') + .prepend(file.preview); + } + if (file.error) { + node + .append('<br>') + .append($('<span class="text-danger"/>').text(file.error)); + } + if (index + 1 === data.files.length) { + data.context.find('button') + .text('Upload') + .prop('disabled', !!data.files.error); + } + }).on('fileuploadprogressall', function (e, data) { + var progress = parseInt(data.loaded / data.total * 100, 10); + $('#progress .progress-bar').css( + 'width', + progress + '%' + ); + }).on('fileuploaddone', function (e, data) { + $.each(data.result.files, function (index, file) { + if (file.url) { + var link = $('<a>') + .attr('target', '_blank') + .prop('href', file.url); + $(data.context.children()[index]) + .wrap(link); + } else if (file.error) { + var error = $('<span class="text-danger"/>').text(file.error); + $(data.context.children()[index]) + .append('<br>') + .append(error); + } + }); + }).on('fileuploadfail', function (e, data) { + $.each(data.files, function (index) { + var error = $('<span class="text-danger"/>').text('File upload failed.'); + $(data.context.children()[index]) + .append('<br>') + .append(error); + }); + }).prop('disabled', !$.support.fileInput) + .parent().addClass($.support.fileInput ? undefined : 'disabled'); +}); +</script> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/basic.html b/vendor/blueimp/jquery-file-upload/basic.html new file mode 100644 index 000000000..232a24624 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/basic.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery File Upload Plugin Basic Demo + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<!-- Force latest IE rendering engine or ChromeFrame if installed --> +<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><![endif]--> +<meta charset="utf-8"> +<title>jQuery File Upload Demo - Basic version</title> +<meta name="description" content="File Upload widget with multiple file selection, drag&drop support and progress bar for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads."> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<!-- Bootstrap styles --> +<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> +<!-- Generic page styles --> +<link rel="stylesheet" href="css/style.css"> +<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars --> +<link rel="stylesheet" href="css/jquery.fileupload.css"> +</head> +<body> +<div class="navbar navbar-default navbar-fixed-top"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-fixed-top .navbar-collapse"> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="https://github.com/blueimp/jQuery-File-Upload">jQuery File Upload</a> + </div> + <div class="navbar-collapse collapse"> + <ul class="nav navbar-nav"> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/tags">Download</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload">Source Code</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/wiki">Documentation</a></li> + <li><a href="https://blueimp.net">© Sebastian Tschan</a></li> + </ul> + </div> + </div> +</div> +<div class="container"> + <h1>jQuery File Upload Demo</h1> + <h2 class="lead">Basic version</h2> + <ul class="nav nav-tabs"> + <li class="active"><a href="basic.html">Basic</a></li> + <li><a href="basic-plus.html">Basic Plus</a></li> + <li><a href="index.html">Basic Plus UI</a></li> + <li><a href="angularjs.html">AngularJS</a></li> + <li><a href="jquery-ui.html">jQuery UI</a></li> + </ul> + <br> + <blockquote> + <p>File Upload widget with multiple file selection, drag&drop support and progress bar for jQuery.<br> + Supports cross-domain, chunked and resumable file uploads.<br> + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.</p> + </blockquote> + <br> + <!-- The fileinput-button span is used to style the file input field as button --> + <span class="btn btn-success fileinput-button"> + <i class="glyphicon glyphicon-plus"></i> + <span>Select files...</span> + <!-- The file input field used as target for the file upload widget --> + <input id="fileupload" type="file" name="files[]" multiple> + </span> + <br> + <br> + <!-- The global progress bar --> + <div id="progress" class="progress"> + <div class="progress-bar progress-bar-success"></div> + </div> + <!-- The container for the uploaded files --> + <div id="files" class="files"></div> + <br> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">Demo Notes</h3> + </div> + <div class="panel-body"> + <ul> + <li>The maximum file size for uploads in this demo is <strong>999 KB</strong> (default file size is unlimited).</li> + <li>Only image files (<strong>JPG, GIF, PNG</strong>) are allowed in this demo (by default there is no file type restriction).</li> + <li>Uploaded files will be deleted automatically after <strong>5 minutes or less</strong> (demo files are stored in memory).</li> + <li>You can <strong>drag & drop</strong> files from your desktop on this webpage (see <a href="https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support">Browser support</a>).</li> + <li>Please refer to the <a href="https://github.com/blueimp/jQuery-File-Upload">project website</a> and <a href="https://github.com/blueimp/jQuery-File-Upload/wiki">documentation</a> for more information.</li> + <li>Built with the <a href="http://getbootstrap.com/">Bootstrap</a> CSS framework and Icons from <a href="http://glyphicons.com/">Glyphicons</a>.</li> + </ul> + </div> + </div> +</div> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> +<!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included --> +<script src="js/vendor/jquery.ui.widget.js"></script> +<!-- The Iframe Transport is required for browsers without support for XHR file uploads --> +<script src="js/jquery.iframe-transport.js"></script> +<!-- The basic File Upload plugin --> +<script src="js/jquery.fileupload.js"></script> +<!-- Bootstrap JS is not required, but included for the responsive demo navigation --> +<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> +<script> +/*jslint unparam: true */ +/*global window, $ */ +$(function () { + 'use strict'; + // Change this to the location of your server-side upload handler: + var url = window.location.hostname === 'blueimp.github.io' ? + '//jquery-file-upload.appspot.com/' : 'server/php/'; + $('#fileupload').fileupload({ + url: url, + dataType: 'json', + done: function (e, data) { + $.each(data.result.files, function (index, file) { + $('<p/>').text(file.name).appendTo('#files'); + }); + }, + progressall: function (e, data) { + var progress = parseInt(data.loaded / data.total * 100, 10); + $('#progress .progress-bar').css( + 'width', + progress + '%' + ); + } + }).prop('disabled', !$.support.fileInput) + .parent().addClass($.support.fileInput ? undefined : 'disabled'); +}); +</script> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/bower-version-update.js b/vendor/blueimp/jquery-file-upload/bower-version-update.js new file mode 100755 index 000000000..09ce3927e --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/bower-version-update.js @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +'use strict'; + +var path = require('path'); +var packageJSON = require(path.join(__dirname, 'package.json')); +var bowerFile = path.join(__dirname, 'bower.json'); +var bowerJSON = require('bower-json').parse( + require(bowerFile), + {normalize: true} +); +bowerJSON.version = packageJSON.version; +require('fs').writeFileSync( + bowerFile, + JSON.stringify(bowerJSON, null, 2) + '\n' +); diff --git a/vendor/blueimp/jquery-file-upload/bower.json b/vendor/blueimp/jquery-file-upload/bower.json new file mode 100644 index 000000000..34594a32f --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/bower.json @@ -0,0 +1,64 @@ +{ + "name": "blueimp-file-upload", + "version": "9.25.1", + "title": "jQuery File Upload", + "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images.", + "keywords": [ + "jquery", + "file", + "upload", + "widget", + "multiple", + "selection", + "drag", + "drop", + "progress", + "preview", + "cross-domain", + "cross-site", + "chunk", + "resume", + "gae", + "go", + "python", + "php", + "bootstrap" + ], + "homepage": "https://github.com/blueimp/jQuery-File-Upload", + "author": { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + }, + "maintainers": [ + { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/blueimp/jQuery-File-Upload.git" + }, + "bugs": "https://github.com/blueimp/jQuery-File-Upload/issues", + "license": "MIT", + "dependencies": { + "jquery": ">=1.6", + "blueimp-tmpl": ">=2.5.4", + "blueimp-load-image": ">=1.13.0", + "blueimp-canvas-to-blob": ">=2.1.1" + }, + "main": [ + "js/jquery.fileupload.js" + ], + "ignore": [ + "/*.*", + "/cors", + "css/jquery-ui-demo-ie8.css", + "css/jquery-ui-demo.css", + "css/style.css", + "js/app.js", + "js/main.js", + "server", + "test" + ] +} diff --git a/vendor/blueimp/jquery-file-upload/composer.json b/vendor/blueimp/jquery-file-upload/composer.json new file mode 100644 index 000000000..d17f4ff9e --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/composer.json @@ -0,0 +1,39 @@ +{ + "name": "blueimp/jquery-file-upload" + , "description": "File Upload widget for jQuery." + , "keywords": ["jquery", + "file", + "upload", + "widget", + "multiple", + "selection", + "drag", + "drop", + "progress", + "preview", + "cross-domain", + "cross-site", + "chunk", + "resume", + "gae", + "go", + "python", + "php", + "bootstrap"] + , "homepage": "https://github.com/blueimp/jQuery-File-Upload" + , "authors": [ + { + "name": "Sebastian Tschan", + "homepage": "https://blueimp.net" + } + ] + , "support": { + "forum": "https://groups.google.com/forum/#!forum/jquery-fileupload" + } + , "license": "MIT" + , "autoload": { + "classmap": [ + "server/php/UploadHandler.php" + ] + } +} diff --git a/vendor/blueimp/jquery-file-upload/cors/postmessage.html b/vendor/blueimp/jquery-file-upload/cors/postmessage.html new file mode 100644 index 000000000..6a56cf0b6 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/cors/postmessage.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery File Upload Plugin postMessage API + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<meta charset="utf-8"> +<title>jQuery File Upload Plugin postMessage API</title> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> +</head> +<body> +<script> +/*jslint unparam: true, regexp: true */ +/*global $, Blob, FormData, location */ +'use strict'; +var origin = /^http:\/\/example.org/, + target = new RegExp('^(http(s)?:)?\\/\\/' + location.host + '\\/'); +$(window).on('message', function (e) { + e = e.originalEvent; + var s = e.data, + xhr = $.ajaxSettings.xhr(), + f; + if (!origin.test(e.origin)) { + throw new Error('Origin "' + e.origin + '" does not match ' + origin); + } + if (!target.test(e.data.url)) { + throw new Error('Target "' + e.data.url + '" does not match ' + target); + } + $(xhr.upload).on('progress', function (ev) { + ev = ev.originalEvent; + e.source.postMessage({ + id: s.id, + type: ev.type, + timeStamp: ev.timeStamp, + lengthComputable: ev.lengthComputable, + loaded: ev.loaded, + total: ev.total + }, e.origin); + }); + s.xhr = function () { + return xhr; + }; + if (!(s.data instanceof Blob)) { + f = new FormData(); + $.each(s.data, function (i, v) { + f.append(v.name, v.value); + }); + s.data = f; + } + $.ajax(s).always(function (result, statusText, jqXHR) { + if (!jqXHR.done) { + jqXHR = result; + result = null; + } + e.source.postMessage({ + id: s.id, + status: jqXHR.status, + statusText: statusText, + result: result, + headers: jqXHR.getAllResponseHeaders() + }, e.origin); + }); +}); +</script> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/cors/result.html b/vendor/blueimp/jquery-file-upload/cors/result.html new file mode 100644 index 000000000..e3d629814 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/cors/result.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery Iframe Transport Plugin Redirect Page + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<meta charset="utf-8"> +<title>jQuery Iframe Transport Plugin Redirect Page</title> +</head> +<body> +<script> +document.body.innerText=document.body.textContent=decodeURIComponent(window.location.search.slice(1)); +</script> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/css/jquery-ui-demo-ie8.css b/vendor/blueimp/jquery-file-upload/css/jquery-ui-demo-ie8.css new file mode 100644 index 000000000..e0e8ea9b0 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/css/jquery-ui-demo-ie8.css @@ -0,0 +1,21 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Demo CSS Fixes for IE<9 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.navigation { + list-style: none; + padding: 0; + margin: 1em 0; +} +.navigation li { + display: inline; + margin-right: 10px; +} diff --git a/vendor/blueimp/jquery-file-upload/css/jquery-ui-demo.css b/vendor/blueimp/jquery-file-upload/css/jquery-ui-demo.css new file mode 100644 index 000000000..d7d524df5 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/css/jquery-ui-demo.css @@ -0,0 +1,67 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Demo CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +body { + max-width: 750px; + margin: 0 auto; + padding: 1em; + font-family: "Lucida Grande", "Lucida Sans Unicode", Arial, sans-serif; + font-size: 1em; + line-height: 1.4em; + background: #222; + color: #fff; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +a { + color: orange; + text-decoration: none; +} +img { + border: 0; + vertical-align: middle; +} +h1 { + line-height: 1em; +} +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eee; +} +table { + width: 100%; + margin: 10px 0; +} + +.fileupload-progress { + margin: 10px 0; +} +.fileupload-progress .progress-extended { + margin-top: 5px; +} +.error { + color: red; +} + +@media (min-width: 481px) { + .navigation { + list-style: none; + padding: 0; + } + .navigation li { + display: inline-block; + } + .navigation li:not(:first-child):before { + content: "| "; + } +} diff --git a/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-noscript.css b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-noscript.css new file mode 100644 index 000000000..2409bfb0a --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-noscript.css @@ -0,0 +1,22 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin NoScript CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button input { + position: static; + opacity: 1; + filter: none; + font-size: inherit !important; + direction: inherit; +} +.fileinput-button span { + display: none; +} diff --git a/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-ui-noscript.css b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-ui-noscript.css new file mode 100644 index 000000000..30651acf0 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-ui-noscript.css @@ -0,0 +1,17 @@ +@charset "UTF-8"; +/* + * jQuery File Upload UI Plugin NoScript CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button i, +.fileupload-buttonbar .delete, +.fileupload-buttonbar .toggle { + display: none; +} diff --git a/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-ui.css b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-ui.css new file mode 100644 index 000000000..9e36c42c5 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload-ui.css @@ -0,0 +1,57 @@ +@charset "UTF-8"; +/* + * jQuery File Upload UI Plugin CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileupload-buttonbar .btn, +.fileupload-buttonbar .toggle { + margin-bottom: 5px; +} +.progress-animated .progress-bar, +.progress-animated .bar { + background: url("../img/progressbar.gif") !important; + filter: none; +} +.fileupload-process { + float: right; + display: none; +} +.fileupload-processing .fileupload-process, +.files .processing .preview { + display: block; + width: 32px; + height: 32px; + background: url("../img/loading.gif") center no-repeat; + background-size: contain; +} +.files audio, +.files video { + max-width: 300px; +} + +@media (max-width: 767px) { + .fileupload-buttonbar .toggle, + .files .toggle, + .files .btn span { + display: none; + } + .files .name { + width: 80px; + word-wrap: break-word; + } + .files audio, + .files video { + max-width: 80px; + } + .files img, + .files canvas { + max-width: 100%; + } +} diff --git a/vendor/blueimp/jquery-file-upload/css/jquery.fileupload.css b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload.css new file mode 100644 index 000000000..8ae3b09d4 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/css/jquery.fileupload.css @@ -0,0 +1,37 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button { + position: relative; + overflow: hidden; + display: inline-block; +} +.fileinput-button input { + position: absolute; + top: 0; + right: 0; + margin: 0; + opacity: 0; + -ms-filter: 'alpha(opacity=0)'; + font-size: 200px !important; + direction: ltr; + cursor: pointer; +} + +/* Fixes for IE < 8 */ +@media screen\9 { + .fileinput-button input { + filter: alpha(opacity=0); + font-size: 100%; + height: 100%; + } +} diff --git a/vendor/blueimp/jquery-file-upload/css/style.css b/vendor/blueimp/jquery-file-upload/css/style.css new file mode 100644 index 000000000..3aee25689 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/css/style.css @@ -0,0 +1,15 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin CSS Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +body { + padding-top: 60px; +} diff --git a/vendor/blueimp/jquery-file-upload/img/loading.gif b/vendor/blueimp/jquery-file-upload/img/loading.gif Binary files differnew file mode 100644 index 000000000..90f28cbdb --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/img/loading.gif diff --git a/vendor/blueimp/jquery-file-upload/img/progressbar.gif b/vendor/blueimp/jquery-file-upload/img/progressbar.gif Binary files differnew file mode 100644 index 000000000..fbcce6bc9 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/img/progressbar.gif diff --git a/vendor/blueimp/jquery-file-upload/index.html b/vendor/blueimp/jquery-file-upload/index.html new file mode 100644 index 000000000..c8c66ad8b --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/index.html @@ -0,0 +1,255 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery File Upload Plugin Demo + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<!-- Force latest IE rendering engine or ChromeFrame if installed --> +<!--[if IE]> +<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> +<![endif]--> +<meta charset="utf-8"> +<title>jQuery File Upload Demo</title> +<meta name="description" content="File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads and client-side image resizing. Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads."> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<!-- Bootstrap styles --> +<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> +<!-- Generic page styles --> +<link rel="stylesheet" href="css/style.css"> +<!-- blueimp Gallery styles --> +<link rel="stylesheet" href="https://blueimp.github.io/Gallery/css/blueimp-gallery.min.css"> +<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars --> +<link rel="stylesheet" href="css/jquery.fileupload.css"> +<link rel="stylesheet" href="css/jquery.fileupload-ui.css"> +<!-- CSS adjustments for browsers with JavaScript disabled --> +<noscript><link rel="stylesheet" href="css/jquery.fileupload-noscript.css"></noscript> +<noscript><link rel="stylesheet" href="css/jquery.fileupload-ui-noscript.css"></noscript> +</head> +<body> +<div class="navbar navbar-default navbar-fixed-top"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-fixed-top .navbar-collapse"> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="https://github.com/blueimp/jQuery-File-Upload">jQuery File Upload</a> + </div> + <div class="navbar-collapse collapse"> + <ul class="nav navbar-nav"> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/tags">Download</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload">Source Code</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/wiki">Documentation</a></li> + <li><a href="https://blueimp.net">© Sebastian Tschan</a></li> + </ul> + </div> + </div> +</div> +<div class="container"> + <h1>jQuery File Upload Demo</h1> + <h2 class="lead">Basic Plus UI version</h2> + <ul class="nav nav-tabs"> + <li><a href="basic.html">Basic</a></li> + <li><a href="basic-plus.html">Basic Plus</a></li> + <li class="active"><a href="index.html">Basic Plus UI</a></li> + <li><a href="angularjs.html">AngularJS</a></li> + <li><a href="jquery-ui.html">jQuery UI</a></li> + </ul> + <br> + <blockquote> + <p>File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery.<br> + Supports cross-domain, chunked and resumable file uploads and client-side image resizing.<br> + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.</p> + </blockquote> + <br> + <!-- The file upload form used as target for the file upload widget --> + <form id="fileupload" action="https://jquery-file-upload.appspot.com/" method="POST" enctype="multipart/form-data"> + <!-- Redirect browsers with JavaScript disabled to the origin page --> + <noscript><input type="hidden" name="redirect" value="https://blueimp.github.io/jQuery-File-Upload/"></noscript> + <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload --> + <div class="row fileupload-buttonbar"> + <div class="col-lg-7"> + <!-- The fileinput-button span is used to style the file input field as button --> + <span class="btn btn-success fileinput-button"> + <i class="glyphicon glyphicon-plus"></i> + <span>Add files...</span> + <input type="file" name="files[]" multiple> + </span> + <button type="submit" class="btn btn-primary start"> + <i class="glyphicon glyphicon-upload"></i> + <span>Start upload</span> + </button> + <button type="reset" class="btn btn-warning cancel"> + <i class="glyphicon glyphicon-ban-circle"></i> + <span>Cancel upload</span> + </button> + <button type="button" class="btn btn-danger delete"> + <i class="glyphicon glyphicon-trash"></i> + <span>Delete</span> + </button> + <input type="checkbox" class="toggle"> + <!-- The global file processing state --> + <span class="fileupload-process"></span> + </div> + <!-- The global progress state --> + <div class="col-lg-5 fileupload-progress fade"> + <!-- The global progress bar --> + <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100"> + <div class="progress-bar progress-bar-success" style="width:0%;"></div> + </div> + <!-- The extended global progress state --> + <div class="progress-extended"> </div> + </div> + </div> + <!-- The table listing the files available for upload/download --> + <table role="presentation" class="table table-striped"><tbody class="files"></tbody></table> + </form> + <br> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">Demo Notes</h3> + </div> + <div class="panel-body"> + <ul> + <li>The maximum file size for uploads in this demo is <strong>999 KB</strong> (default file size is unlimited).</li> + <li>Only image files (<strong>JPG, GIF, PNG</strong>) are allowed in this demo (by default there is no file type restriction).</li> + <li>Uploaded files will be deleted automatically after <strong>5 minutes or less</strong> (demo files are stored in memory).</li> + <li>You can <strong>drag & drop</strong> files from your desktop on this webpage (see <a href="https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support">Browser support</a>).</li> + <li>Please refer to the <a href="https://github.com/blueimp/jQuery-File-Upload">project website</a> and <a href="https://github.com/blueimp/jQuery-File-Upload/wiki">documentation</a> for more information.</li> + <li>Built with the <a href="http://getbootstrap.com/">Bootstrap</a> CSS framework and Icons from <a href="http://glyphicons.com/">Glyphicons</a>.</li> + </ul> + </div> + </div> +</div> +<!-- The blueimp Gallery widget --> +<div id="blueimp-gallery" class="blueimp-gallery blueimp-gallery-controls" data-filter=":even"> + <div class="slides"></div> + <h3 class="title"></h3> + <a class="prev">‹</a> + <a class="next">›</a> + <a class="close">×</a> + <a class="play-pause"></a> + <ol class="indicator"></ol> +</div> +<!-- The template to display files available for upload --> +<script id="template-upload" type="text/x-tmpl"> +{% for (var i=0, file; file=o.files[i]; i++) { %} + <tr class="template-upload fade"> + <td> + <span class="preview"></span> + </td> + <td> + <p class="name">{%=file.name%}</p> + <strong class="error text-danger"></strong> + </td> + <td> + <p class="size">Processing...</p> + <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="progress-bar progress-bar-success" style="width:0%;"></div></div> + </td> + <td> + {% if (!i && !o.options.autoUpload) { %} + <button class="btn btn-primary start" disabled> + <i class="glyphicon glyphicon-upload"></i> + <span>Start</span> + </button> + {% } %} + {% if (!i) { %} + <button class="btn btn-warning cancel"> + <i class="glyphicon glyphicon-ban-circle"></i> + <span>Cancel</span> + </button> + {% } %} + </td> + </tr> +{% } %} +</script> +<!-- The template to display files available for download --> +<script id="template-download" type="text/x-tmpl"> +{% for (var i=0, file; file=o.files[i]; i++) { %} + <tr class="template-download fade"> + <td> + <span class="preview"> + {% if (file.thumbnailUrl) { %} + <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" data-gallery><img src="{%=file.thumbnailUrl%}"></a> + {% } %} + </span> + </td> + <td> + <p class="name"> + {% if (file.url) { %} + <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" {%=file.thumbnailUrl?'data-gallery':''%}>{%=file.name%}</a> + {% } else { %} + <span>{%=file.name%}</span> + {% } %} + </p> + {% if (file.error) { %} + <div><span class="label label-danger">Error</span> {%=file.error%}</div> + {% } %} + </td> + <td> + <span class="size">{%=o.formatFileSize(file.size)%}</span> + </td> + <td> + {% if (file.deleteUrl) { %} + <button class="btn btn-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}> + <i class="glyphicon glyphicon-trash"></i> + <span>Delete</span> + </button> + <input type="checkbox" name="delete" value="1" class="toggle"> + {% } else { %} + <button class="btn btn-warning cancel"> + <i class="glyphicon glyphicon-ban-circle"></i> + <span>Cancel</span> + </button> + {% } %} + </td> + </tr> +{% } %} +</script> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> +<!-- The jQuery UI widget factory, can be omitted if jQuery UI is already included --> +<script src="js/vendor/jquery.ui.widget.js"></script> +<!-- The Templates plugin is included to render the upload/download listings --> +<script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script> +<!-- The Load Image plugin is included for the preview images and image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script> +<!-- The Canvas to Blob plugin is included for image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script> +<!-- Bootstrap JS is not required, but included for the responsive demo navigation --> +<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> +<!-- blueimp Gallery script --> +<script src="https://blueimp.github.io/Gallery/js/jquery.blueimp-gallery.min.js"></script> +<!-- The Iframe Transport is required for browsers without support for XHR file uploads --> +<script src="js/jquery.iframe-transport.js"></script> +<!-- The basic File Upload plugin --> +<script src="js/jquery.fileupload.js"></script> +<!-- The File Upload processing plugin --> +<script src="js/jquery.fileupload-process.js"></script> +<!-- The File Upload image preview & resize plugin --> +<script src="js/jquery.fileupload-image.js"></script> +<!-- The File Upload audio preview plugin --> +<script src="js/jquery.fileupload-audio.js"></script> +<!-- The File Upload video preview plugin --> +<script src="js/jquery.fileupload-video.js"></script> +<!-- The File Upload validation plugin --> +<script src="js/jquery.fileupload-validate.js"></script> +<!-- The File Upload user interface plugin --> +<script src="js/jquery.fileupload-ui.js"></script> +<!-- The main application script --> +<script src="js/main.js"></script> +<!-- The XDomainRequest Transport is included for cross-domain file deletion for IE 8 and IE 9 --> +<!--[if (gte IE 8)&(lt IE 10)]> +<script src="js/cors/jquery.xdr-transport.js"></script> +<![endif]--> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/jquery-ui.html b/vendor/blueimp/jquery-file-upload/jquery-ui.html new file mode 100644 index 000000000..842dd4ca7 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/jquery-ui.html @@ -0,0 +1,252 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery File Upload Plugin jQuery UI Demo + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<!-- Force latest IE rendering engine or ChromeFrame if installed --> +<!--[if IE]> +<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> +<![endif]--> +<meta charset="utf-8"> +<title>jQuery File Upload Demo - jQuery UI version</title> +<meta name="description" content="File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads and client-side image resizing. Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads."> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<!-- jQuery UI styles --> +<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/dark-hive/jquery-ui.css" id="theme"> +<!-- Generic page styles --> +<link rel="stylesheet" href="css/style.css"> +<!-- Demo styles --> +<link rel="stylesheet" href="css/jquery-ui-demo.css"> +<!--[if lte IE 8]> +<link rel="stylesheet" href="css/jquery-ui-demo-ie8.css"> +<![endif]--> +<style> +/* Adjust the jQuery UI widget font-size: */ +.ui-widget { + font-size: 0.95em; +} +</style> +<!-- blueimp Gallery styles --> +<link rel="stylesheet" href="https://blueimp.github.io/Gallery/css/blueimp-gallery.min.css"> +<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars --> +<link rel="stylesheet" href="css/jquery.fileupload.css"> +<link rel="stylesheet" href="css/jquery.fileupload-ui.css"> +<!-- CSS adjustments for browsers with JavaScript disabled --> +<noscript><link rel="stylesheet" href="css/jquery.fileupload-noscript.css"></noscript> +<noscript><link rel="stylesheet" href="css/jquery.fileupload-ui-noscript.css"></noscript> +</head> +<body> +<ul class="navigation"> + <li><h3><a href="https://github.com/blueimp/jQuery-File-Upload">jQuery File Upload</a></h3></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/tags">Download</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload">Source Code</a></li> + <li><a href="https://github.com/blueimp/jQuery-File-Upload/wiki">Documentation</a></li> + <li><a href="https://blueimp.net">© blueimp.net</a></li> +</ul> +<h1>jQuery File Upload Demo</h1> +<h2>jQuery UI version</h2> +<form> + <label for="theme-switcher">Theme:</label> + <select id="theme-switcher" class="pull-right"> + <option value="black-tie">Black Tie</option> + <option value="blitzer">Blitzer</option> + <option value="cupertino">Cupertino</option> + <option value="dark-hive" selected>Dark Hive</option> + <option value="dot-luv">Dot Luv</option> + <option value="eggplant">Eggplant</option> + <option value="excite-bike">Excite Bike</option> + <option value="flick">Flick</option> + <option value="hot-sneaks">Hot sneaks</option> + <option value="humanity">Humanity</option> + <option value="le-frog">Le Frog</option> + <option value="mint-choc">Mint Choc</option> + <option value="overcast">Overcast</option> + <option value="pepper-grinder">Pepper Grinder</option> + <option value="redmond">Redmond</option> + <option value="smoothness">Smoothness</option> + <option value="south-street">South Street</option> + <option value="start">Start</option> + <option value="sunny">Sunny</option> + <option value="swanky-purse">Swanky Purse</option> + <option value="trontastic">Trontastic</option> + <option value="ui-darkness">UI Darkness</option> + <option value="ui-lightness">UI Lightness</option> + <option value="vader">Vader</option> + </select> +</form> +<ul class="navigation"> + <li><a href="basic.html">Basic</a></li> + <li><a href="basic-plus.html">Basic Plus</a></li> + <li><a href="index.html">Basic Plus UI</a></li> + <li><a href="angularjs.html">AngularJS</a></li> + <li class="active"><a href="jquery-ui.html">jQuery UI</a></li> +</ul> +<blockquote> + <p>File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery UI.<br> + Supports cross-domain, chunked and resumable file uploads and client-side image resizing.<br> + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.</p> +</blockquote> +<!-- The file upload form used as target for the file upload widget --> +<form id="fileupload" action="https://jquery-file-upload.appspot.com/" method="POST" enctype="multipart/form-data"> + <!-- Redirect browsers with JavaScript disabled to the origin page --> + <noscript><input type="hidden" name="redirect" value="https://blueimp.github.io/jQuery-File-Upload/"></noscript> + <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload --> + <div class="fileupload-buttonbar"> + <div class="fileupload-buttons"> + <!-- The fileinput-button span is used to style the file input field as button --> + <span class="fileinput-button"> + <span>Add files...</span> + <input type="file" name="files[]" multiple> + </span> + <button type="submit" class="start">Start upload</button> + <button type="reset" class="cancel">Cancel upload</button> + <button type="button" class="delete">Delete</button> + <input type="checkbox" class="toggle"> + <!-- The global file processing state --> + <span class="fileupload-process"></span> + </div> + <!-- The global progress state --> + <div class="fileupload-progress fade" style="display:none"> + <!-- The global progress bar --> + <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100"></div> + <!-- The extended global progress state --> + <div class="progress-extended"> </div> + </div> + </div> + <!-- The table listing the files available for upload/download --> + <table role="presentation"><tbody class="files"></tbody></table> +</form> +<br> +<h3>Demo Notes</h3> +<ul> + <li>The maximum file size for uploads in this demo is <strong>999 KB</strong> (default file size is unlimited).</li> + <li>Only image files (<strong>JPG, GIF, PNG</strong>) are allowed in this demo (by default there is no file type restriction).</li> + <li>Uploaded files will be deleted automatically after <strong>5 minutes or less</strong> (demo files are stored in memory).</li> + <li>You can <strong>drag & drop</strong> files from your desktop on this webpage (see <a href="https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support">Browser support</a>).</li> + <li>Please refer to the <a href="https://github.com/blueimp/jQuery-File-Upload">project website</a> and <a href="https://github.com/blueimp/jQuery-File-Upload/wiki">documentation</a> for more information.</li> + <li>Built with <a href="https://jqueryui.com">jQuery UI</a>.</li> +</ul> +<!-- The blueimp Gallery widget --> +<div id="blueimp-gallery" class="blueimp-gallery blueimp-gallery-controls" data-filter=":even"> + <div class="slides"></div> + <h3 class="title"></h3> + <a class="prev">‹</a> + <a class="next">›</a> + <a class="close">×</a> + <a class="play-pause"></a> + <ol class="indicator"></ol> +</div> +<!-- The template to display files available for upload --> +<script id="template-upload" type="text/x-tmpl"> +{% for (var i=0, file; file=o.files[i]; i++) { %} + <tr class="template-upload fade"> + <td> + <span class="preview"></span> + </td> + <td> + <p class="name">{%=file.name%}</p> + <strong class="error"></strong> + </td> + <td> + <p class="size">Processing...</p> + <div class="progress"></div> + </td> + <td> + {% if (!i && !o.options.autoUpload) { %} + <button class="start" disabled>Start</button> + {% } %} + {% if (!i) { %} + <button class="cancel">Cancel</button> + {% } %} + </td> + </tr> +{% } %} +</script> +<!-- The template to display files available for download --> +<script id="template-download" type="text/x-tmpl"> +{% for (var i=0, file; file=o.files[i]; i++) { %} + <tr class="template-download fade"> + <td> + <span class="preview"> + {% if (file.thumbnailUrl) { %} + <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" data-gallery><img src="{%=file.thumbnailUrl%}"></a> + {% } %} + </span> + </td> + <td> + <p class="name"> + <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" {%=file.thumbnailUrl?'data-gallery':''%}>{%=file.name%}</a> + </p> + {% if (file.error) { %} + <div><span class="error">Error</span> {%=file.error%}</div> + {% } %} + </td> + <td> + <span class="size">{%=o.formatFileSize(file.size)%}</span> + </td> + <td> + <button class="delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}>Delete</button> + <input type="checkbox" name="delete" value="1" class="toggle"> + </td> + </tr> +{% } %} +</script> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> +<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> +<!-- The Templates plugin is included to render the upload/download listings --> +<script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script> +<!-- The Load Image plugin is included for the preview images and image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script> +<!-- The Canvas to Blob plugin is included for image resizing functionality --> +<script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script> +<!-- blueimp Gallery script --> +<script src="https://blueimp.github.io/Gallery/js/jquery.blueimp-gallery.min.js"></script> +<!-- The Iframe Transport is required for browsers without support for XHR file uploads --> +<script src="js/jquery.iframe-transport.js"></script> +<!-- The basic File Upload plugin --> +<script src="js/jquery.fileupload.js"></script> +<!-- The File Upload processing plugin --> +<script src="js/jquery.fileupload-process.js"></script> +<!-- The File Upload image preview & resize plugin --> +<script src="js/jquery.fileupload-image.js"></script> +<!-- The File Upload audio preview plugin --> +<script src="js/jquery.fileupload-audio.js"></script> +<!-- The File Upload video preview plugin --> +<script src="js/jquery.fileupload-video.js"></script> +<!-- The File Upload validation plugin --> +<script src="js/jquery.fileupload-validate.js"></script> +<!-- The File Upload user interface plugin --> +<script src="js/jquery.fileupload-ui.js"></script> +<!-- The File Upload jQuery UI plugin --> +<script src="js/jquery.fileupload-jquery-ui.js"></script> +<!-- The main application script --> +<script src="js/main.js"></script> +<script> +// Initialize the jQuery UI theme switcher: +$('#theme-switcher').change(function () { + var theme = $('#theme'); + theme.prop( + 'href', + theme.prop('href').replace( + /[\w\-]+\/jquery-ui.css/, + $(this).val() + '/jquery-ui.css' + ) + ); +}); +</script> +<!-- The XDomainRequest Transport is included for cross-domain file deletion for IE 8 and IE 9 --> +<!--[if (gte IE 8)&(lt IE 10)]> +<script src="js/cors/jquery.xdr-transport.js"></script> +<![endif]--> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/js/app.js b/vendor/blueimp/jquery-file-upload/js/app.js new file mode 100644 index 000000000..e6b7bce3e --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/app.js @@ -0,0 +1,101 @@ +/* + * jQuery File Upload Plugin Angular JS Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global window, angular */ + +;(function () { + 'use strict'; + + var isOnGitHub = window.location.hostname === 'blueimp.github.io', + url = isOnGitHub ? '//jquery-file-upload.appspot.com/' : 'server/php/'; + + angular.module('demo', [ + 'blueimp.fileupload' + ]) + .config([ + '$httpProvider', 'fileUploadProvider', + function ($httpProvider, fileUploadProvider) { + delete $httpProvider.defaults.headers.common['X-Requested-With']; + fileUploadProvider.defaults.redirect = window.location.href.replace( + /\/[^\/]*$/, + '/cors/result.html?%s' + ); + if (isOnGitHub) { + // Demo settings: + angular.extend(fileUploadProvider.defaults, { + // Enable image resizing, except for Android and Opera, + // which actually support image resizing, but fail to + // send Blob objects via XHR requests: + disableImageResize: /Android(?!.*Chrome)|Opera/ + .test(window.navigator.userAgent), + maxFileSize: 999000, + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i + }); + } + } + ]) + + .controller('DemoFileUploadController', [ + '$scope', '$http', '$filter', '$window', + function ($scope, $http) { + $scope.options = { + url: url + }; + if (!isOnGitHub) { + $scope.loadingFiles = true; + $http.get(url) + .then( + function (response) { + $scope.loadingFiles = false; + $scope.queue = response.data.files || []; + }, + function () { + $scope.loadingFiles = false; + } + ); + } + } + ]) + + .controller('FileDestroyController', [ + '$scope', '$http', + function ($scope, $http) { + var file = $scope.file, + state; + if (file.url) { + file.$state = function () { + return state; + }; + file.$destroy = function () { + state = 'pending'; + return $http({ + url: file.deleteUrl, + method: file.deleteType + }).then( + function () { + state = 'resolved'; + $scope.clear(file); + }, + function () { + state = 'rejected'; + } + ); + }; + } else if (!file.$cancel && !file._index) { + file.$cancel = function () { + $scope.clear(file); + }; + } + } + ]); + +}()); diff --git a/vendor/blueimp/jquery-file-upload/js/cors/jquery.postmessage-transport.js b/vendor/blueimp/jquery-file-upload/js/cors/jquery.postmessage-transport.js new file mode 100644 index 000000000..2a0c38cb6 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/cors/jquery.postmessage-transport.js @@ -0,0 +1,126 @@ +/* + * jQuery postMessage Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + var counter = 0, + names = [ + 'accepts', + 'cache', + 'contents', + 'contentType', + 'crossDomain', + 'data', + 'dataType', + 'headers', + 'ifModified', + 'mimeType', + 'password', + 'processData', + 'timeout', + 'traditional', + 'type', + 'url', + 'username' + ], + convert = function (p) { + return p; + }; + + $.ajaxSetup({ + converters: { + 'postmessage text': convert, + 'postmessage json': convert, + 'postmessage html': convert + } + }); + + $.ajaxTransport('postmessage', function (options) { + if (options.postMessage && window.postMessage) { + var iframe, + loc = $('<a>').prop('href', options.postMessage)[0], + target = loc.protocol + '//' + loc.host, + xhrUpload = options.xhr().upload; + // IE always includes the port for the host property of a link + // element, but not in the location.host or origin property for the + // default http port 80 and https port 443, so we strip it: + if (/^(http:\/\/.+:80)|(https:\/\/.+:443)$/.test(target)) { + target = target.replace(/:(80|443)$/, ''); + } + return { + send: function (_, completeCallback) { + counter += 1; + var message = { + id: 'postmessage-transport-' + counter + }, + eventName = 'message.' + message.id; + iframe = $( + '<iframe style="display:none;" src="' + + options.postMessage + '" name="' + + message.id + '"></iframe>' + ).bind('load', function () { + $.each(names, function (i, name) { + message[name] = options[name]; + }); + message.dataType = message.dataType.replace('postmessage ', ''); + $(window).bind(eventName, function (e) { + e = e.originalEvent; + var data = e.data, + ev; + if (e.origin === target && data.id === message.id) { + if (data.type === 'progress') { + ev = document.createEvent('Event'); + ev.initEvent(data.type, false, true); + $.extend(ev, data); + xhrUpload.dispatchEvent(ev); + } else { + completeCallback( + data.status, + data.statusText, + {postmessage: data.result}, + data.headers + ); + iframe.remove(); + $(window).unbind(eventName); + } + } + }); + iframe[0].contentWindow.postMessage( + message, + target + ); + }).appendTo(document.body); + }, + abort: function () { + if (iframe) { + iframe.remove(); + } + } + }; + } + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/cors/jquery.xdr-transport.js b/vendor/blueimp/jquery-file-upload/js/cors/jquery.xdr-transport.js new file mode 100644 index 000000000..a4e2699c6 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/cors/jquery.xdr-transport.js @@ -0,0 +1,89 @@ +/* + * jQuery XDomainRequest Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on Julian Aubourg's ajaxHooks xdr.js: + * https://github.com/jaubourg/ajaxHooks/ + */ + +/* global define, require, window, XDomainRequest */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + if (window.XDomainRequest && !$.support.cors) { + $.ajaxTransport(function (s) { + if (s.crossDomain && s.async) { + if (s.timeout) { + s.xdrTimeout = s.timeout; + delete s.timeout; + } + var xdr; + return { + send: function (headers, completeCallback) { + var addParamChar = /\?/.test(s.url) ? '&' : '?'; + function callback(status, statusText, responses, responseHeaders) { + xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; + xdr = null; + completeCallback(status, statusText, responses, responseHeaders); + } + xdr = new XDomainRequest(); + // XDomainRequest only supports GET and POST: + if (s.type === 'DELETE') { + s.url = s.url + addParamChar + '_method=DELETE'; + s.type = 'POST'; + } else if (s.type === 'PUT') { + s.url = s.url + addParamChar + '_method=PUT'; + s.type = 'POST'; + } else if (s.type === 'PATCH') { + s.url = s.url + addParamChar + '_method=PATCH'; + s.type = 'POST'; + } + xdr.open(s.type, s.url); + xdr.onload = function () { + callback( + 200, + 'OK', + {text: xdr.responseText}, + 'Content-Type: ' + xdr.contentType + ); + }; + xdr.onerror = function () { + callback(404, 'Not Found'); + }; + if (s.xdrTimeout) { + xdr.ontimeout = function () { + callback(0, 'timeout'); + }; + xdr.timeout = s.xdrTimeout; + } + xdr.send((s.hasContent && s.data) || null); + }, + abort: function () { + if (xdr) { + xdr.onerror = $.noop(); + xdr.abort(); + } + } + }; + } + }); + } +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-angular.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-angular.js new file mode 100644 index 000000000..185907d36 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-angular.js @@ -0,0 +1,438 @@ +/* + * jQuery File Upload AngularJS Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, angular, require */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'angular', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('angular'), + require('./jquery.fileupload-image'), + require('./jquery.fileupload-audio'), + require('./jquery.fileupload-video'), + require('./jquery.fileupload-validate') + ); + } else { + factory(); + } +}(function () { + 'use strict'; + + angular.module('blueimp.fileupload', []) + + // The fileUpload service provides configuration options + // for the fileUpload directive and default handlers for + // File Upload events: + .provider('fileUpload', function () { + var scopeEvalAsync = function (expression) { + var scope = angular.element(this) + .fileupload('option', 'scope'); + // Schedule a new $digest cycle if not already inside of one + // and evaluate the given expression: + scope.$evalAsync(expression); + }, + addFileMethods = function (scope, data) { + var files = data.files, + file = files[0]; + angular.forEach(files, function (file, index) { + file._index = index; + file.$state = function () { + return data.state(); + }; + file.$processing = function () { + return data.processing(); + }; + file.$progress = function () { + return data.progress(); + }; + file.$response = function () { + return data.response(); + }; + }); + file.$submit = function () { + if (!file.error) { + return data.submit(); + } + }; + file.$cancel = function () { + return data.abort(); + }; + }, + $config; + $config = this.defaults = { + handleResponse: function (e, data) { + var files = data.result && data.result.files; + if (files) { + data.scope.replace(data.files, files); + } else if (data.errorThrown || + data.textStatus === 'error') { + data.files[0].error = data.errorThrown || + data.textStatus; + } + }, + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var scope = data.scope, + filesCopy = []; + angular.forEach(data.files, function (file) { + filesCopy.push(file); + }); + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + var method = scope.option('prependFiles') ? + 'unshift' : 'push'; + Array.prototype[method].apply(scope.queue, data.files); + }); + data.process(function () { + return scope.process(data); + }).always(function () { + scope.$parent.$applyAsync(function () { + addFileMethods(scope, data); + scope.replace(filesCopy, data.files); + }); + }).then(function () { + if ((scope.option('autoUpload') || + data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }); + }, + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this; + data.scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = this, + scope = data.scope; + if (data.errorThrown === 'abort') { + scope.clear(data.files); + return; + } + scope.$apply(function () { + data.handleResponse.call(that, e, data); + }); + }, + stop: scopeEvalAsync, + processstart: scopeEvalAsync, + processstop: scopeEvalAsync, + getNumberOfFiles: function () { + var scope = this.scope; + return scope.queue.length - scope.processing(); + }, + dataType: 'json', + autoUpload: false + }; + this.$get = [ + function () { + return { + defaults: $config + }; + } + ]; + }) + + // Format byte numbers to readable presentations: + .provider('formatFileSizeFilter', function () { + var $config = { + // Byte units following the IEC format + // http://en.wikipedia.org/wiki/Kilobyte + units: [ + {size: 1000000000, suffix: ' GB'}, + {size: 1000000, suffix: ' MB'}, + {size: 1000, suffix: ' KB'} + ] + }; + this.defaults = $config; + this.$get = function () { + return function (bytes) { + if (!angular.isNumber(bytes)) { + return ''; + } + var unit = true, + i = 0, + prefix, + suffix; + while (unit) { + unit = $config.units[i]; + prefix = unit.prefix || ''; + suffix = unit.suffix || ''; + if (i === $config.units.length - 1 || bytes >= unit.size) { + return prefix + (bytes / unit.size).toFixed(2) + suffix; + } + i += 1; + } + }; + }; + }) + + // The FileUploadController initializes the fileupload widget and + // provides scope methods to control the File Upload functionality: + .controller('FileUploadController', [ + '$scope', '$element', '$attrs', '$window', 'fileUpload','$q', + function ($scope, $element, $attrs, $window, fileUpload, $q) { + var uploadMethods = { + progress: function () { + return $element.fileupload('progress'); + }, + active: function () { + return $element.fileupload('active'); + }, + option: function (option, data) { + if (arguments.length === 1) { + return $element.fileupload('option', option); + } + $element.fileupload('option', option, data); + }, + add: function (data) { + return $element.fileupload('add', data); + }, + send: function (data) { + return $element.fileupload('send', data); + }, + process: function (data) { + return $element.fileupload('process', data); + }, + processing: function (data) { + return $element.fileupload('processing', data); + } + }; + $scope.disabled = !$window.jQuery.support.fileInput; + $scope.queue = $scope.queue || []; + $scope.clear = function (files) { + var queue = this.queue, + i = queue.length, + file = files, + length = 1; + if (angular.isArray(files)) { + file = files[0]; + length = files.length; + } + while (i) { + i -= 1; + if (queue[i] === file) { + return queue.splice(i, length); + } + } + }; + $scope.replace = function (oldFiles, newFiles) { + var queue = this.queue, + file = oldFiles[0], + i, + j; + for (i = 0; i < queue.length; i += 1) { + if (queue[i] === file) { + for (j = 0; j < newFiles.length; j += 1) { + queue[i + j] = newFiles[j]; + } + return; + } + } + }; + $scope.applyOnQueue = function (method) { + var list = this.queue.slice(0), + i, + file, + promises = []; + for (i = 0; i < list.length; i += 1) { + file = list[i]; + if (file[method]) { + promises.push(file[method]()); + } + } + return $q.all(promises); + }; + $scope.submit = function () { + return this.applyOnQueue('$submit'); + }; + $scope.cancel = function () { + return this.applyOnQueue('$cancel'); + }; + // Add upload methods to the scope: + angular.extend($scope, uploadMethods); + // The fileupload widget will initialize with + // the options provided via "data-"-parameters, + // as well as those given via options object: + $element.fileupload(angular.extend( + {scope: $scope}, + fileUpload.defaults + )).on('fileuploadadd', function (e, data) { + data.scope = $scope; + }).on('fileuploadfail', function (e, data) { + if (data.errorThrown === 'abort') { + return; + } + if (data.dataType && + data.dataType.indexOf('json') === data.dataType.length - 4) { + try { + data.result = angular.fromJson(data.jqXHR.responseText); + } catch (ignore) {} + } + }).on([ + 'fileuploadadd', + 'fileuploadsubmit', + 'fileuploadsend', + 'fileuploaddone', + 'fileuploadfail', + 'fileuploadalways', + 'fileuploadprogress', + 'fileuploadprogressall', + 'fileuploadstart', + 'fileuploadstop', + 'fileuploadchange', + 'fileuploadpaste', + 'fileuploaddrop', + 'fileuploaddragover', + 'fileuploadchunkbeforesend', + 'fileuploadchunksend', + 'fileuploadchunkdone', + 'fileuploadchunkfail', + 'fileuploadchunkalways', + 'fileuploadprocessstart', + 'fileuploadprocess', + 'fileuploadprocessdone', + 'fileuploadprocessfail', + 'fileuploadprocessalways', + 'fileuploadprocessstop' + ].join(' '), function (e, data) { + $scope.$parent.$applyAsync(function () { + if ($scope.$emit(e.type, data).defaultPrevented) { + e.preventDefault(); + } + }); + }).on('remove', function () { + // Remove upload methods from the scope, + // when the widget is removed: + var method; + for (method in uploadMethods) { + if (uploadMethods.hasOwnProperty(method)) { + delete $scope[method]; + } + } + }); + // Observe option changes: + $scope.$watch( + $attrs.fileUpload, + function (newOptions) { + if (newOptions) { + $element.fileupload('option', newOptions); + } + } + ); + } + ]) + + // Provide File Upload progress feedback: + .controller('FileUploadProgressController', [ + '$scope', '$attrs', '$parse', + function ($scope, $attrs, $parse) { + var fn = $parse($attrs.fileUploadProgress), + update = function () { + var progress = fn($scope); + if (!progress || !progress.total) { + return; + } + $scope.num = Math.floor( + progress.loaded / progress.total * 100 + ); + }; + update(); + $scope.$watch( + $attrs.fileUploadProgress + '.loaded', + function (newValue, oldValue) { + if (newValue !== oldValue) { + update(); + } + } + ); + } + ]) + + // Display File Upload previews: + .controller('FileUploadPreviewController', [ + '$scope', '$element', '$attrs', + function ($scope, $element, $attrs) { + $scope.$watch( + $attrs.fileUploadPreview + '.preview', + function (preview) { + $element.empty(); + if (preview) { + $element.append(preview); + } + } + ); + } + ]) + + .directive('fileUpload', function () { + return { + controller: 'FileUploadController', + scope: true + }; + }) + + .directive('fileUploadProgress', function () { + return { + controller: 'FileUploadProgressController', + scope: true + }; + }) + + .directive('fileUploadPreview', function () { + return { + controller: 'FileUploadPreviewController' + }; + }) + + // Enhance the HTML5 download attribute to + // allow drag&drop of files to the desktop: + .directive('download', function () { + return function (scope, elm) { + elm.on('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [ + 'application/octet-stream', + elm.prop('download'), + elm.prop('href') + ].join(':') + ); + } catch (ignore) {} + }); + }; + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-audio.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-audio.js new file mode 100644 index 000000000..a25377619 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-audio.js @@ -0,0 +1,113 @@ +/* + * jQuery File Upload Audio Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadAudio', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableAudioPreview' + }, + { + action: 'setAudio', + name: '@audioPreviewName', + disabled: '@disableAudioPreview' + } + ); + + // The File Upload Audio Preview plugin extends the fileupload widget + // with audio preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of audio files to load, + // matched against the file type: + loadAudioFileTypes: /^audio\/.*$/ + }, + + _audioElement: document.createElement('audio'), + + processActions: { + + // Loads the audio file given via data.files and data.index + // as audio element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadAudio: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + audio; + if (this._audioElement.canPlayType && + this._audioElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + audio = this._audioElement.cloneNode(false); + audio.src = url; + audio.controls = true; + data.audio = audio; + return data; + } + } + return data; + }, + + // Sets the audio element as a property of the file object: + setAudio: function (data, options) { + if (data.audio && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.audio; + } + return data; + } + + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-image.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-image.js new file mode 100644 index 000000000..65fc6d7b8 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-image.js @@ -0,0 +1,326 @@ +/* + * jQuery File Upload Image Preview & Resize Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, Blob */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'load-image-meta', + 'load-image-scale', + 'load-image-exif', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('blueimp-load-image/js/load-image-meta'), + require('blueimp-load-image/js/load-image-scale'), + require('blueimp-load-image/js/load-image-exif'), + require('blueimp-canvas-to-blob'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImageMetaData', + disableImageHead: '@', + disableExif: '@', + disableExifThumbnail: '@', + disableExifSub: '@', + disableExifGps: '@', + disabled: '@disableImageMetaDataLoad' + }, + { + action: 'loadImage', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + noRevoke: '@', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + // Use "image" as prefix for the "@" options: + prefix: 'image', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + forceResize: '@', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + quality: '@imageQuality', + type: '@imageType', + disabled: '@disableImageResize' + }, + { + action: 'saveImageMetaData', + disabled: '@disableImageMetaDataSave' + }, + { + action: 'resizeImage', + // Use "preview" as prefix for the "@" options: + prefix: 'preview', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + thumbnail: '@', + canvas: '@', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + name: '@imagePreviewName', + disabled: '@disableImagePreview' + }, + { + action: 'deleteImageReferences', + disabled: '@disableImageReferencesDeletion' + } + ); + + // The File Upload Resize plugin extends the fileupload widget + // with image resize functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of images to load: + // matched against the file type: + loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 10000000, // 10MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // Defines the image orientation (1-8) or takes the orientation + // value from Exif data if set to true: + imageOrientation: false, + // Define if resized images should be cropped or only scaled: + imageCrop: false, + // Disable the resize image functionality by default: + disableImageResize: true, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // Defines the preview orientation (1-8) or takes the orientation + // value from Exif data if set to true: + previewOrientation: true, + // Create the preview using the Exif data thumbnail: + previewThumbnail: true, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewCanvas: true + }, + + processActions: { + + // Loads the image given via data.files and data.index + // as img element, if the browser supports the File API. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (($.type(options.maxFileSize) === 'number' && + file.size > options.maxFileSize) || + (options.fileTypes && + !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (img.src) { + data.img = img; + } + dfd.resolveWith(that, [data]); + }, + options + )) { + return data; + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Also stores the resized image as preview property. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled || !(data.canvas || data.img)) { + return data; + } + options = $.extend({canvas: true}, options); + var that = this, + dfd = $.Deferred(), + img = (options.canvas && data.canvas) || data.img, + resolve = function (newImg) { + if (newImg && (newImg.width !== img.width || + newImg.height !== img.height || + options.forceResize)) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + } + data.preview = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail; + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + if (thumbnail) { + loadImage(thumbnail, resolve, options); + return dfd.promise(); + } + } + // Prevent orienting the same image twice: + if (data.orientation) { + delete options.orientation; + } else { + data.orientation = options.orientation; + } + } + if (img) { + resolve(loadImage.scale(img, options)); + return dfd.promise(); + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (data.canvas.toBlob) { + data.canvas.toBlob( + function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\.\w+$/, + '.' + blob.type.substr(6) + ); + } + } + // Don't restore invalid meta data: + if (file.type !== blob.type) { + delete data.imageHead; + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }, + options.type || file.type, + options.quality + ); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(); + loadImage.parseMetaData(data.files[data.index], function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, options); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if (!(data.imageHead && data.canvas && + data.canvas.toBlob && !options.disabled)) { + return data; + } + var file = data.files[data.index], + blob = new Blob([ + data.imageHead, + // Resized images always have a head size of 20 bytes, + // including the JPEG marker and a minimal JFIF header: + this._blobSlice.call(file, 20) + ], {type: file.type}); + blob.name = file.name; + data.files[data.index] = blob; + return data; + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + if (data.preview && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.preview; + } + return data; + }, + + deleteImageReferences: function (data, options) { + if (!options.disabled) { + delete data.img; + delete data.canvas; + delete data.preview; + delete data.imageHead; + } + return data; + } + + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-jquery-ui.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-jquery-ui.js new file mode 100644 index 000000000..7b136b379 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-jquery-ui.js @@ -0,0 +1,161 @@ +/* + * jQuery File Upload jQuery UI Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-ui' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./jquery.fileupload-ui') + ); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + processdone: function (e, data) { + data.context.find('.start').button('enable'); + }, + progress: function (e, data) { + if (data.context) { + data.context.find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ); + } + }, + progressall: function (e, data) { + var $this = $(this); + $this.find('.fileupload-progress') + .find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ).end() + .find('.progress-extended').each(function () { + $(this).html( + ($this.data('blueimp-fileupload') || + $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + }); + } + }, + + _renderUpload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.progress').empty().progressbar(); + node.find('.start').button({ + icons: {primary: 'ui-icon-circle-arrow-e'}, + text: showIconText + }); + node.find('.cancel').button({ + icons: {primary: 'ui-icon-cancel'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _renderDownload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.delete').button({ + icons: {primary: 'ui-icon-trash'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _startHandler: function (e) { + $(e.currentTarget).button('disable'); + this._super(e); + }, + + _transition: function (node) { + var deferred = $.Deferred(); + if (node.hasClass('fade')) { + node.fadeToggle( + this.options.transitionDuration, + this.options.transitionEasing, + function () { + deferred.resolveWith(node); + } + ); + } else { + deferred.resolveWith(node); + } + return deferred; + }, + + _create: function () { + this._super(); + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button({icons: {primary: 'ui-icon-plusthick'}}) + .append(input); + }) + .end().find('.start') + .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) + .end().find('.cancel') + .button({icons: {primary: 'ui-icon-cancel'}}) + .end().find('.delete') + .button({icons: {primary: 'ui-icon-trash'}}) + .end().find('.progress').progressbar(); + }, + + _destroy: function () { + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button('destroy') + .append(input); + }) + .end().find('.start') + .button('destroy') + .end().find('.cancel') + .button('destroy') + .end().find('.delete') + .button('destroy') + .end().find('.progress').progressbar('destroy'); + this._super(); + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-process.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-process.js new file mode 100644 index 000000000..638f0d26b --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-process.js @@ -0,0 +1,178 @@ +/* + * jQuery File Upload Processing Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./jquery.fileupload') + ); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data, originalData) { + var that = this, + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + if (originalData.errorThrown) { + return $.Deferred() + .rejectWith(that, [originalData]).promise(); + } + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.then(func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[ + value.slice(1) || (prefix ? prefix + + key.charAt(0).toUpperCase() + key.slice(1) : key) + ]; + } else { + settings[key] = value; + } + + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return that._processFile(opts, data); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.then(func, func) + .always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + this._processingQueue = $.Deferred().resolveWith(this) + .promise(); + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-ui.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-ui.js new file mode 100644 index 000000000..5058084b4 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-ui.js @@ -0,0 +1,714 @@ +/* + * jQuery File Upload User Interface Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'blueimp-tmpl', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-tmpl'), + require('./jquery.fileupload-image'), + require('./jquery.fileupload-audio'), + require('./jquery.fileupload-video'), + require('./jquery.fileupload-validate') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.tmpl + ); + } +}(function ($, tmpl) { + 'use strict'; + + $.blueimp.fileupload.prototype._specialOptions.push( + 'filesContainer', + 'uploadTemplateId', + 'downloadTemplateId' + ); + + // The UI version extends the file upload widget + // and adds complete user interface interaction: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The ID of the upload template: + uploadTemplateId: 'template-upload', + // The ID of the download template: + downloadTemplateId: 'template-download', + // The container for the list of files. If undefined, it is set to + // an element with class "files" inside of the widget element: + filesContainer: undefined, + // By default, files are appended to the files container. + // Set the following option to true, to prepend files instead: + prependFiles: false, + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // Error and info messages: + messages: { + unknownError: 'Unknown error' + }, + + // Function returning the current number of files, + // used by the maxNumberOfFiles validation: + getNumberOfFiles: function () { + return this.filesContainer.children() + .not('.processing').length; + }, + + // Callback to retrieve the list of files from the server response: + getFilesFromResponse: function (data) { + if (data.result && $.isArray(data.result.files)) { + return data.result.files; + } + return []; + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + that = $this.data('blueimp-fileupload') || + $this.data('fileupload'), + options = that.options; + data.context = that._renderUpload(data.files) + .data('data', data) + .addClass('processing'); + options.filesContainer[ + options.prependFiles ? 'prepend' : 'append' + ](data.context); + that._forceReflow(data.context); + that._transition(data.context); + data.process(function () { + return $this.fileupload('process', data); + }).always(function () { + data.context.each(function (index) { + $(this).find('.size').text( + that._formatFileSize(data.files[index].size) + ); + }).removeClass('processing'); + that._renderPreviews(data); + }).done(function () { + data.context.find('.start').prop('disabled', false); + if ((that._trigger('added', e, data) !== false) && + (options.autoUpload || data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }).fail(function () { + if (data.files.error) { + data.context.each(function (index) { + var error = data.files[index].error; + if (error) { + $(this).find('.error').text(error); + } + }); + } + }); + }, + // Callback for the start of each file upload request: + send: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + if (data.context && data.dataType && + data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context + .find('.progress').addClass( + !$.support.transition && 'progress-animated' + ) + .attr('aria-valuenow', 100) + .children().first().css( + 'width', + '100%' + ); + } + return that._trigger('sent', e, data); + }, + // Callback for successful uploads: + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + getFilesFromResponse = data.getFilesFromResponse || + that.options.getFilesFromResponse, + files = getFilesFromResponse(data), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + var file = files[index] || + {error: 'Empty file upload result'}; + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + }); + } else { + template = that._renderDownload(files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer); + that._forceReflow(template); + deferred = that._addFinishedDeferreds(); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = file.error || data.errorThrown || + data.i18n('unknownError'); + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + } else { + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + $(this).remove(); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }); + } else if (data.errorThrown !== 'abort') { + data.context = that._renderUpload(data.files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer) + .data('data', data); + that._forceReflow(data.context); + deferred = that._addFinishedDeferreds(); + that._transition(data.context).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } else { + that._trigger('failed', e, data); + that._trigger('finished', e, data); + that._addFinishedDeferreds().resolve(); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var progress = Math.floor(data.loaded / data.total * 100); + if (data.context) { + data.context.each(function () { + $(this).find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + progress = Math.floor(data.loaded / data.total * 100), + globalProgressNode = $this.find('.fileupload-progress'), + extendedProgressNode = globalProgressNode + .find('.progress-extended'); + if (extendedProgressNode.length) { + extendedProgressNode.html( + ($this.data('blueimp-fileupload') || $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + } + globalProgressNode + .find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + that._resetFinishedDeferreds(); + that._transition($(this).find('.fileupload-progress')).done( + function () { + that._trigger('started', e); + } + ); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + deferred = that._addFinishedDeferreds(); + $.when.apply($, that._getFinishedDeferreds()) + .done(function () { + that._trigger('stopped', e); + }); + that._transition($(this).find('.fileupload-progress')).done( + function () { + $(this).find('.progress') + .attr('aria-valuenow', '0') + .children().first().css('width', '0%'); + $(this).find('.progress-extended').html(' '); + deferred.resolve(); + } + ); + }, + processstart: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).addClass('fileupload-processing'); + }, + processstop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).removeClass('fileupload-processing'); + }, + // Callback for file deletion: + destroy: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + removeNode = function () { + that._transition(data.context).done( + function () { + $(this).remove(); + that._trigger('destroyed', e, data); + } + ); + }; + if (data.url) { + data.dataType = data.dataType || that.options.dataType; + $.ajax(data).done(removeNode).fail(function () { + that._trigger('destroyfailed', e, data); + }); + } else { + removeNode(); + } + } + }, + + _resetFinishedDeferreds: function () { + this._finishedUploads = []; + }, + + _addFinishedDeferreds: function (deferred) { + if (!deferred) { + deferred = $.Deferred(); + } + this._finishedUploads.push(deferred); + return deferred; + }, + + _getFinishedDeferreds: function () { + return this._finishedUploads; + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = link.prop('download'), + type = 'application/octet-stream'; + link.bind('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (ignore) {} + }); + }, + + _formatFileSize: function (bytes) { + if (typeof bytes !== 'number') { + return ''; + } + if (bytes >= 1000000000) { + return (bytes / 1000000000).toFixed(2) + ' GB'; + } + if (bytes >= 1000000) { + return (bytes / 1000000).toFixed(2) + ' MB'; + } + return (bytes / 1000).toFixed(2) + ' KB'; + }, + + _formatBitrate: function (bits) { + if (typeof bits !== 'number') { + return ''; + } + if (bits >= 1000000000) { + return (bits / 1000000000).toFixed(2) + ' Gbit/s'; + } + if (bits >= 1000000) { + return (bits / 1000000).toFixed(2) + ' Mbit/s'; + } + if (bits >= 1000) { + return (bits / 1000).toFixed(2) + ' kbit/s'; + } + return bits.toFixed(2) + ' bit/s'; + }, + + _formatTime: function (seconds) { + var date = new Date(seconds * 1000), + days = Math.floor(seconds / 86400); + days = days ? days + 'd ' : ''; + return days + + ('0' + date.getUTCHours()).slice(-2) + ':' + + ('0' + date.getUTCMinutes()).slice(-2) + ':' + + ('0' + date.getUTCSeconds()).slice(-2); + }, + + _formatPercentage: function (floatValue) { + return (floatValue * 100).toFixed(2) + ' %'; + }, + + _renderExtendedProgress: function (data) { + return this._formatBitrate(data.bitrate) + ' | ' + + this._formatTime( + (data.total - data.loaded) * 8 / data.bitrate + ) + ' | ' + + this._formatPercentage( + data.loaded / data.total + ) + ' | ' + + this._formatFileSize(data.loaded) + ' / ' + + this._formatFileSize(data.total); + }, + + _renderTemplate: function (func, files) { + if (!func) { + return $(); + } + var result = func({ + files: files, + formatFileSize: this._formatFileSize, + options: this.options + }); + if (result instanceof $) { + return result; + } + return $(this.options.templatesContainer).html(result).children(); + }, + + _renderPreviews: function (data) { + data.context.find('.preview').each(function (index, elm) { + $(elm).append(data.files[index].preview); + }); + }, + + _renderUpload: function (files) { + return this._renderTemplate( + this.options.uploadTemplate, + files + ); + }, + + _renderDownload: function (files) { + return this._renderTemplate( + this.options.downloadTemplate, + files + ).find('a[download]').each(this._enableDragToDesktop).end(); + }, + + _startHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget), + template = button.closest('.template-upload'), + data = template.data('data'); + button.prop('disabled', true); + if (data && data.submit) { + data.submit(); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var template = $(e.currentTarget) + .closest('.template-upload,.template-download'), + data = template.data('data') || {}; + data.context = data.context || template; + if (data.abort) { + data.abort(); + } else { + data.errorThrown = 'abort'; + this._trigger('fail', e, data); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget); + this._trigger('destroy', e, $.extend({ + context: button.closest('.template-download'), + type: 'DELETE' + }, button.data())); + }, + + _forceReflow: function (node) { + return $.support.transition && node.length && + node[0].offsetWidth; + }, + + _transition: function (node) { + var dfd = $.Deferred(); + if ($.support.transition && node.hasClass('fade') && node.is(':visible')) { + node.bind( + $.support.transition.end, + function (e) { + // Make sure we don't respond to other transitions events + // in the container element, e.g. from button elements: + if (e.target === node[0]) { + node.unbind($.support.transition.end); + dfd.resolveWith(node); + } + } + ).toggleClass('in'); + } else { + node.toggleClass('in'); + dfd.resolveWith(node); + } + return dfd; + }, + + _initButtonBarEventHandlers: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this.options.filesContainer; + this._on(fileUploadButtonBar.find('.start'), { + click: function (e) { + e.preventDefault(); + filesList.find('.start').click(); + } + }); + this._on(fileUploadButtonBar.find('.cancel'), { + click: function (e) { + e.preventDefault(); + filesList.find('.cancel').click(); + } + }); + this._on(fileUploadButtonBar.find('.delete'), { + click: function (e) { + e.preventDefault(); + filesList.find('.toggle:checked') + .closest('.template-download') + .find('.delete').click(); + fileUploadButtonBar.find('.toggle') + .prop('checked', false); + } + }); + this._on(fileUploadButtonBar.find('.toggle'), { + change: function (e) { + filesList.find('.toggle').prop( + 'checked', + $(e.currentTarget).is(':checked') + ); + } + }); + }, + + _destroyButtonBarEventHandlers: function () { + this._off( + this.element.find('.fileupload-buttonbar') + .find('.start, .cancel, .delete'), + 'click' + ); + this._off( + this.element.find('.fileupload-buttonbar .toggle'), + 'change.' + ); + }, + + _initEventHandlers: function () { + this._super(); + this._on(this.options.filesContainer, { + 'click .start': this._startHandler, + 'click .cancel': this._cancelHandler, + 'click .delete': this._deleteHandler + }); + this._initButtonBarEventHandlers(); + }, + + _destroyEventHandlers: function () { + this._destroyButtonBarEventHandlers(); + this._off(this.options.filesContainer, 'click'); + this._super(); + }, + + _enableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', false) + .parent().removeClass('disabled'); + }, + + _disableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', true) + .parent().addClass('disabled'); + }, + + _initTemplates: function () { + var options = this.options; + options.templatesContainer = this.document[0].createElement( + options.filesContainer.prop('nodeName') + ); + if (tmpl) { + if (options.uploadTemplateId) { + options.uploadTemplate = tmpl(options.uploadTemplateId); + } + if (options.downloadTemplateId) { + options.downloadTemplate = tmpl(options.downloadTemplateId); + } + } + }, + + _initFilesContainer: function () { + var options = this.options; + if (options.filesContainer === undefined) { + options.filesContainer = this.element.find('.files'); + } else if (!(options.filesContainer instanceof $)) { + options.filesContainer = $(options.filesContainer); + } + }, + + _initSpecialOptions: function () { + this._super(); + this._initFilesContainer(); + this._initTemplates(); + }, + + _create: function () { + this._super(); + this._resetFinishedDeferreds(); + if (!$.support.fileInput) { + this._disableFileInputButton(); + } + }, + + enable: function () { + var wasDisabled = false; + if (this.options.disabled) { + wasDisabled = true; + } + this._super(); + if (wasDisabled) { + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + } + }, + + disable: function () { + if (!this.options.disabled) { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + } + this._super(); + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-validate.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-validate.js new file mode 100644 index 000000000..eebeb3733 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-validate.js @@ -0,0 +1,125 @@ +/* + * jQuery File Upload Validation Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require, window */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push( + { + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + } + ); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + + validate: function (data, options) { + if (options.disabled) { + return data; + } + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index], + fileSize; + if (options.minFileSize || options.maxFileSize) { + fileSize = file.size; + } + if ($.type(options.maxNumberOfFiles) === 'number' && + (settings.getNumberOfFiles() || 0) + data.files.length > + options.maxNumberOfFiles) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if (options.acceptFileTypes && + !(options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name))) { + file.error = settings.i18n('acceptFileTypes'); + } else if (fileSize > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(fileSize) === 'number' && + fileSize < options.minFileSize) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-video.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-video.js new file mode 100644 index 000000000..aedcec2ba --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload-video.js @@ -0,0 +1,113 @@ +/* + * jQuery File Upload Video Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadVideo', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableVideoPreview' + }, + { + action: 'setVideo', + name: '@videoPreviewName', + disabled: '@disableVideoPreview' + } + ); + + // The File Upload Video Preview plugin extends the fileupload widget + // with video preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of video files to load, + // matched against the file type: + loadVideoFileTypes: /^video\/.*$/ + }, + + _videoElement: document.createElement('video'), + + processActions: { + + // Loads the video file given via data.files and data.index + // as video element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadVideo: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + video; + if (this._videoElement.canPlayType && + this._videoElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + video = this._videoElement.cloneNode(false); + video.src = url; + video.controls = true; + data.video = video; + return data; + } + } + return data; + }, + + // Sets the video element as a property of the file object: + setVideo: function (data, options) { + if (data.video && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.video; + } + return data; + } + + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.fileupload.js b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload.js new file mode 100644 index 000000000..700f9013c --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.fileupload.js @@ -0,0 +1,1502 @@ +/* + * jQuery File Upload Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, require, window, document, location, Blob, FormData */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery-ui/ui/widget' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('./vendor/jquery.ui.widget') + ); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('<input type="file"/>').prop('disabled')); + + // The FileReader API is not actually used, but works as feature detection, + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // Helper function to create drag handlers for dragover/dragenter/dragleave: + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + type, + $.Event(type, {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback before the start of each chunk upload request (before form data initialization): + // chunkbeforesend: function (e, data) {}, // .bind('fileuploadchunkbeforesend', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false, + timeout: 0 + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if ($.type(options.formData) === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _deinitProgressListener: function (options) { + var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + if (xhr.upload) { + $(xhr.upload).unbind('progress'); + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = $.type(options.paramName) === 'array' ? + options.paramName[0] : options.paramName; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.uploadName || file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type || 'application/octet-stream'; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append( + paramName, + options.blob, + file.uploadName || file.name + ); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + file, + file.uploadName || file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('<a></a>').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise([this])).then( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).then(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + ($.type(mcs) === 'function' ? mcs(o) : mcs), + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Trigger chunkbeforesend to allow form data to be updated for this chunk + that._trigger('chunkbeforesend', null, o); + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }) + .always(function () { + that._deinitProgressListener(o); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._deinitProgressListener(options); + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.then(send); + } else { + this._sequence = this._sequence.then(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, + limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i, + j = 0; + if (!filesLength) { + return false; + } + if (limitSize && files[0].size === undefined) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || + !this._isXHRUpload(options)) { + fileSet = [files]; + paramNameSet = [paramName]; + } else if (!(options.singleFileUploads || limitSize) && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + ((batchSize + files[i + 1].size + overhead) > limitSize) || + (limit && i + 1 - j >= limit)) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } + } else { + paramNameSet = paramName; + } + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); + return result; + }); + return result; + }, + + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true), + restoreFocus = input.is(document.activeElement); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; + $('<form></form>').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // If the fileInput had focus before it was detached, + // restore focus to the inputClone. + if (restoreFocus) { + inputClone.focus(); + } + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + entries = [], + dirReader, + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + successHandler = function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, errorHandler); + }; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + readEntries(); + } else { + // Return an empty list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).then(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).then(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data); + } + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); + } + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _destroy: function () { + this._destroyEventHandlers(); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options, + data = this.element.data(); + // Initialize options set via HTML5 data-attributes: + $.each( + this.element[0].attributes, + function (index, attr) { + var key = attr.name.toLowerCase(), + value; + if (/^data-/.test(key)) { + // Convert hyphen-ated key to camelCase: + key = key.slice(5).replace(/-[a-z]/g, function (str) { + return str.charAt(1).toUpperCase(); + }); + value = data[key]; + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data); + jqXHR.then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/jquery.iframe-transport.js b/vendor/blueimp/jquery-file-upload/js/jquery.iframe-transport.js new file mode 100644 index 000000000..8d25c4641 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/jquery.iframe-transport.js @@ -0,0 +1,224 @@ +/* + * jQuery Iframe Transport Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require, window, document, JSON */ + +;(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Helper variable to create unique names for the transport iframes: + var counter = 0, + jsonAPI = $, + jsonParse = 'parseJSON'; + + if ('JSON' in window && 'parse' in JSON) { + jsonAPI = JSON; + jsonParse = 'parse'; + } + + // The iframe transport accepts four additional options: + // options.fileInput: a jQuery collection of file input fields + // options.paramName: the parameter name for the file form data, + // overrides the name property of the file input field(s), + // can be a string or an array of strings. + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: 'a', value: 1}, {name: 'b', value: 2}] + // options.initialIframeSrc: the URL of the initial iframe src, + // by default set to "javascript:false;" + $.ajaxTransport('iframe', function (options) { + if (options.async) { + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6: + /*jshint scripturl: true */ + var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', + /*jshint scripturl: false */ + form, + iframe, + addParamChar; + return { + send: function (_, completeCallback) { + form = $('<form style="display:none;"></form>'); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + counter += 1; + iframe = $( + '<iframe src="' + initialIframeSrc + + '" name="iframe-transport-' + counter + '"></iframe>' + ).bind('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; + iframe + .unbind('load') + .bind('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': response} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('<iframe src="' + initialIframeSrc + '"></iframe>') + .appendTo(form); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('<input type="hidden"/>') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + // Remove the HTML5 form attribute from the input(s): + options.fileInput.removeAttr('form'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + // Restore the original name and form properties: + $(input) + .prop('name', clone.prop('name')) + .attr('form', clone.attr('form')); + clone.replaceWith(input); + }); + } + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', initialIframeSrc); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe && $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return iframe && jsonAPI[jsonParse]($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); + }, + 'iframe script': function (iframe) { + return iframe && $.globalEval($(iframe[0].body).text()); + } + } + }); + +})); diff --git a/vendor/blueimp/jquery-file-upload/js/main.js b/vendor/blueimp/jquery-file-upload/js/main.js new file mode 100644 index 000000000..0403682e7 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/main.js @@ -0,0 +1,75 @@ +/* + * jQuery File Upload Plugin JS Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global $, window */ + +$(function () { + 'use strict'; + + // Initialize the jQuery File Upload widget: + $('#fileupload').fileupload({ + // Uncomment the following to send cross-domain cookies: + //xhrFields: {withCredentials: true}, + url: 'server/php/' + }); + + // Enable iframe cross-domain access via redirect option: + $('#fileupload').fileupload( + 'option', + 'redirect', + window.location.href.replace( + /\/[^\/]*$/, + '/cors/result.html?%s' + ) + ); + + if (window.location.hostname === 'blueimp.github.io') { + // Demo settings: + $('#fileupload').fileupload('option', { + url: '//jquery-file-upload.appspot.com/', + // Enable image resizing, except for Android and Opera, + // which actually support image resizing, but fail to + // send Blob objects via XHR requests: + disableImageResize: /Android(?!.*Chrome)|Opera/ + .test(window.navigator.userAgent), + maxFileSize: 999000, + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i + }); + // Upload server status check for browsers with CORS support: + if ($.support.cors) { + $.ajax({ + url: '//jquery-file-upload.appspot.com/', + type: 'HEAD' + }).fail(function () { + $('<div class="alert alert-danger"/>') + .text('Upload server currently unavailable - ' + + new Date()) + .appendTo('#fileupload'); + }); + } + } else { + // Load existing files: + $('#fileupload').addClass('fileupload-processing'); + $.ajax({ + // Uncomment the following to send cross-domain cookies: + //xhrFields: {withCredentials: true}, + url: $('#fileupload').fileupload('option', 'url'), + dataType: 'json', + context: $('#fileupload')[0] + }).always(function () { + $(this).removeClass('fileupload-processing'); + }).done(function (result) { + $(this).fileupload('option', 'done') + .call(this, $.Event('done'), {result: result}); + }); + } + +}); diff --git a/vendor/blueimp/jquery-file-upload/js/vendor/jquery.ui.widget.js b/vendor/blueimp/jquery-file-upload/js/vendor/jquery.ui.widget.js new file mode 100644 index 000000000..914b8ffb8 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/js/vendor/jquery.ui.widget.js @@ -0,0 +1,752 @@ +/*! jQuery UI - v1.12.1+CommonJS - 2018-02-10 + * http://jqueryui.com + * Includes: widget.js + * Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else if ( typeof exports === "object" ) { + + // Node/CommonJS + factory( require( "jquery" ) ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + + $.ui = $.ui || {}; + + var version = $.ui.version = "1.12.1"; + + + /*! + * jQuery UI Widget 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + + //>>label: Widget + //>>group: Core + //>>description: Provides a factory for creating stateful widgets with a common API. + //>>docs: http://api.jqueryui.com/jQuery.widget/ + //>>demos: http://jqueryui.com/widget/ + + + + var widgetUuid = 0; + var widgetSlice = Array.prototype.slice; + + $.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // Http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; + } )( $.cleanData ); + + $.widget = function( name, base, prototype ) { + var existingConstructor, constructor, basePrototype; + + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; + + var namespace = name.split( "." )[ 0 ]; + name = name.split( "." )[ 1 ]; + var fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + if ( $.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } + + // Create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + + // Allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // Allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + + // Extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + + // Copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + + // Track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + } ); + + basePrototype = new base(); + + // We need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = ( function() { + function _super() { + return base.prototype[ prop ].apply( this, arguments ); + } + + function _superApply( args ) { + return base.prototype[ prop ].apply( this, args ); + } + + return function() { + var __super = this._super; + var __superApply = this._superApply; + var returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + } )(); + } ); + constructor.prototype = $.widget.extend( basePrototype, { + + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + } ); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // Redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, + child._proto ); + } ); + + // Remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; + }; + + $.widget.extend = function( target ) { + var input = widgetSlice.call( arguments, 1 ); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; + + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; + }; + + $.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string"; + var args = widgetSlice.call( arguments, 1 ); + var returnValue = this; + + if ( isMethodCall ) { + + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var instance = $.data( this, fullName ); + + if ( options === "instance" ) { + returnValue = instance; + return false; + } + + if ( !instance ) { + return $.error( "cannot call methods on " + name + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + + if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + + " widget instance" ); + } + + methodValue = instance[ options ].apply( instance, args ); + + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat( args ) ); + } + + this.each( function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + } ); + } + + return returnValue; + }; + }; + + $.Widget = function( /* options, element */ ) {}; + $.Widget._childConstructors = []; + + $.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "<div>", + + options: { + classes: {}, + disabled: false, + + // Callbacks + create: null + }, + + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widgetUuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + this.classesElementLookup = {}; + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + } ); + this.document = $( element.style ? + + // Element within the document + element.ownerDocument : + + // Element is window or document + element.document || element ); + this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + + if ( this.options.disabled ) { + this._setOptionDisabled( this.options.disabled ); + } + + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + + _getCreateOptions: function() { + return {}; + }, + + _getCreateEventData: $.noop, + + _create: $.noop, + + _init: $.noop, + + destroy: function() { + var that = this; + + this._destroy(); + $.each( this.classesElementLookup, function( key, value ) { + that._removeClass( value, key ); + } ); + + // We can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .off( this.eventNamespace ) + .removeData( this.widgetFullName ); + this.widget() + .off( this.eventNamespace ) + .removeAttr( "aria-disabled" ); + + // Clean up events and states + this.bindings.off( this.eventNamespace ); + }, + + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key; + var parts; + var curOption; + var i; + + if ( arguments.length === 0 ) { + + // Don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + + _setOption: function( key, value ) { + if ( key === "classes" ) { + this._setOptionClasses( value ); + } + + this.options[ key ] = value; + + if ( key === "disabled" ) { + this._setOptionDisabled( value ); + } + + return this; + }, + + _setOptionClasses: function( value ) { + var classKey, elements, currentElements; + + for ( classKey in value ) { + currentElements = this.classesElementLookup[ classKey ]; + if ( value[ classKey ] === this.options.classes[ classKey ] || + !currentElements || + !currentElements.length ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $( currentElements.get() ); + this._removeClass( currentElements, classKey ); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( this._classes( { + element: elements, + keys: classKey, + classes: value, + add: true + } ) ); + } + }, + + _setOptionDisabled: function( value ) { + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this._removeClass( this.hoverable, null, "ui-state-hover" ); + this._removeClass( this.focusable, null, "ui-state-focus" ); + } + }, + + enable: function() { + return this._setOptions( { disabled: false } ); + }, + + disable: function() { + return this._setOptions( { disabled: true } ); + }, + + _classes: function( options ) { + var full = []; + var that = this; + + options = $.extend( { + element: this.element, + classes: this.options.classes || {} + }, options ); + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( current.get().concat( options.element.get() ) ) ); + } else { + current = $( current.not( options.element ).get() ); + } + that.classesElementLookup[ classes[ i ] ] = current; + full.push( classes[ i ] ); + if ( checkOption && options.classes[ classes[ i ] ] ) { + full.push( options.classes[ classes[ i ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + if ( options.keys ) { + processClassString( options.keys.match( /\S+/g ) || [], true ); + } + if ( options.extra ) { + processClassString( options.extra.match( /\S+/g ) || [] ); + } + + return full.join( " " ); + }, + + _untrackClassesElement: function( event ) { + var that = this; + $.each( that.classesElementLookup, function( key, value ) { + if ( $.inArray( event.target, value ) !== -1 ) { + that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); + } + } ); + }, + + _removeClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, false ); + }, + + _addClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, true ); + }, + + _toggleClass: function( element, keys, extra, add ) { + add = ( typeof add === "boolean" ) ? add : extra; + var shift = ( typeof element === "string" || element === null ), + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass( this._classes( options ), add ); + return this; + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement; + var instance = this; + + // No suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // No element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + + // Allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // Copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ); + var eventName = match[ 1 ] + instance.eventNamespace; + var selector = match[ 2 ]; + + if ( selector ) { + delegateElement.on( eventName, selector, handlerProxy ); + } else { + element.on( eventName, handlerProxy ); + } + } ); + }, + + _off: function( element, eventName ) { + eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.off( eventName ).off( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); + }, + mouseleave: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); + } + } ); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); + }, + focusout: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); + } + } ); + }, + + _trigger: function( type, event, data ) { + var prop, orig; + var callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + + // The original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // Copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } + }; + + $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + + var hasOptions; + var effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + + if ( options.delay ) { + element.delay( options.delay ); + } + + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue( function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + } ); + } + }; + } ); + + var widget = $.widget; + + + + +})); diff --git a/vendor/blueimp/jquery-file-upload/package.json b/vendor/blueimp/jquery-file-upload/package.json new file mode 100644 index 000000000..4b33a0362 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/package.json @@ -0,0 +1,55 @@ +{ + "name": "blueimp-file-upload", + "version": "9.25.1", + "title": "jQuery File Upload", + "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (Google App Engine, PHP, Python, Ruby on Rails, Java, etc.) that supports standard HTML form file uploads.", + "keywords": [ + "jquery", + "file", + "upload", + "widget", + "multiple", + "selection", + "drag", + "drop", + "progress", + "preview", + "cross-domain", + "cross-site", + "chunk", + "resume", + "gae", + "go", + "python", + "php", + "bootstrap" + ], + "homepage": "https://github.com/blueimp/jQuery-File-Upload", + "author": { + "name": "Sebastian Tschan", + "url": "https://blueimp.net" + }, + "repository": { + "type": "git", + "url": "git://github.com/blueimp/jQuery-File-Upload.git" + }, + "license": "MIT", + "optionalDependencies": { + "blueimp-canvas-to-blob": "3.5.0", + "blueimp-load-image": "2.12.2", + "blueimp-tmpl": "3.6.0" + }, + "devDependencies": { + "bower-json": "0.8.1", + "jshint": "2.9.3" + }, + "scripts": { + "bower-version-update": "./bower-version-update.js", + "lint": "jshint *.js js/*.js js/cors/*.js", + "test": "npm run lint", + "preversion": "npm test", + "version": "npm run bower-version-update && git add bower.json", + "postversion": "git push --tags origin master && npm publish" + }, + "main": "js/jquery.fileupload.js" +} diff --git a/vendor/blueimp/jquery-file-upload/server/gae-go/app.yaml b/vendor/blueimp/jquery-file-upload/server/gae-go/app.yaml new file mode 100644 index 000000000..b5ac1a2e1 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-go/app.yaml @@ -0,0 +1,10 @@ +runtime: go +api_version: go1 + +handlers: +- url: /(favicon\.ico|robots\.txt) + static_files: static/\1 + upload: static/(.*) + expiration: '1d' +- url: /.* + script: _go_app diff --git a/vendor/blueimp/jquery-file-upload/server/gae-go/main.go b/vendor/blueimp/jquery-file-upload/server/gae-go/main.go new file mode 100644 index 000000000..a92d128c0 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-go/main.go @@ -0,0 +1,361 @@ +/* + * jQuery File Upload Plugin GAE Go Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +package app + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "github.com/disintegration/gift" + "golang.org/x/net/context" + "google.golang.org/appengine" + "google.golang.org/appengine/memcache" + "hash/crc32" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "log" + "mime/multipart" + "net/http" + "net/url" + "path/filepath" + "regexp" + "strings" +) + +const ( + WEBSITE = "https://blueimp.github.io/jQuery-File-Upload/" + MIN_FILE_SIZE = 1 // bytes + // Max file size is memcache limit (1MB) minus key size minus overhead: + MAX_FILE_SIZE = 999000 // bytes + IMAGE_TYPES = "image/(gif|p?jpeg|(x-)?png)" + ACCEPT_FILE_TYPES = IMAGE_TYPES + THUMB_MAX_WIDTH = 80 + THUMB_MAX_HEIGHT = 80 + EXPIRATION_TIME = 300 // seconds + // If empty, only allow redirects to the referer protocol+host. + // Set to a regexp string for custom pattern matching: + REDIRECT_ALLOW_TARGET = "" +) + +var ( + imageTypes = regexp.MustCompile(IMAGE_TYPES) + acceptFileTypes = regexp.MustCompile(ACCEPT_FILE_TYPES) + thumbSuffix = "." + fmt.Sprint(THUMB_MAX_WIDTH) + "x" + + fmt.Sprint(THUMB_MAX_HEIGHT) +) + +func escape(s string) string { + return strings.Replace(url.QueryEscape(s), "+", "%20", -1) +} + +func extractKey(r *http.Request) string { + // Use RequestURI instead of r.URL.Path, as we need the encoded form: + path := strings.Split(r.RequestURI, "?")[0] + // Also adjust double encoded slashes: + return strings.Replace(path[1:], "%252F", "%2F", -1) +} + +func check(err error) { + if err != nil { + panic(err) + } +} + +type FileInfo struct { + Key string `json:"-"` + ThumbnailKey string `json:"-"` + Url string `json:"url,omitempty"` + ThumbnailUrl string `json:"thumbnailUrl,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Size int64 `json:"size"` + Error string `json:"error,omitempty"` + DeleteUrl string `json:"deleteUrl,omitempty"` + DeleteType string `json:"deleteType,omitempty"` +} + +func (fi *FileInfo) ValidateType() (valid bool) { + if acceptFileTypes.MatchString(fi.Type) { + return true + } + fi.Error = "Filetype not allowed" + return false +} + +func (fi *FileInfo) ValidateSize() (valid bool) { + if fi.Size < MIN_FILE_SIZE { + fi.Error = "File is too small" + } else if fi.Size > MAX_FILE_SIZE { + fi.Error = "File is too big" + } else { + return true + } + return false +} + +func (fi *FileInfo) CreateUrls(r *http.Request, c context.Context) { + u := &url.URL{ + Scheme: r.URL.Scheme, + Host: appengine.DefaultVersionHostname(c), + Path: "/", + } + uString := u.String() + fi.Url = uString + fi.Key + fi.DeleteUrl = fi.Url + fi.DeleteType = "DELETE" + if fi.ThumbnailKey != "" { + fi.ThumbnailUrl = uString + fi.ThumbnailKey + } +} + +func (fi *FileInfo) SetKey(checksum uint32) { + fi.Key = escape(string(fi.Type)) + "/" + + escape(fmt.Sprint(checksum)) + "/" + + escape(string(fi.Name)) +} + +func (fi *FileInfo) createThumb(buffer *bytes.Buffer, c context.Context) { + if imageTypes.MatchString(fi.Type) { + src, _, err := image.Decode(bytes.NewReader(buffer.Bytes())) + check(err) + filter := gift.New(gift.ResizeToFit( + THUMB_MAX_WIDTH, + THUMB_MAX_HEIGHT, + gift.LanczosResampling, + )) + dst := image.NewNRGBA(filter.Bounds(src.Bounds())) + filter.Draw(dst, src) + buffer.Reset() + bWriter := bufio.NewWriter(buffer) + switch fi.Type { + case "image/jpeg", "image/pjpeg": + err = jpeg.Encode(bWriter, dst, nil) + case "image/gif": + err = gif.Encode(bWriter, dst, nil) + default: + err = png.Encode(bWriter, dst) + } + check(err) + bWriter.Flush() + thumbnailKey := fi.Key + thumbSuffix + filepath.Ext(fi.Name) + item := &memcache.Item{ + Key: thumbnailKey, + Value: buffer.Bytes(), + } + err = memcache.Set(c, item) + check(err) + fi.ThumbnailKey = thumbnailKey + } +} + +func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) { + fi = &FileInfo{ + Name: p.FileName(), + Type: p.Header.Get("Content-Type"), + } + if !fi.ValidateType() { + return + } + defer func() { + if rec := recover(); rec != nil { + log.Println(rec) + fi.Error = rec.(error).Error() + } + }() + var buffer bytes.Buffer + hash := crc32.NewIEEE() + mw := io.MultiWriter(&buffer, hash) + lr := &io.LimitedReader{R: p, N: MAX_FILE_SIZE + 1} + _, err := io.Copy(mw, lr) + check(err) + fi.Size = MAX_FILE_SIZE + 1 - lr.N + if !fi.ValidateSize() { + return + } + fi.SetKey(hash.Sum32()) + item := &memcache.Item{ + Key: fi.Key, + Value: buffer.Bytes(), + } + context := appengine.NewContext(r) + err = memcache.Set(context, item) + check(err) + fi.createThumb(&buffer, context) + fi.CreateUrls(r, context) + return +} + +func getFormValue(p *multipart.Part) string { + var b bytes.Buffer + io.CopyN(&b, p, int64(1<<20)) // Copy max: 1 MiB + return b.String() +} + +func handleUploads(r *http.Request) (fileInfos []*FileInfo) { + fileInfos = make([]*FileInfo, 0) + mr, err := r.MultipartReader() + check(err) + r.Form, err = url.ParseQuery(r.URL.RawQuery) + check(err) + part, err := mr.NextPart() + for err == nil { + if name := part.FormName(); name != "" { + if part.FileName() != "" { + fileInfos = append(fileInfos, handleUpload(r, part)) + } else { + r.Form[name] = append(r.Form[name], getFormValue(part)) + } + } + part, err = mr.NextPart() + } + return +} + +func validateRedirect(r *http.Request, redirect string) bool { + if redirect != "" { + var redirectAllowTarget *regexp.Regexp + if REDIRECT_ALLOW_TARGET != "" { + redirectAllowTarget = regexp.MustCompile(REDIRECT_ALLOW_TARGET) + } else { + referer := r.Referer() + if referer == "" { + return false + } + refererUrl, err := url.Parse(referer) + if err != nil { + return false + } + redirectAllowTarget = regexp.MustCompile("^" + regexp.QuoteMeta( + refererUrl.Scheme+"://"+refererUrl.Host+"/", + )) + } + return redirectAllowTarget.MatchString(redirect) + } + return false +} + +func get(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.Redirect(w, r, WEBSITE, http.StatusFound) + return + } + // Use RequestURI instead of r.URL.Path, as we need the encoded form: + key := extractKey(r) + parts := strings.Split(key, "/") + if len(parts) == 3 { + context := appengine.NewContext(r) + item, err := memcache.Get(context, key) + if err == nil { + w.Header().Add("X-Content-Type-Options", "nosniff") + contentType, _ := url.QueryUnescape(parts[0]) + if !imageTypes.MatchString(contentType) { + contentType = "application/octet-stream" + } + w.Header().Add("Content-Type", contentType) + w.Header().Add( + "Cache-Control", + fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME), + ) + w.Write(item.Value) + return + } + } + http.Error(w, "404 Not Found", http.StatusNotFound) +} + +func post(w http.ResponseWriter, r *http.Request) { + result := make(map[string][]*FileInfo, 1) + result["files"] = handleUploads(r) + b, err := json.Marshal(result) + check(err) + if redirect := r.FormValue("redirect"); validateRedirect(r, redirect) { + if strings.Contains(redirect, "%s") { + redirect = fmt.Sprintf( + redirect, + escape(string(b)), + ) + } + http.Redirect(w, r, redirect, http.StatusFound) + return + } + w.Header().Set("Cache-Control", "no-cache") + jsonType := "application/json" + if strings.Index(r.Header.Get("Accept"), jsonType) != -1 { + w.Header().Set("Content-Type", jsonType) + } + fmt.Fprintln(w, string(b)) +} + +func delete(w http.ResponseWriter, r *http.Request) { + key := extractKey(r) + parts := strings.Split(key, "/") + if len(parts) == 3 { + result := make(map[string]bool, 1) + context := appengine.NewContext(r) + err := memcache.Delete(context, key) + if err == nil { + result[key] = true + contentType, _ := url.QueryUnescape(parts[0]) + if imageTypes.MatchString(contentType) { + thumbnailKey := key + thumbSuffix + filepath.Ext(parts[2]) + err := memcache.Delete(context, thumbnailKey) + if err == nil { + result[thumbnailKey] = true + } + } + } + w.Header().Set("Content-Type", "application/json") + b, err := json.Marshal(result) + check(err) + fmt.Fprintln(w, string(b)) + } else { + http.Error(w, "405 Method not allowed", http.StatusMethodNotAllowed) + } +} + +func handle(w http.ResponseWriter, r *http.Request) { + params, err := url.ParseQuery(r.URL.RawQuery) + check(err) + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add( + "Access-Control-Allow-Methods", + "OPTIONS, HEAD, GET, POST, DELETE", + ) + w.Header().Add( + "Access-Control-Allow-Headers", + "Content-Type, Content-Range, Content-Disposition", + ) + switch r.Method { + case "OPTIONS", "HEAD": + return + case "GET": + get(w, r) + case "POST": + if len(params["_method"]) > 0 && params["_method"][0] == "DELETE" { + delete(w, r) + } else { + post(w, r) + } + case "DELETE": + delete(w, r) + default: + http.Error(w, "501 Not Implemented", http.StatusNotImplemented) + } +} + +func init() { + http.HandleFunc("/", handle) +} diff --git a/vendor/blueimp/jquery-file-upload/server/gae-go/static/robots.txt b/vendor/blueimp/jquery-file-upload/server/gae-go/static/robots.txt new file mode 100644 index 000000000..eb0536286 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-go/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/vendor/blueimp/jquery-file-upload/server/gae-python/app.yaml b/vendor/blueimp/jquery-file-upload/server/gae-python/app.yaml new file mode 100644 index 000000000..0c49462fa --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-python/app.yaml @@ -0,0 +1,15 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +libraries: +- name: PIL + version: latest + +handlers: +- url: /(favicon\.ico|robots\.txt) + static_files: static/\1 + upload: static/(.*) + expiration: '1d' +- url: /.* + script: main.app diff --git a/vendor/blueimp/jquery-file-upload/server/gae-python/main.py b/vendor/blueimp/jquery-file-upload/server/gae-python/main.py new file mode 100644 index 000000000..1955ac00a --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-python/main.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# +# jQuery File Upload Plugin GAE Python Example +# https://github.com/blueimp/jQuery-File-Upload +# +# Copyright 2011, Sebastian Tschan +# https://blueimp.net +# +# Licensed under the MIT license: +# https://opensource.org/licenses/MIT +# + +from google.appengine.api import memcache, images +import json +import os +import re +import urllib +import webapp2 + +DEBUG=os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') +WEBSITE = 'https://blueimp.github.io/jQuery-File-Upload/' +MIN_FILE_SIZE = 1 # bytes +# Max file size is memcache limit (1MB) minus key size minus overhead: +MAX_FILE_SIZE = 999000 # bytes +IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)') +ACCEPT_FILE_TYPES = IMAGE_TYPES +THUMB_MAX_WIDTH = 80 +THUMB_MAX_HEIGHT = 80 +THUMB_SUFFIX = '.'+str(THUMB_MAX_WIDTH)+'x'+str(THUMB_MAX_HEIGHT)+'.png' +EXPIRATION_TIME = 300 # seconds +# If set to None, only allow redirects to the referer protocol+host. +# Set to a regexp for custom pattern matching against the redirect value: +REDIRECT_ALLOW_TARGET = None + +class CORSHandler(webapp2.RequestHandler): + def cors(self): + headers = self.response.headers + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] =\ + 'OPTIONS, HEAD, GET, POST, DELETE' + headers['Access-Control-Allow-Headers'] =\ + 'Content-Type, Content-Range, Content-Disposition' + + def initialize(self, request, response): + super(CORSHandler, self).initialize(request, response) + self.cors() + + def json_stringify(self, obj): + return json.dumps(obj, separators=(',', ':')) + + def options(self, *args, **kwargs): + pass + +class UploadHandler(CORSHandler): + def validate(self, file): + if file['size'] < MIN_FILE_SIZE: + file['error'] = 'File is too small' + elif file['size'] > MAX_FILE_SIZE: + file['error'] = 'File is too big' + elif not ACCEPT_FILE_TYPES.match(file['type']): + file['error'] = 'Filetype not allowed' + else: + return True + return False + + def validate_redirect(self, redirect): + if redirect: + if REDIRECT_ALLOW_TARGET: + return REDIRECT_ALLOW_TARGET.match(redirect) + referer = self.request.headers['referer'] + if referer: + from urlparse import urlparse + parts = urlparse(referer) + redirect_allow_target = '^' + re.escape( + parts.scheme + '://' + parts.netloc + '/' + ) + return re.match(redirect_allow_target, redirect) + return False + + def get_file_size(self, file): + file.seek(0, 2) # Seek to the end of the file + size = file.tell() # Get the position of EOF + file.seek(0) # Reset the file position to the beginning + return size + + def write_blob(self, data, info): + key = urllib.quote(info['type'].encode('utf-8'), '') +\ + '/' + str(hash(data)) +\ + '/' + urllib.quote(info['name'].encode('utf-8'), '') + try: + memcache.set(key, data, time=EXPIRATION_TIME) + except: #Failed to add to memcache + return (None, None) + thumbnail_key = None + if IMAGE_TYPES.match(info['type']): + try: + img = images.Image(image_data=data) + img.resize( + width=THUMB_MAX_WIDTH, + height=THUMB_MAX_HEIGHT + ) + thumbnail_data = img.execute_transforms() + thumbnail_key = key + THUMB_SUFFIX + memcache.set( + thumbnail_key, + thumbnail_data, + time=EXPIRATION_TIME + ) + except: #Failed to resize Image or add to memcache + thumbnail_key = None + return (key, thumbnail_key) + + def handle_upload(self): + results = [] + for name, fieldStorage in self.request.POST.items(): + if type(fieldStorage) is unicode: + continue + result = {} + result['name'] = urllib.unquote(fieldStorage.filename) + result['type'] = fieldStorage.type + result['size'] = self.get_file_size(fieldStorage.file) + if self.validate(result): + key, thumbnail_key = self.write_blob( + fieldStorage.value, + result + ) + if key is not None: + result['url'] = self.request.host_url + '/' + key + result['deleteUrl'] = result['url'] + result['deleteType'] = 'DELETE' + if thumbnail_key is not None: + result['thumbnailUrl'] = self.request.host_url +\ + '/' + thumbnail_key + else: + result['error'] = 'Failed to store uploaded file.' + results.append(result) + return results + + def head(self): + pass + + def get(self): + self.redirect(WEBSITE) + + def post(self): + if (self.request.get('_method') == 'DELETE'): + return self.delete() + result = {'files': self.handle_upload()} + s = self.json_stringify(result) + redirect = self.request.get('redirect') + if self.validate_redirect(redirect): + return self.redirect(str( + redirect.replace('%s', urllib.quote(s, ''), 1) + )) + if 'application/json' in self.request.headers.get('Accept'): + self.response.headers['Content-Type'] = 'application/json' + self.response.write(s) + +class FileHandler(CORSHandler): + def normalize(self, str): + return urllib.quote(urllib.unquote(str), '') + + def get(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + data = memcache.get(key) + if data is None: + return self.error(404) + # Prevent browsers from MIME-sniffing the content-type: + self.response.headers['X-Content-Type-Options'] = 'nosniff' + content_type = urllib.unquote(content_type) + if not IMAGE_TYPES.match(content_type): + # Force a download dialog for non-image types: + content_type = 'application/octet-stream' + elif file_name.endswith(THUMB_SUFFIX): + content_type = 'image/png' + self.response.headers['Content-Type'] = content_type + # Cache for the expiration time: + self.response.headers['Cache-Control'] = 'public,max-age=%d' \ + % EXPIRATION_TIME + self.response.write(data) + + def delete(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + result = {key: memcache.delete(key)} + content_type = urllib.unquote(content_type) + if IMAGE_TYPES.match(content_type): + thumbnail_key = key + THUMB_SUFFIX + result[thumbnail_key] = memcache.delete(thumbnail_key) + if 'application/json' in self.request.headers.get('Accept'): + self.response.headers['Content-Type'] = 'application/json' + s = self.json_stringify(result) + self.response.write(s) + +app = webapp2.WSGIApplication( + [ + ('/', UploadHandler), + ('/(.+)/([^/]+)/([^/]+)', FileHandler) + ], + debug=DEBUG +) diff --git a/vendor/blueimp/jquery-file-upload/server/gae-python/static/robots.txt b/vendor/blueimp/jquery-file-upload/server/gae-python/static/robots.txt new file mode 100644 index 000000000..eb0536286 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-python/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/vendor/blueimp/jquery-file-upload/server/php/Dockerfile b/vendor/blueimp/jquery-file-upload/server/php/Dockerfile new file mode 100644 index 000000000..8633fee74 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/Dockerfile @@ -0,0 +1,38 @@ +FROM php:7-apache + +# Enable the Apache Headers module: +RUN ln -s /etc/apache2/mods-available/headers.load \ + /etc/apache2/mods-enabled/headers.load + +# Enable the Apache Rewrite module: +RUN ln -s /etc/apache2/mods-available/rewrite.load \ + /etc/apache2/mods-enabled/rewrite.load + +# Install GD, Imagick and ImageMagick as image conversion options: +RUN DEBIAN_FRONTEND=noninteractive \ + apt-get update && apt-get install -y --no-install-recommends \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + imagemagick \ + && pecl install \ + imagick \ + && docker-php-ext-enable \ + imagick \ + && docker-php-ext-configure \ + gd --with-jpeg-dir=/usr/include/ \ + && docker-php-ext-install \ + gd \ + # Uninstall obsolete packages: + && apt-get autoremove -y \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + # Remove obsolete files: + && apt-get clean \ + && rm -rf \ + /tmp/* \ + /usr/share/doc/* \ + /var/cache/* \ + /var/lib/apt/lists/* \ + /var/tmp/* diff --git a/vendor/blueimp/jquery-file-upload/server/php/UploadHandler.php b/vendor/blueimp/jquery-file-upload/server/php/UploadHandler.php new file mode 100755 index 000000000..e44004395 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/UploadHandler.php @@ -0,0 +1,1449 @@ +<?php +/* + * jQuery File Upload Plugin PHP Class + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +class UploadHandler +{ + + protected $options; + + // PHP File Upload error message codes: + // http://php.net/manual/en/features.file-upload.errors.php + protected $error_messages = array( + 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', + 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', + 3 => 'The uploaded file was only partially uploaded', + 4 => 'No file was uploaded', + 6 => 'Missing a temporary folder', + 7 => 'Failed to write file to disk', + 8 => 'A PHP extension stopped the file upload', + 'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini', + 'max_file_size' => 'File is too big', + 'min_file_size' => 'File is too small', + 'accept_file_types' => 'Filetype not allowed', + 'max_number_of_files' => 'Maximum number of files exceeded', + 'max_width' => 'Image exceeds maximum width', + 'min_width' => 'Image requires a minimum width', + 'max_height' => 'Image exceeds maximum height', + 'min_height' => 'Image requires a minimum height', + 'abort' => 'File upload aborted', + 'image_resize' => 'Failed to resize image' + ); + + protected const IMAGETYPE_GIF = 1; + protected const IMAGETYPE_JPEG = 2; + protected const IMAGETYPE_PNG = 3; + + protected $image_objects = array(); + + public function __construct($options = null, $initialize = true, $error_messages = null) { + $this->response = array(); + $this->options = array( + 'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')), + 'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/', + 'upload_url' => $this->get_full_url().'/files/', + 'input_stream' => 'php://input', + 'user_dirs' => false, + 'mkdir_mode' => 0755, + 'param_name' => 'files', + // Set the following option to 'POST', if your server does not support + // DELETE requests. This is a parameter sent to the client: + 'delete_type' => 'DELETE', + 'access_control_allow_origin' => '*', + 'access_control_allow_credentials' => false, + 'access_control_allow_methods' => array( + 'OPTIONS', + 'HEAD', + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE' + ), + 'access_control_allow_headers' => array( + 'Content-Type', + 'Content-Range', + 'Content-Disposition' + ), + // By default, allow redirects to the referer protocol+host: + 'redirect_allow_target' => '/^'.preg_quote( + parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME) + .'://' + .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST) + .'/', // Trailing slash to not match subdomains by mistake + '/' // preg_quote delimiter param + ).'/', + // Enable to provide file downloads via GET requests to the PHP script: + // 1. Set to 1 to download files via readfile method through PHP + // 2. Set to 2 to send a X-Sendfile header for lighttpd/Apache + // 3. Set to 3 to send a X-Accel-Redirect header for nginx + // If set to 2 or 3, adjust the upload_url option to the base path of + // the redirect parameter, e.g. '/files/'. + 'download_via_php' => false, + // Read files in chunks to avoid memory limits when download_via_php + // is enabled, set to 0 to disable chunked reading of files: + 'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB + // Defines which files can be displayed inline when downloaded: + 'inline_file_types' => '/\.(gif|jpe?g|png)$/i', + // Defines which files (based on their names) are accepted for upload. + // By default, only allows file uploads with image file extensions. + // Only change this setting after making sure that any allowed file + // types cannot be executed by the webserver in the files directory, + // e.g. PHP scripts, nor executed by the browser when downloaded, + // e.g. HTML files with embedded JavaScript code. + // Please also read the SECURITY.md document in this repository. + 'accept_file_types' => '/\.(gif|jpe?g|png)$/i', + // Replaces dots in filenames with the given string. + // Can be disabled by setting it to false or an empty string. + // Note that this is a security feature for servers that support + // multiple file extensions, e.g. the Apache AddHandler Directive: + // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler + // Before disabling it, make sure that files uploaded with multiple + // extensions cannot be executed by the webserver, e.g. + // "example.php.png" with embedded PHP code, nor executed by the + // browser when downloaded, e.g. "example.html.gif" with embedded + // JavaScript code. + 'replace_dots_in_filenames' => '-', + // The php.ini settings upload_max_filesize and post_max_size + // take precedence over the following max_file_size setting: + 'max_file_size' => null, + 'min_file_size' => 1, + // The maximum number of files for the upload directory: + 'max_number_of_files' => null, + // Reads first file bytes to identify and correct file extensions: + 'correct_image_extensions' => false, + // Image resolution restrictions: + 'max_width' => null, + 'max_height' => null, + 'min_width' => 1, + 'min_height' => 1, + // Set the following option to false to enable resumable uploads: + 'discard_aborted_uploads' => true, + // Set to 0 to use the GD library to scale and orient images, + // set to 1 to use imagick (if installed, falls back to GD), + // set to 2 to use the ImageMagick convert binary directly: + 'image_library' => 1, + // Uncomment the following to define an array of resource limits + // for imagick: + /* + 'imagick_resource_limits' => array( + imagick::RESOURCETYPE_MAP => 32, + imagick::RESOURCETYPE_MEMORY => 32 + ), + */ + // Command or path for to the ImageMagick convert binary: + 'convert_bin' => 'convert', + // Uncomment the following to add parameters in front of each + // ImageMagick convert call (the limit constraints seem only + // to have an effect if put in front): + /* + 'convert_params' => '-limit memory 32MiB -limit map 32MiB', + */ + // Command or path for to the ImageMagick identify binary: + 'identify_bin' => 'identify', + 'image_versions' => array( + // The empty image version key defines options for the original image. + // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards. + // Also note that the property 'no_cache' is not inherited, since it's not a manipulation. + '' => array( + // Automatically rotate images based on EXIF meta data: + 'auto_orient' => true + ), + // You can add arrays to generate different versions. + // The name of the key is the name of the version (example: 'medium'). + // the array contains the options to apply. + /* + 'medium' => array( + 'max_width' => 800, + 'max_height' => 600 + ), + */ + 'thumbnail' => array( + // Uncomment the following to use a defined directory for the thumbnails + // instead of a subdirectory based on the version identifier. + // Make sure that this directory doesn't allow execution of files if you + // don't pose any restrictions on the type of uploaded files, e.g. by + // copying the .htaccess file from the files directory for Apache: + //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/', + //'upload_url' => $this->get_full_url().'/thumb/', + // Uncomment the following to force the max + // dimensions and e.g. create square thumbnails: + // 'auto_orient' => true, + // 'crop' => true, + // 'jpeg_quality' => 70, + // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!) + // 'strip' => true, (this strips EXIF tags, such as geolocation) + 'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height. + 'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width. + ) + ), + 'print_response' => true + ); + if ($options) { + $this->options = $options + $this->options; + } + if ($error_messages) { + $this->error_messages = $error_messages + $this->error_messages; + } + if ($initialize) { + $this->initialize(); + } + } + + protected function initialize() { + switch ($this->get_server_var('REQUEST_METHOD')) { + case 'OPTIONS': + case 'HEAD': + $this->head(); + break; + case 'GET': + $this->get($this->options['print_response']); + break; + case 'PATCH': + case 'PUT': + case 'POST': + $this->post($this->options['print_response']); + break; + case 'DELETE': + $this->delete($this->options['print_response']); + break; + default: + $this->header('HTTP/1.1 405 Method Not Allowed'); + } + } + + protected function get_full_url() { + $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 || + !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && + strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0; + return + ($https ? 'https://' : 'http://'). + (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : ''). + (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME']. + ($https && $_SERVER['SERVER_PORT'] === 443 || + $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))). + substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/')); + } + + protected function get_user_id() { + @session_start(); + return session_id(); + } + + protected function get_user_path() { + if ($this->options['user_dirs']) { + return $this->get_user_id().'/'; + } + return ''; + } + + protected function get_upload_path($file_name = null, $version = null) { + $file_name = $file_name ? $file_name : ''; + if (empty($version)) { + $version_path = ''; + } else { + $version_dir = @$this->options['image_versions'][$version]['upload_dir']; + if ($version_dir) { + return $version_dir.$this->get_user_path().$file_name; + } + $version_path = $version.'/'; + } + return $this->options['upload_dir'].$this->get_user_path() + .$version_path.$file_name; + } + + protected function get_query_separator($url) { + return strpos($url, '?') === false ? '?' : '&'; + } + + protected function get_download_url($file_name, $version = null, $direct = false) { + if (!$direct && $this->options['download_via_php']) { + $url = $this->options['script_url'] + .$this->get_query_separator($this->options['script_url']) + .$this->get_singular_param_name() + .'='.rawurlencode($file_name); + if ($version) { + $url .= '&version='.rawurlencode($version); + } + return $url.'&download=1'; + } + if (empty($version)) { + $version_path = ''; + } else { + $version_url = @$this->options['image_versions'][$version]['upload_url']; + if ($version_url) { + return $version_url.$this->get_user_path().rawurlencode($file_name); + } + $version_path = rawurlencode($version).'/'; + } + return $this->options['upload_url'].$this->get_user_path() + .$version_path.rawurlencode($file_name); + } + + protected function set_additional_file_properties($file) { + $file->deleteUrl = $this->options['script_url'] + .$this->get_query_separator($this->options['script_url']) + .$this->get_singular_param_name() + .'='.rawurlencode($file->name); + $file->deleteType = $this->options['delete_type']; + if ($file->deleteType !== 'DELETE') { + $file->deleteUrl .= '&_method=DELETE'; + } + if ($this->options['access_control_allow_credentials']) { + $file->deleteWithCredentials = true; + } + } + + // Fix for overflowing signed 32 bit integers, + // works for sizes up to 2^32-1 bytes (4 GiB - 1): + protected function fix_integer_overflow($size) { + if ($size < 0) { + $size += 2.0 * (PHP_INT_MAX + 1); + } + return $size; + } + + protected function get_file_size($file_path, $clear_stat_cache = false) { + if ($clear_stat_cache) { + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + clearstatcache(true, $file_path); + } else { + clearstatcache(); + } + } + return $this->fix_integer_overflow(filesize($file_path)); + } + + protected function is_valid_file_object($file_name) { + $file_path = $this->get_upload_path($file_name); + if (is_file($file_path) && $file_name[0] !== '.') { + return true; + } + return false; + } + + protected function get_file_object($file_name) { + if ($this->is_valid_file_object($file_name)) { + $file = new \stdClass(); + $file->name = $file_name; + $file->size = $this->get_file_size( + $this->get_upload_path($file_name) + ); + $file->url = $this->get_download_url($file->name); + foreach ($this->options['image_versions'] as $version => $options) { + if (!empty($version)) { + if (is_file($this->get_upload_path($file_name, $version))) { + $file->{$version.'Url'} = $this->get_download_url( + $file->name, + $version + ); + } + } + } + $this->set_additional_file_properties($file); + return $file; + } + return null; + } + + protected function get_file_objects($iteration_method = 'get_file_object') { + $upload_dir = $this->get_upload_path(); + if (!is_dir($upload_dir)) { + return array(); + } + return array_values(array_filter(array_map( + array($this, $iteration_method), + scandir($upload_dir) + ))); + } + + protected function count_file_objects() { + return count($this->get_file_objects('is_valid_file_object')); + } + + protected function get_error_message($error) { + return isset($this->error_messages[$error]) ? + $this->error_messages[$error] : $error; + } + + public function get_config_bytes($val) { + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + $val = (int)$val; + switch ($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + return $this->fix_integer_overflow($val); + } + + protected function validate($uploaded_file, $file, $error, $index) { + if ($error) { + $file->error = $this->get_error_message($error); + return false; + } + $content_length = $this->fix_integer_overflow( + (int)$this->get_server_var('CONTENT_LENGTH') + ); + $post_max_size = $this->get_config_bytes(ini_get('post_max_size')); + if ($post_max_size && ($content_length > $post_max_size)) { + $file->error = $this->get_error_message('post_max_size'); + return false; + } + if (!preg_match($this->options['accept_file_types'], $file->name)) { + $file->error = $this->get_error_message('accept_file_types'); + return false; + } + if ($uploaded_file && is_uploaded_file($uploaded_file)) { + $file_size = $this->get_file_size($uploaded_file); + } else { + $file_size = $content_length; + } + if ($this->options['max_file_size'] && ( + $file_size > $this->options['max_file_size'] || + $file->size > $this->options['max_file_size']) + ) { + $file->error = $this->get_error_message('max_file_size'); + return false; + } + if ($this->options['min_file_size'] && + $file_size < $this->options['min_file_size']) { + $file->error = $this->get_error_message('min_file_size'); + return false; + } + if (is_int($this->options['max_number_of_files']) && + ($this->count_file_objects() >= $this->options['max_number_of_files']) && + // Ignore additional chunks of existing files: + !is_file($this->get_upload_path($file->name))) { + $file->error = $this->get_error_message('max_number_of_files'); + return false; + } + $max_width = @$this->options['max_width']; + $max_height = @$this->options['max_height']; + $min_width = @$this->options['min_width']; + $min_height = @$this->options['min_height']; + if (($max_width || $max_height || $min_width || $min_height) + && $this->is_valid_image_file($uploaded_file)) { + list($img_width, $img_height) = $this->get_image_size($uploaded_file); + // If we are auto rotating the image by default, do the checks on + // the correct orientation + if ( + @$this->options['image_versions']['']['auto_orient'] && + function_exists('exif_read_data') && + ($exif = @exif_read_data($uploaded_file)) && + (((int) @$exif['Orientation']) >= 5) + ) { + $tmp = $img_width; + $img_width = $img_height; + $img_height = $tmp; + unset($tmp); + } + } + if (!empty($img_width)) { + if ($max_width && $img_width > $max_width) { + $file->error = $this->get_error_message('max_width'); + return false; + } + if ($max_height && $img_height > $max_height) { + $file->error = $this->get_error_message('max_height'); + return false; + } + if ($min_width && $img_width < $min_width) { + $file->error = $this->get_error_message('min_width'); + return false; + } + if ($min_height && $img_height < $min_height) { + $file->error = $this->get_error_message('min_height'); + return false; + } + } + return true; + } + + protected function upcount_name_callback($matches) { + $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1; + $ext = isset($matches[2]) ? $matches[2] : ''; + return ' ('.$index.')'.$ext; + } + + protected function upcount_name($name) { + return preg_replace_callback( + '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/', + array($this, 'upcount_name_callback'), + $name, + 1 + ); + } + + protected function get_unique_filename($file_path, $name, $size, $type, $error, + $index, $content_range) { + while(is_dir($this->get_upload_path($name))) { + $name = $this->upcount_name($name); + } + // Keep an existing filename if this is part of a chunked upload: + $uploaded_bytes = $this->fix_integer_overflow((int)$content_range[1]); + while (is_file($this->get_upload_path($name))) { + if ($uploaded_bytes === $this->get_file_size( + $this->get_upload_path($name))) { + break; + } + $name = $this->upcount_name($name); + } + return $name; + } + + protected function fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range) { + // Add missing file extension for known image types: + if (strpos($name, '.') === false && + preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) { + $name .= '.'.$matches[1]; + } + if ($this->options['correct_image_extensions']) { + switch ($this->imagetype($file_path)) { + case self::IMAGETYPE_JPEG: + $extensions = array('jpg', 'jpeg'); + break; + case self::IMAGETYPE_PNG: + $extensions = array('png'); + break; + case self::IMAGETYPE_GIF: + $extensions = array('gif'); + break; + } + // Adjust incorrect image file extensions: + if (!empty($extensions)) { + $parts = explode('.', $name); + $extIndex = count($parts) - 1; + $ext = strtolower(@$parts[$extIndex]); + if (!in_array($ext, $extensions)) { + $parts[$extIndex] = $extensions[0]; + $name = implode('.', $parts); + } + } + } + return $name; + } + + protected function trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { + // Remove path information and dots around the filename, to prevent uploading + // into different directories or replacing hidden system files. + // Also remove control characters and spaces (\x00..\x20) around the filename: + $name = trim($this->basename(stripslashes($name)), ".\x00..\x20"); + // Replace dots in filenames to avoid security issues with servers + // that interpret multiple file extensions, e.g. "example.php.png": + $replacement = $this->options['replace_dots_in_filenames']; + if (!empty($replacement)) { + $parts = explode('.', $name); + if (count($parts) > 2) { + $ext = array_pop($parts); + $name = implode($replacement, $parts).'.'.$ext; + } + } + // Use a timestamp for empty filenames: + if (!$name) { + $name = str_replace('.', '-', microtime(true)); + } + return $name; + } + + protected function get_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { + $name = $this->trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range); + return $this->get_unique_filename( + $file_path, + $this->fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range), + $size, + $type, + $error, + $index, + $content_range + ); + } + + protected function get_scaled_image_file_paths($file_name, $version) { + $file_path = $this->get_upload_path($file_name); + if (!empty($version)) { + $version_dir = $this->get_upload_path(null, $version); + if (!is_dir($version_dir)) { + mkdir($version_dir, $this->options['mkdir_mode'], true); + } + $new_file_path = $version_dir.'/'.$file_name; + } else { + $new_file_path = $file_path; + } + return array($file_path, $new_file_path); + } + + protected function gd_get_image_object($file_path, $func, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $func($file_path); + } + return $this->image_objects[$file_path]; + } + + protected function gd_set_image_object($file_path, $image) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function gd_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && imagedestroy($image); + } + + protected function gd_imageflip($image, $mode) { + if (function_exists('imageflip')) { + return imageflip($image, $mode); + } + $new_width = $src_width = imagesx($image); + $new_height = $src_height = imagesy($image); + $new_img = imagecreatetruecolor($new_width, $new_height); + $src_x = 0; + $src_y = 0; + switch ($mode) { + case '1': // flip on the horizontal axis + $src_y = $new_height - 1; + $src_height = -$new_height; + break; + case '2': // flip on the vertical axis + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + case '3': // flip on both axes + $src_y = $new_height - 1; + $src_height = -$new_height; + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + default: + return $image; + } + imagecopyresampled( + $new_img, + $image, + 0, + 0, + $src_x, + $src_y, + $new_width, + $new_height, + $src_width, + $src_height + ); + return $new_img; + } + + protected function gd_orient_image($file_path, $src_img) { + if (!function_exists('exif_read_data')) { + return false; + } + $exif = @exif_read_data($file_path); + if ($exif === false) { + return false; + } + $orientation = (int)@$exif['Orientation']; + if ($orientation < 2 || $orientation > 8) { + return false; + } + switch ($orientation) { + case 2: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + break; + case 3: + $new_img = imagerotate($src_img, 180, 0); + break; + case 4: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + break; + case 5: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); + break; + case 6: + $new_img = imagerotate($src_img, 270, 0); + break; + case 7: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); + break; + case 8: + $new_img = imagerotate($src_img, 90, 0); + break; + default: + return false; + } + $this->gd_set_image_object($file_path, $new_img); + return true; + } + + protected function gd_create_scaled_image($file_name, $version, $options) { + if (!function_exists('imagecreatetruecolor')) { + error_log('Function not found: imagecreatetruecolor'); + return false; + } + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + $src_func = 'imagecreatefromjpeg'; + $write_func = 'imagejpeg'; + $image_quality = isset($options['jpeg_quality']) ? + $options['jpeg_quality'] : 75; + break; + case 'gif': + $src_func = 'imagecreatefromgif'; + $write_func = 'imagegif'; + $image_quality = null; + break; + case 'png': + $src_func = 'imagecreatefrompng'; + $write_func = 'imagepng'; + $image_quality = isset($options['png_quality']) ? + $options['png_quality'] : 9; + break; + default: + return false; + } + $src_img = $this->gd_get_image_object( + $file_path, + $src_func, + !empty($options['no_cache']) + ); + $image_oriented = false; + if (!empty($options['auto_orient']) && $this->gd_orient_image( + $file_path, + $src_img + )) { + $image_oriented = true; + $src_img = $this->gd_get_image_object( + $file_path, + $src_func + ); + } + $max_width = $img_width = imagesx($src_img); + $max_height = $img_height = imagesy($src_img); + if (!empty($options['max_width'])) { + $max_width = $options['max_width']; + } + if (!empty($options['max_height'])) { + $max_height = $options['max_height']; + } + $scale = min( + $max_width / $img_width, + $max_height / $img_height + ); + if ($scale >= 1) { + if ($image_oriented) { + return $write_func($src_img, $new_file_path, $image_quality); + } + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + if (empty($options['crop'])) { + $new_width = $img_width * $scale; + $new_height = $img_height * $scale; + $dst_x = 0; + $dst_y = 0; + $new_img = imagecreatetruecolor($new_width, $new_height); + } else { + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = $img_width / ($img_height / $max_height); + $new_height = $max_height; + } else { + $new_width = $max_width; + $new_height = $img_height / ($img_width / $max_width); + } + $dst_x = 0 - ($new_width - $max_width) / 2; + $dst_y = 0 - ($new_height - $max_height) / 2; + $new_img = imagecreatetruecolor($max_width, $max_height); + } + // Handle transparency in GIF and PNG images: + switch ($type) { + case 'gif': + case 'png': + imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0)); + case 'png': + imagealphablending($new_img, false); + imagesavealpha($new_img, true); + break; + } + $success = imagecopyresampled( + $new_img, + $src_img, + $dst_x, + $dst_y, + 0, + 0, + $new_width, + $new_height, + $img_width, + $img_height + ) && $write_func($new_img, $new_file_path, $image_quality); + $this->gd_set_image_object($file_path, $new_img); + return $success; + } + + protected function imagick_get_image_object($file_path, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->imagick_destroy_image_object($file_path); + $image = new \Imagick(); + if (!empty($this->options['imagick_resource_limits'])) { + foreach ($this->options['imagick_resource_limits'] as $type => $limit) { + $image->setResourceLimit($type, $limit); + } + } + $image->readImage($file_path); + $this->image_objects[$file_path] = $image; + } + return $this->image_objects[$file_path]; + } + + protected function imagick_set_image_object($file_path, $image) { + $this->imagick_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function imagick_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && $image->destroy(); + } + + protected function imagick_orient_image($image) { + $orientation = $image->getImageOrientation(); + $background = new \ImagickPixel('none'); + switch ($orientation) { + case \imagick::ORIENTATION_TOPRIGHT: // 2 + $image->flopImage(); // horizontal flop around y-axis + break; + case \imagick::ORIENTATION_BOTTOMRIGHT: // 3 + $image->rotateImage($background, 180); + break; + case \imagick::ORIENTATION_BOTTOMLEFT: // 4 + $image->flipImage(); // vertical flip around x-axis + break; + case \imagick::ORIENTATION_LEFTTOP: // 5 + $image->flopImage(); // horizontal flop around y-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_RIGHTTOP: // 6 + $image->rotateImage($background, 90); + break; + case \imagick::ORIENTATION_RIGHTBOTTOM: // 7 + $image->flipImage(); // vertical flip around x-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_LEFTBOTTOM: // 8 + $image->rotateImage($background, 270); + break; + default: + return false; + } + $image->setImageOrientation(\imagick::ORIENTATION_TOPLEFT); // 1 + return true; + } + + protected function imagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $image = $this->imagick_get_image_object( + $file_path, + !empty($options['crop']) || !empty($options['no_cache']) + ); + if ($image->getImageFormat() === 'GIF') { + // Handle animated GIFs: + $images = $image->coalesceImages(); + foreach ($images as $frame) { + $image = $frame; + $this->imagick_set_image_object($file_name, $image); + break; + } + } + $image_oriented = false; + if (!empty($options['auto_orient'])) { + $image_oriented = $this->imagick_orient_image($image); + } + + $image_resize = false; + $new_width = $max_width = $img_width = $image->getImageWidth(); + $new_height = $max_height = $img_height = $image->getImageHeight(); + + // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty() + if (isset($options['max_width'])) { + $image_resize = true; + $new_width = $max_width = $options['max_width']; + } + if (isset($options['max_height'])) { + $image_resize = true; + $new_height = $max_height = $options['max_height']; + } + + $image_strip = (isset($options['strip']) ? $options['strip'] : false); + + if ( !$image_oriented && ($max_width >= $img_width) && ($max_height >= $img_height) && !$image_strip && empty($options["jpeg_quality"]) ) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $crop = (isset($options['crop']) ? $options['crop'] : false); + + if ($crop) { + $x = 0; + $y = 0; + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = 0; // Enables proportional scaling based on max_height + $x = ($img_width / ($img_height / $max_height) - $max_width) / 2; + } else { + $new_height = 0; // Enables proportional scaling based on max_width + $y = ($img_height / ($img_width / $max_width) - $max_height) / 2; + } + } + $success = $image->resizeImage( + $new_width, + $new_height, + isset($options['filter']) ? $options['filter'] : \imagick::FILTER_LANCZOS, + isset($options['blur']) ? $options['blur'] : 1, + $new_width && $new_height // fit image into constraints if not to be cropped + ); + if ($success && $crop) { + $success = $image->cropImage( + $max_width, + $max_height, + $x, + $y + ); + if ($success) { + $success = $image->setImagePage($max_width, $max_height, 0, 0); + } + } + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + if (!empty($options['jpeg_quality'])) { + $image->setImageCompression(\imagick::COMPRESSION_JPEG); + $image->setImageCompressionQuality($options['jpeg_quality']); + } + break; + } + if ( $image_strip ) { + $image->stripImage(); + } + return $success && $image->writeImage($new_file_path); + } + + protected function imagemagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $resize = @$options['max_width'] + .(empty($options['max_height']) ? '' : 'X'.$options['max_height']); + if (!$resize && empty($options['auto_orient'])) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $cmd = $this->options['convert_bin']; + if (!empty($this->options['convert_params'])) { + $cmd .= ' '.$this->options['convert_params']; + } + $cmd .= ' '.escapeshellarg($file_path); + if (!empty($options['auto_orient'])) { + $cmd .= ' -auto-orient'; + } + if ($resize) { + // Handle animated GIFs: + $cmd .= ' -coalesce'; + if (empty($options['crop'])) { + $cmd .= ' -resize '.escapeshellarg($resize.'>'); + } else { + $cmd .= ' -resize '.escapeshellarg($resize.'^'); + $cmd .= ' -gravity center'; + $cmd .= ' -crop '.escapeshellarg($resize.'+0+0'); + } + // Make sure the page dimensions are correct (fixes offsets of animated GIFs): + $cmd .= ' +repage'; + } + if (!empty($options['convert_params'])) { + $cmd .= ' '.$options['convert_params']; + } + $cmd .= ' '.escapeshellarg($new_file_path); + exec($cmd, $output, $error); + if ($error) { + error_log(implode('\n', $output)); + return false; + } + return true; + } + + protected function get_image_size($file_path) { + if ($this->options['image_library']) { + if (extension_loaded('imagick')) { + $image = new \Imagick(); + try { + if (@$image->pingImage($file_path)) { + $dimensions = array($image->getImageWidth(), $image->getImageHeight()); + $image->destroy(); + return $dimensions; + } + return false; + } catch (\Exception $e) { + error_log($e->getMessage()); + } + } + if ($this->options['image_library'] === 2) { + $cmd = $this->options['identify_bin']; + $cmd .= ' -ping '.escapeshellarg($file_path); + exec($cmd, $output, $error); + if (!$error && !empty($output)) { + // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000 + $infos = preg_split('/\s+/', substr($output[0], strlen($file_path))); + $dimensions = preg_split('/x/', $infos[2]); + return $dimensions; + } + return false; + } + } + if (!function_exists('getimagesize')) { + error_log('Function not found: getimagesize'); + return false; + } + return @getimagesize($file_path); + } + + protected function create_scaled_image($file_name, $version, $options) { + if ($this->options['image_library'] === 2) { + return $this->imagemagick_create_scaled_image($file_name, $version, $options); + } + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_create_scaled_image($file_name, $version, $options); + } + return $this->gd_create_scaled_image($file_name, $version, $options); + } + + protected function destroy_image_object($file_path) { + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_destroy_image_object($file_path); + } + } + + protected function imagetype($file_path) { + $fp = fopen($file_path, 'r'); + $data = fread($fp, 4); + fclose($fp); + // GIF: 47 49 46 + if (substr($data, 0, 3) === 'GIF') { + return self::IMAGETYPE_GIF; + } + // JPG: FF D8 + if (bin2hex(substr($data, 0, 2)) === 'ffd8') { + return self::IMAGETYPE_JPEG; + } + // PNG: 89 50 4E 47 + if (bin2hex(@$data[0]).substr($data, 1, 4) === '89PNG') { + return self::IMAGETYPE_PNG; + } + return false; + } + + protected function is_valid_image_file($file_path) { + return !!$this->imagetype($file_path); + } + + protected function handle_image_file($file_path, $file) { + $failed_versions = array(); + foreach ($this->options['image_versions'] as $version => $options) { + if ($this->create_scaled_image($file->name, $version, $options)) { + if (!empty($version)) { + $file->{$version.'Url'} = $this->get_download_url( + $file->name, + $version + ); + } else { + $file->size = $this->get_file_size($file_path, true); + } + } else { + $failed_versions[] = $version ? $version : 'original'; + } + } + if (count($failed_versions)) { + $file->error = $this->get_error_message('image_resize') + .' ('.implode($failed_versions, ', ').')'; + } + // Free memory: + $this->destroy_image_object($file_path); + } + + protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, + $index = null, $content_range = null) { + $file = new \stdClass(); + $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error, + $index, $content_range); + $file->size = $this->fix_integer_overflow((int)$size); + $file->type = $type; + if ($this->validate($uploaded_file, $file, $error, $index)) { + $this->handle_form_data($file, $index); + $upload_dir = $this->get_upload_path(); + if (!is_dir($upload_dir)) { + mkdir($upload_dir, $this->options['mkdir_mode'], true); + } + $file_path = $this->get_upload_path($file->name); + $append_file = $content_range && is_file($file_path) && + $file->size > $this->get_file_size($file_path); + if ($uploaded_file && is_uploaded_file($uploaded_file)) { + // multipart/formdata uploads (POST method uploads) + if ($append_file) { + file_put_contents( + $file_path, + fopen($uploaded_file, 'r'), + FILE_APPEND + ); + } else { + move_uploaded_file($uploaded_file, $file_path); + } + } else { + // Non-multipart uploads (PUT method support) + file_put_contents( + $file_path, + fopen($this->options['input_stream'], 'r'), + $append_file ? FILE_APPEND : 0 + ); + } + $file_size = $this->get_file_size($file_path, $append_file); + if ($file_size === $file->size) { + $file->url = $this->get_download_url($file->name); + if ($this->is_valid_image_file($file_path)) { + $this->handle_image_file($file_path, $file); + } + } else { + $file->size = $file_size; + if (!$content_range && $this->options['discard_aborted_uploads']) { + unlink($file_path); + $file->error = $this->get_error_message('abort'); + } + } + $this->set_additional_file_properties($file); + } + return $file; + } + + protected function readfile($file_path) { + $file_size = $this->get_file_size($file_path); + $chunk_size = $this->options['readfile_chunk_size']; + if ($chunk_size && $file_size > $chunk_size) { + $handle = fopen($file_path, 'rb'); + while (!feof($handle)) { + echo fread($handle, $chunk_size); + @ob_flush(); + @flush(); + } + fclose($handle); + return $file_size; + } + return readfile($file_path); + } + + protected function body($str) { + echo $str; + } + + protected function header($str) { + header($str); + } + + protected function get_upload_data($id) { + return @$_FILES[$id]; + } + + protected function get_post_param($id) { + return @$_POST[$id]; + } + + protected function get_query_param($id) { + return @$_GET[$id]; + } + + protected function get_server_var($id) { + return @$_SERVER[$id]; + } + + protected function handle_form_data($file, $index) { + // Handle form data, e.g. $_POST['description'][$index] + } + + protected function get_version_param() { + return $this->basename(stripslashes($this->get_query_param('version'))); + } + + protected function get_singular_param_name() { + return substr($this->options['param_name'], 0, -1); + } + + protected function get_file_name_param() { + $name = $this->get_singular_param_name(); + return $this->basename(stripslashes($this->get_query_param($name))); + } + + protected function get_file_names_params() { + $params = $this->get_query_param($this->options['param_name']); + if (!$params) { + return null; + } + foreach ($params as $key => $value) { + $params[$key] = $this->basename(stripslashes($value)); + } + return $params; + } + + protected function get_file_type($file_path) { + switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) { + case 'jpeg': + case 'jpg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + default: + return ''; + } + } + + protected function download() { + switch ($this->options['download_via_php']) { + case 1: + $redirect_header = null; + break; + case 2: + $redirect_header = 'X-Sendfile'; + break; + case 3: + $redirect_header = 'X-Accel-Redirect'; + break; + default: + return $this->header('HTTP/1.1 403 Forbidden'); + } + $file_name = $this->get_file_name_param(); + if (!$this->is_valid_file_object($file_name)) { + return $this->header('HTTP/1.1 404 Not Found'); + } + if ($redirect_header) { + return $this->header( + $redirect_header.': '.$this->get_download_url( + $file_name, + $this->get_version_param(), + true + ) + ); + } + $file_path = $this->get_upload_path($file_name, $this->get_version_param()); + // Prevent browsers from MIME-sniffing the content-type: + $this->header('X-Content-Type-Options: nosniff'); + if (!preg_match($this->options['inline_file_types'], $file_name)) { + $this->header('Content-Type: application/octet-stream'); + $this->header('Content-Disposition: attachment; filename="'.$file_name.'"'); + } else { + $this->header('Content-Type: '.$this->get_file_type($file_path)); + $this->header('Content-Disposition: inline; filename="'.$file_name.'"'); + } + $this->header('Content-Length: '.$this->get_file_size($file_path)); + $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path))); + $this->readfile($file_path); + } + + protected function send_content_type_header() { + $this->header('Vary: Accept'); + if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) { + $this->header('Content-type: application/json'); + } else { + $this->header('Content-type: text/plain'); + } + } + + protected function send_access_control_headers() { + $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']); + $this->header('Access-Control-Allow-Credentials: ' + .($this->options['access_control_allow_credentials'] ? 'true' : 'false')); + $this->header('Access-Control-Allow-Methods: ' + .implode(', ', $this->options['access_control_allow_methods'])); + $this->header('Access-Control-Allow-Headers: ' + .implode(', ', $this->options['access_control_allow_headers'])); + } + + public function generate_response($content, $print_response = true) { + $this->response = $content; + if ($print_response) { + $json = json_encode($content); + $redirect = stripslashes($this->get_post_param('redirect')); + if ($redirect && preg_match($this->options['redirect_allow_target'], $redirect)) { + $this->header('Location: '.sprintf($redirect, rawurlencode($json))); + return; + } + $this->head(); + if ($this->get_server_var('HTTP_CONTENT_RANGE')) { + $files = isset($content[$this->options['param_name']]) ? + $content[$this->options['param_name']] : null; + if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { + $this->header('Range: 0-'.( + $this->fix_integer_overflow((int)$files[0]->size) - 1 + )); + } + } + $this->body($json); + } + return $content; + } + + public function get_response () { + return $this->response; + } + + public function head() { + $this->header('Pragma: no-cache'); + $this->header('Cache-Control: no-store, no-cache, must-revalidate'); + $this->header('Content-Disposition: inline; filename="files.json"'); + // Prevent Internet Explorer from MIME-sniffing the content-type: + $this->header('X-Content-Type-Options: nosniff'); + if ($this->options['access_control_allow_origin']) { + $this->send_access_control_headers(); + } + $this->send_content_type_header(); + } + + public function get($print_response = true) { + if ($print_response && $this->get_query_param('download')) { + return $this->download(); + } + $file_name = $this->get_file_name_param(); + if ($file_name) { + $response = array( + $this->get_singular_param_name() => $this->get_file_object($file_name) + ); + } else { + $response = array( + $this->options['param_name'] => $this->get_file_objects() + ); + } + return $this->generate_response($response, $print_response); + } + + public function post($print_response = true) { + if ($this->get_query_param('_method') === 'DELETE') { + return $this->delete($print_response); + } + $upload = $this->get_upload_data($this->options['param_name']); + // Parse the Content-Disposition header, if available: + $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION'); + $file_name = $content_disposition_header ? + rawurldecode(preg_replace( + '/(^[^"]+")|("$)/', + '', + $content_disposition_header + )) : null; + // Parse the Content-Range header, which has the following form: + // Content-Range: bytes 0-524287/2000000 + $content_range_header = $this->get_server_var('HTTP_CONTENT_RANGE'); + $content_range = $content_range_header ? + preg_split('/[^0-9]+/', $content_range_header) : null; + $size = $content_range ? $content_range[3] : null; + $files = array(); + if ($upload) { + if (is_array($upload['tmp_name'])) { + // param_name is an array identifier like "files[]", + // $upload is a multi-dimensional array: + foreach ($upload['tmp_name'] as $index => $value) { + $files[] = $this->handle_file_upload( + $upload['tmp_name'][$index], + $file_name ? $file_name : $upload['name'][$index], + $size ? $size : $upload['size'][$index], + $upload['type'][$index], + $upload['error'][$index], + $index, + $content_range + ); + } + } else { + // param_name is a single object identifier like "file", + // $upload is a one-dimensional array: + $files[] = $this->handle_file_upload( + isset($upload['tmp_name']) ? $upload['tmp_name'] : null, + $file_name ? $file_name : (isset($upload['name']) ? + $upload['name'] : null), + $size ? $size : (isset($upload['size']) ? + $upload['size'] : $this->get_server_var('CONTENT_LENGTH')), + isset($upload['type']) ? + $upload['type'] : $this->get_server_var('CONTENT_TYPE'), + isset($upload['error']) ? $upload['error'] : null, + null, + $content_range + ); + } + } + $response = array($this->options['param_name'] => $files); + return $this->generate_response($response, $print_response); + } + + public function delete($print_response = true) { + $file_names = $this->get_file_names_params(); + if (empty($file_names)) { + $file_names = array($this->get_file_name_param()); + } + $response = array(); + foreach ($file_names as $file_name) { + $file_path = $this->get_upload_path($file_name); + $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path); + if ($success) { + foreach ($this->options['image_versions'] as $version => $options) { + if (!empty($version)) { + $file = $this->get_upload_path($file_name, $version); + if (is_file($file)) { + unlink($file); + } + } + } + } + $response[$file_name] = $success; + } + return $this->generate_response($response, $print_response); + } + + protected function basename($filepath, $suffix = null) { + $splited = preg_split('/\//', rtrim ($filepath, '/ ')); + return substr(basename('X'.$splited[count($splited)-1], $suffix), 1); + } +} diff --git a/vendor/blueimp/jquery-file-upload/server/php/docker-compose.yml b/vendor/blueimp/jquery-file-upload/server/php/docker-compose.yml new file mode 100644 index 000000000..74eabf7dc --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/docker-compose.yml @@ -0,0 +1,9 @@ +version: '2.3' +services: + apache: + build: ./ + network_mode: bridge + ports: + - "80:80" + volumes: + - "../../:/var/www/html" diff --git a/vendor/blueimp/jquery-file-upload/server/php/files/.gitignore b/vendor/blueimp/jquery-file-upload/server/php/files/.gitignore new file mode 100644 index 000000000..e24a60fae --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/files/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.htaccess diff --git a/vendor/blueimp/jquery-file-upload/server/php/files/.htaccess b/vendor/blueimp/jquery-file-upload/server/php/files/.htaccess new file mode 100644 index 000000000..6f454afb9 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/files/.htaccess @@ -0,0 +1,26 @@ +# To enable the Headers module, execute the following command and reload Apache: +# sudo a2enmod headers + +# The following directives prevent the execution of script files +# in the context of the website. +# They also force the content-type application/octet-stream and +# force browsers to display a download dialog for non-image files. +SetHandler default-handler +ForceType application/octet-stream +Header set Content-Disposition attachment + +# The following unsets the forced type and Content-Disposition headers +# for known image files: +<FilesMatch "(?i)\.(gif|jpe?g|png)$"> + ForceType none + Header unset Content-Disposition +</FilesMatch> + +# The following directive prevents browsers from MIME-sniffing the content-type. +# This is an important complement to the ForceType directive above: +Header set X-Content-Type-Options nosniff + +# Uncomment the following lines to prevent unauthorized download of files: +#AuthName "Authorization required" +#AuthType Basic +#require valid-user diff --git a/vendor/blueimp/jquery-file-upload/server/php/index.php b/vendor/blueimp/jquery-file-upload/server/php/index.php new file mode 100644 index 000000000..9c2cfb8eb --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/index.php @@ -0,0 +1,15 @@ +<?php +/* + * jQuery File Upload Plugin PHP Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +exit; +error_reporting(E_ALL | E_STRICT); +require('UploadHandler.php'); +$upload_handler = new UploadHandler(); diff --git a/vendor/blueimp/jquery-file-upload/test/index.html b/vendor/blueimp/jquery-file-upload/test/index.html new file mode 100644 index 000000000..0b5cf57b7 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/test/index.html @@ -0,0 +1,172 @@ +<!DOCTYPE HTML> +<!-- +/* + * jQuery File Upload Plugin Test + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +--> +<html lang="en"> +<head> +<!-- Force latest IE rendering engine or ChromeFrame if installed --> +<!--[if IE]> +<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> +<![endif]--> +<meta charset="utf-8"> +<title>jQuery File Upload Plugin Test</title> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.23.1.css"> +</head> +<body> +<h1 id="qunit-header">jQuery File Upload Plugin Test</h1> +<h2 id="qunit-banner"></h2> +<div id="qunit-testrunner-toolbar"></div> +<h2 id="qunit-userAgent"></h2> +<ol id="qunit-tests"></ol> +<div id="qunit-fixture"> + <!-- The file upload form used as target for the file upload widget --> + <form id="fileupload" action="../server/php/" method="POST" enctype="multipart/form-data"> + <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload --> + <div class="row fileupload-buttonbar"> + <div class="col-lg-7"> + <!-- The fileinput-button span is used to style the file input field as button --> + <span class="btn btn-success fileinput-button"> + <i class="icon-plus icon-white"></i> + <span>Add files...</span> + <input type="file" name="files[]" multiple> + </span> + <button type="submit" class="btn btn-primary start"> + <i class="icon-upload icon-white"></i> + <span>Start upload</span> + </button> + <button type="reset" class="btn btn-warning cancel"> + <i class="icon-ban-circle icon-white"></i> + <span>Cancel upload</span> + </button> + <button type="button" class="btn btn-danger delete"> + <i class="icon-trash icon-white"></i> + <span>Delete</span> + </button> + <input type="checkbox" class="toggle"> + <!-- The global file processing state --> + <span class="fileupload-process"></span> + </div> + <!-- The global progress state --> + <div class="col-lg-5 fileupload-progress"> + <!-- The global progress bar --> + <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100"> + <div class="progress-bar progress-bar-success" style="width:0%;"></div> + </div> + <!-- The extended global progress state --> + <div class="progress-extended"> </div> + </div> + </div> + <!-- The table listing the files available for upload/download --> + <table role="presentation" class="table table-striped"><tbody class="files"></tbody></table> + </form> +</div> +<!-- The template to display files available for upload --> +<script id="template-upload" type="text/x-tmpl"> +{% for (var i=0, file; file=o.files[i]; i++) { %} + <tr class="template-upload"> + <td> + <span class="preview"></span> + </td> + <td> + <p class="name">{%=file.name%}</p> + <strong class="error text-danger"></strong> + </td> + <td> + <p class="size">Processing...</p> + <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="progress-bar progress-bar-success" style="width:0%;"></div></div> + </td> + <td> + {% if (!i && !o.options.autoUpload) { %} + <button class="btn btn-primary start" disabled> + <i class="glyphicon glyphicon-upload"></i> + <span>Start</span> + </button> + {% } %} + {% if (!i) { %} + <button class="btn btn-warning cancel"> + <i class="glyphicon glyphicon-ban-circle"></i> + <span>Cancel</span> + </button> + {% } %} + </td> + </tr> +{% } %} +</script> +<!-- The template to display files available for download --> +<script id="template-download" type="text/x-tmpl"> +{% for (var i=0, file; file=o.files[i]; i++) { %} + <tr class="template-download"> + <td> + <span class="preview"> + {% if (file.thumbnailUrl) { %} + <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" data-gallery><img src="{%=file.thumbnailUrl%}"></a> + {% } %} + </span> + </td> + <td> + <p class="name"> + {% if (file.url) { %} + <a href="{%=file.url%}" title="{%=file.name%}" download="{%=file.name%}" {%=file.thumbnailUrl?'data-gallery':''%}>{%=file.name%}</a> + {% } else { %} + <span>{%=file.name%}</span> + {% } %} + </p> + {% if (file.error) { %} + <div><span class="label label-danger">Error</span> {%=file.error%}</div> + {% } %} + </td> + <td> + <span class="size">{%=o.formatFileSize(file.size)%}</span> + </td> + <td> + {% if (file.deleteUrl) { %} + <button class="btn btn-danger delete" data-type="{%=file.deleteType%}" data-url="{%=file.deleteUrl%}"{% if (file.deleteWithCredentials) { %} data-xhr-fields='{"withCredentials":true}'{% } %}> + <i class="glyphicon glyphicon-trash"></i> + <span>Delete</span> + </button> + <input type="checkbox" name="delete" value="1" class="toggle"> + {% } else { %} + <button class="btn btn-warning cancel"> + <i class="glyphicon glyphicon-ban-circle"></i> + <span>Cancel</span> + </button> + {% } %} + </td> + </tr> +{% } %} +</script> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> +<script src="../js/vendor/jquery.ui.widget.js"></script> +<script src="https://blueimp.github.io/JavaScript-Templates/js/tmpl.min.js"></script> +<script src="https://blueimp.github.io/JavaScript-Load-Image/js/load-image.all.min.js"></script> +<script src="https://blueimp.github.io/JavaScript-Canvas-to-Blob/js/canvas-to-blob.min.js"></script> +<script src="../js/jquery.iframe-transport.js"></script> +<script src="../js/jquery.fileupload.js"></script> +<script> +/* global window, $ */ +window.testBasicWidget = $.blueimp.fileupload; +</script> +<script src="../js/jquery.fileupload-process.js"></script> +<script src="../js/jquery.fileupload-image.js"></script> +<script src="../js/jquery.fileupload-audio.js"></script> +<script src="../js/jquery.fileupload-video.js"></script> +<script src="../js/jquery.fileupload-validate.js"></script> +<script src="../js/jquery.fileupload-ui.js"></script> +<script> +/* global window, $ */ +window.testUIWidget = $.blueimp.fileupload; +</script> +<script src="https://code.jquery.com/qunit/qunit-1.23.1.js"></script> +<script src="test.js"></script> +</body> +</html> diff --git a/vendor/blueimp/jquery-file-upload/test/test.js b/vendor/blueimp/jquery-file-upload/test/test.js new file mode 100644 index 000000000..452127567 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/test/test.js @@ -0,0 +1,1292 @@ +/* + * jQuery File Upload Plugin Test + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global $, QUnit, window, document, expect, module, test, asyncTest, start, ok, strictEqual, notStrictEqual */ + +$(function () { + // jshint nomen:false + 'use strict'; + + QUnit.done = function () { + // Delete all uploaded files: + var url = $('#fileupload').prop('action'); + $.getJSON(url, function (result) { + $.each(result.files, function (index, file) { + $.ajax({ + url: url + '?file=' + encodeURIComponent(file.name), + type: 'DELETE' + }); + }); + }); + }; + + var lifecycle = { + setup: function () { + // Set the .fileupload method to the basic widget method: + $.widget('blueimp.fileupload', window.testBasicWidget, {}); + }, + teardown: function () { + // Remove all remaining event listeners: + $(document).unbind(); + } + }, + lifecycleUI = { + setup: function () { + // Set the .fileupload method to the UI widget method: + $.widget('blueimp.fileupload', window.testUIWidget, {}); + }, + teardown: function () { + // Remove all remaining event listeners: + $(document).unbind(); + } + }; + + module('Initialization', lifecycle); + + test('Widget initialization', function () { + var fu = $('#fileupload').fileupload(); + ok(fu.data('blueimp-fileupload') || fu.data('fileupload')); + }); + + test('Data attribute options', function () { + $('#fileupload').attr('data-url', 'http://example.org'); + $('#fileupload').fileupload(); + strictEqual( + $('#fileupload').fileupload('option', 'url'), + 'http://example.org' + ); + }); + + test('File input initialization', function () { + var fu = $('#fileupload').fileupload(); + ok( + fu.fileupload('option', 'fileInput').length, + 'File input field inside of the widget' + ); + ok( + fu.fileupload('option', 'fileInput').length, + 'Widget element as file input field' + ); + }); + + test('Drop zone initialization', function () { + ok($('#fileupload').fileupload() + .fileupload('option', 'dropZone').length); + }); + + test('Paste zone initialization', function () { + ok($('#fileupload').fileupload({pasteZone: document}) + .fileupload('option', 'pasteZone').length); + }); + + test('Event listeners initialization', function () { + expect( + $.support.xhrFormDataFileUpload ? 4 : 1 + ); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + fu = $('#fileupload').fileupload({ + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + module('API', lifecycle); + + test('destroy', function () { + expect(4); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + options = { + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }, + fu = $('#fileupload').fileupload(options), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + dropZone.bind('dragover', options.dragover); + dropZone.bind('drop', options.drop); + pasteZone.bind('paste', options.paste); + fileInput.bind('change', options.change); + fu.fileupload('destroy'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + test('disable/enable', function () { + expect( + $.support.xhrFormDataFileUpload ? 4 : 1 + ); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + fu = $('#fileupload').fileupload({ + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + fu.fileupload('disable'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + fu.fileupload('enable'); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + test('option', function () { + expect( + $.support.xhrFormDataFileUpload ? 10 : 7 + ); + var eo = { + originalEvent: { + dataTransfer: {files: [{}], types: ['Files']}, + clipboardData: {items: [{}]} + } + }, + fu = $('#fileupload').fileupload({ + pasteZone: document, + dragover: function () { + ok(true, 'Triggers dragover callback'); + return false; + }, + drop: function () { + ok(true, 'Triggers drop callback'); + return false; + }, + paste: function () { + ok(true, 'Triggers paste callback'); + return false; + }, + change: function () { + ok(true, 'Triggers change callback'); + return false; + } + }), + fileInput = fu.fileupload('option', 'fileInput'), + dropZone = fu.fileupload('option', 'dropZone'), + pasteZone = fu.fileupload('option', 'pasteZone'); + fu.fileupload('option', 'fileInput', null); + fu.fileupload('option', 'dropZone', null); + fu.fileupload('option', 'pasteZone', null); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + fu.fileupload('option', 'dropZone', 'body'); + strictEqual( + fu.fileupload('option', 'dropZone')[0], + document.body, + 'Allow a query string as parameter for the dropZone option' + ); + fu.fileupload('option', 'dropZone', document); + strictEqual( + fu.fileupload('option', 'dropZone')[0], + document, + 'Allow a document element as parameter for the dropZone option' + ); + fu.fileupload('option', 'pasteZone', 'body'); + strictEqual( + fu.fileupload('option', 'pasteZone')[0], + document.body, + 'Allow a query string as parameter for the pasteZone option' + ); + fu.fileupload('option', 'pasteZone', document); + strictEqual( + fu.fileupload('option', 'pasteZone')[0], + document, + 'Allow a document element as parameter for the pasteZone option' + ); + fu.fileupload('option', 'fileInput', ':file'); + strictEqual( + fu.fileupload('option', 'fileInput')[0], + $(':file')[0], + 'Allow a query string as parameter for the fileInput option' + ); + fu.fileupload('option', 'fileInput', $(':file')[0]); + strictEqual( + fu.fileupload('option', 'fileInput')[0], + $(':file')[0], + 'Allow a document element as parameter for the fileInput option' + ); + fu.fileupload('option', 'fileInput', fileInput); + fu.fileupload('option', 'dropZone', dropZone); + fu.fileupload('option', 'pasteZone', pasteZone); + fileInput.trigger($.Event('change', eo)); + dropZone.trigger($.Event('dragover', eo)); + dropZone.trigger($.Event('drop', eo)); + pasteZone.trigger($.Event('paste', eo)); + }); + + asyncTest('add', function () { + expect(2); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + add: function (e, data) { + strictEqual( + data.files[0].name, + param.files[0].name, + 'Triggers add callback' + ); + } + }).fileupload('add', param).fileupload( + 'option', + 'add', + function (e, data) { + data.submit().complete(function () { + ok(true, 'data.submit() Returns a jqXHR object'); + start(); + }); + } + ).fileupload('add', param); + }); + + asyncTest('send', function () { + expect(3); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + send: function (e, data) { + strictEqual( + data.files[0].name, + 'test', + 'Triggers send callback' + ); + } + }).fileupload('send', param).fail(function () { + ok(true, 'Allows to abort the request'); + }).complete(function () { + ok(true, 'Returns a jqXHR object'); + start(); + }).abort(); + }); + + module('Callbacks', lifecycle); + + asyncTest('add', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + add: function () { + ok(true, 'Triggers add callback'); + start(); + } + }).fileupload('add', param); + }); + + asyncTest('submit', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + submit: function () { + ok(true, 'Triggers submit callback'); + start(); + return false; + } + }).fileupload('add', param); + }); + + asyncTest('send', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + send: function () { + ok(true, 'Triggers send callback'); + start(); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('done', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + done: function () { + ok(true, 'Triggers done callback'); + start(); + } + }).fileupload('send', param); + }); + + asyncTest('fail', function () { + expect(1); + var param = {files: [{name: 'test'}]}, + fu = $('#fileupload').fileupload({ + url: '404', + fail: function () { + ok(true, 'Triggers fail callback'); + start(); + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('send', param); + }); + + asyncTest('always', function () { + expect(2); + var param = {files: [{name: 'test'}]}, + counter = 0, + fu = $('#fileupload').fileupload({ + always: function () { + ok(true, 'Triggers always callback'); + if (counter === 1) { + start(); + } else { + counter += 1; + } + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('add', param).fileupload( + 'option', + 'url', + '404' + ).fileupload('add', param); + }); + + asyncTest('progress', function () { + expect(1); + var param = {files: [{name: 'test'}]}, + counter = 0; + $('#fileupload').fileupload({ + forceIframeTransport: true, + progress: function () { + ok(true, 'Triggers progress callback'); + if (counter === 0) { + start(); + } else { + counter += 1; + } + } + }).fileupload('send', param); + }); + + asyncTest('progressall', function () { + expect(1); + var param = {files: [{name: 'test'}]}, + counter = 0; + $('#fileupload').fileupload({ + forceIframeTransport: true, + progressall: function () { + ok(true, 'Triggers progressall callback'); + if (counter === 0) { + start(); + } else { + counter += 1; + } + } + }).fileupload('send', param); + }); + + asyncTest('start', function () { + expect(1); + var param = {files: [{name: '1'}, {name: '2'}]}, + active = 0; + $('#fileupload').fileupload({ + send: function () { + active += 1; + }, + start: function () { + ok(!active, 'Triggers start callback before uploads'); + start(); + } + }).fileupload('send', param); + }); + + asyncTest('stop', function () { + expect(1); + var param = {files: [{name: '1'}, {name: '2'}]}, + active = 0; + $('#fileupload').fileupload({ + send: function () { + active += 1; + }, + always: function () { + active -= 1; + }, + stop: function () { + ok(!active, 'Triggers stop callback after uploads'); + start(); + } + }).fileupload('send', param); + }); + + test('change', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'), + fileInput = fu.fileupload('option', 'fileInput'); + expect(2); + fu.fileupload({ + change: function (e, data) { + ok(true, 'Triggers change callback'); + strictEqual( + data.files.length, + 0, + 'Returns empty files list' + ); + }, + add: $.noop + }); + fuo._onChange({ + data: {fileupload: fuo}, + target: fileInput[0] + }); + }); + + test('paste', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); + expect(1); + fu.fileupload({ + paste: function () { + ok(true, 'Triggers paste callback'); + }, + add: $.noop + }); + fuo._onPaste({ + data: {fileupload: fuo}, + originalEvent: { + dataTransfer: {files: [{}]}, + clipboardData: {items: [{}]} + }, + preventDefault: $.noop + }); + }); + + test('drop', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); + expect(1); + fu.fileupload({ + drop: function () { + ok(true, 'Triggers drop callback'); + }, + add: $.noop + }); + fuo._onDrop({ + data: {fileupload: fuo}, + originalEvent: { + dataTransfer: {files: [{}]}, + clipboardData: {items: [{}]} + }, + preventDefault: $.noop + }); + }); + + test('dragover', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); + expect(1); + fu.fileupload({ + dragover: function () { + ok(true, 'Triggers dragover callback'); + }, + add: $.noop + }); + fuo._onDragOver({ + data: {fileupload: fuo}, + originalEvent: {dataTransfer: {types: ['Files']}}, + preventDefault: $.noop + }); + }); + + module('Options', lifecycle); + + test('paramName', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + paramName: null, + send: function (e, data) { + strictEqual( + data.paramName[0], + data.fileInput.prop('name'), + 'Takes paramName from file input field if not set' + ); + return false; + } + }).fileupload('send', param); + }); + + test('url', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + url: null, + send: function (e, data) { + strictEqual( + data.url, + $(data.fileInput.prop('form')).prop('action'), + 'Takes url from form action if not set' + ); + return false; + } + }).fileupload('send', param); + }); + + test('type', function () { + expect(2); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + type: null, + send: function (e, data) { + strictEqual( + data.type, + 'POST', + 'Request type is "POST" if not set to "PUT"' + ); + return false; + } + }).fileupload('send', param); + $('#fileupload').fileupload({ + type: 'PUT', + send: function (e, data) { + strictEqual( + data.type, + 'PUT', + 'Request type is "PUT" if set to "PUT"' + ); + return false; + } + }).fileupload('send', param); + }); + + test('replaceFileInput', function () { + var fu = $('#fileupload').fileupload(), + fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'), + fileInput = fu.fileupload('option', 'fileInput'), + fileInputElement = fileInput[0]; + expect(2); + fu.fileupload({ + replaceFileInput: false, + change: function () { + strictEqual( + fu.fileupload('option', 'fileInput')[0], + fileInputElement, + 'Keeps file input with replaceFileInput: false' + ); + }, + add: $.noop + }); + fuo._onChange({ + data: {fileupload: fuo}, + target: fileInput[0] + }); + fu.fileupload({ + replaceFileInput: true, + change: function () { + notStrictEqual( + fu.fileupload('option', 'fileInput')[0], + fileInputElement, + 'Replaces file input with replaceFileInput: true' + ); + }, + add: $.noop + }); + fuo._onChange({ + data: {fileupload: fuo}, + target: fileInput[0] + }); + }); + + asyncTest('forceIframeTransport', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + forceIframeTransport: true, + done: function (e, data) { + strictEqual( + data.dataType.substr(0, 6), + 'iframe', + 'Iframe Transport is used' + ); + start(); + } + }).fileupload('send', param); + }); + + test('singleFileUploads', function () { + expect(3); + var fu = $('#fileupload').fileupload(), + param = {files: [{name: '1'}, {name: '2'}]}, + index = 1; + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + $('#fileupload').fileupload({ + singleFileUploads: true, + add: function () { + ok(true, 'Triggers callback number ' + index.toString()); + index += 1; + } + }).fileupload('add', param).fileupload( + 'option', + 'singleFileUploads', + false + ).fileupload('add', param); + }); + + test('limitMultiFileUploads', function () { + expect(3); + var fu = $('#fileupload').fileupload(), + param = {files: [ + {name: '1'}, + {name: '2'}, + {name: '3'}, + {name: '4'}, + {name: '5'} + ]}, + index = 1; + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + $('#fileupload').fileupload({ + singleFileUploads: false, + limitMultiFileUploads: 2, + add: function () { + ok(true, 'Triggers callback number ' + index.toString()); + index += 1; + } + }).fileupload('add', param); + }); + + test('limitMultiFileUploadSize', function () { + expect(7); + var fu = $('#fileupload').fileupload(), + param = {files: [ + {name: '1-1', size: 100000}, + {name: '1-2', size: 40000}, + {name: '2-1', size: 100000}, + {name: '3-1', size: 50000}, + {name: '3-2', size: 40000}, + {name: '4-1', size: 45000} // New request due to limitMultiFileUploads + ]}, + param2 = {files: [ + {name: '5-1'}, + {name: '5-2'}, + {name: '6-1'}, + {name: '6-2'}, + {name: '7-1'} + ]}, + index = 1; + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + $('#fileupload').fileupload({ + singleFileUploads: false, + limitMultiFileUploads: 2, + limitMultiFileUploadSize: 150000, + limitMultiFileUploadSizeOverhead: 5000, + add: function () { + ok(true, 'Triggers callback number ' + index.toString()); + index += 1; + } + }).fileupload('add', param).fileupload('add', param2); + }); + + asyncTest('sequentialUploads', function () { + expect(6); + var param = {files: [ + {name: '1'}, + {name: '2'}, + {name: '3'}, + {name: '4'}, + {name: '5'}, + {name: '6'} + ]}, + addIndex = 0, + sendIndex = 0, + loadIndex = 0, + fu = $('#fileupload').fileupload({ + sequentialUploads: true, + add: function (e, data) { + addIndex += 1; + if (addIndex === 4) { + data.submit().abort(); + } else { + data.submit(); + } + }, + send: function () { + sendIndex += 1; + }, + done: function () { + loadIndex += 1; + strictEqual(sendIndex, loadIndex, 'upload in order'); + }, + fail: function (e, data) { + strictEqual(data.errorThrown, 'abort', 'upload aborted'); + }, + stop: function () { + start(); + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('add', param); + }); + + asyncTest('limitConcurrentUploads', function () { + expect(12); + var param = {files: [ + {name: '1'}, + {name: '2'}, + {name: '3'}, + {name: '4'}, + {name: '5'}, + {name: '6'}, + {name: '7'}, + {name: '8'}, + {name: '9'}, + {name: '10'}, + {name: '11'}, + {name: '12'} + ]}, + addIndex = 0, + sendIndex = 0, + loadIndex = 0, + fu = $('#fileupload').fileupload({ + limitConcurrentUploads: 3, + add: function (e, data) { + addIndex += 1; + if (addIndex === 4) { + data.submit().abort(); + } else { + data.submit(); + } + }, + send: function () { + sendIndex += 1; + }, + done: function () { + loadIndex += 1; + ok(sendIndex - loadIndex < 3); + }, + fail: function (e, data) { + strictEqual(data.errorThrown, 'abort', 'upload aborted'); + }, + stop: function () { + start(); + } + }); + (fu.data('blueimp-fileupload') || fu.data('fileupload')) + ._isXHRUpload = function () { + return true; + }; + fu.fileupload('add', param); + }); + + if ($.support.xhrFileUpload) { + asyncTest('multipart', function () { + expect(2); + var param = {files: [{ + name: 'test.png', + size: 123, + type: 'image/png' + }]}, + fu = $('#fileupload').fileupload({ + multipart: false, + always: function (e, data) { + strictEqual( + data.contentType, + param.files[0].type, + 'non-multipart upload sets file type as contentType' + ); + strictEqual( + data.headers['Content-Disposition'], + 'attachment; filename="' + param.files[0].name + '"', + 'non-multipart upload sets Content-Disposition header' + ); + start(); + } + }); + fu.fileupload('send', param); + }); + } + + module('UI Initialization', lifecycleUI); + + test('Widget initialization', function () { + var fu = $('#fileupload').fileupload(); + ok(fu.data('blueimp-fileupload') || fu.data('fileupload')); + ok( + $('#fileupload').fileupload('option', 'uploadTemplate').length, + 'Initialized upload template' + ); + ok( + $('#fileupload').fileupload('option', 'downloadTemplate').length, + 'Initialized download template' + ); + }); + + test('Buttonbar event listeners', function () { + var buttonbar = $('#fileupload .fileupload-buttonbar'), + files = [{name: 'test'}]; + expect(4); + $('#fileupload').fileupload({ + send: function () { + ok(true, 'Started file upload via global start button'); + }, + fail: function (e, data) { + ok(true, 'Canceled file upload via global cancel button'); + data.context.remove(); + }, + destroy: function () { + ok(true, 'Delete action called via global delete button'); + } + }); + $('#fileupload').fileupload('add', {files: files}); + buttonbar.find('.cancel').click(); + $('#fileupload').fileupload('add', {files: files}); + buttonbar.find('.start').click(); + buttonbar.find('.cancel').click(); + files[0].deleteUrl = 'http://example.org/banana.jpg'; + ($('#fileupload').data('blueimp-fileupload') || + $('#fileupload').data('fileupload')) + ._renderDownload(files) + .appendTo($('#fileupload .files')).show() + .find('.toggle').click(); + buttonbar.find('.delete').click(); + }); + + module('UI API', lifecycleUI); + + test('destroy', function () { + var buttonbar = $('#fileupload .fileupload-buttonbar'), + files = [{name: 'test'}]; + expect(1); + $('#fileupload').fileupload({ + send: function () { + ok(true, 'This test should not run'); + return false; + } + }) + .fileupload('add', {files: files}) + .fileupload('destroy'); + buttonbar.find('.start').click(function () { + ok(true, 'Clicked global start button'); + return false; + }).click(); + }); + + test('disable/enable', function () { + var buttonbar = $('#fileupload .fileupload-buttonbar'); + $('#fileupload').fileupload(); + $('#fileupload').fileupload('disable'); + strictEqual( + buttonbar.find('input[type=file], button').not(':disabled').length, + 0, + 'Disables the buttonbar buttons' + ); + $('#fileupload').fileupload('enable'); + strictEqual( + buttonbar.find('input[type=file], button').not(':disabled').length, + 4, + 'Enables the buttonbar buttons' + ); + }); + + module('UI Callbacks', lifecycleUI); + + test('destroy', function () { + expect(3); + $('#fileupload').fileupload({ + destroy: function (e, data) { + ok(true, 'Triggers destroy callback'); + strictEqual( + data.url, + 'test', + 'Passes over deletion url parameter' + ); + strictEqual( + data.type, + 'DELETE', + 'Passes over deletion request type parameter' + ); + } + }); + ($('#fileupload').data('blueimp-fileupload') || + $('#fileupload').data('fileupload')) + ._renderDownload([{ + name: 'test', + deleteUrl: 'test', + deleteType: 'DELETE' + }]) + .appendTo($('#fileupload .files')) + .show() + .find('.toggle').click(); + $('#fileupload .fileupload-buttonbar .delete').click(); + }); + + asyncTest('added', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + added: function (e, data) { + start(); + strictEqual( + data.files[0].name, + param.files[0].name, + 'Triggers added callback' + ); + }, + send: function () { + return false; + } + }).fileupload('add', param); + }); + + asyncTest('started', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + started: function () { + start(); + ok('Triggers started callback'); + return false; + }, + sent: function () { + return false; + } + }).fileupload('send', param); + }); + + asyncTest('sent', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + sent: function (e, data) { + start(); + strictEqual( + data.files[0].name, + param.files[0].name, + 'Triggers sent callback' + ); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('completed', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + completed: function () { + start(); + ok('Triggers completed callback'); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('failed', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + failed: function () { + start(); + ok('Triggers failed callback'); + return false; + } + }).fileupload('send', param).abort(); + }); + + asyncTest('stopped', function () { + expect(1); + var param = {files: [{name: 'test'}]}; + $('#fileupload').fileupload({ + stopped: function () { + start(); + ok('Triggers stopped callback'); + return false; + } + }).fileupload('send', param); + }); + + asyncTest('destroyed', function () { + expect(1); + $('#fileupload').fileupload({ + dataType: 'html', + destroyed: function () { + start(); + ok(true, 'Triggers destroyed callback'); + } + }); + ($('#fileupload').data('blueimp-fileupload') || + $('#fileupload').data('fileupload')) + ._renderDownload([{ + name: 'test', + deleteUrl: '.', + deleteType: 'GET' + }]) + .appendTo($('#fileupload .files')) + .show() + .find('.toggle').click(); + $('#fileupload .fileupload-buttonbar .delete').click(); + }); + + module('UI Options', lifecycleUI); + + test('autoUpload', function () { + expect(1); + $('#fileupload') + .fileupload({ + autoUpload: true, + send: function () { + ok(true, 'Started file upload automatically'); + return false; + } + }) + .fileupload('add', {files: [{name: 'test'}]}) + .fileupload('option', 'autoUpload', false) + .fileupload('add', {files: [{name: 'test'}]}); + }); + + test('maxNumberOfFiles', function () { + expect(3); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + maxNumberOfFiles: 3, + singleFileUploads: false, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + }, + progress: $.noop, + progressall: $.noop, + done: $.noop, + stop: $.noop + }) + .fileupload('add', {files: [{name: (addIndex += 1)}]}) + .fileupload('add', {files: [{name: (addIndex += 1)}]}) + .fileupload('add', {files: [{name: (addIndex += 1)}]}) + .fileupload('add', {files: [{name: 'test'}]}); + }); + + test('maxFileSize', function () { + expect(2); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + maxFileSize: 1000, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + return false; + } + }) + .fileupload('add', {files: [{ + name: (addIndex += 1) + }]}) + .fileupload('add', {files: [{ + name: (addIndex += 1), + size: 999 + }]}) + .fileupload('add', {files: [{ + name: 'test', + size: 1001 + }]}) + .fileupload({ + send: function (e, data) { + ok( + !$.blueimp.fileupload.prototype.options + .send.call(this, e, data) + ); + return false; + } + }); + }); + + test('minFileSize', function () { + expect(2); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + minFileSize: 1000, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + return false; + } + }) + .fileupload('add', {files: [{ + name: (addIndex += 1) + }]}) + .fileupload('add', {files: [{ + name: (addIndex += 1), + size: 1001 + }]}) + .fileupload('add', {files: [{ + name: 'test', + size: 999 + }]}) + .fileupload({ + send: function (e, data) { + ok( + !$.blueimp.fileupload.prototype.options + .send.call(this, e, data) + ); + return false; + } + }); + }); + + test('acceptFileTypes', function () { + expect(2); + var addIndex = 0, + sendIndex = 0; + $('#fileupload') + .fileupload({ + autoUpload: true, + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + disableImageMetaDataLoad: true, + send: function () { + strictEqual( + sendIndex += 1, + addIndex + ); + return false; + } + }) + .fileupload('add', {files: [{ + name: (addIndex += 1) + '.jpg' + }]}) + .fileupload('add', {files: [{ + name: (addIndex += 1), + type: 'image/jpeg' + }]}) + .fileupload('add', {files: [{ + name: 'test.txt', + type: 'text/plain' + }]}) + .fileupload({ + send: function (e, data) { + ok( + !$.blueimp.fileupload.prototype.options + .send.call(this, e, data) + ); + return false; + } + }); + }); + + test('acceptFileTypes as HTML5 data attribute', function () { + expect(2); + var regExp = /(\.|\/)(gif|jpe?g|png)$/i; + $('#fileupload') + .attr('data-accept-file-types', regExp.toString()) + .fileupload(); + strictEqual( + $.type($('#fileupload').fileupload('option', 'acceptFileTypes')), + $.type(regExp) + ); + strictEqual( + $('#fileupload').fileupload('option', 'acceptFileTypes').toString(), + regExp.toString() + ); + }); + +}); |