├── 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 |
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 |
208 |
209 |
210 |
216 |
222 |
228 |
236 |
237 |
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 |