├── README.md ├── SECURITY.md ├── VULNERABILITIES.md ├── cors ├── postmessage.html └── result.html ├── css ├── jquery.fileupload-noscript.css ├── jquery.fileupload-ui-noscript.css ├── jquery.fileupload-ui.css └── jquery.fileupload.css ├── img ├── loading.gif └── progressbar.gif ├── index.html ├── js ├── cors │ ├── jquery.postmessage-transport.js │ └── jquery.xdr-transport.js ├── demo.js ├── jquery.fileupload-audio.js ├── jquery.fileupload-image.js ├── jquery.fileupload-process.js ├── jquery.fileupload-ui.js ├── jquery.fileupload-validate.js ├── jquery.fileupload-video.js ├── jquery.fileupload.js ├── jquery.iframe-transport.js └── vendor │ └── jquery.ui.widget.js └── server └── php ├── .dockerignore ├── Dockerfile ├── UploadHandler.php ├── files ├── .gitignore └── .htaccess ├── index.php └── php.ini /README.md: -------------------------------------------------------------------------------- 1 | # jQuery File Upload 2 | 3 | ## Contents 4 | 5 | - [Description](#description) 6 | - [Demo](#demo) 7 | - [Features](#features) 8 | - [Security](#security) 9 | - [Setup](#setup) 10 | - [Requirements](#requirements) 11 | - [Mandatory requirements](#mandatory-requirements) 12 | - [Optional requirements](#optional-requirements) 13 | - [Cross-domain requirements](#cross-domain-requirements) 14 | - [Browsers](#browsers) 15 | - [Desktop browsers](#desktop-browsers) 16 | - [Mobile browsers](#mobile-browsers) 17 | - [Extended browser support information](#extended-browser-support-information) 18 | - [Testing](#testing) 19 | - [Support](#support) 20 | - [License](#license) 21 | 22 | ## Description 23 | 24 | > File Upload widget with multiple file selection, drag&drop support, progress 25 | > bars, validation and preview images, audio and video for jQuery. 26 | > Supports cross-domain, chunked and resumable file uploads and client-side 27 | > image resizing. 28 | > Works with any server-side platform (PHP, Python, Ruby on Rails, Java, 29 | > Node.js, Go etc.) that supports standard HTML form file uploads. 30 | 31 | ## Demo 32 | 33 | [Demo File Upload](https://blueimp.github.io/jQuery-File-Upload/) 34 | 35 | ## Features 36 | 37 | - **Multiple file upload:** 38 | Allows to select multiple files at once and upload them simultaneously. 39 | - **Drag & Drop support:** 40 | Allows to upload files by dragging them from your desktop or file manager and 41 | dropping them on your browser window. 42 | - **Upload progress bar:** 43 | Shows a progress bar indicating the upload progress for individual files and 44 | for all uploads combined. 45 | - **Cancelable uploads:** 46 | Individual file uploads can be canceled to stop the upload progress. 47 | - **Resumable uploads:** 48 | Aborted uploads can be resumed with browsers supporting the Blob API. 49 | - **Chunked uploads:** 50 | Large files can be uploaded in smaller chunks with browsers supporting the 51 | Blob API. 52 | - **Client-side image resizing:** 53 | Images can be automatically resized on client-side with browsers supporting 54 | the required JS APIs. 55 | - **Preview images, audio and video:** 56 | A preview of image, audio and video files can be displayed before uploading 57 | with browsers supporting the required APIs. 58 | - **No browser plugins (e.g. Adobe Flash) required:** 59 | The implementation is based on open standards like HTML5 and JavaScript and 60 | requires no additional browser plugins. 61 | - **Graceful fallback for legacy browsers:** 62 | Uploads files via XMLHttpRequests if supported and uses iframes as fallback 63 | for legacy browsers. 64 | - **HTML file upload form fallback:** 65 | Allows progressive enhancement by using a standard HTML file upload form as 66 | widget element. 67 | - **Cross-site file uploads:** 68 | Supports uploading files to a different domain with cross-site XMLHttpRequests 69 | or iframe redirects. 70 | - **Multiple plugin instances:** 71 | Allows to use multiple plugin instances on the same webpage. 72 | - **Customizable and extensible:** 73 | Provides an API to set individual options and define callback methods for 74 | various upload events. 75 | - **Multipart and file contents stream uploads:** 76 | Files can be uploaded as standard "multipart/form-data" or file contents 77 | stream (HTTP PUT file upload). 78 | - **Compatible with any server-side application platform:** 79 | Works with any server-side platform (PHP, Python, Ruby on Rails, Java, 80 | Node.js, Go etc.) that supports standard HTML form file uploads. 81 | 82 | ## Security 83 | 84 | ⚠️ Please read the [VULNERABILITIES](VULNERABILITIES.md) document for a list of 85 | fixed vulnerabilities 86 | 87 | Please also read the [SECURITY](SECURITY.md) document for instructions on how to 88 | securely configure your Web server for file uploads. 89 | 90 | ## Setup 91 | 92 | jQuery File Upload can be installed via [NPM](https://www.npmjs.com/): 93 | 94 | ```sh 95 | npm install blueimp-file-upload 96 | ``` 97 | 98 | This allows you to include [jquery.fileupload.js](js/jquery.fileupload.js) and 99 | its extensions via `node_modules`, e.g: 100 | 101 | ```html 102 | 103 | ``` 104 | 105 | The widget can then be initialized on a file upload form the following way: 106 | 107 | ```js 108 | $('#fileupload').fileupload(); 109 | ``` 110 | 111 | For further information, please refer to the following guides: 112 | 113 | - [Main documentation page](https://github.com/blueimp/jQuery-File-Upload/wiki) 114 | - [List of all available Options](https://github.com/blueimp/jQuery-File-Upload/wiki/Options) 115 | - [The plugin API](https://github.com/blueimp/jQuery-File-Upload/wiki/API) 116 | - [How to setup the plugin on your website](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) 117 | - [How to use only the basic plugin.](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin) 118 | 119 | ## Requirements 120 | 121 | ### Mandatory requirements 122 | 123 | - [jQuery](https://jquery.com/) v1.7+ 124 | - [jQuery UI widget factory](https://api.jqueryui.com/jQuery.widget/) v1.9+ 125 | (included): Required for the basic File Upload plugin, but very lightweight 126 | without any other dependencies from the jQuery UI suite. 127 | - [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) 128 | (included): Required for 129 | [browsers without XHR file upload support](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). 130 | 131 | ### Optional requirements 132 | 133 | - [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates) 134 | v3+: Used to render the selected and uploaded files. 135 | - [JavaScript Load Image library](https://github.com/blueimp/JavaScript-Load-Image) 136 | v2+: Required for the image previews and resizing functionality. 137 | - [JavaScript Canvas to Blob polyfill](https://github.com/blueimp/JavaScript-Canvas-to-Blob) 138 | v3+:Required for the resizing functionality. 139 | - [blueimp Gallery](https://github.com/blueimp/Gallery) v2+: Used to display the 140 | uploaded images in a lightbox. 141 | - [Bootstrap](https://getbootstrap.com/) v3+: Used for the demo design. 142 | - [Glyphicons](https://glyphicons.com/) Icon set used by Bootstrap. 143 | 144 | ### Cross-domain requirements 145 | 146 | [Cross-domain File Uploads](https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads) 147 | using the 148 | [Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) 149 | require a redirect back to the origin server to retrieve the upload results. The 150 | [example implementation](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/main.js) 151 | makes use of 152 | [result.html](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/result.html) 153 | as a static redirect page for the origin server. 154 | 155 | The repository also includes the 156 | [jQuery XDomainRequest Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/cors/jquery.xdr-transport.js), 157 | which enables limited cross-domain AJAX requests in Microsoft Internet Explorer 158 | 8 and 9 (IE 10 supports cross-domain XHR requests). 159 | The XDomainRequest object allows GET and POST requests only and doesn't support 160 | file uploads. It is used on the 161 | [Demo](https://blueimp.github.io/jQuery-File-Upload/) to delete uploaded files 162 | from the cross-domain demo file upload service. 163 | 164 | ## Browsers 165 | 166 | ### Desktop browsers 167 | 168 | The File Upload plugin is regularly tested with the latest browser versions and 169 | supports the following minimal versions: 170 | 171 | - Google Chrome 172 | - Apple Safari 4.0+ 173 | - Mozilla Firefox 3.0+ 174 | - Opera 11.0+ 175 | - Microsoft Internet Explorer 6.0+ 176 | 177 | ### Mobile browsers 178 | 179 | The File Upload plugin has been tested with and supports the following mobile 180 | browsers: 181 | 182 | - Apple Safari on iOS 6.0+ 183 | - Google Chrome on iOS 6.0+ 184 | - Google Chrome on Android 4.0+ 185 | - Default Browser on Android 2.3+ 186 | - Opera Mobile 12.0+ 187 | 188 | ### Extended browser support information 189 | 190 | For a detailed overview of the features supported by each browser version and 191 | known operating system / browser bugs, please have a look at the 192 | [Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). 193 | 194 | ## Testing 195 | 196 | The project comes with three sets of tests: 197 | 198 | 1. Code linting using [ESLint](https://eslint.org/). 199 | 2. Unit tests using [Mocha](https://mochajs.org/). 200 | 3. End-to-end tests using [blueimp/wdio](https://github.com/blueimp/wdio). 201 | 202 | To run the tests, follow these steps: 203 | 204 | 1. Start [Docker](https://docs.docker.com/). 205 | 2. Install development dependencies: 206 | ```sh 207 | npm install 208 | ``` 209 | 3. Run the tests: 210 | ```sh 211 | npm test 212 | ``` 213 | 214 | ## Support 215 | 216 | This project is actively maintained, but there is no official support channel. 217 | If you have a question that another developer might help you with, please post 218 | to 219 | [Stack Overflow](https://stackoverflow.com/questions/tagged/blueimp+jquery+file-upload) 220 | and tag your question with `blueimp jquery file upload`. 221 | 222 | ## License 223 | 224 | Released under the [MIT license](https://opensource.org/licenses/MIT). 225 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # File Upload Security 2 | 3 | ## Contents 4 | 5 | - [Introduction](#introduction) 6 | - [Purpose of this project](#purpose-of-this-project) 7 | - [Mitigations against file upload risks](#mitigations-against-file-upload-risks) 8 | - [Prevent code execution on the server](#prevent-code-execution-on-the-server) 9 | - [Prevent code execution in the browser](#prevent-code-execution-in-the-browser) 10 | - [Prevent distribution of malware](#prevent-distribution-of-malware) 11 | - [Secure file upload serving configurations](#secure-file-upload-serving-configurations) 12 | - [Apache config](#apache-config) 13 | - [NGINX config](#nginx-config) 14 | - [Secure image processing configurations](#secure-image-processing-configurations) 15 | - [ImageMagick config](#imagemagick-config) 16 | 17 | ## Introduction 18 | 19 | For an in-depth understanding of the potential security risks of providing file 20 | uploads and possible mitigations, please refer to the 21 | [OWASP - Unrestricted File Upload](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload) 22 | documentation. 23 | 24 | To securely setup the project to serve uploaded files, please refer to the 25 | sample 26 | [Secure file upload serving configurations](#secure-file-upload-serving-configurations). 27 | 28 | To mitigate potential vulnerabilities in image processing libraries, please 29 | refer to the 30 | [Secure image processing configurations](#secure-image-processing-configurations). 31 | 32 | By default, all sample upload handlers allow only upload of image files, which 33 | mitigates some attack vectors, but should not be relied on as the only 34 | protection. 35 | 36 | Please also have a look at the 37 | [list of fixed vulnerabilities](VULNERABILITIES.md) in jQuery File Upload, which 38 | relates mostly to the sample server-side upload handlers and how they have been 39 | configured. 40 | 41 | ## Purpose of this project 42 | 43 | Please note that this project is not a complete file management product, but 44 | foremost a client-side file upload library for [jQuery](https://jquery.com/). 45 | The server-side sample upload handlers are just examples to demonstrate the 46 | client-side file upload functionality. 47 | 48 | To make this very clear, there is **no user authentication** by default: 49 | 50 | - **everyone can upload files** 51 | - **everyone can delete uploaded files** 52 | 53 | In some cases this can be acceptable, but for most projects you will want to 54 | extend the sample upload handlers to integrate user authentication, or implement 55 | your own. 56 | 57 | It is also up to you to configure your web server to securely serve the uploaded 58 | files, e.g. using the 59 | [sample server configurations](#secure-file-upload-serving-configurations). 60 | 61 | ## Mitigations against file upload risks 62 | 63 | ### Prevent code execution on the server 64 | 65 | To prevent execution of scripts or binaries on server-side, the upload directory 66 | must be configured to not execute files in the upload directory (e.g. 67 | `server/php/files` as the default for the PHP upload handler) and only treat 68 | uploaded files as static content. 69 | 70 | The recommended way to do this is to configure the upload directory path to 71 | point outside of the web application root. 72 | Then the web server can be configured to serve files from the upload directory 73 | with their default static files handler only. 74 | 75 | Limiting file uploads to a whitelist of safe file types (e.g. image files) also 76 | mitigates this issue, but should not be the only protection. 77 | 78 | ### Prevent code execution in the browser 79 | 80 | To prevent execution of scripts on client-side, the following headers must be 81 | sent when delivering generic uploaded files to the client: 82 | 83 | ``` 84 | Content-Type: application/octet-stream 85 | X-Content-Type-Options: nosniff 86 | ``` 87 | 88 | The `Content-Type: application/octet-stream` header instructs browsers to 89 | display a download dialog instead of parsing it and possibly executing script 90 | content e.g. in HTML files. 91 | 92 | The `X-Content-Type-Options: nosniff` header prevents browsers to try to detect 93 | the file mime type despite the given content-type header. 94 | 95 | For known safe files, the content-type header can be adjusted using a 96 | **whitelist**, e.g. sending `Content-Type: image/png` for PNG files. 97 | 98 | ### Prevent distribution of malware 99 | 100 | To prevent attackers from uploading and distributing malware (e.g. computer 101 | viruses), it is recommended to limit file uploads only to a whitelist of safe 102 | file types. 103 | 104 | Please note that the detection of file types in the sample file upload handlers 105 | is based on the file extension and not the actual file content. This makes it 106 | still possible for attackers to upload malware by giving their files an image 107 | file extension, but should prevent automatic execution on client computers when 108 | opening those files. 109 | 110 | It does not protect at all from exploiting vulnerabilities in image display 111 | programs, nor from users renaming file extensions to inadvertently execute the 112 | contained malicious code. 113 | 114 | ## Secure file upload serving configurations 115 | 116 | The following configurations serve uploaded files as static files with the 117 | proper headers as 118 | [mitigation against file upload risks](#mitigations-against-file-upload-risks). 119 | Please do not simply copy&paste these configurations, but make sure you 120 | understand what they are doing and that you have implemented them correctly. 121 | 122 | > Always test your own setup and make sure that it is secure! 123 | 124 | e.g. try uploading PHP scripts (as "example.php", "example.php.png" and 125 | "example.png") to see if they get executed by your web server, e.g. the content 126 | of the following sample: 127 | 128 | ```php 129 | GIF89ad 140 | # Some of the directives require the Apache Headers module. If it is not 141 | # already enabled, please execute the following command and reload Apache: 142 | # sudo a2enmod headers 143 | # 144 | # Please note that the order of directives across configuration files matters, 145 | # see also: 146 | # https://httpd.apache.org/docs/current/sections.html#merging 147 | 148 | # The following directive matches all files and forces them to be handled as 149 | # static content, which prevents the server from parsing and executing files 150 | # that are associated with a dynamic runtime, e.g. PHP files. 151 | # It also forces their Content-Type header to "application/octet-stream" and 152 | # adds a "Content-Disposition: attachment" header to force a download dialog, 153 | # which prevents browsers from interpreting files in the context of the 154 | # web server, e.g. HTML files containing JavaScript. 155 | # Lastly it also prevents browsers from MIME-sniffing the Content-Type, 156 | # preventing them from interpreting a file as a different Content-Type than 157 | # the one sent by the webserver. 158 | 159 | SetHandler default-handler 160 | ForceType application/octet-stream 161 | Header set Content-Disposition attachment 162 | Header set X-Content-Type-Options nosniff 163 | 164 | 165 | # The following directive matches known image files and unsets the forced 166 | # Content-Type so they can be served with their original mime type. 167 | # It also unsets the Content-Disposition header to allow displaying them 168 | # inline in the browser. 169 | 170 | ForceType none 171 | Header unset Content-Disposition 172 | 173 | 174 | ``` 175 | 176 | ### NGINX config 177 | 178 | Add the following directive to the NGINX config, replacing the directory path 179 | with the absolute path to the upload directory: 180 | 181 | ```Nginx 182 | location ^~ /path/to/project/server/php/files { 183 | root html; 184 | default_type application/octet-stream; 185 | types { 186 | image/gif gif; 187 | image/jpeg jpg; 188 | image/png png; 189 | } 190 | add_header X-Content-Type-Options 'nosniff'; 191 | if ($request_filename ~ /(((?!\.(jpg)|(png)|(gif)$)[^/])+$)) { 192 | add_header Content-Disposition 'attachment; filename="$1"'; 193 | # Add X-Content-Type-Options again, as using add_header in a new context 194 | # dismisses all previous add_header calls: 195 | add_header X-Content-Type-Options 'nosniff'; 196 | } 197 | } 198 | ``` 199 | 200 | ## Secure image processing configurations 201 | 202 | The following configuration mitigates 203 | [potential image processing vulnerabilities with ImageMagick](VULNERABILITIES.md#potential-vulnerabilities-with-php-imagemagick) 204 | by limiting the attack vectors to a small subset of image types 205 | (`GIF/JPEG/PNG`). 206 | 207 | Please also consider using alternative, safer image processing libraries like 208 | [libvips](https://github.com/libvips/libvips) or 209 | [imageflow](https://github.com/imazen/imageflow). 210 | 211 | ## ImageMagick config 212 | 213 | It is recommended to disable all non-required ImageMagick coders via 214 | [policy.xml](https://wiki.debian.org/imagemagick/security). 215 | To do so, locate the ImageMagick `policy.xml` configuration file and add the 216 | following policies: 217 | 218 | ```xml 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | ``` 228 | -------------------------------------------------------------------------------- /VULNERABILITIES.md: -------------------------------------------------------------------------------- 1 | # List of fixed vulnerabilities 2 | 3 | ## Contents 4 | 5 | - [Potential vulnerabilities with PHP+ImageMagick](#potential-vulnerabilities-with-phpimagemagick) 6 | - [Remote code execution vulnerability in the PHP component](#remote-code-execution-vulnerability-in-the-php-component) 7 | - [Open redirect vulnerability in the GAE components](#open-redirect-vulnerability-in-the-gae-components) 8 | - [Cross-site scripting vulnerability in the Iframe Transport](#cross-site-scripting-vulnerability-in-the-iframe-transport) 9 | 10 | ## Potential vulnerabilities with PHP+ImageMagick 11 | 12 | > Mitigated: 2018-10-25 (GMT) 13 | 14 | The sample [PHP upload handler](server/php/UploadHandler.php) before 15 | [v9.25.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.25.1) 16 | did not validate file signatures before invoking 17 | [ImageMagick](https://www.imagemagick.org/) (via 18 | [Imagick](https://php.net/manual/en/book.imagick.php)). 19 | Verifying those 20 | [magic bytes](https://en.wikipedia.org/wiki/List_of_file_signatures) mitigates 21 | potential vulnerabilities when handling input files other than `GIF/JPEG/PNG`. 22 | 23 | Please also configure ImageMagick to only enable the coders required for 24 | `GIF/JPEG/PNG` processing, e.g. with the sample 25 | [ImageMagick config](SECURITY.md#imagemagick-config). 26 | 27 | **Further information:** 28 | 29 | - Commit containing the mitigation: 30 | [fe44d34](https://github.com/blueimp/jQuery-File-Upload/commit/fe44d34be43be32c6b8d507932f318dababb25dd) 31 | - [ImageTragick](https://imagetragick.com/) 32 | - [CERT Vulnerability Note VU#332928](https://www.kb.cert.org/vuls/id/332928) 33 | - [ImageMagick CVE entries](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=imagemagick) 34 | 35 | ## Remote code execution vulnerability in the PHP component 36 | 37 | > Fixed: 2018-10-23 (GMT) 38 | 39 | The sample [PHP upload handler](server/php/UploadHandler.php) before 40 | [v9.24.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.24.1) 41 | allowed to upload all file types by default. 42 | This opens up a remote code execution vulnerability, unless the server is 43 | configured to not execute (PHP) files in the upload directory 44 | (`server/php/files`). 45 | 46 | The provided [.htaccess](server/php/files/.htaccess) file includes instructions 47 | for Apache to disable script execution, however 48 | [.htaccess support](https://httpd.apache.org/docs/current/howto/htaccess.html) 49 | is disabled by default since Apache `v2.3.9` via 50 | [AllowOverride Directive](https://httpd.apache.org/docs/current/mod/core.html#allowoverride). 51 | 52 | **You are affected if you:** 53 | 54 | 1. A) Uploaded jQuery File Upload < `v9.24.1` on a Webserver that executes files 55 | with `.php` as part of the file extension (e.g. "example.php.png"), e.g. 56 | Apache with `mod_php` enabled and the following directive (_not a recommended 57 | configuration_): 58 | ```ApacheConf 59 | AddHandler php5-script .php 60 | ``` 61 | B) Uploaded jQuery File Upload < `v9.22.1` on a Webserver that executes files 62 | with the file extension `.php`, e.g. Apache with `mod_php` enabled and the 63 | following directive: 64 | ```ApacheConf 65 | 66 | SetHandler application/x-httpd-php 67 | 68 | ``` 69 | 2. Did not actively configure your Webserver to not execute files in the upload 70 | directory (`server/php/files`). 71 | 3. Are running Apache `v2.3.9+` with the default `AllowOverride` Directive set 72 | to `None` or another Webserver with no `.htaccess` support. 73 | 74 | **How to fix it:** 75 | 76 | 1. Upgrade to the latest version of jQuery File Upload. 77 | 2. Configure your Webserver to not execute files in the upload directory, e.g. 78 | with the [sample Apache configuration](SECURITY.md#apache-config) 79 | 80 | **Further information:** 81 | 82 | - Commits containing the security fix: 83 | [aeb47e5](https://github.com/blueimp/jQuery-File-Upload/commit/aeb47e51c67df8a504b7726595576c1c66b5dc2f), 84 | [ad4aefd](https://github.com/blueimp/jQuery-File-Upload/commit/ad4aefd96e4056deab6fea2690f0d8cf56bb2d7d) 85 | - [Full disclosure post on Hacker News](https://news.ycombinator.com/item?id=18267309). 86 | - [CVE-2018-9206](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-9206) 87 | - [OWASP - Unrestricted File Upload](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload) 88 | 89 | ## Open redirect vulnerability in the GAE components 90 | 91 | > Fixed: 2015-06-12 (GMT) 92 | 93 | The sample Google App Engine upload handlers before 94 | v[9.10.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/9.10.1) 95 | accepted any URL as redirect target, making it possible to use the Webserver's 96 | domain for phishing attacks. 97 | 98 | **Further information:** 99 | 100 | - Commit containing the security fix: 101 | [f74d2a8](https://github.com/blueimp/jQuery-File-Upload/commit/f74d2a8c3e3b1e8e336678d2899facd5bcdb589f) 102 | - [OWASP - Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) 103 | 104 | ## Cross-site scripting vulnerability in the Iframe Transport 105 | 106 | > Fixed: 2012-08-09 (GMT) 107 | 108 | The [redirect page](cors/result.html) for the 109 | [Iframe Transport](js/jquery.iframe-transport.js) before commit 110 | [4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a) 111 | (_fixed in all tagged releases_) allowed executing arbitrary JavaScript in the 112 | context of the Webserver. 113 | 114 | **Further information:** 115 | 116 | - Commit containing the security fix: 117 | [4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a) 118 | - [OWASP - Cross-site Scripting (XSS)](https://owasp.org/www-community/attacks/xss/) 119 | -------------------------------------------------------------------------------- /cors/postmessage.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | jQuery File Upload Plugin postMessage API 18 | 23 | 24 | 25 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /cors/result.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | jQuery Iframe Transport Plugin Redirect Page 18 | 19 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /css/jquery.fileupload-noscript.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Plugin NoScript CSS 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * https://opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button input { 14 | position: static; 15 | opacity: 1; 16 | filter: none; 17 | font-size: inherit !important; 18 | direction: inherit; 19 | } 20 | .fileinput-button span { 21 | display: none; 22 | } 23 | -------------------------------------------------------------------------------- /css/jquery.fileupload-ui-noscript.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload UI Plugin NoScript CSS 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2012, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * https://opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button i, 14 | .fileupload-buttonbar .delete, 15 | .fileupload-buttonbar .toggle { 16 | display: none; 17 | } 18 | -------------------------------------------------------------------------------- /css/jquery.fileupload-ui.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload UI Plugin CSS 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2010, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * https://opensource.org/licenses/MIT 11 | */ 12 | 13 | .progress-animated .progress-bar, 14 | .progress-animated .bar { 15 | background: url('../img/progressbar.gif') !important; 16 | filter: none; 17 | } 18 | .fileupload-process { 19 | float: right; 20 | display: none; 21 | } 22 | .fileupload-processing .fileupload-process, 23 | .files .processing .preview { 24 | display: block; 25 | width: 32px; 26 | height: 32px; 27 | background: url('../img/loading.gif') center no-repeat; 28 | background-size: contain; 29 | } 30 | .files audio, 31 | .files video { 32 | max-width: 300px; 33 | } 34 | .files .name { 35 | word-wrap: break-word; 36 | overflow-wrap: anywhere; 37 | -webkit-hyphens: auto; 38 | hyphens: auto; 39 | } 40 | .files button { 41 | margin-bottom: 5px; 42 | } 43 | .toggle[type='checkbox'] { 44 | transform: scale(2); 45 | margin-left: 10px; 46 | } 47 | 48 | @media (max-width: 767px) { 49 | .fileupload-buttonbar .btn { 50 | margin-bottom: 5px; 51 | } 52 | .fileupload-buttonbar .delete, 53 | .fileupload-buttonbar .toggle, 54 | .files .toggle, 55 | .files .btn span { 56 | display: none; 57 | } 58 | .files audio, 59 | .files video { 60 | max-width: 80px; 61 | } 62 | } 63 | 64 | @media (max-width: 480px) { 65 | .files .image td:nth-child(2) { 66 | display: none; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /css/jquery.fileupload.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Plugin CSS 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * https://opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button { 14 | position: relative; 15 | overflow: hidden; 16 | display: inline-block; 17 | } 18 | .fileinput-button input { 19 | position: absolute; 20 | top: 0; 21 | right: 0; 22 | margin: 0; 23 | height: 100%; 24 | opacity: 0; 25 | filter: alpha(opacity=0); 26 | font-size: 200px !important; 27 | direction: ltr; 28 | cursor: pointer; 29 | } 30 | 31 | /* Fixes for IE < 8 */ 32 | @media screen\9 { 33 | .fileinput-button input { 34 | font-size: 150% !important; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Huniko519/JQuery-Muiltiple-File-Upload/6e48df161a01878d30e65b9ac7a930b2b63be48e/img/loading.gif -------------------------------------------------------------------------------- /img/progressbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Huniko519/JQuery-Muiltiple-File-Upload/6e48df161a01878d30e65b9ac7a930b2b63be48e/img/progressbar.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 20 | 21 | jQuery File Upload Demo 22 | 26 | 27 | 28 | 34 | 35 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 58 | 61 | 62 | 63 |
64 | 78 |

jQuery File Upload Demo

79 |
80 |

81 | File Upload widget with multiple file selection, drag&drop 82 | support, progress bars, validation and preview images, audio and video 83 | for jQuery.
84 | Supports cross-domain, chunked and resumable file uploads and 85 | client-side image resizing.
86 | Works with any server-side platform (PHP, Python, Ruby on Rails, Java, 87 | Node.js, Go etc.) that supports standard HTML form file uploads. 88 |

89 |
90 | 91 |
97 | 98 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | Add files... 111 | 112 | 113 | 117 | 121 | 125 | 126 | 127 | 128 |
129 | 130 |
131 | 132 |
138 |
142 |
143 | 144 |
 
145 |
146 |
147 | 148 | 149 | 150 | 151 |
152 |
153 |
154 |

Demo Notes

155 |
156 |
157 |
    158 |
  • 159 | The maximum file size for uploads in this demo is 160 | 999 KB (default file size is unlimited). 161 |
  • 162 |
  • 163 | Only image files (JPG, GIF, PNG) are allowed in 164 | this demo (by default there is no file type restriction). 165 |
  • 166 |
  • 167 | Uploaded files will be deleted automatically after 168 | 5 minutes or less (demo files are stored in 169 | memory). 170 |
  • 171 |
  • 172 | You can drag & drop files from your desktop 173 | on this webpage (see 174 | Browser support). 178 |
  • 179 |
  • 180 | Please refer to the 181 | project website 184 | and 185 | documentation 188 | for more information. 189 |
  • 190 |
  • 191 | Built with the 192 | Bootstrap CSS framework 193 | and Icons from Glyphicons. 194 |
  • 195 |
196 |
197 |
198 |
199 | 200 | 238 | 239 | 276 | 277 | 319 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /js/cors/jquery.postmessage-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery postMessage Transport Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory(require('jquery')); 22 | } else { 23 | // Browser globals: 24 | factory(window.jQuery); 25 | } 26 | })(function ($) { 27 | 'use strict'; 28 | 29 | var counter = 0, 30 | names = [ 31 | 'accepts', 32 | 'cache', 33 | 'contents', 34 | 'contentType', 35 | 'crossDomain', 36 | 'data', 37 | 'dataType', 38 | 'headers', 39 | 'ifModified', 40 | 'mimeType', 41 | 'password', 42 | 'processData', 43 | 'timeout', 44 | 'traditional', 45 | 'type', 46 | 'url', 47 | 'username' 48 | ], 49 | convert = function (p) { 50 | return p; 51 | }; 52 | 53 | $.ajaxSetup({ 54 | converters: { 55 | 'postmessage text': convert, 56 | 'postmessage json': convert, 57 | 'postmessage html': convert 58 | } 59 | }); 60 | 61 | $.ajaxTransport('postmessage', function (options) { 62 | if (options.postMessage && window.postMessage) { 63 | var iframe, 64 | loc = $('').prop('href', options.postMessage)[0], 65 | target = loc.protocol + '//' + loc.host, 66 | xhrUpload = options.xhr().upload; 67 | // IE always includes the port for the host property of a link 68 | // element, but not in the location.host or origin property for the 69 | // default http port 80 and https port 443, so we strip it: 70 | if (/^(http:\/\/.+:80)|(https:\/\/.+:443)$/.test(target)) { 71 | target = target.replace(/:(80|443)$/, ''); 72 | } 73 | return { 74 | send: function (_, completeCallback) { 75 | counter += 1; 76 | var message = { 77 | id: 'postmessage-transport-' + counter 78 | }, 79 | eventName = 'message.' + message.id; 80 | iframe = $( 81 | '' 86 | ) 87 | .on('load', function () { 88 | $.each(names, function (i, name) { 89 | message[name] = options[name]; 90 | }); 91 | message.dataType = message.dataType.replace('postmessage ', ''); 92 | $(window).on(eventName, function (event) { 93 | var e = event.originalEvent; 94 | var data = e.data; 95 | var ev; 96 | if (e.origin === target && data.id === message.id) { 97 | if (data.type === 'progress') { 98 | ev = document.createEvent('Event'); 99 | ev.initEvent(data.type, false, true); 100 | $.extend(ev, data); 101 | xhrUpload.dispatchEvent(ev); 102 | } else { 103 | completeCallback( 104 | data.status, 105 | data.statusText, 106 | { postmessage: data.result }, 107 | data.headers 108 | ); 109 | iframe.remove(); 110 | $(window).off(eventName); 111 | } 112 | } 113 | }); 114 | iframe[0].contentWindow.postMessage(message, target); 115 | }) 116 | .appendTo(document.body); 117 | }, 118 | abort: function () { 119 | if (iframe) { 120 | iframe.remove(); 121 | } 122 | } 123 | }; 124 | } 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /js/cors/jquery.xdr-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery XDomainRequest Transport Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | * 11 | * Based on Julian Aubourg's ajaxHooks xdr.js: 12 | * https://github.com/jaubourg/ajaxHooks/ 13 | */ 14 | 15 | /* global define, require, XDomainRequest */ 16 | 17 | (function (factory) { 18 | 'use strict'; 19 | if (typeof define === 'function' && define.amd) { 20 | // Register as an anonymous AMD module: 21 | define(['jquery'], factory); 22 | } else if (typeof exports === 'object') { 23 | // Node/CommonJS: 24 | factory(require('jquery')); 25 | } else { 26 | // Browser globals: 27 | factory(window.jQuery); 28 | } 29 | })(function ($) { 30 | 'use strict'; 31 | if (window.XDomainRequest && !$.support.cors) { 32 | $.ajaxTransport(function (s) { 33 | if (s.crossDomain && s.async) { 34 | if (s.timeout) { 35 | s.xdrTimeout = s.timeout; 36 | delete s.timeout; 37 | } 38 | var xdr; 39 | return { 40 | send: function (headers, completeCallback) { 41 | var addParamChar = /\?/.test(s.url) ? '&' : '?'; 42 | /** 43 | * Callback wrapper function 44 | * 45 | * @param {number} status HTTP status code 46 | * @param {string} statusText HTTP status text 47 | * @param {object} [responses] Content-type specific responses 48 | * @param {string} [responseHeaders] Response headers string 49 | */ 50 | function callback(status, statusText, responses, responseHeaders) { 51 | xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; 52 | xdr = null; 53 | completeCallback(status, statusText, responses, responseHeaders); 54 | } 55 | xdr = new XDomainRequest(); 56 | // XDomainRequest only supports GET and POST: 57 | if (s.type === 'DELETE') { 58 | s.url = s.url + addParamChar + '_method=DELETE'; 59 | s.type = 'POST'; 60 | } else if (s.type === 'PUT') { 61 | s.url = s.url + addParamChar + '_method=PUT'; 62 | s.type = 'POST'; 63 | } else if (s.type === 'PATCH') { 64 | s.url = s.url + addParamChar + '_method=PATCH'; 65 | s.type = 'POST'; 66 | } 67 | xdr.open(s.type, s.url); 68 | xdr.onload = function () { 69 | callback( 70 | 200, 71 | 'OK', 72 | { text: xdr.responseText }, 73 | 'Content-Type: ' + xdr.contentType 74 | ); 75 | }; 76 | xdr.onerror = function () { 77 | callback(404, 'Not Found'); 78 | }; 79 | if (s.xdrTimeout) { 80 | xdr.ontimeout = function () { 81 | callback(0, 'timeout'); 82 | }; 83 | xdr.timeout = s.xdrTimeout; 84 | } 85 | xdr.send((s.hasContent && s.data) || null); 86 | }, 87 | abort: function () { 88 | if (xdr) { 89 | xdr.onerror = $.noop(); 90 | xdr.abort(); 91 | } 92 | } 93 | }; 94 | } 95 | }); 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /js/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Demo 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global $ */ 13 | 14 | $(function () { 15 | 'use strict'; 16 | 17 | // Initialize the jQuery File Upload widget: 18 | $('#fileupload').fileupload({ 19 | // Uncomment the following to send cross-domain cookies: 20 | //xhrFields: {withCredentials: true}, 21 | url: 'server/php/' 22 | }); 23 | 24 | // Enable iframe cross-domain access via redirect option: 25 | $('#fileupload').fileupload( 26 | 'option', 27 | 'redirect', 28 | window.location.href.replace(/\/[^/]*$/, '/cors/result.html?%s') 29 | ); 30 | 31 | if (window.location.hostname === 'blueimp.github.io') { 32 | // Demo settings: 33 | $('#fileupload').fileupload('option', { 34 | url: '//jquery-file-upload.appspot.com/', 35 | // Enable image resizing, except for Android and Opera, 36 | // which actually support image resizing, but fail to 37 | // send Blob objects via XHR requests: 38 | disableImageResize: /Android(?!.*Chrome)|Opera/.test( 39 | window.navigator.userAgent 40 | ), 41 | maxFileSize: 999000, 42 | acceptFileTypes: /(\.|\/)(xls?x|csv|xml|json|txt)$/i 43 | }); 44 | // Upload server status check for browsers with CORS support: 45 | if ($.support.cors) { 46 | $.ajax({ 47 | url: '//jquery-file-upload.appspot.com/', 48 | type: 'HEAD' 49 | }).fail(function () { 50 | $('
') 51 | .text('Upload server currently unavailable - ' + new Date()) 52 | .appendTo('#fileupload'); 53 | }); 54 | } 55 | } else { 56 | // Load existing files: 57 | $('#fileupload').addClass('fileupload-processing'); 58 | $.ajax({ 59 | // Uncomment the following to send cross-domain cookies: 60 | //xhrFields: {withCredentials: true}, 61 | url: $('#fileupload').fileupload('option', 'url'), 62 | dataType: 'json', 63 | context: $('#fileupload')[0] 64 | }) 65 | .always(function () { 66 | $(this).removeClass('fileupload-processing'); 67 | }) 68 | .done(function (result) { 69 | $(this) 70 | .fileupload('option', 'done') 71 | // eslint-disable-next-line new-cap 72 | .call(this, $.Event('done'), { result: result }); 73 | }); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /js/jquery.fileupload-audio.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Audio Preview Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery', 'load-image', './jquery.fileupload-process'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory( 22 | require('jquery'), 23 | require('blueimp-load-image/js/load-image'), 24 | require('./jquery.fileupload-process') 25 | ); 26 | } else { 27 | // Browser globals: 28 | factory(window.jQuery, window.loadImage); 29 | } 30 | })(function ($, loadImage) { 31 | 'use strict'; 32 | 33 | // Prepend to the default processQueue: 34 | $.blueimp.fileupload.prototype.options.processQueue.unshift( 35 | { 36 | action: 'loadAudio', 37 | // Use the action as prefix for the "@" options: 38 | prefix: true, 39 | fileTypes: '@', 40 | maxFileSize: '@', 41 | disabled: '@disableAudioPreview' 42 | }, 43 | { 44 | action: 'setAudio', 45 | name: '@audioPreviewName', 46 | disabled: '@disableAudioPreview' 47 | } 48 | ); 49 | 50 | // The File Upload Audio Preview plugin extends the fileupload widget 51 | // with audio preview functionality: 52 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 53 | options: { 54 | // The regular expression for the types of audio files to load, 55 | // matched against the file type: 56 | loadAudioFileTypes: /^audio\/.*$/ 57 | }, 58 | 59 | _audioElement: document.createElement('audio'), 60 | 61 | processActions: { 62 | // Loads the audio file given via data.files and data.index 63 | // as audio element if the browser supports playing it. 64 | // Accepts the options fileTypes (regular expression) 65 | // and maxFileSize (integer) to limit the files to load: 66 | loadAudio: function (data, options) { 67 | if (options.disabled) { 68 | return data; 69 | } 70 | var file = data.files[data.index], 71 | url, 72 | audio; 73 | if ( 74 | this._audioElement.canPlayType && 75 | this._audioElement.canPlayType(file.type) && 76 | ($.type(options.maxFileSize) !== 'number' || 77 | file.size <= options.maxFileSize) && 78 | (!options.fileTypes || options.fileTypes.test(file.type)) 79 | ) { 80 | url = loadImage.createObjectURL(file); 81 | if (url) { 82 | audio = this._audioElement.cloneNode(false); 83 | audio.src = url; 84 | audio.controls = true; 85 | data.audio = audio; 86 | return data; 87 | } 88 | } 89 | return data; 90 | }, 91 | 92 | // Sets the audio element as a property of the file object: 93 | setAudio: function (data, options) { 94 | if (data.audio && !options.disabled) { 95 | data.files[data.index][options.name || 'preview'] = data.audio; 96 | } 97 | return data; 98 | } 99 | } 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /js/jquery.fileupload-image.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Image Preview & Resize Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define([ 19 | 'jquery', 20 | 'load-image', 21 | 'load-image-meta', 22 | 'load-image-scale', 23 | 'load-image-exif', 24 | 'load-image-orientation', 25 | 'canvas-to-blob', 26 | './jquery.fileupload-process' 27 | ], factory); 28 | } else if (typeof exports === 'object') { 29 | // Node/CommonJS: 30 | factory( 31 | require('jquery'), 32 | require('blueimp-load-image/js/load-image'), 33 | require('blueimp-load-image/js/load-image-meta'), 34 | require('blueimp-load-image/js/load-image-scale'), 35 | require('blueimp-load-image/js/load-image-exif'), 36 | require('blueimp-load-image/js/load-image-orientation'), 37 | require('blueimp-canvas-to-blob'), 38 | require('./jquery.fileupload-process') 39 | ); 40 | } else { 41 | // Browser globals: 42 | factory(window.jQuery, window.loadImage); 43 | } 44 | })(function ($, loadImage) { 45 | 'use strict'; 46 | 47 | // Prepend to the default processQueue: 48 | $.blueimp.fileupload.prototype.options.processQueue.unshift( 49 | { 50 | action: 'loadImageMetaData', 51 | maxMetaDataSize: '@', 52 | disableImageHead: '@', 53 | disableMetaDataParsers: '@', 54 | disableExif: '@', 55 | disableExifOffsets: '@', 56 | includeExifTags: '@', 57 | excludeExifTags: '@', 58 | disableIptc: '@', 59 | disableIptcOffsets: '@', 60 | includeIptcTags: '@', 61 | excludeIptcTags: '@', 62 | disabled: '@disableImageMetaDataLoad' 63 | }, 64 | { 65 | action: 'loadImage', 66 | // Use the action as prefix for the "@" options: 67 | prefix: true, 68 | fileTypes: '@', 69 | maxFileSize: '@', 70 | noRevoke: '@', 71 | disabled: '@disableImageLoad' 72 | }, 73 | { 74 | action: 'resizeImage', 75 | // Use "image" as prefix for the "@" options: 76 | prefix: 'image', 77 | maxWidth: '@', 78 | maxHeight: '@', 79 | minWidth: '@', 80 | minHeight: '@', 81 | crop: '@', 82 | orientation: '@', 83 | forceResize: '@', 84 | disabled: '@disableImageResize', 85 | imageSmoothingQuality: '@imageSmoothingQuality' 86 | }, 87 | { 88 | action: 'saveImage', 89 | quality: '@imageQuality', 90 | type: '@imageType', 91 | disabled: '@disableImageResize' 92 | }, 93 | { 94 | action: 'saveImageMetaData', 95 | disabled: '@disableImageMetaDataSave' 96 | }, 97 | { 98 | action: 'resizeImage', 99 | // Use "preview" as prefix for the "@" options: 100 | prefix: 'preview', 101 | maxWidth: '@', 102 | maxHeight: '@', 103 | minWidth: '@', 104 | minHeight: '@', 105 | crop: '@', 106 | orientation: '@', 107 | thumbnail: '@', 108 | canvas: '@', 109 | disabled: '@disableImagePreview' 110 | }, 111 | { 112 | action: 'setImage', 113 | name: '@imagePreviewName', 114 | disabled: '@disableImagePreview' 115 | }, 116 | { 117 | action: 'deleteImageReferences', 118 | disabled: '@disableImageReferencesDeletion' 119 | } 120 | ); 121 | 122 | // The File Upload Resize plugin extends the fileupload widget 123 | // with image resize functionality: 124 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 125 | options: { 126 | // The regular expression for the types of images to load: 127 | // matched against the file type: 128 | loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, 129 | // The maximum file size of images to load: 130 | loadImageMaxFileSize: 10000000, // 10MB 131 | // The maximum width of resized images: 132 | imageMaxWidth: 1920, 133 | // The maximum height of resized images: 134 | imageMaxHeight: 1080, 135 | // Defines the image orientation (1-8) or takes the orientation 136 | // value from Exif data if set to true: 137 | imageOrientation: true, 138 | // Define if resized images should be cropped or only scaled: 139 | imageCrop: false, 140 | // Disable the resize image functionality by default: 141 | disableImageResize: true, 142 | // The maximum width of the preview images: 143 | previewMaxWidth: 80, 144 | // The maximum height of the preview images: 145 | previewMaxHeight: 80, 146 | // Defines the preview orientation (1-8) or takes the orientation 147 | // value from Exif data if set to true: 148 | previewOrientation: true, 149 | // Create the preview using the Exif data thumbnail: 150 | previewThumbnail: true, 151 | // Define if preview images should be cropped or only scaled: 152 | previewCrop: false, 153 | // Define if preview images should be resized as canvas elements: 154 | previewCanvas: true 155 | }, 156 | 157 | processActions: { 158 | // Loads the image given via data.files and data.index 159 | // as img element, if the browser supports the File API. 160 | // Accepts the options fileTypes (regular expression) 161 | // and maxFileSize (integer) to limit the files to load: 162 | loadImage: function (data, options) { 163 | if (options.disabled) { 164 | return data; 165 | } 166 | var that = this, 167 | file = data.files[data.index], 168 | // eslint-disable-next-line new-cap 169 | dfd = $.Deferred(); 170 | if ( 171 | ($.type(options.maxFileSize) === 'number' && 172 | file.size > options.maxFileSize) || 173 | (options.fileTypes && !options.fileTypes.test(file.type)) || 174 | !loadImage( 175 | file, 176 | function (img) { 177 | if (img.src) { 178 | data.img = img; 179 | } 180 | dfd.resolveWith(that, [data]); 181 | }, 182 | options 183 | ) 184 | ) { 185 | return data; 186 | } 187 | return dfd.promise(); 188 | }, 189 | 190 | // Resizes the image given as data.canvas or data.img 191 | // and updates data.canvas or data.img with the resized image. 192 | // Also stores the resized image as preview property. 193 | // Accepts the options maxWidth, maxHeight, minWidth, 194 | // minHeight, canvas and crop: 195 | resizeImage: function (data, options) { 196 | if (options.disabled || !(data.canvas || data.img)) { 197 | return data; 198 | } 199 | // eslint-disable-next-line no-param-reassign 200 | options = $.extend({ canvas: true }, options); 201 | var that = this, 202 | // eslint-disable-next-line new-cap 203 | dfd = $.Deferred(), 204 | img = (options.canvas && data.canvas) || data.img, 205 | resolve = function (newImg) { 206 | if ( 207 | newImg && 208 | (newImg.width !== img.width || 209 | newImg.height !== img.height || 210 | options.forceResize) 211 | ) { 212 | data[newImg.getContext ? 'canvas' : 'img'] = newImg; 213 | } 214 | data.preview = newImg; 215 | dfd.resolveWith(that, [data]); 216 | }, 217 | thumbnail, 218 | thumbnailBlob; 219 | if (data.exif && options.thumbnail) { 220 | thumbnail = data.exif.get('Thumbnail'); 221 | thumbnailBlob = thumbnail && thumbnail.get('Blob'); 222 | if (thumbnailBlob) { 223 | options.orientation = data.exif.get('Orientation'); 224 | loadImage(thumbnailBlob, resolve, options); 225 | return dfd.promise(); 226 | } 227 | } 228 | if (data.orientation) { 229 | // Prevent orienting the same image twice: 230 | delete options.orientation; 231 | } else { 232 | data.orientation = options.orientation || loadImage.orientation; 233 | } 234 | if (img) { 235 | resolve(loadImage.scale(img, options, data)); 236 | return dfd.promise(); 237 | } 238 | return data; 239 | }, 240 | 241 | // Saves the processed image given as data.canvas 242 | // inplace at data.index of data.files: 243 | saveImage: function (data, options) { 244 | if (!data.canvas || options.disabled) { 245 | return data; 246 | } 247 | var that = this, 248 | file = data.files[data.index], 249 | // eslint-disable-next-line new-cap 250 | dfd = $.Deferred(); 251 | if (data.canvas.toBlob) { 252 | data.canvas.toBlob( 253 | function (blob) { 254 | if (!blob.name) { 255 | if (file.type === blob.type) { 256 | blob.name = file.name; 257 | } else if (file.name) { 258 | blob.name = file.name.replace( 259 | /\.\w+$/, 260 | '.' + blob.type.substr(6) 261 | ); 262 | } 263 | } 264 | // Don't restore invalid meta data: 265 | if (file.type !== blob.type) { 266 | delete data.imageHead; 267 | } 268 | // Store the created blob at the position 269 | // of the original file in the files list: 270 | data.files[data.index] = blob; 271 | dfd.resolveWith(that, [data]); 272 | }, 273 | options.type || file.type, 274 | options.quality 275 | ); 276 | } else { 277 | return data; 278 | } 279 | return dfd.promise(); 280 | }, 281 | 282 | loadImageMetaData: function (data, options) { 283 | if (options.disabled) { 284 | return data; 285 | } 286 | var that = this, 287 | // eslint-disable-next-line new-cap 288 | dfd = $.Deferred(); 289 | loadImage.parseMetaData( 290 | data.files[data.index], 291 | function (result) { 292 | $.extend(data, result); 293 | dfd.resolveWith(that, [data]); 294 | }, 295 | options 296 | ); 297 | return dfd.promise(); 298 | }, 299 | 300 | saveImageMetaData: function (data, options) { 301 | if ( 302 | !( 303 | data.imageHead && 304 | data.canvas && 305 | data.canvas.toBlob && 306 | !options.disabled 307 | ) 308 | ) { 309 | return data; 310 | } 311 | var that = this, 312 | file = data.files[data.index], 313 | // eslint-disable-next-line new-cap 314 | dfd = $.Deferred(); 315 | if (data.orientation === true && data.exifOffsets) { 316 | // Reset Exif Orientation data: 317 | loadImage.writeExifData(data.imageHead, data, 'Orientation', 1); 318 | } 319 | loadImage.replaceHead(file, data.imageHead, function (blob) { 320 | blob.name = file.name; 321 | data.files[data.index] = blob; 322 | dfd.resolveWith(that, [data]); 323 | }); 324 | return dfd.promise(); 325 | }, 326 | 327 | // Sets the resized version of the image as a property of the 328 | // file object, must be called after "saveImage": 329 | setImage: function (data, options) { 330 | if (data.preview && !options.disabled) { 331 | data.files[data.index][options.name || 'preview'] = data.preview; 332 | } 333 | return data; 334 | }, 335 | 336 | deleteImageReferences: function (data, options) { 337 | if (!options.disabled) { 338 | delete data.img; 339 | delete data.canvas; 340 | delete data.preview; 341 | delete data.imageHead; 342 | } 343 | return data; 344 | } 345 | } 346 | }); 347 | }); 348 | -------------------------------------------------------------------------------- /js/jquery.fileupload-process.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Processing Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2012, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery', './jquery.fileupload'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory(require('jquery'), require('./jquery.fileupload')); 22 | } else { 23 | // Browser globals: 24 | factory(window.jQuery); 25 | } 26 | })(function ($) { 27 | 'use strict'; 28 | 29 | var originalAdd = $.blueimp.fileupload.prototype.options.add; 30 | 31 | // The File Upload Processing plugin extends the fileupload widget 32 | // with file processing functionality: 33 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 34 | options: { 35 | // The list of processing actions: 36 | processQueue: [ 37 | /* 38 | { 39 | action: 'log', 40 | type: 'debug' 41 | } 42 | */ 43 | ], 44 | add: function (e, data) { 45 | var $this = $(this); 46 | data.process(function () { 47 | return $this.fileupload('process', data); 48 | }); 49 | originalAdd.call(this, e, data); 50 | } 51 | }, 52 | 53 | processActions: { 54 | /* 55 | log: function (data, options) { 56 | console[options.type]( 57 | 'Processing "' + data.files[data.index].name + '"' 58 | ); 59 | } 60 | */ 61 | }, 62 | 63 | _processFile: function (data, originalData) { 64 | var that = this, 65 | // eslint-disable-next-line new-cap 66 | dfd = $.Deferred().resolveWith(that, [data]), 67 | chain = dfd.promise(); 68 | this._trigger('process', null, data); 69 | $.each(data.processQueue, function (i, settings) { 70 | var func = function (data) { 71 | if (originalData.errorThrown) { 72 | // eslint-disable-next-line new-cap 73 | return $.Deferred().rejectWith(that, [originalData]).promise(); 74 | } 75 | return that.processActions[settings.action].call( 76 | that, 77 | data, 78 | settings 79 | ); 80 | }; 81 | chain = chain[that._promisePipe](func, settings.always && func); 82 | }); 83 | chain 84 | .done(function () { 85 | that._trigger('processdone', null, data); 86 | that._trigger('processalways', null, data); 87 | }) 88 | .fail(function () { 89 | that._trigger('processfail', null, data); 90 | that._trigger('processalways', null, data); 91 | }); 92 | return chain; 93 | }, 94 | 95 | // Replaces the settings of each processQueue item that 96 | // are strings starting with an "@", using the remaining 97 | // substring as key for the option map, 98 | // e.g. "@autoUpload" is replaced with options.autoUpload: 99 | _transformProcessQueue: function (options) { 100 | var processQueue = []; 101 | $.each(options.processQueue, function () { 102 | var settings = {}, 103 | action = this.action, 104 | prefix = this.prefix === true ? action : this.prefix; 105 | $.each(this, function (key, value) { 106 | if ($.type(value) === 'string' && value.charAt(0) === '@') { 107 | settings[key] = 108 | options[ 109 | value.slice(1) || 110 | (prefix 111 | ? prefix + key.charAt(0).toUpperCase() + key.slice(1) 112 | : key) 113 | ]; 114 | } else { 115 | settings[key] = value; 116 | } 117 | }); 118 | processQueue.push(settings); 119 | }); 120 | options.processQueue = processQueue; 121 | }, 122 | 123 | // Returns the number of files currently in the processing queue: 124 | processing: function () { 125 | return this._processing; 126 | }, 127 | 128 | // Processes the files given as files property of the data parameter, 129 | // returns a Promise object that allows to bind callbacks: 130 | process: function (data) { 131 | var that = this, 132 | options = $.extend({}, this.options, data); 133 | if (options.processQueue && options.processQueue.length) { 134 | this._transformProcessQueue(options); 135 | if (this._processing === 0) { 136 | this._trigger('processstart'); 137 | } 138 | $.each(data.files, function (index) { 139 | var opts = index ? $.extend({}, options) : options, 140 | func = function () { 141 | if (data.errorThrown) { 142 | // eslint-disable-next-line new-cap 143 | return $.Deferred().rejectWith(that, [data]).promise(); 144 | } 145 | return that._processFile(opts, data); 146 | }; 147 | opts.index = index; 148 | that._processing += 1; 149 | that._processingQueue = that._processingQueue[that._promisePipe]( 150 | func, 151 | func 152 | ).always(function () { 153 | that._processing -= 1; 154 | if (that._processing === 0) { 155 | that._trigger('processstop'); 156 | } 157 | }); 158 | }); 159 | } 160 | return this._processingQueue; 161 | }, 162 | 163 | _create: function () { 164 | this._super(); 165 | this._processing = 0; 166 | // eslint-disable-next-line new-cap 167 | this._processingQueue = $.Deferred().resolveWith(this).promise(); 168 | } 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /js/jquery.fileupload-ui.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload User Interface Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define([ 19 | 'jquery', 20 | 'blueimp-tmpl', 21 | './jquery.fileupload-image', 22 | './jquery.fileupload-audio', 23 | './jquery.fileupload-video', 24 | './jquery.fileupload-validate' 25 | ], factory); 26 | } else if (typeof exports === 'object') { 27 | // Node/CommonJS: 28 | factory( 29 | require('jquery'), 30 | require('blueimp-tmpl'), 31 | require('./jquery.fileupload-image'), 32 | require('./jquery.fileupload-audio'), 33 | require('./jquery.fileupload-video'), 34 | require('./jquery.fileupload-validate') 35 | ); 36 | } else { 37 | // Browser globals: 38 | factory(window.jQuery, window.tmpl); 39 | } 40 | })(function ($, tmpl) { 41 | 'use strict'; 42 | 43 | $.blueimp.fileupload.prototype._specialOptions.push( 44 | 'filesContainer', 45 | 'uploadTemplateId', 46 | 'downloadTemplateId' 47 | ); 48 | 49 | // The UI version extends the file upload widget 50 | // and adds complete user interface interaction: 51 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 52 | options: { 53 | // By default, files added to the widget are uploaded as soon 54 | // as the user clicks on the start buttons. To enable automatic 55 | // uploads, set the following option to true: 56 | autoUpload: false, 57 | // The class to show/hide UI elements: 58 | showElementClass: 'in', 59 | // The ID of the upload template: 60 | uploadTemplateId: 'template-upload', 61 | // The ID of the download template: 62 | downloadTemplateId: 'template-download', 63 | // The container for the list of files. If undefined, it is set to 64 | // an element with class "files" inside of the widget element: 65 | filesContainer: undefined, 66 | // By default, files are appended to the files container. 67 | // Set the following option to true, to prepend files instead: 68 | prependFiles: false, 69 | // The expected data type of the upload response, sets the dataType 70 | // option of the $.ajax upload requests: 71 | dataType: 'json', 72 | 73 | // Error and info messages: 74 | messages: { 75 | unknownError: 'Unknown error' 76 | }, 77 | 78 | // Function returning the current number of files, 79 | // used by the maxNumberOfFiles validation: 80 | getNumberOfFiles: function () { 81 | return this.filesContainer.children().not('.processing').length; 82 | }, 83 | 84 | // Callback to retrieve the list of files from the server response: 85 | getFilesFromResponse: function (data) { 86 | if (data.result && $.isArray(data.result.files)) { 87 | return data.result.files; 88 | } 89 | return []; 90 | }, 91 | 92 | // The add callback is invoked as soon as files are added to the fileupload 93 | // widget (via file input selection, drag & drop or add API call). 94 | // See the basic file upload widget for more information: 95 | add: function (e, data) { 96 | if (e.isDefaultPrevented()) { 97 | return false; 98 | } 99 | var $this = $(this), 100 | that = $this.data('blueimp-fileupload') || $this.data('fileupload'), 101 | options = that.options; 102 | data.context = that 103 | ._renderUpload(data.files) 104 | .data('data', data) 105 | .addClass('processing'); 106 | options.filesContainer[options.prependFiles ? 'prepend' : 'append']( 107 | data.context 108 | ); 109 | that._forceReflow(data.context); 110 | that._transition(data.context); 111 | data 112 | .process(function () { 113 | return $this.fileupload('process', data); 114 | }) 115 | .always(function () { 116 | data.context 117 | .each(function (index) { 118 | $(this) 119 | .find('.size') 120 | .text(that._formatFileSize(data.files[index].size)); 121 | }) 122 | .removeClass('processing'); 123 | that._renderPreviews(data); 124 | }) 125 | .done(function () { 126 | data.context.find('.edit,.start').prop('disabled', false); 127 | if ( 128 | that._trigger('added', e, data) !== false && 129 | (options.autoUpload || data.autoUpload) && 130 | data.autoUpload !== false 131 | ) { 132 | data.submit(); 133 | } 134 | }) 135 | .fail(function () { 136 | if (data.files.error) { 137 | data.context.each(function (index) { 138 | var error = data.files[index].error; 139 | if (error) { 140 | $(this).find('.error').text(error); 141 | } 142 | }); 143 | } 144 | }); 145 | }, 146 | // Callback for the start of each file upload request: 147 | send: function (e, data) { 148 | if (e.isDefaultPrevented()) { 149 | return false; 150 | } 151 | var that = 152 | $(this).data('blueimp-fileupload') || $(this).data('fileupload'); 153 | if ( 154 | data.context && 155 | data.dataType && 156 | data.dataType.substr(0, 6) === 'iframe' 157 | ) { 158 | // Iframe Transport does not support progress events. 159 | // In lack of an indeterminate progress bar, we set 160 | // the progress to 100%, showing the full animated bar: 161 | data.context 162 | .find('.progress') 163 | .addClass(!$.support.transition && 'progress-animated') 164 | .attr('aria-valuenow', 100) 165 | .children() 166 | .first() 167 | .css('width', '100%'); 168 | } 169 | return that._trigger('sent', e, data); 170 | }, 171 | // Callback for successful uploads: 172 | done: function (e, data) { 173 | if (e.isDefaultPrevented()) { 174 | return false; 175 | } 176 | var that = 177 | $(this).data('blueimp-fileupload') || $(this).data('fileupload'), 178 | getFilesFromResponse = 179 | data.getFilesFromResponse || that.options.getFilesFromResponse, 180 | files = getFilesFromResponse(data), 181 | template, 182 | deferred; 183 | if (data.context) { 184 | data.context.each(function (index) { 185 | var file = files[index] || { error: 'Empty file upload result' }; 186 | deferred = that._addFinishedDeferreds(); 187 | that._transition($(this)).done(function () { 188 | var node = $(this); 189 | template = that._renderDownload([file]).replaceAll(node); 190 | that._forceReflow(template); 191 | that._transition(template).done(function () { 192 | data.context = $(this); 193 | that._trigger('completed', e, data); 194 | that._trigger('finished', e, data); 195 | deferred.resolve(); 196 | }); 197 | }); 198 | }); 199 | } else { 200 | template = that 201 | ._renderDownload(files) 202 | [that.options.prependFiles ? 'prependTo' : 'appendTo']( 203 | that.options.filesContainer 204 | ); 205 | that._forceReflow(template); 206 | deferred = that._addFinishedDeferreds(); 207 | that._transition(template).done(function () { 208 | data.context = $(this); 209 | that._trigger('completed', e, data); 210 | that._trigger('finished', e, data); 211 | deferred.resolve(); 212 | }); 213 | } 214 | }, 215 | // Callback for failed (abort or error) uploads: 216 | fail: function (e, data) { 217 | if (e.isDefaultPrevented()) { 218 | return false; 219 | } 220 | var that = 221 | $(this).data('blueimp-fileupload') || $(this).data('fileupload'), 222 | template, 223 | deferred; 224 | if (data.context) { 225 | data.context.each(function (index) { 226 | if (data.errorThrown !== 'abort') { 227 | var file = data.files[index]; 228 | file.error = 229 | file.error || data.errorThrown || data.i18n('unknownError'); 230 | deferred = that._addFinishedDeferreds(); 231 | that._transition($(this)).done(function () { 232 | var node = $(this); 233 | template = that._renderDownload([file]).replaceAll(node); 234 | that._forceReflow(template); 235 | that._transition(template).done(function () { 236 | data.context = $(this); 237 | that._trigger('failed', e, data); 238 | that._trigger('finished', e, data); 239 | deferred.resolve(); 240 | }); 241 | }); 242 | } else { 243 | deferred = that._addFinishedDeferreds(); 244 | that._transition($(this)).done(function () { 245 | $(this).remove(); 246 | that._trigger('failed', e, data); 247 | that._trigger('finished', e, data); 248 | deferred.resolve(); 249 | }); 250 | } 251 | }); 252 | } else if (data.errorThrown !== 'abort') { 253 | data.context = that 254 | ._renderUpload(data.files) 255 | [that.options.prependFiles ? 'prependTo' : 'appendTo']( 256 | that.options.filesContainer 257 | ) 258 | .data('data', data); 259 | that._forceReflow(data.context); 260 | deferred = that._addFinishedDeferreds(); 261 | that._transition(data.context).done(function () { 262 | data.context = $(this); 263 | that._trigger('failed', e, data); 264 | that._trigger('finished', e, data); 265 | deferred.resolve(); 266 | }); 267 | } else { 268 | that._trigger('failed', e, data); 269 | that._trigger('finished', e, data); 270 | that._addFinishedDeferreds().resolve(); 271 | } 272 | }, 273 | // Callback for upload progress events: 274 | progress: function (e, data) { 275 | if (e.isDefaultPrevented()) { 276 | return false; 277 | } 278 | var progress = Math.floor((data.loaded / data.total) * 100); 279 | if (data.context) { 280 | data.context.each(function () { 281 | $(this) 282 | .find('.progress') 283 | .attr('aria-valuenow', progress) 284 | .children() 285 | .first() 286 | .css('width', progress + '%'); 287 | }); 288 | } 289 | }, 290 | // Callback for global upload progress events: 291 | progressall: function (e, data) { 292 | if (e.isDefaultPrevented()) { 293 | return false; 294 | } 295 | var $this = $(this), 296 | progress = Math.floor((data.loaded / data.total) * 100), 297 | globalProgressNode = $this.find('.fileupload-progress'), 298 | extendedProgressNode = globalProgressNode.find('.progress-extended'); 299 | if (extendedProgressNode.length) { 300 | extendedProgressNode.html( 301 | ( 302 | $this.data('blueimp-fileupload') || $this.data('fileupload') 303 | )._renderExtendedProgress(data) 304 | ); 305 | } 306 | globalProgressNode 307 | .find('.progress') 308 | .attr('aria-valuenow', progress) 309 | .children() 310 | .first() 311 | .css('width', progress + '%'); 312 | }, 313 | // Callback for uploads start, equivalent to the global ajaxStart event: 314 | start: function (e) { 315 | if (e.isDefaultPrevented()) { 316 | return false; 317 | } 318 | var that = 319 | $(this).data('blueimp-fileupload') || $(this).data('fileupload'); 320 | that._resetFinishedDeferreds(); 321 | that 322 | ._transition($(this).find('.fileupload-progress')) 323 | .done(function () { 324 | that._trigger('started', e); 325 | }); 326 | }, 327 | // Callback for uploads stop, equivalent to the global ajaxStop event: 328 | stop: function (e) { 329 | if (e.isDefaultPrevented()) { 330 | return false; 331 | } 332 | var that = 333 | $(this).data('blueimp-fileupload') || $(this).data('fileupload'), 334 | deferred = that._addFinishedDeferreds(); 335 | $.when.apply($, that._getFinishedDeferreds()).done(function () { 336 | that._trigger('stopped', e); 337 | }); 338 | that 339 | ._transition($(this).find('.fileupload-progress')) 340 | .done(function () { 341 | $(this) 342 | .find('.progress') 343 | .attr('aria-valuenow', '0') 344 | .children() 345 | .first() 346 | .css('width', '0%'); 347 | $(this).find('.progress-extended').html(' '); 348 | deferred.resolve(); 349 | }); 350 | }, 351 | processstart: function (e) { 352 | if (e.isDefaultPrevented()) { 353 | return false; 354 | } 355 | $(this).addClass('fileupload-processing'); 356 | }, 357 | processstop: function (e) { 358 | if (e.isDefaultPrevented()) { 359 | return false; 360 | } 361 | $(this).removeClass('fileupload-processing'); 362 | }, 363 | // Callback for file deletion: 364 | destroy: function (e, data) { 365 | if (e.isDefaultPrevented()) { 366 | return false; 367 | } 368 | var that = 369 | $(this).data('blueimp-fileupload') || $(this).data('fileupload'), 370 | removeNode = function () { 371 | that._transition(data.context).done(function () { 372 | $(this).remove(); 373 | that._trigger('destroyed', e, data); 374 | }); 375 | }; 376 | if (data.url) { 377 | data.dataType = data.dataType || that.options.dataType; 378 | $.ajax(data) 379 | .done(removeNode) 380 | .fail(function () { 381 | that._trigger('destroyfailed', e, data); 382 | }); 383 | } else { 384 | removeNode(); 385 | } 386 | } 387 | }, 388 | 389 | _resetFinishedDeferreds: function () { 390 | this._finishedUploads = []; 391 | }, 392 | 393 | _addFinishedDeferreds: function (deferred) { 394 | // eslint-disable-next-line new-cap 395 | var promise = deferred || $.Deferred(); 396 | this._finishedUploads.push(promise); 397 | return promise; 398 | }, 399 | 400 | _getFinishedDeferreds: function () { 401 | return this._finishedUploads; 402 | }, 403 | 404 | // Link handler, that allows to download files 405 | // by drag & drop of the links to the desktop: 406 | _enableDragToDesktop: function () { 407 | var link = $(this), 408 | url = link.prop('href'), 409 | name = link.prop('download'), 410 | type = 'application/octet-stream'; 411 | link.on('dragstart', function (e) { 412 | try { 413 | e.originalEvent.dataTransfer.setData( 414 | 'DownloadURL', 415 | [type, name, url].join(':') 416 | ); 417 | } catch (ignore) { 418 | // Ignore exceptions 419 | } 420 | }); 421 | }, 422 | 423 | _formatFileSize: function (bytes) { 424 | if (typeof bytes !== 'number') { 425 | return ''; 426 | } 427 | if (bytes >= 1000000000) { 428 | return (bytes / 1000000000).toFixed(2) + ' GB'; 429 | } 430 | if (bytes >= 1000000) { 431 | return (bytes / 1000000).toFixed(2) + ' MB'; 432 | } 433 | return (bytes / 1000).toFixed(2) + ' KB'; 434 | }, 435 | 436 | _formatBitrate: function (bits) { 437 | if (typeof bits !== 'number') { 438 | return ''; 439 | } 440 | if (bits >= 1000000000) { 441 | return (bits / 1000000000).toFixed(2) + ' Gbit/s'; 442 | } 443 | if (bits >= 1000000) { 444 | return (bits / 1000000).toFixed(2) + ' Mbit/s'; 445 | } 446 | if (bits >= 1000) { 447 | return (bits / 1000).toFixed(2) + ' kbit/s'; 448 | } 449 | return bits.toFixed(2) + ' bit/s'; 450 | }, 451 | 452 | _formatTime: function (seconds) { 453 | var date = new Date(seconds * 1000), 454 | days = Math.floor(seconds / 86400); 455 | days = days ? days + 'd ' : ''; 456 | return ( 457 | days + 458 | ('0' + date.getUTCHours()).slice(-2) + 459 | ':' + 460 | ('0' + date.getUTCMinutes()).slice(-2) + 461 | ':' + 462 | ('0' + date.getUTCSeconds()).slice(-2) 463 | ); 464 | }, 465 | 466 | _formatPercentage: function (floatValue) { 467 | return (floatValue * 100).toFixed(2) + ' %'; 468 | }, 469 | 470 | _renderExtendedProgress: function (data) { 471 | return ( 472 | this._formatBitrate(data.bitrate) + 473 | ' | ' + 474 | this._formatTime(((data.total - data.loaded) * 8) / data.bitrate) + 475 | ' | ' + 476 | this._formatPercentage(data.loaded / data.total) + 477 | ' | ' + 478 | this._formatFileSize(data.loaded) + 479 | ' / ' + 480 | this._formatFileSize(data.total) 481 | ); 482 | }, 483 | 484 | _renderTemplate: function (func, files) { 485 | if (!func) { 486 | return $(); 487 | } 488 | var result = func({ 489 | files: files, 490 | formatFileSize: this._formatFileSize, 491 | options: this.options 492 | }); 493 | if (result instanceof $) { 494 | return result; 495 | } 496 | return $(this.options.templatesContainer).html(result).children(); 497 | }, 498 | 499 | _renderPreviews: function (data) { 500 | data.context.find('.preview').each(function (index, elm) { 501 | $(elm).empty().append(data.files[index].preview); 502 | }); 503 | }, 504 | 505 | _renderUpload: function (files) { 506 | return this._renderTemplate(this.options.uploadTemplate, files); 507 | }, 508 | 509 | _renderDownload: function (files) { 510 | return this._renderTemplate(this.options.downloadTemplate, files) 511 | .find('a[download]') 512 | .each(this._enableDragToDesktop) 513 | .end(); 514 | }, 515 | 516 | _editHandler: function (e) { 517 | e.preventDefault(); 518 | if (!this.options.edit) return; 519 | var that = this, 520 | button = $(e.currentTarget), 521 | template = button.closest('.template-upload'), 522 | data = template.data('data'), 523 | index = button.data().index; 524 | this.options.edit(data.files[index]).then(function (file) { 525 | if (!file) return; 526 | data.files[index] = file; 527 | data.context.addClass('processing'); 528 | template.find('.edit,.start').prop('disabled', true); 529 | $(that.element) 530 | .fileupload('process', data) 531 | .always(function () { 532 | template 533 | .find('.size') 534 | .text(that._formatFileSize(data.files[index].size)); 535 | data.context.removeClass('processing'); 536 | that._renderPreviews(data); 537 | }) 538 | .done(function () { 539 | template.find('.edit,.start').prop('disabled', false); 540 | }) 541 | .fail(function () { 542 | template.find('.edit').prop('disabled', false); 543 | var error = data.files[index].error; 544 | if (error) { 545 | template.find('.error').text(error); 546 | } 547 | }); 548 | }); 549 | }, 550 | 551 | _startHandler: function (e) { 552 | e.preventDefault(); 553 | var button = $(e.currentTarget), 554 | template = button.closest('.template-upload'), 555 | data = template.data('data'); 556 | button.prop('disabled', true); 557 | if (data && data.submit) { 558 | data.submit(); 559 | } 560 | }, 561 | 562 | _cancelHandler: function (e) { 563 | e.preventDefault(); 564 | var template = $(e.currentTarget).closest( 565 | '.template-upload,.template-download' 566 | ), 567 | data = template.data('data') || {}; 568 | data.context = data.context || template; 569 | if (data.abort) { 570 | data.abort(); 571 | } else { 572 | data.errorThrown = 'abort'; 573 | this._trigger('fail', e, data); 574 | } 575 | }, 576 | 577 | _deleteHandler: function (e) { 578 | e.preventDefault(); 579 | var button = $(e.currentTarget); 580 | this._trigger( 581 | 'destroy', 582 | e, 583 | $.extend( 584 | { 585 | context: button.closest('.template-download'), 586 | type: 'DELETE' 587 | }, 588 | button.data() 589 | ) 590 | ); 591 | }, 592 | 593 | _forceReflow: function (node) { 594 | return $.support.transition && node.length && node[0].offsetWidth; 595 | }, 596 | 597 | _transition: function (node) { 598 | // eslint-disable-next-line new-cap 599 | var dfd = $.Deferred(); 600 | if ( 601 | $.support.transition && 602 | node.hasClass('fade') && 603 | node.is(':visible') 604 | ) { 605 | var transitionEndHandler = function (e) { 606 | // Make sure we don't respond to other transition events 607 | // in the container element, e.g. from button elements: 608 | if (e.target === node[0]) { 609 | node.off($.support.transition.end, transitionEndHandler); 610 | dfd.resolveWith(node); 611 | } 612 | }; 613 | node 614 | .on($.support.transition.end, transitionEndHandler) 615 | .toggleClass(this.options.showElementClass); 616 | } else { 617 | node.toggleClass(this.options.showElementClass); 618 | dfd.resolveWith(node); 619 | } 620 | return dfd; 621 | }, 622 | 623 | _initButtonBarEventHandlers: function () { 624 | var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), 625 | filesList = this.options.filesContainer; 626 | this._on(fileUploadButtonBar.find('.start'), { 627 | click: function (e) { 628 | e.preventDefault(); 629 | filesList.find('.start').trigger('click'); 630 | } 631 | }); 632 | this._on(fileUploadButtonBar.find('.cancel'), { 633 | click: function (e) { 634 | e.preventDefault(); 635 | filesList.find('.cancel').trigger('click'); 636 | } 637 | }); 638 | this._on(fileUploadButtonBar.find('.delete'), { 639 | click: function (e) { 640 | e.preventDefault(); 641 | filesList 642 | .find('.toggle:checked') 643 | .closest('.template-download') 644 | .find('.delete') 645 | .trigger('click'); 646 | fileUploadButtonBar.find('.toggle').prop('checked', false); 647 | } 648 | }); 649 | this._on(fileUploadButtonBar.find('.toggle'), { 650 | change: function (e) { 651 | filesList 652 | .find('.toggle') 653 | .prop('checked', $(e.currentTarget).is(':checked')); 654 | } 655 | }); 656 | }, 657 | 658 | _destroyButtonBarEventHandlers: function () { 659 | this._off( 660 | this.element 661 | .find('.fileupload-buttonbar') 662 | .find('.start, .cancel, .delete'), 663 | 'click' 664 | ); 665 | this._off(this.element.find('.fileupload-buttonbar .toggle'), 'change.'); 666 | }, 667 | 668 | _initEventHandlers: function () { 669 | this._super(); 670 | this._on(this.options.filesContainer, { 671 | 'click .edit': this._editHandler, 672 | 'click .start': this._startHandler, 673 | 'click .cancel': this._cancelHandler, 674 | 'click .delete': this._deleteHandler 675 | }); 676 | this._initButtonBarEventHandlers(); 677 | }, 678 | 679 | _destroyEventHandlers: function () { 680 | this._destroyButtonBarEventHandlers(); 681 | this._off(this.options.filesContainer, 'click'); 682 | this._super(); 683 | }, 684 | 685 | _enableFileInputButton: function () { 686 | this.element 687 | .find('.fileinput-button input') 688 | .prop('disabled', false) 689 | .parent() 690 | .removeClass('disabled'); 691 | }, 692 | 693 | _disableFileInputButton: function () { 694 | this.element 695 | .find('.fileinput-button input') 696 | .prop('disabled', true) 697 | .parent() 698 | .addClass('disabled'); 699 | }, 700 | 701 | _initTemplates: function () { 702 | var options = this.options; 703 | options.templatesContainer = this.document[0].createElement( 704 | options.filesContainer.prop('nodeName') 705 | ); 706 | if (tmpl) { 707 | if (options.uploadTemplateId) { 708 | options.uploadTemplate = tmpl(options.uploadTemplateId); 709 | } 710 | if (options.downloadTemplateId) { 711 | options.downloadTemplate = tmpl(options.downloadTemplateId); 712 | } 713 | } 714 | }, 715 | 716 | _initFilesContainer: function () { 717 | var options = this.options; 718 | if (options.filesContainer === undefined) { 719 | options.filesContainer = this.element.find('.files'); 720 | } else if (!(options.filesContainer instanceof $)) { 721 | options.filesContainer = $(options.filesContainer); 722 | } 723 | }, 724 | 725 | _initSpecialOptions: function () { 726 | this._super(); 727 | this._initFilesContainer(); 728 | this._initTemplates(); 729 | }, 730 | 731 | _create: function () { 732 | this._super(); 733 | this._resetFinishedDeferreds(); 734 | if (!$.support.fileInput) { 735 | this._disableFileInputButton(); 736 | } 737 | }, 738 | 739 | enable: function () { 740 | var wasDisabled = false; 741 | if (this.options.disabled) { 742 | wasDisabled = true; 743 | } 744 | this._super(); 745 | if (wasDisabled) { 746 | this.element.find('input, button').prop('disabled', false); 747 | this._enableFileInputButton(); 748 | } 749 | }, 750 | 751 | disable: function () { 752 | if (!this.options.disabled) { 753 | this.element.find('input, button').prop('disabled', true); 754 | this._disableFileInputButton(); 755 | } 756 | this._super(); 757 | } 758 | }); 759 | }); 760 | -------------------------------------------------------------------------------- /js/jquery.fileupload-validate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Validation Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery', './jquery.fileupload-process'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory(require('jquery'), require('./jquery.fileupload-process')); 22 | } else { 23 | // Browser globals: 24 | factory(window.jQuery); 25 | } 26 | })(function ($) { 27 | 'use strict'; 28 | 29 | // Append to the default processQueue: 30 | $.blueimp.fileupload.prototype.options.processQueue.push({ 31 | action: 'validate', 32 | // Always trigger this action, 33 | // even if the previous action was rejected: 34 | always: true, 35 | // Options taken from the global options map: 36 | acceptFileTypes: '@', 37 | maxFileSize: '@', 38 | minFileSize: '@', 39 | maxNumberOfFiles: '@', 40 | disabled: '@disableValidation' 41 | }); 42 | 43 | // The File Upload Validation plugin extends the fileupload widget 44 | // with file validation functionality: 45 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 46 | options: { 47 | /* 48 | // The regular expression for allowed file types, matches 49 | // against either file type or file name: 50 | acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, 51 | // The maximum allowed file size in bytes: 52 | maxFileSize: 10000000, // 10 MB 53 | // The minimum allowed file size in bytes: 54 | minFileSize: undefined, // No minimal file size 55 | // The limit of files to be uploaded: 56 | maxNumberOfFiles: 10, 57 | */ 58 | 59 | // Function returning the current number of files, 60 | // has to be overridden for maxNumberOfFiles validation: 61 | getNumberOfFiles: $.noop, 62 | 63 | // Error and info messages: 64 | messages: { 65 | maxNumberOfFiles: 'Maximum number of files exceeded', 66 | acceptFileTypes: 'File type not allowed', 67 | maxFileSize: 'File is too large', 68 | minFileSize: 'File is too small' 69 | } 70 | }, 71 | 72 | processActions: { 73 | validate: function (data, options) { 74 | if (options.disabled) { 75 | return data; 76 | } 77 | // eslint-disable-next-line new-cap 78 | var dfd = $.Deferred(), 79 | settings = this.options, 80 | file = data.files[data.index], 81 | fileSize; 82 | if (options.minFileSize || options.maxFileSize) { 83 | fileSize = file.size; 84 | } 85 | if ( 86 | $.type(options.maxNumberOfFiles) === 'number' && 87 | (settings.getNumberOfFiles() || 0) + data.files.length > 88 | options.maxNumberOfFiles 89 | ) { 90 | file.error = settings.i18n('maxNumberOfFiles'); 91 | } else if ( 92 | options.acceptFileTypes && 93 | !( 94 | options.acceptFileTypes.test(file.type) || 95 | options.acceptFileTypes.test(file.name) 96 | ) 97 | ) { 98 | file.error = settings.i18n('acceptFileTypes'); 99 | } else if (fileSize > options.maxFileSize) { 100 | file.error = settings.i18n('maxFileSize'); 101 | } else if ( 102 | $.type(fileSize) === 'number' && 103 | fileSize < options.minFileSize 104 | ) { 105 | file.error = settings.i18n('minFileSize'); 106 | } else { 107 | delete file.error; 108 | } 109 | if (file.error || data.files.error) { 110 | data.files.error = true; 111 | dfd.rejectWith(this, [data]); 112 | } else { 113 | dfd.resolveWith(this, [data]); 114 | } 115 | return dfd.promise(); 116 | } 117 | } 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /js/jquery.fileupload-video.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Video Preview Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery', 'load-image', './jquery.fileupload-process'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory( 22 | require('jquery'), 23 | require('blueimp-load-image/js/load-image'), 24 | require('./jquery.fileupload-process') 25 | ); 26 | } else { 27 | // Browser globals: 28 | factory(window.jQuery, window.loadImage); 29 | } 30 | })(function ($, loadImage) { 31 | 'use strict'; 32 | 33 | // Prepend to the default processQueue: 34 | $.blueimp.fileupload.prototype.options.processQueue.unshift( 35 | { 36 | action: 'loadVideo', 37 | // Use the action as prefix for the "@" options: 38 | prefix: true, 39 | fileTypes: '@', 40 | maxFileSize: '@', 41 | disabled: '@disableVideoPreview' 42 | }, 43 | { 44 | action: 'setVideo', 45 | name: '@videoPreviewName', 46 | disabled: '@disableVideoPreview' 47 | } 48 | ); 49 | 50 | // The File Upload Video Preview plugin extends the fileupload widget 51 | // with video preview functionality: 52 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 53 | options: { 54 | // The regular expression for the types of video files to load, 55 | // matched against the file type: 56 | loadVideoFileTypes: /^video\/.*$/ 57 | }, 58 | 59 | _videoElement: document.createElement('video'), 60 | 61 | processActions: { 62 | // Loads the video file given via data.files and data.index 63 | // as video element if the browser supports playing it. 64 | // Accepts the options fileTypes (regular expression) 65 | // and maxFileSize (integer) to limit the files to load: 66 | loadVideo: function (data, options) { 67 | if (options.disabled) { 68 | return data; 69 | } 70 | var file = data.files[data.index], 71 | url, 72 | video; 73 | if ( 74 | this._videoElement.canPlayType && 75 | this._videoElement.canPlayType(file.type) && 76 | ($.type(options.maxFileSize) !== 'number' || 77 | file.size <= options.maxFileSize) && 78 | (!options.fileTypes || options.fileTypes.test(file.type)) 79 | ) { 80 | url = loadImage.createObjectURL(file); 81 | if (url) { 82 | video = this._videoElement.cloneNode(false); 83 | video.src = url; 84 | video.controls = true; 85 | data.video = video; 86 | return data; 87 | } 88 | } 89 | return data; 90 | }, 91 | 92 | // Sets the video element as a property of the file object: 93 | setVideo: function (data, options) { 94 | if (data.video && !options.disabled) { 95 | data.files[data.index][options.name || 'preview'] = data.video; 96 | } 97 | return data; 98 | } 99 | } 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /js/jquery.fileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | /* eslint-disable new-cap */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define(['jquery', 'jquery-ui/ui/widget'], factory); 20 | } else if (typeof exports === 'object') { 21 | // Node/CommonJS: 22 | factory(require('jquery'), require('./vendor/jquery.ui.widget')); 23 | } else { 24 | // Browser globals: 25 | factory(window.jQuery); 26 | } 27 | })(function ($) { 28 | 'use strict'; 29 | 30 | // Detect file input support, based on 31 | // https://viljamis.com/2012/file-upload-support-on-mobile/ 32 | $.support.fileInput = !( 33 | new RegExp( 34 | // Handle devices which give false positives for the feature detection: 35 | '(Android (1\\.[0156]|2\\.[01]))' + 36 | '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + 37 | '|(w(eb)?OSBrowser)|(webOS)' + 38 | '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' 39 | ).test(window.navigator.userAgent) || 40 | // Feature detection for all other devices: 41 | $('').prop('disabled') 42 | ); 43 | 44 | // The FileReader API is not actually used, but works as feature detection, 45 | // as some Safari versions (5?) support XHR file uploads via the FormData API, 46 | // but not non-multipart XHR file uploads. 47 | // window.XMLHttpRequestUpload is not available on IE10, so we check for 48 | // window.ProgressEvent instead to detect XHR2 file upload capability: 49 | $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); 50 | $.support.xhrFormDataFileUpload = !!window.FormData; 51 | 52 | // Detect support for Blob slicing (required for chunked uploads): 53 | $.support.blobSlice = 54 | window.Blob && 55 | (Blob.prototype.slice || 56 | Blob.prototype.webkitSlice || 57 | Blob.prototype.mozSlice); 58 | 59 | /** 60 | * Helper function to create drag handlers for dragover/dragenter/dragleave 61 | * 62 | * @param {string} type Event type 63 | * @returns {Function} Drag handler 64 | */ 65 | function getDragHandler(type) { 66 | var isDragOver = type === 'dragover'; 67 | return function (e) { 68 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 69 | var dataTransfer = e.dataTransfer; 70 | if ( 71 | dataTransfer && 72 | $.inArray('Files', dataTransfer.types) !== -1 && 73 | this._trigger(type, $.Event(type, { delegatedEvent: e })) !== false 74 | ) { 75 | e.preventDefault(); 76 | if (isDragOver) { 77 | dataTransfer.dropEffect = 'copy'; 78 | } 79 | } 80 | }; 81 | } 82 | 83 | // The fileupload widget listens for change events on file input fields defined 84 | // via fileInput setting and paste or drop events of the given dropZone. 85 | // In addition to the default jQuery Widget methods, the fileupload widget 86 | // exposes the "add" and "send" methods, to add or directly send files using 87 | // the fileupload API. 88 | // By default, files added via file input selection, paste, drag & drop or 89 | // "add" method are uploaded immediately, but it is possible to override 90 | // the "add" callback option to queue file uploads. 91 | $.widget('blueimp.fileupload', { 92 | options: { 93 | // The drop target element(s), by the default the complete document. 94 | // Set to null to disable drag & drop support: 95 | dropZone: $(document), 96 | // The paste target element(s), by the default undefined. 97 | // Set to a DOM node or jQuery object to enable file pasting: 98 | pasteZone: undefined, 99 | // The file input field(s), that are listened to for change events. 100 | // If undefined, it is set to the file input fields inside 101 | // of the widget element on plugin initialization. 102 | // Set to null to disable the change listener. 103 | fileInput: undefined, 104 | // By default, the file input field is replaced with a clone after 105 | // each input field change event. This is required for iframe transport 106 | // queues and allows change events to be fired for the same file 107 | // selection, but can be disabled by setting the following option to false: 108 | replaceFileInput: true, 109 | // The parameter name for the file form data (the request argument name). 110 | // If undefined or empty, the name property of the file input field is 111 | // used, or "files[]" if the file input name property is also empty, 112 | // can be a string or an array of strings: 113 | paramName: undefined, 114 | // By default, each file of a selection is uploaded using an individual 115 | // request for XHR type uploads. Set to false to upload file 116 | // selections in one request each: 117 | singleFileUploads: true, 118 | // To limit the number of files uploaded with one XHR request, 119 | // set the following option to an integer greater than 0: 120 | limitMultiFileUploads: undefined, 121 | // The following option limits the number of files uploaded with one 122 | // XHR request to keep the request size under or equal to the defined 123 | // limit in bytes: 124 | limitMultiFileUploadSize: undefined, 125 | // Multipart file uploads add a number of bytes to each uploaded file, 126 | // therefore the following option adds an overhead for each file used 127 | // in the limitMultiFileUploadSize configuration: 128 | limitMultiFileUploadSizeOverhead: 512, 129 | // Set the following option to true to issue all file upload requests 130 | // in a sequential order: 131 | sequentialUploads: false, 132 | // To limit the number of concurrent uploads, 133 | // set the following option to an integer greater than 0: 134 | limitConcurrentUploads: undefined, 135 | // Set the following option to true to force iframe transport uploads: 136 | forceIframeTransport: false, 137 | // Set the following option to the location of a redirect url on the 138 | // origin server, for cross-domain iframe transport uploads: 139 | redirect: undefined, 140 | // The parameter name for the redirect url, sent as part of the form 141 | // data and set to 'redirect' if this option is empty: 142 | redirectParamName: undefined, 143 | // Set the following option to the location of a postMessage window, 144 | // to enable postMessage transport uploads: 145 | postMessage: undefined, 146 | // By default, XHR file uploads are sent as multipart/form-data. 147 | // The iframe transport is always using multipart/form-data. 148 | // Set to false to enable non-multipart XHR uploads: 149 | multipart: true, 150 | // To upload large files in smaller chunks, set the following option 151 | // to a preferred maximum chunk size. If set to 0, null or undefined, 152 | // or the browser does not support the required Blob API, files will 153 | // be uploaded as a whole. 154 | maxChunkSize: undefined, 155 | // When a non-multipart upload or a chunked multipart upload has been 156 | // aborted, this option can be used to resume the upload by setting 157 | // it to the size of the already uploaded bytes. This option is most 158 | // useful when modifying the options object inside of the "add" or 159 | // "send" callbacks, as the options are cloned for each file upload. 160 | uploadedBytes: undefined, 161 | // By default, failed (abort or error) file uploads are removed from the 162 | // global progress calculation. Set the following option to false to 163 | // prevent recalculating the global progress data: 164 | recalculateProgress: true, 165 | // Interval in milliseconds to calculate and trigger progress events: 166 | progressInterval: 100, 167 | // Interval in milliseconds to calculate progress bitrate: 168 | bitrateInterval: 500, 169 | // By default, uploads are started automatically when adding files: 170 | autoUpload: true, 171 | // By default, duplicate file names are expected to be handled on 172 | // the server-side. If this is not possible (e.g. when uploading 173 | // files directly to Amazon S3), the following option can be set to 174 | // an empty object or an object mapping existing filenames, e.g.: 175 | // { "image.jpg": true, "image (1).jpg": true } 176 | // If it is set, all files will be uploaded with unique filenames, 177 | // adding increasing number suffixes if necessary, e.g.: 178 | // "image (2).jpg" 179 | uniqueFilenames: undefined, 180 | 181 | // Error and info messages: 182 | messages: { 183 | uploadedBytes: 'Uploaded bytes exceed file size' 184 | }, 185 | 186 | // Translation function, gets the message key to be translated 187 | // and an object with context specific data as arguments: 188 | i18n: function (message, context) { 189 | // eslint-disable-next-line no-param-reassign 190 | message = this.messages[message] || message.toString(); 191 | if (context) { 192 | $.each(context, function (key, value) { 193 | // eslint-disable-next-line no-param-reassign 194 | message = message.replace('{' + key + '}', value); 195 | }); 196 | } 197 | return message; 198 | }, 199 | 200 | // Additional form data to be sent along with the file uploads can be set 201 | // using this option, which accepts an array of objects with name and 202 | // value properties, a function returning such an array, a FormData 203 | // object (for XHR file uploads), or a simple object. 204 | // The form of the first fileInput is given as parameter to the function: 205 | formData: function (form) { 206 | return form.serializeArray(); 207 | }, 208 | 209 | // The add callback is invoked as soon as files are added to the fileupload 210 | // widget (via file input selection, drag & drop, paste or add API call). 211 | // If the singleFileUploads option is enabled, this callback will be 212 | // called once for each file in the selection for XHR file uploads, else 213 | // once for each file selection. 214 | // 215 | // The upload starts when the submit method is invoked on the data parameter. 216 | // The data object contains a files property holding the added files 217 | // and allows you to override plugin options as well as define ajax settings. 218 | // 219 | // Listeners for this callback can also be bound the following way: 220 | // .on('fileuploadadd', func); 221 | // 222 | // data.submit() returns a Promise object and allows to attach additional 223 | // handlers using jQuery's Deferred callbacks: 224 | // data.submit().done(func).fail(func).always(func); 225 | add: function (e, data) { 226 | if (e.isDefaultPrevented()) { 227 | return false; 228 | } 229 | if ( 230 | data.autoUpload || 231 | (data.autoUpload !== false && 232 | $(this).fileupload('option', 'autoUpload')) 233 | ) { 234 | data.process().done(function () { 235 | data.submit(); 236 | }); 237 | } 238 | }, 239 | 240 | // Other callbacks: 241 | 242 | // Callback for the submit event of each file upload: 243 | // submit: function (e, data) {}, // .on('fileuploadsubmit', func); 244 | 245 | // Callback for the start of each file upload request: 246 | // send: function (e, data) {}, // .on('fileuploadsend', func); 247 | 248 | // Callback for successful uploads: 249 | // done: function (e, data) {}, // .on('fileuploaddone', func); 250 | 251 | // Callback for failed (abort or error) uploads: 252 | // fail: function (e, data) {}, // .on('fileuploadfail', func); 253 | 254 | // Callback for completed (success, abort or error) requests: 255 | // always: function (e, data) {}, // .on('fileuploadalways', func); 256 | 257 | // Callback for upload progress events: 258 | // progress: function (e, data) {}, // .on('fileuploadprogress', func); 259 | 260 | // Callback for global upload progress events: 261 | // progressall: function (e, data) {}, // .on('fileuploadprogressall', func); 262 | 263 | // Callback for uploads start, equivalent to the global ajaxStart event: 264 | // start: function (e) {}, // .on('fileuploadstart', func); 265 | 266 | // Callback for uploads stop, equivalent to the global ajaxStop event: 267 | // stop: function (e) {}, // .on('fileuploadstop', func); 268 | 269 | // Callback for change events of the fileInput(s): 270 | // change: function (e, data) {}, // .on('fileuploadchange', func); 271 | 272 | // Callback for paste events to the pasteZone(s): 273 | // paste: function (e, data) {}, // .on('fileuploadpaste', func); 274 | 275 | // Callback for drop events of the dropZone(s): 276 | // drop: function (e, data) {}, // .on('fileuploaddrop', func); 277 | 278 | // Callback for dragover events of the dropZone(s): 279 | // dragover: function (e) {}, // .on('fileuploaddragover', func); 280 | 281 | // Callback before the start of each chunk upload request (before form data initialization): 282 | // chunkbeforesend: function (e, data) {}, // .on('fileuploadchunkbeforesend', func); 283 | 284 | // Callback for the start of each chunk upload request: 285 | // chunksend: function (e, data) {}, // .on('fileuploadchunksend', func); 286 | 287 | // Callback for successful chunk uploads: 288 | // chunkdone: function (e, data) {}, // .on('fileuploadchunkdone', func); 289 | 290 | // Callback for failed (abort or error) chunk uploads: 291 | // chunkfail: function (e, data) {}, // .on('fileuploadchunkfail', func); 292 | 293 | // Callback for completed (success, abort or error) chunk upload requests: 294 | // chunkalways: function (e, data) {}, // .on('fileuploadchunkalways', func); 295 | 296 | // The plugin options are used as settings object for the ajax calls. 297 | // The following are jQuery ajax settings required for the file uploads: 298 | processData: false, 299 | contentType: false, 300 | cache: false, 301 | timeout: 0 302 | }, 303 | 304 | // jQuery versions before 1.8 require promise.pipe if the return value is 305 | // used, as promise.then in older versions has a different behavior, see: 306 | // https://blog.jquery.com/2012/08/09/jquery-1-8-released/ 307 | // https://bugs.jquery.com/ticket/11010 308 | // https://github.com/blueimp/jQuery-File-Upload/pull/3435 309 | _promisePipe: (function () { 310 | var parts = $.fn.jquery.split('.'); 311 | return Number(parts[0]) > 1 || Number(parts[1]) > 7 ? 'then' : 'pipe'; 312 | })(), 313 | 314 | // A list of options that require reinitializing event listeners and/or 315 | // special initialization code: 316 | _specialOptions: [ 317 | 'fileInput', 318 | 'dropZone', 319 | 'pasteZone', 320 | 'multipart', 321 | 'forceIframeTransport' 322 | ], 323 | 324 | _blobSlice: 325 | $.support.blobSlice && 326 | function () { 327 | var slice = this.slice || this.webkitSlice || this.mozSlice; 328 | return slice.apply(this, arguments); 329 | }, 330 | 331 | _BitrateTimer: function () { 332 | this.timestamp = Date.now ? Date.now() : new Date().getTime(); 333 | this.loaded = 0; 334 | this.bitrate = 0; 335 | this.getBitrate = function (now, loaded, interval) { 336 | var timeDiff = now - this.timestamp; 337 | if (!this.bitrate || !interval || timeDiff > interval) { 338 | this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; 339 | this.loaded = loaded; 340 | this.timestamp = now; 341 | } 342 | return this.bitrate; 343 | }; 344 | }, 345 | 346 | _isXHRUpload: function (options) { 347 | return ( 348 | !options.forceIframeTransport && 349 | ((!options.multipart && $.support.xhrFileUpload) || 350 | $.support.xhrFormDataFileUpload) 351 | ); 352 | }, 353 | 354 | _getFormData: function (options) { 355 | var formData; 356 | if ($.type(options.formData) === 'function') { 357 | return options.formData(options.form); 358 | } 359 | if ($.isArray(options.formData)) { 360 | return options.formData; 361 | } 362 | if ($.type(options.formData) === 'object') { 363 | formData = []; 364 | $.each(options.formData, function (name, value) { 365 | formData.push({ name: name, value: value }); 366 | }); 367 | return formData; 368 | } 369 | return []; 370 | }, 371 | 372 | _getTotal: function (files) { 373 | var total = 0; 374 | $.each(files, function (index, file) { 375 | total += file.size || 1; 376 | }); 377 | return total; 378 | }, 379 | 380 | _initProgressObject: function (obj) { 381 | var progress = { 382 | loaded: 0, 383 | total: 0, 384 | bitrate: 0 385 | }; 386 | if (obj._progress) { 387 | $.extend(obj._progress, progress); 388 | } else { 389 | obj._progress = progress; 390 | } 391 | }, 392 | 393 | _initResponseObject: function (obj) { 394 | var prop; 395 | if (obj._response) { 396 | for (prop in obj._response) { 397 | if (Object.prototype.hasOwnProperty.call(obj._response, prop)) { 398 | delete obj._response[prop]; 399 | } 400 | } 401 | } else { 402 | obj._response = {}; 403 | } 404 | }, 405 | 406 | _onProgress: function (e, data) { 407 | if (e.lengthComputable) { 408 | var now = Date.now ? Date.now() : new Date().getTime(), 409 | loaded; 410 | if ( 411 | data._time && 412 | data.progressInterval && 413 | now - data._time < data.progressInterval && 414 | e.loaded !== e.total 415 | ) { 416 | return; 417 | } 418 | data._time = now; 419 | loaded = 420 | Math.floor( 421 | (e.loaded / e.total) * (data.chunkSize || data._progress.total) 422 | ) + (data.uploadedBytes || 0); 423 | // Add the difference from the previously loaded state 424 | // to the global loaded counter: 425 | this._progress.loaded += loaded - data._progress.loaded; 426 | this._progress.bitrate = this._bitrateTimer.getBitrate( 427 | now, 428 | this._progress.loaded, 429 | data.bitrateInterval 430 | ); 431 | data._progress.loaded = data.loaded = loaded; 432 | data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( 433 | now, 434 | loaded, 435 | data.bitrateInterval 436 | ); 437 | // Trigger a custom progress event with a total data property set 438 | // to the file size(s) of the current upload and a loaded data 439 | // property calculated accordingly: 440 | this._trigger( 441 | 'progress', 442 | $.Event('progress', { delegatedEvent: e }), 443 | data 444 | ); 445 | // Trigger a global progress event for all current file uploads, 446 | // including ajax calls queued for sequential file uploads: 447 | this._trigger( 448 | 'progressall', 449 | $.Event('progressall', { delegatedEvent: e }), 450 | this._progress 451 | ); 452 | } 453 | }, 454 | 455 | _initProgressListener: function (options) { 456 | var that = this, 457 | xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 458 | // Access to the native XHR object is required to add event listeners 459 | // for the upload progress event: 460 | if (xhr.upload) { 461 | $(xhr.upload).on('progress', function (e) { 462 | var oe = e.originalEvent; 463 | // Make sure the progress event properties get copied over: 464 | e.lengthComputable = oe.lengthComputable; 465 | e.loaded = oe.loaded; 466 | e.total = oe.total; 467 | that._onProgress(e, options); 468 | }); 469 | options.xhr = function () { 470 | return xhr; 471 | }; 472 | } 473 | }, 474 | 475 | _deinitProgressListener: function (options) { 476 | var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 477 | if (xhr.upload) { 478 | $(xhr.upload).off('progress'); 479 | } 480 | }, 481 | 482 | _isInstanceOf: function (type, obj) { 483 | // Cross-frame instanceof check 484 | return Object.prototype.toString.call(obj) === '[object ' + type + ']'; 485 | }, 486 | 487 | _getUniqueFilename: function (name, map) { 488 | // eslint-disable-next-line no-param-reassign 489 | name = String(name); 490 | if (map[name]) { 491 | // eslint-disable-next-line no-param-reassign 492 | name = name.replace( 493 | /(?: \(([\d]+)\))?(\.[^.]+)?$/, 494 | function (_, p1, p2) { 495 | var index = p1 ? Number(p1) + 1 : 1; 496 | var ext = p2 || ''; 497 | return ' (' + index + ')' + ext; 498 | } 499 | ); 500 | return this._getUniqueFilename(name, map); 501 | } 502 | map[name] = true; 503 | return name; 504 | }, 505 | 506 | _initXHRData: function (options) { 507 | var that = this, 508 | formData, 509 | file = options.files[0], 510 | // Ignore non-multipart setting if not supported: 511 | multipart = options.multipart || !$.support.xhrFileUpload, 512 | paramName = 513 | $.type(options.paramName) === 'array' 514 | ? options.paramName[0] 515 | : options.paramName; 516 | options.headers = $.extend({}, options.headers); 517 | if (options.contentRange) { 518 | options.headers['Content-Range'] = options.contentRange; 519 | } 520 | if (!multipart || options.blob || !this._isInstanceOf('File', file)) { 521 | options.headers['Content-Disposition'] = 522 | 'attachment; filename="' + 523 | encodeURI(file.uploadName || file.name) + 524 | '"'; 525 | } 526 | if (!multipart) { 527 | options.contentType = file.type || 'application/octet-stream'; 528 | options.data = options.blob || file; 529 | } else if ($.support.xhrFormDataFileUpload) { 530 | if (options.postMessage) { 531 | // window.postMessage does not allow sending FormData 532 | // objects, so we just add the File/Blob objects to 533 | // the formData array and let the postMessage window 534 | // create the FormData object out of this array: 535 | formData = this._getFormData(options); 536 | if (options.blob) { 537 | formData.push({ 538 | name: paramName, 539 | value: options.blob 540 | }); 541 | } else { 542 | $.each(options.files, function (index, file) { 543 | formData.push({ 544 | name: 545 | ($.type(options.paramName) === 'array' && 546 | options.paramName[index]) || 547 | paramName, 548 | value: file 549 | }); 550 | }); 551 | } 552 | } else { 553 | if (that._isInstanceOf('FormData', options.formData)) { 554 | formData = options.formData; 555 | } else { 556 | formData = new FormData(); 557 | $.each(this._getFormData(options), function (index, field) { 558 | formData.append(field.name, field.value); 559 | }); 560 | } 561 | if (options.blob) { 562 | formData.append( 563 | paramName, 564 | options.blob, 565 | file.uploadName || file.name 566 | ); 567 | } else { 568 | $.each(options.files, function (index, file) { 569 | // This check allows the tests to run with 570 | // dummy objects: 571 | if ( 572 | that._isInstanceOf('File', file) || 573 | that._isInstanceOf('Blob', file) 574 | ) { 575 | var fileName = file.uploadName || file.name; 576 | if (options.uniqueFilenames) { 577 | fileName = that._getUniqueFilename( 578 | fileName, 579 | options.uniqueFilenames 580 | ); 581 | } 582 | formData.append( 583 | ($.type(options.paramName) === 'array' && 584 | options.paramName[index]) || 585 | paramName, 586 | file, 587 | fileName 588 | ); 589 | } 590 | }); 591 | } 592 | } 593 | options.data = formData; 594 | } 595 | // Blob reference is not needed anymore, free memory: 596 | options.blob = null; 597 | }, 598 | 599 | _initIframeSettings: function (options) { 600 | var targetHost = $('').prop('href', options.url).prop('host'); 601 | // Setting the dataType to iframe enables the iframe transport: 602 | options.dataType = 'iframe ' + (options.dataType || ''); 603 | // The iframe transport accepts a serialized array as form data: 604 | options.formData = this._getFormData(options); 605 | // Add redirect url to form data on cross-domain uploads: 606 | if (options.redirect && targetHost && targetHost !== location.host) { 607 | options.formData.push({ 608 | name: options.redirectParamName || 'redirect', 609 | value: options.redirect 610 | }); 611 | } 612 | }, 613 | 614 | _initDataSettings: function (options) { 615 | if (this._isXHRUpload(options)) { 616 | if (!this._chunkedUpload(options, true)) { 617 | if (!options.data) { 618 | this._initXHRData(options); 619 | } 620 | this._initProgressListener(options); 621 | } 622 | if (options.postMessage) { 623 | // Setting the dataType to postmessage enables the 624 | // postMessage transport: 625 | options.dataType = 'postmessage ' + (options.dataType || ''); 626 | } 627 | } else { 628 | this._initIframeSettings(options); 629 | } 630 | }, 631 | 632 | _getParamName: function (options) { 633 | var fileInput = $(options.fileInput), 634 | paramName = options.paramName; 635 | if (!paramName) { 636 | paramName = []; 637 | fileInput.each(function () { 638 | var input = $(this), 639 | name = input.prop('name') || 'files[]', 640 | i = (input.prop('files') || [1]).length; 641 | while (i) { 642 | paramName.push(name); 643 | i -= 1; 644 | } 645 | }); 646 | if (!paramName.length) { 647 | paramName = [fileInput.prop('name') || 'files[]']; 648 | } 649 | } else if (!$.isArray(paramName)) { 650 | paramName = [paramName]; 651 | } 652 | return paramName; 653 | }, 654 | 655 | _initFormSettings: function (options) { 656 | // Retrieve missing options from the input field and the 657 | // associated form, if available: 658 | if (!options.form || !options.form.length) { 659 | options.form = $(options.fileInput.prop('form')); 660 | // If the given file input doesn't have an associated form, 661 | // use the default widget file input's form: 662 | if (!options.form.length) { 663 | options.form = $(this.options.fileInput.prop('form')); 664 | } 665 | } 666 | options.paramName = this._getParamName(options); 667 | if (!options.url) { 668 | options.url = options.form.prop('action') || location.href; 669 | } 670 | // The HTTP request method must be "POST" or "PUT": 671 | options.type = ( 672 | options.type || 673 | ($.type(options.form.prop('method')) === 'string' && 674 | options.form.prop('method')) || 675 | '' 676 | ).toUpperCase(); 677 | if ( 678 | options.type !== 'POST' && 679 | options.type !== 'PUT' && 680 | options.type !== 'PATCH' 681 | ) { 682 | options.type = 'POST'; 683 | } 684 | if (!options.formAcceptCharset) { 685 | options.formAcceptCharset = options.form.attr('accept-charset'); 686 | } 687 | }, 688 | 689 | _getAJAXSettings: function (data) { 690 | var options = $.extend({}, this.options, data); 691 | this._initFormSettings(options); 692 | this._initDataSettings(options); 693 | return options; 694 | }, 695 | 696 | // jQuery 1.6 doesn't provide .state(), 697 | // while jQuery 1.8+ removed .isRejected() and .isResolved(): 698 | _getDeferredState: function (deferred) { 699 | if (deferred.state) { 700 | return deferred.state(); 701 | } 702 | if (deferred.isResolved()) { 703 | return 'resolved'; 704 | } 705 | if (deferred.isRejected()) { 706 | return 'rejected'; 707 | } 708 | return 'pending'; 709 | }, 710 | 711 | // Maps jqXHR callbacks to the equivalent 712 | // methods of the given Promise object: 713 | _enhancePromise: function (promise) { 714 | promise.success = promise.done; 715 | promise.error = promise.fail; 716 | promise.complete = promise.always; 717 | return promise; 718 | }, 719 | 720 | // Creates and returns a Promise object enhanced with 721 | // the jqXHR methods abort, success, error and complete: 722 | _getXHRPromise: function (resolveOrReject, context, args) { 723 | var dfd = $.Deferred(), 724 | promise = dfd.promise(); 725 | // eslint-disable-next-line no-param-reassign 726 | context = context || this.options.context || promise; 727 | if (resolveOrReject === true) { 728 | dfd.resolveWith(context, args); 729 | } else if (resolveOrReject === false) { 730 | dfd.rejectWith(context, args); 731 | } 732 | promise.abort = dfd.promise; 733 | return this._enhancePromise(promise); 734 | }, 735 | 736 | // Adds convenience methods to the data callback argument: 737 | _addConvenienceMethods: function (e, data) { 738 | var that = this, 739 | getPromise = function (args) { 740 | return $.Deferred().resolveWith(that, args).promise(); 741 | }; 742 | data.process = function (resolveFunc, rejectFunc) { 743 | if (resolveFunc || rejectFunc) { 744 | data._processQueue = this._processQueue = (this._processQueue || 745 | getPromise([this])) 746 | [that._promisePipe](function () { 747 | if (data.errorThrown) { 748 | return $.Deferred().rejectWith(that, [data]).promise(); 749 | } 750 | return getPromise(arguments); 751 | }) 752 | [that._promisePipe](resolveFunc, rejectFunc); 753 | } 754 | return this._processQueue || getPromise([this]); 755 | }; 756 | data.submit = function () { 757 | if (this.state() !== 'pending') { 758 | data.jqXHR = this.jqXHR = 759 | that._trigger( 760 | 'submit', 761 | $.Event('submit', { delegatedEvent: e }), 762 | this 763 | ) !== false && that._onSend(e, this); 764 | } 765 | return this.jqXHR || that._getXHRPromise(); 766 | }; 767 | data.abort = function () { 768 | if (this.jqXHR) { 769 | return this.jqXHR.abort(); 770 | } 771 | this.errorThrown = 'abort'; 772 | that._trigger('fail', null, this); 773 | return that._getXHRPromise(false); 774 | }; 775 | data.state = function () { 776 | if (this.jqXHR) { 777 | return that._getDeferredState(this.jqXHR); 778 | } 779 | if (this._processQueue) { 780 | return that._getDeferredState(this._processQueue); 781 | } 782 | }; 783 | data.processing = function () { 784 | return ( 785 | !this.jqXHR && 786 | this._processQueue && 787 | that._getDeferredState(this._processQueue) === 'pending' 788 | ); 789 | }; 790 | data.progress = function () { 791 | return this._progress; 792 | }; 793 | data.response = function () { 794 | return this._response; 795 | }; 796 | }, 797 | 798 | // Parses the Range header from the server response 799 | // and returns the uploaded bytes: 800 | _getUploadedBytes: function (jqXHR) { 801 | var range = jqXHR.getResponseHeader('Range'), 802 | parts = range && range.split('-'), 803 | upperBytesPos = parts && parts.length > 1 && parseInt(parts[1], 10); 804 | return upperBytesPos && upperBytesPos + 1; 805 | }, 806 | 807 | // Uploads a file in multiple, sequential requests 808 | // by splitting the file up in multiple blob chunks. 809 | // If the second parameter is true, only tests if the file 810 | // should be uploaded in chunks, but does not invoke any 811 | // upload requests: 812 | _chunkedUpload: function (options, testOnly) { 813 | options.uploadedBytes = options.uploadedBytes || 0; 814 | var that = this, 815 | file = options.files[0], 816 | fs = file.size, 817 | ub = options.uploadedBytes, 818 | mcs = options.maxChunkSize || fs, 819 | slice = this._blobSlice, 820 | dfd = $.Deferred(), 821 | promise = dfd.promise(), 822 | jqXHR, 823 | upload; 824 | if ( 825 | !( 826 | this._isXHRUpload(options) && 827 | slice && 828 | (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs) 829 | ) || 830 | options.data 831 | ) { 832 | return false; 833 | } 834 | if (testOnly) { 835 | return true; 836 | } 837 | if (ub >= fs) { 838 | file.error = options.i18n('uploadedBytes'); 839 | return this._getXHRPromise(false, options.context, [ 840 | null, 841 | 'error', 842 | file.error 843 | ]); 844 | } 845 | // The chunk upload method: 846 | upload = function () { 847 | // Clone the options object for each chunk upload: 848 | var o = $.extend({}, options), 849 | currentLoaded = o._progress.loaded; 850 | o.blob = slice.call( 851 | file, 852 | ub, 853 | ub + ($.type(mcs) === 'function' ? mcs(o) : mcs), 854 | file.type 855 | ); 856 | // Store the current chunk size, as the blob itself 857 | // will be dereferenced after data processing: 858 | o.chunkSize = o.blob.size; 859 | // Expose the chunk bytes position range: 860 | o.contentRange = 861 | 'bytes ' + ub + '-' + (ub + o.chunkSize - 1) + '/' + fs; 862 | // Trigger chunkbeforesend to allow form data to be updated for this chunk 863 | that._trigger('chunkbeforesend', null, o); 864 | // Process the upload data (the blob and potential form data): 865 | that._initXHRData(o); 866 | // Add progress listeners for this chunk upload: 867 | that._initProgressListener(o); 868 | jqXHR = ( 869 | (that._trigger('chunksend', null, o) !== false && $.ajax(o)) || 870 | that._getXHRPromise(false, o.context) 871 | ) 872 | .done(function (result, textStatus, jqXHR) { 873 | ub = that._getUploadedBytes(jqXHR) || ub + o.chunkSize; 874 | // Create a progress event if no final progress event 875 | // with loaded equaling total has been triggered 876 | // for this chunk: 877 | if (currentLoaded + o.chunkSize - o._progress.loaded) { 878 | that._onProgress( 879 | $.Event('progress', { 880 | lengthComputable: true, 881 | loaded: ub - o.uploadedBytes, 882 | total: ub - o.uploadedBytes 883 | }), 884 | o 885 | ); 886 | } 887 | options.uploadedBytes = o.uploadedBytes = ub; 888 | o.result = result; 889 | o.textStatus = textStatus; 890 | o.jqXHR = jqXHR; 891 | that._trigger('chunkdone', null, o); 892 | that._trigger('chunkalways', null, o); 893 | if (ub < fs) { 894 | // File upload not yet complete, 895 | // continue with the next chunk: 896 | upload(); 897 | } else { 898 | dfd.resolveWith(o.context, [result, textStatus, jqXHR]); 899 | } 900 | }) 901 | .fail(function (jqXHR, textStatus, errorThrown) { 902 | o.jqXHR = jqXHR; 903 | o.textStatus = textStatus; 904 | o.errorThrown = errorThrown; 905 | that._trigger('chunkfail', null, o); 906 | that._trigger('chunkalways', null, o); 907 | dfd.rejectWith(o.context, [jqXHR, textStatus, errorThrown]); 908 | }) 909 | .always(function () { 910 | that._deinitProgressListener(o); 911 | }); 912 | }; 913 | this._enhancePromise(promise); 914 | promise.abort = function () { 915 | return jqXHR.abort(); 916 | }; 917 | upload(); 918 | return promise; 919 | }, 920 | 921 | _beforeSend: function (e, data) { 922 | if (this._active === 0) { 923 | // the start callback is triggered when an upload starts 924 | // and no other uploads are currently running, 925 | // equivalent to the global ajaxStart event: 926 | this._trigger('start'); 927 | // Set timer for global bitrate progress calculation: 928 | this._bitrateTimer = new this._BitrateTimer(); 929 | // Reset the global progress values: 930 | this._progress.loaded = this._progress.total = 0; 931 | this._progress.bitrate = 0; 932 | } 933 | // Make sure the container objects for the .response() and 934 | // .progress() methods on the data object are available 935 | // and reset to their initial state: 936 | this._initResponseObject(data); 937 | this._initProgressObject(data); 938 | data._progress.loaded = data.loaded = data.uploadedBytes || 0; 939 | data._progress.total = data.total = this._getTotal(data.files) || 1; 940 | data._progress.bitrate = data.bitrate = 0; 941 | this._active += 1; 942 | // Initialize the global progress values: 943 | this._progress.loaded += data.loaded; 944 | this._progress.total += data.total; 945 | }, 946 | 947 | _onDone: function (result, textStatus, jqXHR, options) { 948 | var total = options._progress.total, 949 | response = options._response; 950 | if (options._progress.loaded < total) { 951 | // Create a progress event if no final progress event 952 | // with loaded equaling total has been triggered: 953 | this._onProgress( 954 | $.Event('progress', { 955 | lengthComputable: true, 956 | loaded: total, 957 | total: total 958 | }), 959 | options 960 | ); 961 | } 962 | response.result = options.result = result; 963 | response.textStatus = options.textStatus = textStatus; 964 | response.jqXHR = options.jqXHR = jqXHR; 965 | this._trigger('done', null, options); 966 | }, 967 | 968 | _onFail: function (jqXHR, textStatus, errorThrown, options) { 969 | var response = options._response; 970 | if (options.recalculateProgress) { 971 | // Remove the failed (error or abort) file upload from 972 | // the global progress calculation: 973 | this._progress.loaded -= options._progress.loaded; 974 | this._progress.total -= options._progress.total; 975 | } 976 | response.jqXHR = options.jqXHR = jqXHR; 977 | response.textStatus = options.textStatus = textStatus; 978 | response.errorThrown = options.errorThrown = errorThrown; 979 | this._trigger('fail', null, options); 980 | }, 981 | 982 | _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { 983 | // jqXHRorResult, textStatus and jqXHRorError are added to the 984 | // options object via done and fail callbacks 985 | this._trigger('always', null, options); 986 | }, 987 | 988 | _onSend: function (e, data) { 989 | if (!data.submit) { 990 | this._addConvenienceMethods(e, data); 991 | } 992 | var that = this, 993 | jqXHR, 994 | aborted, 995 | slot, 996 | pipe, 997 | options = that._getAJAXSettings(data), 998 | send = function () { 999 | that._sending += 1; 1000 | // Set timer for bitrate progress calculation: 1001 | options._bitrateTimer = new that._BitrateTimer(); 1002 | jqXHR = 1003 | jqXHR || 1004 | ( 1005 | ((aborted || 1006 | that._trigger( 1007 | 'send', 1008 | $.Event('send', { delegatedEvent: e }), 1009 | options 1010 | ) === false) && 1011 | that._getXHRPromise(false, options.context, aborted)) || 1012 | that._chunkedUpload(options) || 1013 | $.ajax(options) 1014 | ) 1015 | .done(function (result, textStatus, jqXHR) { 1016 | that._onDone(result, textStatus, jqXHR, options); 1017 | }) 1018 | .fail(function (jqXHR, textStatus, errorThrown) { 1019 | that._onFail(jqXHR, textStatus, errorThrown, options); 1020 | }) 1021 | .always(function (jqXHRorResult, textStatus, jqXHRorError) { 1022 | that._deinitProgressListener(options); 1023 | that._onAlways( 1024 | jqXHRorResult, 1025 | textStatus, 1026 | jqXHRorError, 1027 | options 1028 | ); 1029 | that._sending -= 1; 1030 | that._active -= 1; 1031 | if ( 1032 | options.limitConcurrentUploads && 1033 | options.limitConcurrentUploads > that._sending 1034 | ) { 1035 | // Start the next queued upload, 1036 | // that has not been aborted: 1037 | var nextSlot = that._slots.shift(); 1038 | while (nextSlot) { 1039 | if (that._getDeferredState(nextSlot) === 'pending') { 1040 | nextSlot.resolve(); 1041 | break; 1042 | } 1043 | nextSlot = that._slots.shift(); 1044 | } 1045 | } 1046 | if (that._active === 0) { 1047 | // The stop callback is triggered when all uploads have 1048 | // been completed, equivalent to the global ajaxStop event: 1049 | that._trigger('stop'); 1050 | } 1051 | }); 1052 | return jqXHR; 1053 | }; 1054 | this._beforeSend(e, options); 1055 | if ( 1056 | this.options.sequentialUploads || 1057 | (this.options.limitConcurrentUploads && 1058 | this.options.limitConcurrentUploads <= this._sending) 1059 | ) { 1060 | if (this.options.limitConcurrentUploads > 1) { 1061 | slot = $.Deferred(); 1062 | this._slots.push(slot); 1063 | pipe = slot[that._promisePipe](send); 1064 | } else { 1065 | this._sequence = this._sequence[that._promisePipe](send, send); 1066 | pipe = this._sequence; 1067 | } 1068 | // Return the piped Promise object, enhanced with an abort method, 1069 | // which is delegated to the jqXHR object of the current upload, 1070 | // and jqXHR callbacks mapped to the equivalent Promise methods: 1071 | pipe.abort = function () { 1072 | aborted = [undefined, 'abort', 'abort']; 1073 | if (!jqXHR) { 1074 | if (slot) { 1075 | slot.rejectWith(options.context, aborted); 1076 | } 1077 | return send(); 1078 | } 1079 | return jqXHR.abort(); 1080 | }; 1081 | return this._enhancePromise(pipe); 1082 | } 1083 | return send(); 1084 | }, 1085 | 1086 | _onAdd: function (e, data) { 1087 | var that = this, 1088 | result = true, 1089 | options = $.extend({}, this.options, data), 1090 | files = data.files, 1091 | filesLength = files.length, 1092 | limit = options.limitMultiFileUploads, 1093 | limitSize = options.limitMultiFileUploadSize, 1094 | overhead = options.limitMultiFileUploadSizeOverhead, 1095 | batchSize = 0, 1096 | paramName = this._getParamName(options), 1097 | paramNameSet, 1098 | paramNameSlice, 1099 | fileSet, 1100 | i, 1101 | j = 0; 1102 | if (!filesLength) { 1103 | return false; 1104 | } 1105 | if (limitSize && files[0].size === undefined) { 1106 | limitSize = undefined; 1107 | } 1108 | if ( 1109 | !(options.singleFileUploads || limit || limitSize) || 1110 | !this._isXHRUpload(options) 1111 | ) { 1112 | fileSet = [files]; 1113 | paramNameSet = [paramName]; 1114 | } else if (!(options.singleFileUploads || limitSize) && limit) { 1115 | fileSet = []; 1116 | paramNameSet = []; 1117 | for (i = 0; i < filesLength; i += limit) { 1118 | fileSet.push(files.slice(i, i + limit)); 1119 | paramNameSlice = paramName.slice(i, i + limit); 1120 | if (!paramNameSlice.length) { 1121 | paramNameSlice = paramName; 1122 | } 1123 | paramNameSet.push(paramNameSlice); 1124 | } 1125 | } else if (!options.singleFileUploads && limitSize) { 1126 | fileSet = []; 1127 | paramNameSet = []; 1128 | for (i = 0; i < filesLength; i = i + 1) { 1129 | batchSize += files[i].size + overhead; 1130 | if ( 1131 | i + 1 === filesLength || 1132 | batchSize + files[i + 1].size + overhead > limitSize || 1133 | (limit && i + 1 - j >= limit) 1134 | ) { 1135 | fileSet.push(files.slice(j, i + 1)); 1136 | paramNameSlice = paramName.slice(j, i + 1); 1137 | if (!paramNameSlice.length) { 1138 | paramNameSlice = paramName; 1139 | } 1140 | paramNameSet.push(paramNameSlice); 1141 | j = i + 1; 1142 | batchSize = 0; 1143 | } 1144 | } 1145 | } else { 1146 | paramNameSet = paramName; 1147 | } 1148 | data.originalFiles = files; 1149 | $.each(fileSet || files, function (index, element) { 1150 | var newData = $.extend({}, data); 1151 | newData.files = fileSet ? element : [element]; 1152 | newData.paramName = paramNameSet[index]; 1153 | that._initResponseObject(newData); 1154 | that._initProgressObject(newData); 1155 | that._addConvenienceMethods(e, newData); 1156 | result = that._trigger( 1157 | 'add', 1158 | $.Event('add', { delegatedEvent: e }), 1159 | newData 1160 | ); 1161 | return result; 1162 | }); 1163 | return result; 1164 | }, 1165 | 1166 | _replaceFileInput: function (data) { 1167 | var input = data.fileInput, 1168 | inputClone = input.clone(true), 1169 | restoreFocus = input.is(document.activeElement); 1170 | // Add a reference for the new cloned file input to the data argument: 1171 | data.fileInputClone = inputClone; 1172 | $('
').append(inputClone)[0].reset(); 1173 | // Detaching allows to insert the fileInput on another form 1174 | // without losing the file input value: 1175 | input.after(inputClone).detach(); 1176 | // If the fileInput had focus before it was detached, 1177 | // restore focus to the inputClone. 1178 | if (restoreFocus) { 1179 | inputClone.trigger('focus'); 1180 | } 1181 | // Avoid memory leaks with the detached file input: 1182 | $.cleanData(input.off('remove')); 1183 | // Replace the original file input element in the fileInput 1184 | // elements set with the clone, which has been copied including 1185 | // event handlers: 1186 | this.options.fileInput = this.options.fileInput.map(function (i, el) { 1187 | if (el === input[0]) { 1188 | return inputClone[0]; 1189 | } 1190 | return el; 1191 | }); 1192 | // If the widget has been initialized on the file input itself, 1193 | // override this.element with the file input clone: 1194 | if (input[0] === this.element[0]) { 1195 | this.element = inputClone; 1196 | } 1197 | }, 1198 | 1199 | _handleFileTreeEntry: function (entry, path) { 1200 | var that = this, 1201 | dfd = $.Deferred(), 1202 | entries = [], 1203 | dirReader, 1204 | errorHandler = function (e) { 1205 | if (e && !e.entry) { 1206 | e.entry = entry; 1207 | } 1208 | // Since $.when returns immediately if one 1209 | // Deferred is rejected, we use resolve instead. 1210 | // This allows valid files and invalid items 1211 | // to be returned together in one set: 1212 | dfd.resolve([e]); 1213 | }, 1214 | successHandler = function (entries) { 1215 | that 1216 | ._handleFileTreeEntries(entries, path + entry.name + '/') 1217 | .done(function (files) { 1218 | dfd.resolve(files); 1219 | }) 1220 | .fail(errorHandler); 1221 | }, 1222 | readEntries = function () { 1223 | dirReader.readEntries(function (results) { 1224 | if (!results.length) { 1225 | successHandler(entries); 1226 | } else { 1227 | entries = entries.concat(results); 1228 | readEntries(); 1229 | } 1230 | }, errorHandler); 1231 | }; 1232 | // eslint-disable-next-line no-param-reassign 1233 | path = path || ''; 1234 | if (entry.isFile) { 1235 | if (entry._file) { 1236 | // Workaround for Chrome bug #149735 1237 | entry._file.relativePath = path; 1238 | dfd.resolve(entry._file); 1239 | } else { 1240 | entry.file(function (file) { 1241 | file.relativePath = path; 1242 | dfd.resolve(file); 1243 | }, errorHandler); 1244 | } 1245 | } else if (entry.isDirectory) { 1246 | dirReader = entry.createReader(); 1247 | readEntries(); 1248 | } else { 1249 | // Return an empty list for file system items 1250 | // other than files or directories: 1251 | dfd.resolve([]); 1252 | } 1253 | return dfd.promise(); 1254 | }, 1255 | 1256 | _handleFileTreeEntries: function (entries, path) { 1257 | var that = this; 1258 | return $.when 1259 | .apply( 1260 | $, 1261 | $.map(entries, function (entry) { 1262 | return that._handleFileTreeEntry(entry, path); 1263 | }) 1264 | ) 1265 | [this._promisePipe](function () { 1266 | return Array.prototype.concat.apply([], arguments); 1267 | }); 1268 | }, 1269 | 1270 | _getDroppedFiles: function (dataTransfer) { 1271 | // eslint-disable-next-line no-param-reassign 1272 | dataTransfer = dataTransfer || {}; 1273 | var items = dataTransfer.items; 1274 | if ( 1275 | items && 1276 | items.length && 1277 | (items[0].webkitGetAsEntry || items[0].getAsEntry) 1278 | ) { 1279 | return this._handleFileTreeEntries( 1280 | $.map(items, function (item) { 1281 | var entry; 1282 | if (item.webkitGetAsEntry) { 1283 | entry = item.webkitGetAsEntry(); 1284 | if (entry) { 1285 | // Workaround for Chrome bug #149735: 1286 | entry._file = item.getAsFile(); 1287 | } 1288 | return entry; 1289 | } 1290 | return item.getAsEntry(); 1291 | }) 1292 | ); 1293 | } 1294 | return $.Deferred().resolve($.makeArray(dataTransfer.files)).promise(); 1295 | }, 1296 | 1297 | _getSingleFileInputFiles: function (fileInput) { 1298 | // eslint-disable-next-line no-param-reassign 1299 | fileInput = $(fileInput); 1300 | var entries = fileInput.prop('entries'), 1301 | files, 1302 | value; 1303 | if (entries && entries.length) { 1304 | return this._handleFileTreeEntries(entries); 1305 | } 1306 | files = $.makeArray(fileInput.prop('files')); 1307 | if (!files.length) { 1308 | value = fileInput.prop('value'); 1309 | if (!value) { 1310 | return $.Deferred().resolve([]).promise(); 1311 | } 1312 | // If the files property is not available, the browser does not 1313 | // support the File API and we add a pseudo File object with 1314 | // the input value as name with path information removed: 1315 | files = [{ name: value.replace(/^.*\\/, '') }]; 1316 | } else if (files[0].name === undefined && files[0].fileName) { 1317 | // File normalization for Safari 4 and Firefox 3: 1318 | $.each(files, function (index, file) { 1319 | file.name = file.fileName; 1320 | file.size = file.fileSize; 1321 | }); 1322 | } 1323 | return $.Deferred().resolve(files).promise(); 1324 | }, 1325 | 1326 | _getFileInputFiles: function (fileInput) { 1327 | if (!(fileInput instanceof $) || fileInput.length === 1) { 1328 | return this._getSingleFileInputFiles(fileInput); 1329 | } 1330 | return $.when 1331 | .apply($, $.map(fileInput, this._getSingleFileInputFiles)) 1332 | [this._promisePipe](function () { 1333 | return Array.prototype.concat.apply([], arguments); 1334 | }); 1335 | }, 1336 | 1337 | _onChange: function (e) { 1338 | var that = this, 1339 | data = { 1340 | fileInput: $(e.target), 1341 | form: $(e.target.form) 1342 | }; 1343 | this._getFileInputFiles(data.fileInput).always(function (files) { 1344 | data.files = files; 1345 | if (that.options.replaceFileInput) { 1346 | that._replaceFileInput(data); 1347 | } 1348 | if ( 1349 | that._trigger( 1350 | 'change', 1351 | $.Event('change', { delegatedEvent: e }), 1352 | data 1353 | ) !== false 1354 | ) { 1355 | that._onAdd(e, data); 1356 | } 1357 | }); 1358 | }, 1359 | 1360 | _onPaste: function (e) { 1361 | var items = 1362 | e.originalEvent && 1363 | e.originalEvent.clipboardData && 1364 | e.originalEvent.clipboardData.items, 1365 | data = { files: [] }; 1366 | if (items && items.length) { 1367 | $.each(items, function (index, item) { 1368 | var file = item.getAsFile && item.getAsFile(); 1369 | if (file) { 1370 | data.files.push(file); 1371 | } 1372 | }); 1373 | if ( 1374 | this._trigger( 1375 | 'paste', 1376 | $.Event('paste', { delegatedEvent: e }), 1377 | data 1378 | ) !== false 1379 | ) { 1380 | this._onAdd(e, data); 1381 | } 1382 | } 1383 | }, 1384 | 1385 | _onDrop: function (e) { 1386 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 1387 | var that = this, 1388 | dataTransfer = e.dataTransfer, 1389 | data = {}; 1390 | if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { 1391 | e.preventDefault(); 1392 | this._getDroppedFiles(dataTransfer).always(function (files) { 1393 | data.files = files; 1394 | if ( 1395 | that._trigger( 1396 | 'drop', 1397 | $.Event('drop', { delegatedEvent: e }), 1398 | data 1399 | ) !== false 1400 | ) { 1401 | that._onAdd(e, data); 1402 | } 1403 | }); 1404 | } 1405 | }, 1406 | 1407 | _onDragOver: getDragHandler('dragover'), 1408 | 1409 | _onDragEnter: getDragHandler('dragenter'), 1410 | 1411 | _onDragLeave: getDragHandler('dragleave'), 1412 | 1413 | _initEventHandlers: function () { 1414 | if (this._isXHRUpload(this.options)) { 1415 | this._on(this.options.dropZone, { 1416 | dragover: this._onDragOver, 1417 | drop: this._onDrop, 1418 | // event.preventDefault() on dragenter is required for IE10+: 1419 | dragenter: this._onDragEnter, 1420 | // dragleave is not required, but added for completeness: 1421 | dragleave: this._onDragLeave 1422 | }); 1423 | this._on(this.options.pasteZone, { 1424 | paste: this._onPaste 1425 | }); 1426 | } 1427 | if ($.support.fileInput) { 1428 | this._on(this.options.fileInput, { 1429 | change: this._onChange 1430 | }); 1431 | } 1432 | }, 1433 | 1434 | _destroyEventHandlers: function () { 1435 | this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); 1436 | this._off(this.options.pasteZone, 'paste'); 1437 | this._off(this.options.fileInput, 'change'); 1438 | }, 1439 | 1440 | _destroy: function () { 1441 | this._destroyEventHandlers(); 1442 | }, 1443 | 1444 | _setOption: function (key, value) { 1445 | var reinit = $.inArray(key, this._specialOptions) !== -1; 1446 | if (reinit) { 1447 | this._destroyEventHandlers(); 1448 | } 1449 | this._super(key, value); 1450 | if (reinit) { 1451 | this._initSpecialOptions(); 1452 | this._initEventHandlers(); 1453 | } 1454 | }, 1455 | 1456 | _initSpecialOptions: function () { 1457 | var options = this.options; 1458 | if (options.fileInput === undefined) { 1459 | options.fileInput = this.element.is('input[type="file"]') 1460 | ? this.element 1461 | : this.element.find('input[type="file"]'); 1462 | } else if (!(options.fileInput instanceof $)) { 1463 | options.fileInput = $(options.fileInput); 1464 | } 1465 | if (!(options.dropZone instanceof $)) { 1466 | options.dropZone = $(options.dropZone); 1467 | } 1468 | if (!(options.pasteZone instanceof $)) { 1469 | options.pasteZone = $(options.pasteZone); 1470 | } 1471 | }, 1472 | 1473 | _getRegExp: function (str) { 1474 | var parts = str.split('/'), 1475 | modifiers = parts.pop(); 1476 | parts.shift(); 1477 | return new RegExp(parts.join('/'), modifiers); 1478 | }, 1479 | 1480 | _isRegExpOption: function (key, value) { 1481 | return ( 1482 | key !== 'url' && 1483 | $.type(value) === 'string' && 1484 | /^\/.*\/[igm]{0,3}$/.test(value) 1485 | ); 1486 | }, 1487 | 1488 | _initDataAttributes: function () { 1489 | var that = this, 1490 | options = this.options, 1491 | data = this.element.data(); 1492 | // Initialize options set via HTML5 data-attributes: 1493 | $.each(this.element[0].attributes, function (index, attr) { 1494 | var key = attr.name.toLowerCase(), 1495 | value; 1496 | if (/^data-/.test(key)) { 1497 | // Convert hyphen-ated key to camelCase: 1498 | key = key.slice(5).replace(/-[a-z]/g, function (str) { 1499 | return str.charAt(1).toUpperCase(); 1500 | }); 1501 | value = data[key]; 1502 | if (that._isRegExpOption(key, value)) { 1503 | value = that._getRegExp(value); 1504 | } 1505 | options[key] = value; 1506 | } 1507 | }); 1508 | }, 1509 | 1510 | _create: function () { 1511 | this._initDataAttributes(); 1512 | this._initSpecialOptions(); 1513 | this._slots = []; 1514 | this._sequence = this._getXHRPromise(true); 1515 | this._sending = this._active = 0; 1516 | this._initProgressObject(this); 1517 | this._initEventHandlers(); 1518 | }, 1519 | 1520 | // This method is exposed to the widget API and allows to query 1521 | // the number of active uploads: 1522 | active: function () { 1523 | return this._active; 1524 | }, 1525 | 1526 | // This method is exposed to the widget API and allows to query 1527 | // the widget upload progress. 1528 | // It returns an object with loaded, total and bitrate properties 1529 | // for the running uploads: 1530 | progress: function () { 1531 | return this._progress; 1532 | }, 1533 | 1534 | // This method is exposed to the widget API and allows adding files 1535 | // using the fileupload API. The data parameter accepts an object which 1536 | // must have a files property and can contain additional options: 1537 | // .fileupload('add', {files: filesList}); 1538 | add: function (data) { 1539 | var that = this; 1540 | if (!data || this.options.disabled) { 1541 | return; 1542 | } 1543 | if (data.fileInput && !data.files) { 1544 | this._getFileInputFiles(data.fileInput).always(function (files) { 1545 | data.files = files; 1546 | that._onAdd(null, data); 1547 | }); 1548 | } else { 1549 | data.files = $.makeArray(data.files); 1550 | this._onAdd(null, data); 1551 | } 1552 | }, 1553 | 1554 | // This method is exposed to the widget API and allows sending files 1555 | // using the fileupload API. The data parameter accepts an object which 1556 | // must have a files or fileInput property and can contain additional options: 1557 | // .fileupload('send', {files: filesList}); 1558 | // The method returns a Promise object for the file upload call. 1559 | send: function (data) { 1560 | if (data && !this.options.disabled) { 1561 | if (data.fileInput && !data.files) { 1562 | var that = this, 1563 | dfd = $.Deferred(), 1564 | promise = dfd.promise(), 1565 | jqXHR, 1566 | aborted; 1567 | promise.abort = function () { 1568 | aborted = true; 1569 | if (jqXHR) { 1570 | return jqXHR.abort(); 1571 | } 1572 | dfd.reject(null, 'abort', 'abort'); 1573 | return promise; 1574 | }; 1575 | this._getFileInputFiles(data.fileInput).always(function (files) { 1576 | if (aborted) { 1577 | return; 1578 | } 1579 | if (!files.length) { 1580 | dfd.reject(); 1581 | return; 1582 | } 1583 | data.files = files; 1584 | jqXHR = that._onSend(null, data); 1585 | jqXHR.then( 1586 | function (result, textStatus, jqXHR) { 1587 | dfd.resolve(result, textStatus, jqXHR); 1588 | }, 1589 | function (jqXHR, textStatus, errorThrown) { 1590 | dfd.reject(jqXHR, textStatus, errorThrown); 1591 | } 1592 | ); 1593 | }); 1594 | return this._enhancePromise(promise); 1595 | } 1596 | data.files = $.makeArray(data.files); 1597 | if (data.files.length) { 1598 | return this._onSend(null, data); 1599 | } 1600 | } 1601 | return this._getXHRPromise(false, data && data.context); 1602 | } 1603 | }); 1604 | }); 1605 | -------------------------------------------------------------------------------- /js/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Iframe Transport Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory(require('jquery')); 22 | } else { 23 | // Browser globals: 24 | factory(window.jQuery); 25 | } 26 | })(function ($) { 27 | 'use strict'; 28 | 29 | // Helper variable to create unique names for the transport iframes: 30 | var counter = 0, 31 | jsonAPI = $, 32 | jsonParse = 'parseJSON'; 33 | 34 | if ('JSON' in window && 'parse' in JSON) { 35 | jsonAPI = JSON; 36 | jsonParse = 'parse'; 37 | } 38 | 39 | // The iframe transport accepts four additional options: 40 | // options.fileInput: a jQuery collection of file input fields 41 | // options.paramName: the parameter name for the file form data, 42 | // overrides the name property of the file input field(s), 43 | // can be a string or an array of strings. 44 | // options.formData: an array of objects with name and value properties, 45 | // equivalent to the return data of .serializeArray(), e.g.: 46 | // [{name: 'a', value: 1}, {name: 'b', value: 2}] 47 | // options.initialIframeSrc: the URL of the initial iframe src, 48 | // by default set to "javascript:false;" 49 | $.ajaxTransport('iframe', function (options) { 50 | if (options.async) { 51 | // javascript:false as initial iframe src 52 | // prevents warning popups on HTTPS in IE6: 53 | // eslint-disable-next-line no-script-url 54 | var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', 55 | form, 56 | iframe, 57 | addParamChar; 58 | return { 59 | send: function (_, completeCallback) { 60 | form = $('
'); 61 | form.attr('accept-charset', options.formAcceptCharset); 62 | addParamChar = /\?/.test(options.url) ? '&' : '?'; 63 | // XDomainRequest only supports GET and POST: 64 | if (options.type === 'DELETE') { 65 | options.url = options.url + addParamChar + '_method=DELETE'; 66 | options.type = 'POST'; 67 | } else if (options.type === 'PUT') { 68 | options.url = options.url + addParamChar + '_method=PUT'; 69 | options.type = 'POST'; 70 | } else if (options.type === 'PATCH') { 71 | options.url = options.url + addParamChar + '_method=PATCH'; 72 | options.type = 'POST'; 73 | } 74 | // IE versions below IE8 cannot set the name property of 75 | // elements that have already been added to the DOM, 76 | // so we set the name along with the iframe HTML markup: 77 | counter += 1; 78 | iframe = $( 79 | '' 84 | ).on('load', function () { 85 | var fileInputClones, 86 | paramNames = $.isArray(options.paramName) 87 | ? options.paramName 88 | : [options.paramName]; 89 | iframe.off('load').on('load', function () { 90 | var response; 91 | // Wrap in a try/catch block to catch exceptions thrown 92 | // when trying to access cross-domain iframe contents: 93 | try { 94 | response = iframe.contents(); 95 | // Google Chrome and Firefox do not throw an 96 | // exception when calling iframe.contents() on 97 | // cross-domain requests, so we unify the response: 98 | if (!response.length || !response[0].firstChild) { 99 | throw new Error(); 100 | } 101 | } catch (e) { 102 | response = undefined; 103 | } 104 | // The complete callback returns the 105 | // iframe content document as response object: 106 | completeCallback(200, 'success', { iframe: response }); 107 | // Fix for IE endless progress bar activity bug 108 | // (happens on form submits to iframe targets): 109 | $('').appendTo( 110 | form 111 | ); 112 | window.setTimeout(function () { 113 | // Removing the form in a setTimeout call 114 | // allows Chrome's developer tools to display 115 | // the response result 116 | form.remove(); 117 | }, 0); 118 | }); 119 | form 120 | .prop('target', iframe.prop('name')) 121 | .prop('action', options.url) 122 | .prop('method', options.type); 123 | if (options.formData) { 124 | $.each(options.formData, function (index, field) { 125 | $('') 126 | .prop('name', field.name) 127 | .val(field.value) 128 | .appendTo(form); 129 | }); 130 | } 131 | if ( 132 | options.fileInput && 133 | options.fileInput.length && 134 | options.type === 'POST' 135 | ) { 136 | fileInputClones = options.fileInput.clone(); 137 | // Insert a clone for each file input field: 138 | options.fileInput.after(function (index) { 139 | return fileInputClones[index]; 140 | }); 141 | if (options.paramName) { 142 | options.fileInput.each(function (index) { 143 | $(this).prop('name', paramNames[index] || options.paramName); 144 | }); 145 | } 146 | // Appending the file input fields to the hidden form 147 | // removes them from their original location: 148 | form 149 | .append(options.fileInput) 150 | .prop('enctype', 'multipart/form-data') 151 | // enctype must be set as encoding for IE: 152 | .prop('encoding', 'multipart/form-data'); 153 | // Remove the HTML5 form attribute from the input(s): 154 | options.fileInput.removeAttr('form'); 155 | } 156 | window.setTimeout(function () { 157 | // Submitting the form in a setTimeout call fixes an issue with 158 | // Safari 13 not triggering the iframe load event after resetting 159 | // the load event handler, see also: 160 | // https://github.com/blueimp/jQuery-File-Upload/issues/3633 161 | form.submit(); 162 | // Insert the file input fields at their original location 163 | // by replacing the clones with the originals: 164 | if (fileInputClones && fileInputClones.length) { 165 | options.fileInput.each(function (index, input) { 166 | var clone = $(fileInputClones[index]); 167 | // Restore the original name and form properties: 168 | $(input) 169 | .prop('name', clone.prop('name')) 170 | .attr('form', clone.attr('form')); 171 | clone.replaceWith(input); 172 | }); 173 | } 174 | }, 0); 175 | }); 176 | form.append(iframe).appendTo(document.body); 177 | }, 178 | abort: function () { 179 | if (iframe) { 180 | // javascript:false as iframe src aborts the request 181 | // and prevents warning popups on HTTPS in IE6. 182 | iframe.off('load').prop('src', initialIframeSrc); 183 | } 184 | if (form) { 185 | form.remove(); 186 | } 187 | } 188 | }; 189 | } 190 | }); 191 | 192 | // The iframe transport returns the iframe content document as response. 193 | // The following adds converters from iframe to text, json, html, xml 194 | // and script. 195 | // Please note that the Content-Type for JSON responses has to be text/plain 196 | // or text/html, if the browser doesn't include application/json in the 197 | // Accept header, else IE will show a download dialog. 198 | // The Content-Type for XML responses on the other hand has to be always 199 | // application/xml or text/xml, so IE properly parses the XML response. 200 | // See also 201 | // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation 202 | $.ajaxSetup({ 203 | converters: { 204 | 'iframe text': function (iframe) { 205 | return iframe && $(iframe[0].body).text(); 206 | }, 207 | 'iframe json': function (iframe) { 208 | return iframe && jsonAPI[jsonParse]($(iframe[0].body).text()); 209 | }, 210 | 'iframe html': function (iframe) { 211 | return iframe && $(iframe[0].body).html(); 212 | }, 213 | 'iframe xml': function (iframe) { 214 | var xmlDoc = iframe && iframe[0]; 215 | return xmlDoc && $.isXMLDoc(xmlDoc) 216 | ? xmlDoc 217 | : $.parseXML( 218 | (xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || 219 | $(xmlDoc.body).html() 220 | ); 221 | }, 222 | 'iframe script': function (iframe) { 223 | return iframe && $.globalEval($(iframe[0].body).text()); 224 | } 225 | } 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /js/vendor/jquery.ui.widget.js: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1+0b7246b6eeadfa9e2696e22f3230f6452f8129dc - 2020-02-20 2 | * http://jqueryui.com 3 | * Includes: widget.js 4 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 5 | 6 | /* global define, require */ 7 | /* eslint-disable no-param-reassign, new-cap, jsdoc/require-jsdoc */ 8 | 9 | (function (factory) { 10 | 'use strict'; 11 | if (typeof define === 'function' && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define(['jquery'], factory); 14 | } else if (typeof exports === 'object') { 15 | // Node/CommonJS 16 | factory(require('jquery')); 17 | } else { 18 | // Browser globals 19 | factory(window.jQuery); 20 | } 21 | })(function ($) { 22 | ('use strict'); 23 | 24 | $.ui = $.ui || {}; 25 | 26 | $.ui.version = '1.12.1'; 27 | 28 | /*! 29 | * jQuery UI Widget 1.12.1 30 | * http://jqueryui.com 31 | * 32 | * Copyright jQuery Foundation and other contributors 33 | * Released under the MIT license. 34 | * http://jquery.org/license 35 | */ 36 | 37 | //>>label: Widget 38 | //>>group: Core 39 | //>>description: Provides a factory for creating stateful widgets with a common API. 40 | //>>docs: http://api.jqueryui.com/jQuery.widget/ 41 | //>>demos: http://jqueryui.com/widget/ 42 | 43 | // Support: jQuery 1.9.x or older 44 | // $.expr[ ":" ] is deprecated. 45 | if (!$.expr.pseudos) { 46 | $.expr.pseudos = $.expr[':']; 47 | } 48 | 49 | // Support: jQuery 1.11.x or older 50 | // $.unique has been renamed to $.uniqueSort 51 | if (!$.uniqueSort) { 52 | $.uniqueSort = $.unique; 53 | } 54 | 55 | var widgetUuid = 0; 56 | var widgetHasOwnProperty = Array.prototype.hasOwnProperty; 57 | var widgetSlice = Array.prototype.slice; 58 | 59 | $.cleanData = (function (orig) { 60 | return function (elems) { 61 | var events, elem, i; 62 | // eslint-disable-next-line eqeqeq 63 | for (i = 0; (elem = elems[i]) != null; i++) { 64 | // Only trigger remove when necessary to save time 65 | events = $._data(elem, 'events'); 66 | if (events && events.remove) { 67 | $(elem).triggerHandler('remove'); 68 | } 69 | } 70 | orig(elems); 71 | }; 72 | })($.cleanData); 73 | 74 | $.widget = function (name, base, prototype) { 75 | var existingConstructor, constructor, basePrototype; 76 | 77 | // ProxiedPrototype allows the provided prototype to remain unmodified 78 | // so that it can be used as a mixin for multiple widgets (#8876) 79 | var proxiedPrototype = {}; 80 | 81 | var namespace = name.split('.')[0]; 82 | name = name.split('.')[1]; 83 | var fullName = namespace + '-' + name; 84 | 85 | if (!prototype) { 86 | prototype = base; 87 | base = $.Widget; 88 | } 89 | 90 | if ($.isArray(prototype)) { 91 | prototype = $.extend.apply(null, [{}].concat(prototype)); 92 | } 93 | 94 | // Create selector for plugin 95 | $.expr.pseudos[fullName.toLowerCase()] = function (elem) { 96 | return !!$.data(elem, fullName); 97 | }; 98 | 99 | $[namespace] = $[namespace] || {}; 100 | existingConstructor = $[namespace][name]; 101 | constructor = $[namespace][name] = function (options, element) { 102 | // Allow instantiation without "new" keyword 103 | if (!this._createWidget) { 104 | return new constructor(options, element); 105 | } 106 | 107 | // Allow instantiation without initializing for simple inheritance 108 | // must use "new" keyword (the code above always passes args) 109 | if (arguments.length) { 110 | this._createWidget(options, element); 111 | } 112 | }; 113 | 114 | // Extend with the existing constructor to carry over any static properties 115 | $.extend(constructor, existingConstructor, { 116 | version: prototype.version, 117 | 118 | // Copy the object used to create the prototype in case we need to 119 | // redefine the widget later 120 | _proto: $.extend({}, prototype), 121 | 122 | // Track widgets that inherit from this widget in case this widget is 123 | // redefined after a widget inherits from it 124 | _childConstructors: [] 125 | }); 126 | 127 | basePrototype = new base(); 128 | 129 | // We need to make the options hash a property directly on the new instance 130 | // otherwise we'll modify the options hash on the prototype that we're 131 | // inheriting from 132 | basePrototype.options = $.widget.extend({}, basePrototype.options); 133 | $.each(prototype, function (prop, value) { 134 | if (!$.isFunction(value)) { 135 | proxiedPrototype[prop] = value; 136 | return; 137 | } 138 | proxiedPrototype[prop] = (function () { 139 | function _super() { 140 | return base.prototype[prop].apply(this, arguments); 141 | } 142 | 143 | function _superApply(args) { 144 | return base.prototype[prop].apply(this, args); 145 | } 146 | 147 | return function () { 148 | var __super = this._super; 149 | var __superApply = this._superApply; 150 | var returnValue; 151 | 152 | this._super = _super; 153 | this._superApply = _superApply; 154 | 155 | returnValue = value.apply(this, arguments); 156 | 157 | this._super = __super; 158 | this._superApply = __superApply; 159 | 160 | return returnValue; 161 | }; 162 | })(); 163 | }); 164 | constructor.prototype = $.widget.extend( 165 | basePrototype, 166 | { 167 | // TODO: remove support for widgetEventPrefix 168 | // always use the name + a colon as the prefix, e.g., draggable:start 169 | // don't prefix for widgets that aren't DOM-based 170 | widgetEventPrefix: existingConstructor 171 | ? basePrototype.widgetEventPrefix || name 172 | : name 173 | }, 174 | proxiedPrototype, 175 | { 176 | constructor: constructor, 177 | namespace: namespace, 178 | widgetName: name, 179 | widgetFullName: fullName 180 | } 181 | ); 182 | 183 | // If this widget is being redefined then we need to find all widgets that 184 | // are inheriting from it and redefine all of them so that they inherit from 185 | // the new version of this widget. We're essentially trying to replace one 186 | // level in the prototype chain. 187 | if (existingConstructor) { 188 | $.each(existingConstructor._childConstructors, function (i, child) { 189 | var childPrototype = child.prototype; 190 | 191 | // Redefine the child widget using the same prototype that was 192 | // originally used, but inherit from the new version of the base 193 | $.widget( 194 | childPrototype.namespace + '.' + childPrototype.widgetName, 195 | constructor, 196 | child._proto 197 | ); 198 | }); 199 | 200 | // Remove the list of existing child constructors from the old constructor 201 | // so the old child constructors can be garbage collected 202 | delete existingConstructor._childConstructors; 203 | } else { 204 | base._childConstructors.push(constructor); 205 | } 206 | 207 | $.widget.bridge(name, constructor); 208 | 209 | return constructor; 210 | }; 211 | 212 | $.widget.extend = function (target) { 213 | var input = widgetSlice.call(arguments, 1); 214 | var inputIndex = 0; 215 | var inputLength = input.length; 216 | var key; 217 | var value; 218 | 219 | for (; inputIndex < inputLength; inputIndex++) { 220 | for (key in input[inputIndex]) { 221 | value = input[inputIndex][key]; 222 | if ( 223 | widgetHasOwnProperty.call(input[inputIndex], key) && 224 | value !== undefined 225 | ) { 226 | // Clone objects 227 | if ($.isPlainObject(value)) { 228 | target[key] = $.isPlainObject(target[key]) 229 | ? $.widget.extend({}, target[key], value) 230 | : // Don't extend strings, arrays, etc. with objects 231 | $.widget.extend({}, value); 232 | 233 | // Copy everything else by reference 234 | } else { 235 | target[key] = value; 236 | } 237 | } 238 | } 239 | } 240 | return target; 241 | }; 242 | 243 | $.widget.bridge = function (name, object) { 244 | var fullName = object.prototype.widgetFullName || name; 245 | $.fn[name] = function (options) { 246 | var isMethodCall = typeof options === 'string'; 247 | var args = widgetSlice.call(arguments, 1); 248 | var returnValue = this; 249 | 250 | if (isMethodCall) { 251 | // If this is an empty collection, we need to have the instance method 252 | // return undefined instead of the jQuery instance 253 | if (!this.length && options === 'instance') { 254 | returnValue = undefined; 255 | } else { 256 | this.each(function () { 257 | var methodValue; 258 | var instance = $.data(this, fullName); 259 | 260 | if (options === 'instance') { 261 | returnValue = instance; 262 | return false; 263 | } 264 | 265 | if (!instance) { 266 | return $.error( 267 | 'cannot call methods on ' + 268 | name + 269 | ' prior to initialization; ' + 270 | "attempted to call method '" + 271 | options + 272 | "'" 273 | ); 274 | } 275 | 276 | if (!$.isFunction(instance[options]) || options.charAt(0) === '_') { 277 | return $.error( 278 | "no such method '" + 279 | options + 280 | "' for " + 281 | name + 282 | ' widget instance' 283 | ); 284 | } 285 | 286 | methodValue = instance[options].apply(instance, args); 287 | 288 | if (methodValue !== instance && methodValue !== undefined) { 289 | returnValue = 290 | methodValue && methodValue.jquery 291 | ? returnValue.pushStack(methodValue.get()) 292 | : methodValue; 293 | return false; 294 | } 295 | }); 296 | } 297 | } else { 298 | // Allow multiple hashes to be passed on init 299 | if (args.length) { 300 | options = $.widget.extend.apply(null, [options].concat(args)); 301 | } 302 | 303 | this.each(function () { 304 | var instance = $.data(this, fullName); 305 | if (instance) { 306 | instance.option(options || {}); 307 | if (instance._init) { 308 | instance._init(); 309 | } 310 | } else { 311 | $.data(this, fullName, new object(options, this)); 312 | } 313 | }); 314 | } 315 | 316 | return returnValue; 317 | }; 318 | }; 319 | 320 | $.Widget = function (/* options, element */) {}; 321 | $.Widget._childConstructors = []; 322 | 323 | $.Widget.prototype = { 324 | widgetName: 'widget', 325 | widgetEventPrefix: '', 326 | defaultElement: '
', 327 | 328 | options: { 329 | classes: {}, 330 | disabled: false, 331 | 332 | // Callbacks 333 | create: null 334 | }, 335 | 336 | _createWidget: function (options, element) { 337 | element = $(element || this.defaultElement || this)[0]; 338 | this.element = $(element); 339 | this.uuid = widgetUuid++; 340 | this.eventNamespace = '.' + this.widgetName + this.uuid; 341 | 342 | this.bindings = $(); 343 | this.hoverable = $(); 344 | this.focusable = $(); 345 | this.classesElementLookup = {}; 346 | 347 | if (element !== this) { 348 | $.data(element, this.widgetFullName, this); 349 | this._on(true, this.element, { 350 | remove: function (event) { 351 | if (event.target === element) { 352 | this.destroy(); 353 | } 354 | } 355 | }); 356 | this.document = $( 357 | element.style 358 | ? // Element within the document 359 | element.ownerDocument 360 | : // Element is window or document 361 | element.document || element 362 | ); 363 | this.window = $( 364 | this.document[0].defaultView || this.document[0].parentWindow 365 | ); 366 | } 367 | 368 | this.options = $.widget.extend( 369 | {}, 370 | this.options, 371 | this._getCreateOptions(), 372 | options 373 | ); 374 | 375 | this._create(); 376 | 377 | if (this.options.disabled) { 378 | this._setOptionDisabled(this.options.disabled); 379 | } 380 | 381 | this._trigger('create', null, this._getCreateEventData()); 382 | this._init(); 383 | }, 384 | 385 | _getCreateOptions: function () { 386 | return {}; 387 | }, 388 | 389 | _getCreateEventData: $.noop, 390 | 391 | _create: $.noop, 392 | 393 | _init: $.noop, 394 | 395 | destroy: function () { 396 | var that = this; 397 | 398 | this._destroy(); 399 | $.each(this.classesElementLookup, function (key, value) { 400 | that._removeClass(value, key); 401 | }); 402 | 403 | // We can probably remove the unbind calls in 2.0 404 | // all event bindings should go through this._on() 405 | this.element.off(this.eventNamespace).removeData(this.widgetFullName); 406 | this.widget().off(this.eventNamespace).removeAttr('aria-disabled'); 407 | 408 | // Clean up events and states 409 | this.bindings.off(this.eventNamespace); 410 | }, 411 | 412 | _destroy: $.noop, 413 | 414 | widget: function () { 415 | return this.element; 416 | }, 417 | 418 | option: function (key, value) { 419 | var options = key; 420 | var parts; 421 | var curOption; 422 | var i; 423 | 424 | if (arguments.length === 0) { 425 | // Don't return a reference to the internal hash 426 | return $.widget.extend({}, this.options); 427 | } 428 | 429 | if (typeof key === 'string') { 430 | // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } 431 | options = {}; 432 | parts = key.split('.'); 433 | key = parts.shift(); 434 | if (parts.length) { 435 | curOption = options[key] = $.widget.extend({}, this.options[key]); 436 | for (i = 0; i < parts.length - 1; i++) { 437 | curOption[parts[i]] = curOption[parts[i]] || {}; 438 | curOption = curOption[parts[i]]; 439 | } 440 | key = parts.pop(); 441 | if (arguments.length === 1) { 442 | return curOption[key] === undefined ? null : curOption[key]; 443 | } 444 | curOption[key] = value; 445 | } else { 446 | if (arguments.length === 1) { 447 | return this.options[key] === undefined ? null : this.options[key]; 448 | } 449 | options[key] = value; 450 | } 451 | } 452 | 453 | this._setOptions(options); 454 | 455 | return this; 456 | }, 457 | 458 | _setOptions: function (options) { 459 | var key; 460 | 461 | for (key in options) { 462 | this._setOption(key, options[key]); 463 | } 464 | 465 | return this; 466 | }, 467 | 468 | _setOption: function (key, value) { 469 | if (key === 'classes') { 470 | this._setOptionClasses(value); 471 | } 472 | 473 | this.options[key] = value; 474 | 475 | if (key === 'disabled') { 476 | this._setOptionDisabled(value); 477 | } 478 | 479 | return this; 480 | }, 481 | 482 | _setOptionClasses: function (value) { 483 | var classKey, elements, currentElements; 484 | 485 | for (classKey in value) { 486 | currentElements = this.classesElementLookup[classKey]; 487 | if ( 488 | value[classKey] === this.options.classes[classKey] || 489 | !currentElements || 490 | !currentElements.length 491 | ) { 492 | continue; 493 | } 494 | 495 | // We are doing this to create a new jQuery object because the _removeClass() call 496 | // on the next line is going to destroy the reference to the current elements being 497 | // tracked. We need to save a copy of this collection so that we can add the new classes 498 | // below. 499 | elements = $(currentElements.get()); 500 | this._removeClass(currentElements, classKey); 501 | 502 | // We don't use _addClass() here, because that uses this.options.classes 503 | // for generating the string of classes. We want to use the value passed in from 504 | // _setOption(), this is the new value of the classes option which was passed to 505 | // _setOption(). We pass this value directly to _classes(). 506 | elements.addClass( 507 | this._classes({ 508 | element: elements, 509 | keys: classKey, 510 | classes: value, 511 | add: true 512 | }) 513 | ); 514 | } 515 | }, 516 | 517 | _setOptionDisabled: function (value) { 518 | this._toggleClass( 519 | this.widget(), 520 | this.widgetFullName + '-disabled', 521 | null, 522 | !!value 523 | ); 524 | 525 | // If the widget is becoming disabled, then nothing is interactive 526 | if (value) { 527 | this._removeClass(this.hoverable, null, 'ui-state-hover'); 528 | this._removeClass(this.focusable, null, 'ui-state-focus'); 529 | } 530 | }, 531 | 532 | enable: function () { 533 | return this._setOptions({ disabled: false }); 534 | }, 535 | 536 | disable: function () { 537 | return this._setOptions({ disabled: true }); 538 | }, 539 | 540 | _classes: function (options) { 541 | var full = []; 542 | var that = this; 543 | 544 | options = $.extend( 545 | { 546 | element: this.element, 547 | classes: this.options.classes || {} 548 | }, 549 | options 550 | ); 551 | 552 | function bindRemoveEvent() { 553 | options.element.each(function (_, element) { 554 | var isTracked = $.map(that.classesElementLookup, function (elements) { 555 | return elements; 556 | }).some(function (elements) { 557 | return elements.is(element); 558 | }); 559 | 560 | if (!isTracked) { 561 | that._on($(element), { 562 | remove: '_untrackClassesElement' 563 | }); 564 | } 565 | }); 566 | } 567 | 568 | function processClassString(classes, checkOption) { 569 | var current, i; 570 | for (i = 0; i < classes.length; i++) { 571 | current = that.classesElementLookup[classes[i]] || $(); 572 | if (options.add) { 573 | bindRemoveEvent(); 574 | current = $( 575 | $.uniqueSort(current.get().concat(options.element.get())) 576 | ); 577 | } else { 578 | current = $(current.not(options.element).get()); 579 | } 580 | that.classesElementLookup[classes[i]] = current; 581 | full.push(classes[i]); 582 | if (checkOption && options.classes[classes[i]]) { 583 | full.push(options.classes[classes[i]]); 584 | } 585 | } 586 | } 587 | 588 | if (options.keys) { 589 | processClassString(options.keys.match(/\S+/g) || [], true); 590 | } 591 | if (options.extra) { 592 | processClassString(options.extra.match(/\S+/g) || []); 593 | } 594 | 595 | return full.join(' '); 596 | }, 597 | 598 | _untrackClassesElement: function (event) { 599 | var that = this; 600 | $.each(that.classesElementLookup, function (key, value) { 601 | if ($.inArray(event.target, value) !== -1) { 602 | that.classesElementLookup[key] = $(value.not(event.target).get()); 603 | } 604 | }); 605 | 606 | this._off($(event.target)); 607 | }, 608 | 609 | _removeClass: function (element, keys, extra) { 610 | return this._toggleClass(element, keys, extra, false); 611 | }, 612 | 613 | _addClass: function (element, keys, extra) { 614 | return this._toggleClass(element, keys, extra, true); 615 | }, 616 | 617 | _toggleClass: function (element, keys, extra, add) { 618 | add = typeof add === 'boolean' ? add : extra; 619 | var shift = typeof element === 'string' || element === null, 620 | options = { 621 | extra: shift ? keys : extra, 622 | keys: shift ? element : keys, 623 | element: shift ? this.element : element, 624 | add: add 625 | }; 626 | options.element.toggleClass(this._classes(options), add); 627 | return this; 628 | }, 629 | 630 | _on: function (suppressDisabledCheck, element, handlers) { 631 | var delegateElement; 632 | var instance = this; 633 | 634 | // No suppressDisabledCheck flag, shuffle arguments 635 | if (typeof suppressDisabledCheck !== 'boolean') { 636 | handlers = element; 637 | element = suppressDisabledCheck; 638 | suppressDisabledCheck = false; 639 | } 640 | 641 | // No element argument, shuffle and use this.element 642 | if (!handlers) { 643 | handlers = element; 644 | element = this.element; 645 | delegateElement = this.widget(); 646 | } else { 647 | element = delegateElement = $(element); 648 | this.bindings = this.bindings.add(element); 649 | } 650 | 651 | $.each(handlers, function (event, handler) { 652 | function handlerProxy() { 653 | // Allow widgets to customize the disabled handling 654 | // - disabled as an array instead of boolean 655 | // - disabled class as method for disabling individual parts 656 | if ( 657 | !suppressDisabledCheck && 658 | (instance.options.disabled === true || 659 | $(this).hasClass('ui-state-disabled')) 660 | ) { 661 | return; 662 | } 663 | return ( 664 | typeof handler === 'string' ? instance[handler] : handler 665 | ).apply(instance, arguments); 666 | } 667 | 668 | // Copy the guid so direct unbinding works 669 | if (typeof handler !== 'string') { 670 | handlerProxy.guid = handler.guid = 671 | handler.guid || handlerProxy.guid || $.guid++; 672 | } 673 | 674 | var match = event.match(/^([\w:-]*)\s*(.*)$/); 675 | var eventName = match[1] + instance.eventNamespace; 676 | var selector = match[2]; 677 | 678 | if (selector) { 679 | delegateElement.on(eventName, selector, handlerProxy); 680 | } else { 681 | element.on(eventName, handlerProxy); 682 | } 683 | }); 684 | }, 685 | 686 | _off: function (element, eventName) { 687 | eventName = 688 | (eventName || '').split(' ').join(this.eventNamespace + ' ') + 689 | this.eventNamespace; 690 | element.off(eventName); 691 | 692 | // Clear the stack to avoid memory leaks (#10056) 693 | this.bindings = $(this.bindings.not(element).get()); 694 | this.focusable = $(this.focusable.not(element).get()); 695 | this.hoverable = $(this.hoverable.not(element).get()); 696 | }, 697 | 698 | _delay: function (handler, delay) { 699 | var instance = this; 700 | function handlerProxy() { 701 | return ( 702 | typeof handler === 'string' ? instance[handler] : handler 703 | ).apply(instance, arguments); 704 | } 705 | return setTimeout(handlerProxy, delay || 0); 706 | }, 707 | 708 | _hoverable: function (element) { 709 | this.hoverable = this.hoverable.add(element); 710 | this._on(element, { 711 | mouseenter: function (event) { 712 | this._addClass($(event.currentTarget), null, 'ui-state-hover'); 713 | }, 714 | mouseleave: function (event) { 715 | this._removeClass($(event.currentTarget), null, 'ui-state-hover'); 716 | } 717 | }); 718 | }, 719 | 720 | _focusable: function (element) { 721 | this.focusable = this.focusable.add(element); 722 | this._on(element, { 723 | focusin: function (event) { 724 | this._addClass($(event.currentTarget), null, 'ui-state-focus'); 725 | }, 726 | focusout: function (event) { 727 | this._removeClass($(event.currentTarget), null, 'ui-state-focus'); 728 | } 729 | }); 730 | }, 731 | 732 | _trigger: function (type, event, data) { 733 | var prop, orig; 734 | var callback = this.options[type]; 735 | 736 | data = data || {}; 737 | event = $.Event(event); 738 | event.type = ( 739 | type === this.widgetEventPrefix ? type : this.widgetEventPrefix + type 740 | ).toLowerCase(); 741 | 742 | // The original event may come from any element 743 | // so we need to reset the target on the new event 744 | event.target = this.element[0]; 745 | 746 | // Copy original event properties over to the new event 747 | orig = event.originalEvent; 748 | if (orig) { 749 | for (prop in orig) { 750 | if (!(prop in event)) { 751 | event[prop] = orig[prop]; 752 | } 753 | } 754 | } 755 | 756 | this.element.trigger(event, data); 757 | return !( 758 | ($.isFunction(callback) && 759 | callback.apply(this.element[0], [event].concat(data)) === false) || 760 | event.isDefaultPrevented() 761 | ); 762 | } 763 | }; 764 | 765 | $.each({ show: 'fadeIn', hide: 'fadeOut' }, function (method, defaultEffect) { 766 | $.Widget.prototype['_' + method] = function (element, options, callback) { 767 | if (typeof options === 'string') { 768 | options = { effect: options }; 769 | } 770 | 771 | var hasOptions; 772 | var effectName = !options 773 | ? method 774 | : options === true || typeof options === 'number' 775 | ? defaultEffect 776 | : options.effect || defaultEffect; 777 | 778 | options = options || {}; 779 | if (typeof options === 'number') { 780 | options = { duration: options }; 781 | } 782 | 783 | hasOptions = !$.isEmptyObject(options); 784 | options.complete = callback; 785 | 786 | if (options.delay) { 787 | element.delay(options.delay); 788 | } 789 | 790 | if (hasOptions && $.effects && $.effects.effect[effectName]) { 791 | element[method](options); 792 | } else if (effectName !== method && element[effectName]) { 793 | element[effectName](options.duration, options.easing, callback); 794 | } else { 795 | element.queue(function (next) { 796 | $(this)[method](); 797 | if (callback) { 798 | callback.call(element[0]); 799 | } 800 | next(); 801 | }); 802 | } 803 | }; 804 | }); 805 | }); 806 | -------------------------------------------------------------------------------- /server/php/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !php.ini 3 | -------------------------------------------------------------------------------- /server/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0.11-apache 2 | 3 | # Enable the Apache Headers module: 4 | RUN ln -s /etc/apache2/mods-available/headers.load \ 5 | /etc/apache2/mods-enabled/headers.load 6 | 7 | # Enable the Apache Rewrite module: 8 | RUN ln -s /etc/apache2/mods-available/rewrite.load \ 9 | /etc/apache2/mods-enabled/rewrite.load 10 | 11 | # Install GD, Imagick and ImageMagick as image conversion options: 12 | RUN DEBIAN_FRONTEND=noninteractive \ 13 | apt-get update && apt-get install -y --no-install-recommends \ 14 | libpng-dev \ 15 | libjpeg-dev \ 16 | libmagickwand-dev \ 17 | imagemagick \ 18 | && pecl install \ 19 | imagick \ 20 | && docker-php-ext-enable \ 21 | imagick \ 22 | && docker-php-ext-configure \ 23 | gd --with-jpeg=/usr/include/ \ 24 | && docker-php-ext-install \ 25 | gd \ 26 | # Uninstall obsolete packages: 27 | && apt-get autoremove -y \ 28 | libpng-dev \ 29 | libjpeg-dev \ 30 | libmagickwand-dev \ 31 | # Remove obsolete files: 32 | && apt-get clean \ 33 | && rm -rf \ 34 | /tmp/* \ 35 | /usr/share/doc/* \ 36 | /var/cache/* \ 37 | /var/lib/apt/lists/* \ 38 | /var/tmp/* 39 | 40 | # Use the default development configuration: 41 | RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" 42 | 43 | # Add a custom configuration file: 44 | COPY php.ini "$PHP_INI_DIR/conf.d/" 45 | -------------------------------------------------------------------------------- /server/php/files/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !.htaccess 4 | -------------------------------------------------------------------------------- /server/php/files/.htaccess: -------------------------------------------------------------------------------- 1 | # If you have not done so already, please first read SECURITY.md in the root 2 | # directory of this project or online: 3 | # https://github.com/blueimp/jQuery-File-Upload/blob/master/SECURITY.md 4 | # 5 | # The settings in this file require Apache to support configuration overrides 6 | # in .htaccess files, which is disabled by default since Apache v2.3.9 and needs 7 | # to be enabled for the directives in this file to have any effect, see also: 8 | # https://httpd.apache.org/docs/current/mod/core.html#allowoverride 9 | # 10 | # If you have full control over the web server, it is preferrable to define the 11 | # settings in the Apache configuration (e.g. /etc/apache2/apache2.conf) itself. 12 | # 13 | # Some of the directives require the Apache Headers module. If it is not 14 | # already enabled, please execute the following command and reload Apache: 15 | # sudo a2enmod headers 16 | # 17 | # Please note that the order of directives across configuration files matters, 18 | # see also: 19 | # https://httpd.apache.org/docs/current/sections.html#merging 20 | 21 | # The following directive matches all files and forces them to be handled as 22 | # static content, which prevents the server from parsing and executing files 23 | # that are associated with a dynamic runtime, e.g. PHP files. 24 | # It also forces their Content-Type header to "application/octet-stream" and 25 | # adds a "Content-Disposition: attachment" header to force a download dialog, 26 | # which prevents browsers from interpreting files in the context of the 27 | # web server, e.g. HTML files containing JavaScript. 28 | # Lastly it also prevents browsers from MIME-sniffing the Content-Type, 29 | # preventing them from interpreting a file as a different Content-Type than 30 | # the one sent by the webserver. 31 | 32 | SetHandler default-handler 33 | ForceType application/octet-stream 34 | Header set Content-Disposition attachment 35 | Header set X-Content-Type-Options nosniff 36 | 37 | 38 | # The following directive matches known image files and unsets the forced 39 | # Content-Type so they can be served with their original mime type. 40 | # It also unsets the Content-Disposition header to allow displaying them 41 | # inline in the browser. 42 | 43 | ForceType none 44 | Header unset Content-Disposition 45 | 46 | 47 | # Uncomment the following lines to prevent unauthorized download of files: 48 | #AuthName "Authorization required" 49 | #AuthType Basic 50 | #require valid-user 51 | -------------------------------------------------------------------------------- /server/php/index.php: -------------------------------------------------------------------------------- 1 |