├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE.txt ├── 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 ├── docker-compose.yml ├── 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 ├── package-lock.json ├── package.json ├── server ├── gae-python │ ├── app.yaml │ ├── main.py │ └── static │ │ ├── favicon.ico │ │ └── robots.txt └── php │ ├── .dockerignore │ ├── Dockerfile │ ├── UploadHandler.php │ ├── files │ ├── .gitignore │ └── .htaccess │ ├── index.php │ └── php.ini ├── test ├── index.html ├── unit.js └── vendor │ ├── chai.js │ ├── mocha.css │ └── mocha.js └── wdio ├── .eslintrc.js ├── .prettierrc.js ├── LICENSE.txt ├── assets ├── black+white-3x2.jpg └── black+white-60x40.gif ├── conf ├── chrome.js └── firefox.js ├── hooks └── index.js ├── reports └── .gitignore ├── test ├── pages │ └── file-upload.js └── specs │ └── 01-file-upload.js └── wdio.conf.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [blueimp] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14, 16] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - run: npm install 17 | - run: npm run lint 18 | 19 | mocha: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: chmod 24 | run: chmod -R 777 server/php/files 25 | - name: docker-compose build 26 | run: docker-compose build example mocha 27 | - name: mocha 28 | run: docker-compose run --rm mocha 29 | - name: docker-compose logs 30 | if: always() 31 | run: docker-compose logs example 32 | - name: docker-compose down 33 | if: always() 34 | run: docker-compose down -v 35 | 36 | wdio-chrome: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: chmod 41 | run: chmod -R 777 server/php/files wdio/reports 42 | - name: docker-compose build 43 | run: docker-compose build example 44 | - name: wdio chrome 45 | run: docker-compose run --rm wdio 46 | - name: docker-compose logs 47 | if: always() 48 | run: docker-compose logs example 49 | - name: docker-compose down 50 | if: always() 51 | run: docker-compose down -v 52 | - name: Upload reports 53 | if: always() 54 | uses: actions/upload-artifact@v2 55 | with: 56 | name: reports 57 | path: wdio/reports 58 | 59 | wdio-firefox: 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v2 63 | - name: chmod 64 | run: chmod -R 777 server/php/files wdio/reports 65 | - name: docker-compose build 66 | run: docker-compose build example 67 | - name: wdio firefox 68 | run: docker-compose run --rm wdio conf/firefox.js 69 | - name: docker-compose logs 70 | if: always() 71 | run: docker-compose logs example 72 | - name: docker-compose down 73 | if: always() 74 | run: docker-compose down -v 75 | - name: Upload reports 76 | if: always() 77 | uses: actions/upload-artifact@v2 78 | with: 79 | name: reports 80 | path: wdio/reports 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .env 3 | node_modules 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2010 Sebastian Tschan, https://blueimp.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | example: 4 | build: server/php 5 | ports: 6 | - 127.0.0.1:80:80 7 | volumes: 8 | - .:/var/www/html 9 | mocha: 10 | image: blueimp/mocha-chrome 11 | command: http://example/test 12 | environment: 13 | - WAIT_FOR_HOSTS=example:80 14 | depends_on: 15 | - example 16 | chromedriver: 17 | image: blueimp/chromedriver 18 | tmpfs: /tmp 19 | environment: 20 | - DISABLE_X11=false 21 | - ENABLE_VNC=true 22 | - EXPOSE_X11=true 23 | volumes: 24 | - ./wdio/assets:/home/webdriver/assets:ro 25 | ports: 26 | - 127.0.0.1:5900:5900 27 | geckodriver: 28 | image: blueimp/geckodriver 29 | tmpfs: /tmp 30 | shm_size: 2g 31 | environment: 32 | - DISABLE_X11=false 33 | - ENABLE_VNC=true 34 | - EXPOSE_X11=true 35 | volumes: 36 | - ./wdio/assets:/home/webdriver/assets:ro 37 | ports: 38 | - 127.0.0.1:5901:5900 39 | wdio: 40 | image: blueimp/wdio 41 | read_only: true 42 | tmpfs: 43 | - /tmp 44 | environment: 45 | - WAIT_FOR_HOSTS=chromedriver:4444 geckodriver:4444 example:80 46 | - WINDOWS_HOST 47 | - MACOS_ASSETS_DIR=$PWD/wdio/assets/ 48 | - WINDOWS_ASSETS_DIR 49 | volumes: 50 | - ./wdio:/app:ro 51 | - ./wdio/reports:/app/reports 52 | depends_on: 53 | - chromedriver 54 | - geckodriver 55 | - example 56 | -------------------------------------------------------------------------------- /img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueimp/jQuery-File-Upload/0e92a4d4613d4ed5231ee0d8513519f2e04f99ba/img/loading.gif -------------------------------------------------------------------------------- /img/progressbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueimp/jQuery-File-Upload/0e92a4d4613d4ed5231ee0d8513519f2e04f99ba/img/progressbar.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 20 | 21 | jQuery File Upload Demo 22 | 26 | 27 | 28 | 34 | 35 | 46 | 47 | 51 | 52 | 53 | 54 | 55 | 58 | 61 | 62 | 63 |
64 | 78 |

jQuery File Upload Demo

79 |
80 |

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

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

Demo Notes

155 |
156 |
157 |
    158 |
  • 159 | The maximum file size for uploads in this demo is 160 | 999 KB (default file size is unlimited). 161 |
  • 162 |
  • 163 | Only image files (JPG, GIF, PNG) are allowed in 164 | this demo (by default there is no file type restriction). 165 |
  • 166 |
  • 167 | Uploaded files will be deleted automatically after 168 | 5 minutes or less (demo files are stored in 169 | memory). 170 |
  • 171 |
  • 172 | You can drag & drop files from your desktop 173 | on this webpage (see 174 | Browser support). 178 |
  • 179 |
  • 180 | Please refer to the 181 | project website 184 | and 185 | documentation 188 | for more information. 189 |
  • 190 |
  • 191 | Built with the 192 | Bootstrap CSS framework 193 | and Icons from Glyphicons. 194 |
  • 195 |
196 |
197 |
198 |
199 | 200 | 238 | 239 | 276 | 277 | 319 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /js/cors/jquery.postmessage-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery postMessage Transport Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, require */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery'], factory); 19 | } else if (typeof exports === 'object') { 20 | // Node/CommonJS: 21 | factory(require('jquery')); 22 | } else { 23 | // Browser globals: 24 | factory(window.jQuery); 25 | } 26 | })(function ($) { 27 | 'use strict'; 28 | 29 | var counter = 0, 30 | names = [ 31 | 'accepts', 32 | 'cache', 33 | 'contents', 34 | 'contentType', 35 | 'crossDomain', 36 | 'data', 37 | 'dataType', 38 | 'headers', 39 | 'ifModified', 40 | 'mimeType', 41 | 'password', 42 | 'processData', 43 | 'timeout', 44 | 'traditional', 45 | 'type', 46 | 'url', 47 | 'username' 48 | ], 49 | convert = function (p) { 50 | return p; 51 | }; 52 | 53 | $.ajaxSetup({ 54 | converters: { 55 | 'postmessage text': convert, 56 | 'postmessage json': convert, 57 | 'postmessage html': convert 58 | } 59 | }); 60 | 61 | $.ajaxTransport('postmessage', function (options) { 62 | if (options.postMessage && window.postMessage) { 63 | var iframe, 64 | loc = $('').prop('href', options.postMessage)[0], 65 | target = loc.protocol + '//' + loc.host, 66 | xhrUpload = options.xhr().upload; 67 | // IE always includes the port for the host property of a link 68 | // element, but not in the location.host or origin property for the 69 | // default http port 80 and https port 443, so we strip it: 70 | if (/^(http:\/\/.+:80)|(https:\/\/.+:443)$/.test(target)) { 71 | target = target.replace(/:(80|443)$/, ''); 72 | } 73 | return { 74 | send: function (_, completeCallback) { 75 | counter += 1; 76 | var message = { 77 | id: 'postmessage-transport-' + counter 78 | }, 79 | eventName = 'message.' + message.id; 80 | iframe = $( 81 | '' 86 | ) 87 | .on('load', function () { 88 | $.each(names, function (i, name) { 89 | message[name] = options[name]; 90 | }); 91 | message.dataType = message.dataType.replace('postmessage ', ''); 92 | $(window).on(eventName, function (event) { 93 | var e = event.originalEvent; 94 | var data = e.data; 95 | var ev; 96 | if (e.origin === target && data.id === message.id) { 97 | if (data.type === 'progress') { 98 | ev = document.createEvent('Event'); 99 | ev.initEvent(data.type, false, true); 100 | $.extend(ev, data); 101 | xhrUpload.dispatchEvent(ev); 102 | } else { 103 | completeCallback( 104 | data.status, 105 | data.statusText, 106 | { postmessage: data.result }, 107 | data.headers 108 | ); 109 | iframe.remove(); 110 | $(window).off(eventName); 111 | } 112 | } 113 | }); 114 | iframe[0].contentWindow.postMessage(message, target); 115 | }) 116 | .appendTo(document.body); 117 | }, 118 | abort: function () { 119 | if (iframe) { 120 | iframe.remove(); 121 | } 122 | } 123 | }; 124 | } 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /js/cors/jquery.xdr-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery XDomainRequest Transport Plugin 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | * 11 | * Based on Julian Aubourg's ajaxHooks xdr.js: 12 | * https://github.com/jaubourg/ajaxHooks/ 13 | */ 14 | 15 | /* global define, require, XDomainRequest */ 16 | 17 | (function (factory) { 18 | 'use strict'; 19 | if (typeof define === 'function' && define.amd) { 20 | // Register as an anonymous AMD module: 21 | define(['jquery'], factory); 22 | } else if (typeof exports === 'object') { 23 | // Node/CommonJS: 24 | factory(require('jquery')); 25 | } else { 26 | // Browser globals: 27 | factory(window.jQuery); 28 | } 29 | })(function ($) { 30 | 'use strict'; 31 | if (window.XDomainRequest && !$.support.cors) { 32 | $.ajaxTransport(function (s) { 33 | if (s.crossDomain && s.async) { 34 | if (s.timeout) { 35 | s.xdrTimeout = s.timeout; 36 | delete s.timeout; 37 | } 38 | var xdr; 39 | return { 40 | send: function (headers, completeCallback) { 41 | var addParamChar = /\?/.test(s.url) ? '&' : '?'; 42 | /** 43 | * Callback wrapper function 44 | * 45 | * @param {number} status HTTP status code 46 | * @param {string} statusText HTTP status text 47 | * @param {object} [responses] Content-type specific responses 48 | * @param {string} [responseHeaders] Response headers string 49 | */ 50 | function callback(status, statusText, responses, responseHeaders) { 51 | xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; 52 | xdr = null; 53 | completeCallback(status, statusText, responses, responseHeaders); 54 | } 55 | xdr = new XDomainRequest(); 56 | // XDomainRequest only supports GET and POST: 57 | if (s.type === 'DELETE') { 58 | s.url = s.url + addParamChar + '_method=DELETE'; 59 | s.type = 'POST'; 60 | } else if (s.type === 'PUT') { 61 | s.url = s.url + addParamChar + '_method=PUT'; 62 | s.type = 'POST'; 63 | } else if (s.type === 'PATCH') { 64 | s.url = s.url + addParamChar + '_method=PATCH'; 65 | s.type = 'POST'; 66 | } 67 | xdr.open(s.type, s.url); 68 | xdr.onload = function () { 69 | callback( 70 | 200, 71 | 'OK', 72 | { text: xdr.responseText }, 73 | 'Content-Type: ' + xdr.contentType 74 | ); 75 | }; 76 | xdr.onerror = function () { 77 | callback(404, 'Not Found'); 78 | }; 79 | if (s.xdrTimeout) { 80 | xdr.ontimeout = function () { 81 | callback(0, 'timeout'); 82 | }; 83 | xdr.timeout = s.xdrTimeout; 84 | } 85 | xdr.send((s.hasContent && s.data) || null); 86 | }, 87 | abort: function () { 88 | if (xdr) { 89 | xdr.onerror = $.noop(); 90 | xdr.abort(); 91 | } 92 | } 93 | }; 94 | } 95 | }); 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /js/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Demo 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global $ */ 13 | 14 | $(function () { 15 | 'use strict'; 16 | 17 | // Initialize the jQuery File Upload widget: 18 | $('#fileupload').fileupload({ 19 | // Uncomment the following to send cross-domain cookies: 20 | //xhrFields: {withCredentials: true}, 21 | url: 'server/php/' 22 | }); 23 | 24 | // Enable iframe cross-domain access via redirect option: 25 | $('#fileupload').fileupload( 26 | 'option', 27 | 'redirect', 28 | window.location.href.replace(/\/[^/]*$/, '/cors/result.html?%s') 29 | ); 30 | 31 | if (window.location.hostname === 'blueimp.github.io') { 32 | // Demo settings: 33 | $('#fileupload').fileupload('option', { 34 | url: '//jquery-file-upload.appspot.com/', 35 | // Enable image resizing, except for Android and Opera, 36 | // which actually support image resizing, but fail to 37 | // send Blob objects via XHR requests: 38 | disableImageResize: /Android(?!.*Chrome)|Opera/.test( 39 | window.navigator.userAgent 40 | ), 41 | maxFileSize: 999000, 42 | acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/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.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blueimp-file-upload", 3 | "version": "10.32.0", 4 | "title": "jQuery File Upload", 5 | "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (Google App Engine, PHP, Python, Ruby on Rails, Java, etc.) that supports standard HTML form file uploads.", 6 | "keywords": [ 7 | "jquery", 8 | "file", 9 | "upload", 10 | "widget", 11 | "multiple", 12 | "selection", 13 | "drag", 14 | "drop", 15 | "progress", 16 | "preview", 17 | "cross-domain", 18 | "cross-site", 19 | "chunk", 20 | "resume", 21 | "gae", 22 | "go", 23 | "python", 24 | "php", 25 | "bootstrap" 26 | ], 27 | "homepage": "https://github.com/blueimp/jQuery-File-Upload", 28 | "author": { 29 | "name": "Sebastian Tschan", 30 | "url": "https://blueimp.net" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/blueimp/jQuery-File-Upload.git" 35 | }, 36 | "license": "MIT", 37 | "peerDependencies": { 38 | "jquery": ">=1.7" 39 | }, 40 | "optionalDependencies": { 41 | "blueimp-canvas-to-blob": "3", 42 | "blueimp-load-image": "5", 43 | "blueimp-tmpl": "3" 44 | }, 45 | "devDependencies": { 46 | "eslint": "7", 47 | "eslint-config-blueimp": "2", 48 | "eslint-config-prettier": "8", 49 | "eslint-plugin-jsdoc": "36", 50 | "eslint-plugin-prettier": "4", 51 | "prettier": "2", 52 | "stylelint": "13", 53 | "stylelint-config-prettier": "8", 54 | "stylelint-config-recommended": "5" 55 | }, 56 | "stylelint": { 57 | "extends": [ 58 | "stylelint-config-recommended", 59 | "stylelint-config-prettier" 60 | ], 61 | "ignoreFiles": [ 62 | "css/*.min.css", 63 | "css/vendor/*", 64 | "test/vendor/*" 65 | ] 66 | }, 67 | "eslintConfig": { 68 | "extends": [ 69 | "blueimp", 70 | "plugin:jsdoc/recommended", 71 | "plugin:prettier/recommended" 72 | ], 73 | "env": { 74 | "browser": true 75 | } 76 | }, 77 | "eslintIgnore": [ 78 | "*.min.js", 79 | "test/vendor" 80 | ], 81 | "prettier": { 82 | "arrowParens": "avoid", 83 | "proseWrap": "always", 84 | "singleQuote": true, 85 | "trailingComma": "none" 86 | }, 87 | "scripts": { 88 | "lint": "stylelint '**/*.css' && eslint .", 89 | "unit": "docker-compose run --rm mocha", 90 | "wdio": "docker-compose run --rm wdio", 91 | "test": "npm run lint && npm run unit && npm run wdio && npm run wdio -- conf/firefox.js", 92 | "posttest": "docker-compose down -v", 93 | "preversion": "npm test", 94 | "postversion": "git push --tags origin master && npm publish" 95 | }, 96 | "files": [ 97 | "css/jquery.fileupload-noscript.css", 98 | "css/jquery.fileupload-ui-noscript.css", 99 | "css/jquery.fileupload-ui.css", 100 | "css/jquery.fileupload.css", 101 | "img/loading.gif", 102 | "img/progressbar.gif", 103 | "js/cors/jquery.postmessage-transport.js", 104 | "js/cors/jquery.xdr-transport.js", 105 | "js/vendor/jquery.ui.widget.js", 106 | "js/jquery.fileupload-audio.js", 107 | "js/jquery.fileupload-image.js", 108 | "js/jquery.fileupload-process.js", 109 | "js/jquery.fileupload-ui.js", 110 | "js/jquery.fileupload-validate.js", 111 | "js/jquery.fileupload-video.js", 112 | "js/jquery.fileupload.js", 113 | "js/jquery.iframe-transport.js" 114 | ], 115 | "main": "js/jquery.fileupload.js" 116 | } 117 | -------------------------------------------------------------------------------- /server/gae-python/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | 5 | libraries: 6 | - name: PIL 7 | version: latest 8 | 9 | handlers: 10 | - url: /(favicon\.ico|robots\.txt) 11 | static_files: static/\1 12 | upload: static/(.*) 13 | expiration: '1d' 14 | - url: /.* 15 | script: main.app 16 | 17 | automatic_scaling: 18 | max_instances: 1 19 | -------------------------------------------------------------------------------- /server/gae-python/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # jQuery File Upload Plugin GAE Python Example 4 | # https://github.com/blueimp/jQuery-File-Upload 5 | # 6 | # Copyright 2011, Sebastian Tschan 7 | # https://blueimp.net 8 | # 9 | # Licensed under the MIT license: 10 | # https://opensource.org/licenses/MIT 11 | # 12 | 13 | from google.appengine.api import memcache, images 14 | import json 15 | import os 16 | import re 17 | import urllib 18 | import webapp2 19 | 20 | DEBUG=os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') 21 | WEBSITE = 'https://blueimp.github.io/jQuery-File-Upload/' 22 | MIN_FILE_SIZE = 1 # bytes 23 | # Max file size is memcache limit (1MB) minus key size minus overhead: 24 | MAX_FILE_SIZE = 999000 # bytes 25 | IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)') 26 | ACCEPT_FILE_TYPES = IMAGE_TYPES 27 | THUMB_MAX_WIDTH = 80 28 | THUMB_MAX_HEIGHT = 80 29 | THUMB_SUFFIX = '.'+str(THUMB_MAX_WIDTH)+'x'+str(THUMB_MAX_HEIGHT)+'.png' 30 | EXPIRATION_TIME = 300 # seconds 31 | # If set to None, only allow redirects to the referer protocol+host. 32 | # Set to a regexp for custom pattern matching against the redirect value: 33 | REDIRECT_ALLOW_TARGET = None 34 | 35 | class CORSHandler(webapp2.RequestHandler): 36 | def cors(self): 37 | headers = self.response.headers 38 | headers['Access-Control-Allow-Origin'] = '*' 39 | headers['Access-Control-Allow-Methods'] =\ 40 | 'OPTIONS, HEAD, GET, POST, DELETE' 41 | headers['Access-Control-Allow-Headers'] =\ 42 | 'Content-Type, Content-Range, Content-Disposition' 43 | 44 | def initialize(self, request, response): 45 | super(CORSHandler, self).initialize(request, response) 46 | self.cors() 47 | 48 | def json_stringify(self, obj): 49 | return json.dumps(obj, separators=(',', ':')) 50 | 51 | def options(self, *args, **kwargs): 52 | pass 53 | 54 | class UploadHandler(CORSHandler): 55 | def validate(self, file): 56 | if file['size'] < MIN_FILE_SIZE: 57 | file['error'] = 'File is too small' 58 | elif file['size'] > MAX_FILE_SIZE: 59 | file['error'] = 'File is too big' 60 | elif not ACCEPT_FILE_TYPES.match(file['type']): 61 | file['error'] = 'Filetype not allowed' 62 | else: 63 | return True 64 | return False 65 | 66 | def validate_redirect(self, redirect): 67 | if redirect: 68 | if REDIRECT_ALLOW_TARGET: 69 | return REDIRECT_ALLOW_TARGET.match(redirect) 70 | referer = self.request.headers['referer'] 71 | if referer: 72 | from urlparse import urlparse 73 | parts = urlparse(referer) 74 | redirect_allow_target = '^' + re.escape( 75 | parts.scheme + '://' + parts.netloc + '/' 76 | ) 77 | return re.match(redirect_allow_target, redirect) 78 | return False 79 | 80 | def get_file_size(self, file): 81 | file.seek(0, 2) # Seek to the end of the file 82 | size = file.tell() # Get the position of EOF 83 | file.seek(0) # Reset the file position to the beginning 84 | return size 85 | 86 | def write_blob(self, data, info): 87 | key = urllib.quote(info['type'].encode('utf-8'), '') +\ 88 | '/' + str(hash(data)) +\ 89 | '/' + urllib.quote(info['name'].encode('utf-8'), '') 90 | try: 91 | memcache.set(key, data, time=EXPIRATION_TIME) 92 | except: #Failed to add to memcache 93 | return (None, None) 94 | thumbnail_key = None 95 | if IMAGE_TYPES.match(info['type']): 96 | try: 97 | img = images.Image(image_data=data) 98 | img.resize( 99 | width=THUMB_MAX_WIDTH, 100 | height=THUMB_MAX_HEIGHT 101 | ) 102 | thumbnail_data = img.execute_transforms() 103 | thumbnail_key = key + THUMB_SUFFIX 104 | memcache.set( 105 | thumbnail_key, 106 | thumbnail_data, 107 | time=EXPIRATION_TIME 108 | ) 109 | except: #Failed to resize Image or add to memcache 110 | thumbnail_key = None 111 | return (key, thumbnail_key) 112 | 113 | def handle_upload(self): 114 | results = [] 115 | for name, fieldStorage in self.request.POST.items(): 116 | if type(fieldStorage) is unicode: 117 | continue 118 | result = {} 119 | result['name'] = urllib.unquote(fieldStorage.filename) 120 | result['type'] = fieldStorage.type 121 | result['size'] = self.get_file_size(fieldStorage.file) 122 | if self.validate(result): 123 | key, thumbnail_key = self.write_blob( 124 | fieldStorage.value, 125 | result 126 | ) 127 | if key is not None: 128 | result['url'] = self.request.host_url + '/' + key 129 | result['deleteUrl'] = result['url'] 130 | result['deleteType'] = 'DELETE' 131 | if thumbnail_key is not None: 132 | result['thumbnailUrl'] = self.request.host_url +\ 133 | '/' + thumbnail_key 134 | else: 135 | result['error'] = 'Failed to store uploaded file.' 136 | results.append(result) 137 | return results 138 | 139 | def head(self): 140 | pass 141 | 142 | def get(self): 143 | self.redirect(WEBSITE) 144 | 145 | def post(self): 146 | if (self.request.get('_method') == 'DELETE'): 147 | return self.delete() 148 | result = {'files': self.handle_upload()} 149 | s = self.json_stringify(result) 150 | redirect = self.request.get('redirect') 151 | if self.validate_redirect(redirect): 152 | return self.redirect(str( 153 | redirect.replace('%s', urllib.quote(s, ''), 1) 154 | )) 155 | if 'application/json' in self.request.headers.get('Accept'): 156 | self.response.headers['Content-Type'] = 'application/json' 157 | self.response.write(s) 158 | 159 | class FileHandler(CORSHandler): 160 | def normalize(self, str): 161 | return urllib.quote(urllib.unquote(str), '') 162 | 163 | def get(self, content_type, data_hash, file_name): 164 | content_type = self.normalize(content_type) 165 | file_name = self.normalize(file_name) 166 | key = content_type + '/' + data_hash + '/' + file_name 167 | data = memcache.get(key) 168 | if data is None: 169 | return self.error(404) 170 | # Prevent browsers from MIME-sniffing the content-type: 171 | self.response.headers['X-Content-Type-Options'] = 'nosniff' 172 | content_type = urllib.unquote(content_type) 173 | if not IMAGE_TYPES.match(content_type): 174 | # Force a download dialog for non-image types: 175 | content_type = 'application/octet-stream' 176 | elif file_name.endswith(THUMB_SUFFIX): 177 | content_type = 'image/png' 178 | self.response.headers['Content-Type'] = content_type 179 | # Cache for the expiration time: 180 | self.response.headers['Cache-Control'] = 'public,max-age=%d' \ 181 | % EXPIRATION_TIME 182 | self.response.write(data) 183 | 184 | def delete(self, content_type, data_hash, file_name): 185 | content_type = self.normalize(content_type) 186 | file_name = self.normalize(file_name) 187 | key = content_type + '/' + data_hash + '/' + file_name 188 | result = {key: memcache.delete(key)} 189 | content_type = urllib.unquote(content_type) 190 | if IMAGE_TYPES.match(content_type): 191 | thumbnail_key = key + THUMB_SUFFIX 192 | result[thumbnail_key] = memcache.delete(thumbnail_key) 193 | if 'application/json' in self.request.headers.get('Accept'): 194 | self.response.headers['Content-Type'] = 'application/json' 195 | s = self.json_stringify(result) 196 | self.response.write(s) 197 | 198 | app = webapp2.WSGIApplication( 199 | [ 200 | ('/', UploadHandler), 201 | ('/(.+)/([^/]+)/([^/]+)', FileHandler) 202 | ], 203 | debug=DEBUG 204 | ) 205 | -------------------------------------------------------------------------------- /server/gae-python/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueimp/jQuery-File-Upload/0e92a4d4613d4ed5231ee0d8513519f2e04f99ba/server/gae-python/static/favicon.ico -------------------------------------------------------------------------------- /server/gae-python/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /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 | 2 | 14 | 15 | 16 | 17 | jQuery File Upload Test 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Test 3 | * https://github.com/blueimp/JavaScript-Load-Image 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 beforeEach, afterEach, describe, it */ 13 | /* eslint-disable new-cap */ 14 | 15 | (function (expect, $) { 16 | 'use strict'; 17 | 18 | var canCreateBlob = !!window.dataURLtoBlob; 19 | // 80x60px GIF image (color black, base64 data): 20 | var b64DataGIF = 21 | 'R0lGODdhUAA8AIABAAAAAP///ywAAAAAUAA8AAACS4SPqcvtD6' + 22 | 'OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3n+s73/g8MCofE' + 23 | 'ovGITCqXzKbzCY1Kp9Sq9YrNarfcrvcLDovH5PKsAAA7'; 24 | var imageUrlGIF = 'data:image/gif;base64,' + b64DataGIF; 25 | var blobGIF = canCreateBlob && window.dataURLtoBlob(imageUrlGIF); 26 | 27 | // 2x1px JPEG (color white, with the Exif orientation flag set to 6 and the 28 | // IPTC ObjectName (2:5) set to 'objectname'): 29 | var b64DataJPEG = 30 | '/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAA' + 31 | 'BgASAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAA8cAgUACm9iamVj' + 32 | 'dG5hbWUA/9sAQwABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB' + 33 | 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/9sAQwEBAQEBAQEBAQEBAQEBAQEB' + 34 | 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB' + 35 | '/8AAEQgAAQACAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYH' + 36 | 'CAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGh' + 37 | 'CCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldY' + 38 | 'WVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1' + 39 | 'tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8B' + 40 | 'AAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAEC' + 41 | 'dwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBka' + 42 | 'JicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWG' + 43 | 'h4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ' + 44 | '2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A/v4ooooA/9k='; 45 | var imageUrlJPEG = 'data:image/jpeg;base64,' + b64DataJPEG; 46 | var blobJPEG = canCreateBlob && window.dataURLtoBlob(imageUrlJPEG); 47 | 48 | var fileGIF, fileJPEG, files, items, eventObject; 49 | 50 | var uploadURL = '../server/php/'; 51 | 52 | /** 53 | * Creates a fileupload form and adds it to the DOM 54 | * 55 | * @returns {object} jQuery node 56 | */ 57 | function createFileuploadForm() { 58 | return $('
') 59 | .prop({ 60 | action: uploadURL, 61 | method: 'POST', 62 | enctype: 'multipart/form-data' 63 | }) 64 | .css({ display: 'none' }) 65 | .appendTo(document.body); 66 | } 67 | 68 | /** 69 | * Deletes all files from the upload server 70 | * 71 | * @param {Array} files Response files list 72 | * @param {Function} callback Callback function 73 | */ 74 | function deleteFiles(files, callback) { 75 | $.when( 76 | files.map(function (file) { 77 | return $.ajax({ 78 | type: file.deleteType, 79 | url: file.deleteUrl 80 | }); 81 | }) 82 | ).always(function () { 83 | callback(); 84 | }); 85 | } 86 | 87 | beforeEach(function () { 88 | fileGIF = new File([blobGIF], 'example.gif', { type: 'image/gif' }); 89 | fileJPEG = new File([blobJPEG], 'example.jpg', { type: 'image/jpeg' }); 90 | files = [fileGIF, fileJPEG]; 91 | items = [ 92 | { 93 | getAsFile: function () { 94 | return files[0]; 95 | } 96 | }, 97 | { 98 | getAsFile: function () { 99 | return files[1]; 100 | } 101 | } 102 | ]; 103 | eventObject = { 104 | originalEvent: { 105 | dataTransfer: { files: files, types: ['Files'] }, 106 | clipboardData: { items: items } 107 | } 108 | }; 109 | }); 110 | 111 | afterEach(function (done) { 112 | $.getJSON(uploadURL).then(function (result) { 113 | deleteFiles(result.files, done); 114 | }); 115 | }); 116 | 117 | describe('Initialization', function () { 118 | var form; 119 | 120 | beforeEach(function () { 121 | form = createFileuploadForm(); 122 | }); 123 | 124 | afterEach(function () { 125 | form.remove(); 126 | }); 127 | 128 | it('widget', function () { 129 | form.fileupload(); 130 | expect(form.data('blueimp-fileupload')).to.be.an('object'); 131 | }); 132 | 133 | it('file input', function () { 134 | form.fileupload(); 135 | expect(form.fileupload('option', 'fileInput').length).to.equal(1); 136 | }); 137 | 138 | it('drop zone', function () { 139 | form.fileupload(); 140 | expect(form.fileupload('option', 'dropZone').length).to.equal(1); 141 | }); 142 | 143 | it('paste zone', function () { 144 | form.fileupload({ pasteZone: document }); 145 | expect(form.fileupload('option', 'pasteZone').length).to.equal(1); 146 | }); 147 | 148 | it('data attributes', function () { 149 | form.attr('data-url', 'https://example.org'); 150 | form.fileupload(); 151 | expect(form.fileupload('option', 'url')).to.equal('https://example.org'); 152 | expect(form.data('blueimp-fileupload')).to.be.an('object'); 153 | }); 154 | 155 | it('event listeners', function () { 156 | var eventsData = {}; 157 | form.fileupload({ 158 | autoUpload: false, 159 | pasteZone: document, 160 | dragover: function () { 161 | eventsData.dragover = true; 162 | }, 163 | dragenter: function () { 164 | eventsData.dragenter = true; 165 | }, 166 | dragleave: function () { 167 | eventsData.dragleave = true; 168 | }, 169 | drop: function (e, data) { 170 | eventsData.drop = data; 171 | }, 172 | paste: function (e, data) { 173 | eventsData.paste = data; 174 | }, 175 | change: function () { 176 | eventsData.change = true; 177 | } 178 | }); 179 | form 180 | .fileupload('option', 'fileInput') 181 | .trigger($.Event('change', eventObject)); 182 | expect(eventsData.change).to.equal(true); 183 | form 184 | .fileupload('option', 'dropZone') 185 | .trigger($.Event('dragover', eventObject)) 186 | .trigger($.Event('dragenter', eventObject)) 187 | .trigger($.Event('dragleave', eventObject)) 188 | .trigger($.Event('drop', eventObject)); 189 | expect(eventsData.dragover).to.equal(true); 190 | expect(eventsData.dragenter).to.equal(true); 191 | expect(eventsData.dragleave).to.equal(true); 192 | expect(eventsData.drop.files).to.deep.equal(files); 193 | form 194 | .fileupload('option', 'pasteZone') 195 | .trigger($.Event('paste', eventObject)); 196 | expect(eventsData.paste.files).to.deep.equal(files); 197 | }); 198 | }); 199 | 200 | describe('API', function () { 201 | var form; 202 | 203 | beforeEach(function () { 204 | form = createFileuploadForm().fileupload({ 205 | dataType: 'json', 206 | autoUpload: false 207 | }); 208 | }); 209 | 210 | afterEach(function () { 211 | form.remove(); 212 | }); 213 | 214 | it('destroy', function () { 215 | var eventsData = {}; 216 | form.fileupload('option', { 217 | pasteZone: document, 218 | dragover: function () { 219 | eventsData.dragover = true; 220 | }, 221 | dragenter: function () { 222 | eventsData.dragenter = true; 223 | }, 224 | dragleave: function () { 225 | eventsData.dragleave = true; 226 | }, 227 | drop: function (e, data) { 228 | eventsData.drop = data; 229 | }, 230 | paste: function (e, data) { 231 | eventsData.paste = data; 232 | }, 233 | change: function () { 234 | eventsData.change = true; 235 | } 236 | }); 237 | var fileInput = form.fileupload('option', 'fileInput'); 238 | var dropZone = form.fileupload('option', 'dropZone'); 239 | var pasteZone = form.fileupload('option', 'pasteZone'); 240 | form.fileupload('destroy'); 241 | expect(form.data('blueimp-fileupload')).to.equal(); 242 | fileInput.trigger($.Event('change', eventObject)); 243 | expect(eventsData.change).to.equal(); 244 | dropZone 245 | .trigger($.Event('dragover', eventObject)) 246 | .trigger($.Event('dragenter', eventObject)) 247 | .trigger($.Event('dragleave', eventObject)) 248 | .trigger($.Event('drop', eventObject)); 249 | expect(eventsData.dragover).to.equal(); 250 | expect(eventsData.dragenter).to.equal(); 251 | expect(eventsData.dragleave).to.equal(); 252 | expect(eventsData.drop).to.equal(); 253 | pasteZone.trigger($.Event('paste', eventObject)); 254 | expect(eventsData.paste).to.equal(); 255 | }); 256 | 257 | it('disable', function () { 258 | var eventsData = {}; 259 | form.fileupload('option', { 260 | pasteZone: document, 261 | dragover: function () { 262 | eventsData.dragover = true; 263 | }, 264 | dragenter: function () { 265 | eventsData.dragenter = true; 266 | }, 267 | dragleave: function () { 268 | eventsData.dragleave = true; 269 | }, 270 | drop: function (e, data) { 271 | eventsData.drop = data; 272 | }, 273 | paste: function (e, data) { 274 | eventsData.paste = data; 275 | }, 276 | change: function () { 277 | eventsData.change = true; 278 | } 279 | }); 280 | form.fileupload('disable'); 281 | form 282 | .fileupload('option', 'fileInput') 283 | .trigger($.Event('change', eventObject)); 284 | expect(eventsData.change).to.equal(); 285 | form 286 | .fileupload('option', 'dropZone') 287 | .trigger($.Event('dragover', eventObject)) 288 | .trigger($.Event('dragenter', eventObject)) 289 | .trigger($.Event('dragleave', eventObject)) 290 | .trigger($.Event('drop', eventObject)); 291 | expect(eventsData.dragover).to.equal(); 292 | expect(eventsData.dragenter).to.equal(); 293 | expect(eventsData.dragleave).to.equal(); 294 | expect(eventsData.drop).to.equal(); 295 | form 296 | .fileupload('option', 'pasteZone') 297 | .trigger($.Event('paste', eventObject)); 298 | expect(eventsData.paste).to.equal(); 299 | }); 300 | 301 | it('enable', function () { 302 | var eventsData = {}; 303 | form.fileupload('option', { 304 | pasteZone: document, 305 | dragover: function () { 306 | eventsData.dragover = true; 307 | }, 308 | dragenter: function () { 309 | eventsData.dragenter = true; 310 | }, 311 | dragleave: function () { 312 | eventsData.dragleave = true; 313 | }, 314 | drop: function (e, data) { 315 | eventsData.drop = data; 316 | }, 317 | paste: function (e, data) { 318 | eventsData.paste = data; 319 | }, 320 | change: function () { 321 | eventsData.change = true; 322 | } 323 | }); 324 | form.fileupload('disable'); 325 | form.fileupload('enable'); 326 | form 327 | .fileupload('option', 'fileInput') 328 | .trigger($.Event('change', eventObject)); 329 | expect(eventsData.change).to.equal(true); 330 | form 331 | .fileupload('option', 'dropZone') 332 | .trigger($.Event('dragover', eventObject)) 333 | .trigger($.Event('dragenter', eventObject)) 334 | .trigger($.Event('dragleave', eventObject)) 335 | .trigger($.Event('drop', eventObject)); 336 | expect(eventsData.dragover).to.equal(true); 337 | expect(eventsData.dragenter).to.equal(true); 338 | expect(eventsData.dragleave).to.equal(true); 339 | expect(eventsData.drop.files).to.deep.equal(files); 340 | form 341 | .fileupload('option', 'pasteZone') 342 | .trigger($.Event('paste', eventObject)); 343 | expect(eventsData.paste.files).to.deep.equal(files); 344 | }); 345 | 346 | it('option', function () { 347 | var eventsData = {}; 348 | form.fileupload('option', 'drop', function (e, data) { 349 | eventsData.drop = data; 350 | }); 351 | var dropZone = form 352 | .fileupload('option', 'dropZone') 353 | .trigger($.Event('drop', eventObject)); 354 | expect(eventsData.drop.files).to.deep.equal(files); 355 | delete eventsData.drop; 356 | form.fileupload('option', 'dropZone', null); 357 | dropZone.trigger($.Event('drop', eventObject)); 358 | expect(eventsData.drop).to.equal(); 359 | form.fileupload('option', { 360 | dropZone: dropZone 361 | }); 362 | dropZone.trigger($.Event('drop', eventObject)); 363 | expect(eventsData.drop.files).to.deep.equal(files); 364 | }); 365 | 366 | it('add', function () { 367 | var eventData = []; 368 | form.fileupload('option', 'add', function (e, data) { 369 | eventData.push(data); 370 | }); 371 | form.fileupload('add', { files: files }); 372 | expect(eventData.length).to.equal(2); 373 | expect(eventData[0].files[0]).to.equal(files[0]); 374 | expect(eventData[1].files[0]).to.equal(files[1]); 375 | }); 376 | 377 | it('send', function (done) { 378 | this.slow(200); 379 | form.fileupload('send', { files: files }).complete(function (result) { 380 | var uploadedFiles = result.responseJSON.files; 381 | expect(uploadedFiles.length).to.equal(2); 382 | expect(uploadedFiles[0].type).to.equal(files[0].type); 383 | expect(uploadedFiles[0].error).to.equal(); 384 | expect(uploadedFiles[1].type).to.equal(files[1].type); 385 | expect(uploadedFiles[1].error).to.equal(); 386 | done(); 387 | }); 388 | }); 389 | }); 390 | 391 | describe('Callbacks', function () { 392 | var form; 393 | 394 | beforeEach(function () { 395 | form = createFileuploadForm().fileupload({ dataType: 'json' }); 396 | }); 397 | 398 | afterEach(function () { 399 | form.remove(); 400 | }); 401 | 402 | it('add', function () { 403 | var eventData = []; 404 | form.fileupload('option', 'add', function (e, data) { 405 | eventData.push(data); 406 | }); 407 | form.fileupload('add', { files: files }); 408 | expect(eventData.length).to.equal(2); 409 | expect(eventData[0].files[0]).to.equal(files[0]); 410 | expect(eventData[1].files[0]).to.equal(files[1]); 411 | }); 412 | 413 | it('submit', function (done) { 414 | this.slow(200); 415 | var eventData = []; 416 | form.fileupload('option', { 417 | submit: function (e, data) { 418 | eventData.push(data); 419 | }, 420 | stop: function () { 421 | if (eventData.length < 2) return; 422 | expect(eventData[0].files[0]).to.equal(files[0]); 423 | expect(eventData[1].files[0]).to.equal(files[1]); 424 | done(); 425 | } 426 | }); 427 | form.fileupload('add', { files: files }); 428 | }); 429 | 430 | it('send', function (done) { 431 | this.slow(200); 432 | var eventData = []; 433 | form.fileupload('option', { 434 | send: function (e, data) { 435 | eventData.push(data); 436 | }, 437 | stop: function () { 438 | expect(eventData.length).to.equal(1); 439 | expect(eventData[0].files).to.deep.equal(files); 440 | done(); 441 | } 442 | }); 443 | form.fileupload('send', { files: files }); 444 | }); 445 | 446 | it('done', function (done) { 447 | this.slow(200); 448 | var eventData = []; 449 | form.fileupload('option', { 450 | done: function (e, data) { 451 | eventData.push(data); 452 | }, 453 | stop: function () { 454 | if (eventData.length < 2) return; 455 | expect(eventData[0].result.files.length).to.equal(1); 456 | expect(eventData[1].result.files.length).to.equal(1); 457 | done(); 458 | } 459 | }); 460 | form.fileupload('add', { files: files }); 461 | }); 462 | 463 | it('fail', function (done) { 464 | this.slow(200); 465 | var eventData = []; 466 | form.fileupload('option', { 467 | url: uploadURL + '404', 468 | fail: function (e, data) { 469 | eventData.push(data); 470 | }, 471 | stop: function () { 472 | if (eventData.length < 2) return; 473 | expect(eventData[0].result).to.equal(); 474 | expect(eventData[1].result).to.equal(); 475 | done(); 476 | } 477 | }); 478 | form.fileupload('add', { files: files }); 479 | }); 480 | 481 | it('always', function (done) { 482 | this.slow(200); 483 | var eventData = []; 484 | form.fileupload('option', { 485 | always: function (e, data) { 486 | eventData.push(data); 487 | }, 488 | stop: function () { 489 | if (eventData.length < 2) { 490 | expect(eventData[0].result).to.equal(); 491 | form.fileupload('add', { files: [fileGIF] }); 492 | return; 493 | } 494 | expect(eventData[1].result.files.length).to.equal(1); 495 | done(); 496 | } 497 | }); 498 | form.fileupload('add', { files: [fileGIF], url: uploadURL + '404' }); 499 | }); 500 | 501 | it('progress', function (done) { 502 | this.slow(200); 503 | var loaded; 504 | var total; 505 | form.fileupload('option', { 506 | progress: function (e, data) { 507 | loaded = data.loaded; 508 | total = data.total; 509 | expect(loaded).to.be.at.most(total); 510 | }, 511 | stop: function () { 512 | expect(loaded).to.equal(total); 513 | done(); 514 | } 515 | }); 516 | form.fileupload('add', { files: [fileGIF] }); 517 | }); 518 | 519 | it('progressall', function (done) { 520 | this.slow(200); 521 | var loaded; 522 | var total; 523 | var completed = 0; 524 | form.fileupload('option', { 525 | progressall: function (e, data) { 526 | loaded = data.loaded; 527 | total = data.total; 528 | expect(loaded).to.be.at.most(total); 529 | }, 530 | always: function () { 531 | completed++; 532 | }, 533 | stop: function () { 534 | if (completed < 2) return; 535 | expect(loaded).to.equal(total); 536 | done(); 537 | } 538 | }); 539 | form.fileupload('add', { files: files }); 540 | }); 541 | 542 | it('start', function (done) { 543 | this.slow(200); 544 | var started; 545 | form.fileupload('option', { 546 | start: function () { 547 | started = true; 548 | }, 549 | stop: function () { 550 | expect(started).to.equal(true); 551 | done(); 552 | } 553 | }); 554 | form.fileupload('add', { files: [fileGIF] }); 555 | }); 556 | 557 | it('stop', function (done) { 558 | this.slow(200); 559 | form.fileupload('option', { 560 | stop: function () { 561 | done(); 562 | } 563 | }); 564 | form.fileupload('add', { files: [fileGIF] }); 565 | }); 566 | 567 | it('dragover', function () { 568 | var eventsData = {}; 569 | form.fileupload('option', { 570 | autoUpload: false, 571 | dragover: function () { 572 | eventsData.dragover = true; 573 | } 574 | }); 575 | form 576 | .fileupload('option', 'dropZone') 577 | .trigger($.Event('dragover', eventObject)); 578 | expect(eventsData.dragover).to.equal(true); 579 | }); 580 | 581 | it('dragenter', function () { 582 | var eventsData = {}; 583 | form.fileupload('option', { 584 | autoUpload: false, 585 | dragenter: function () { 586 | eventsData.dragenter = true; 587 | } 588 | }); 589 | form 590 | .fileupload('option', 'dropZone') 591 | .trigger($.Event('dragenter', eventObject)); 592 | expect(eventsData.dragenter).to.equal(true); 593 | }); 594 | 595 | it('dragleave', function () { 596 | var eventsData = {}; 597 | form.fileupload('option', { 598 | autoUpload: false, 599 | dragleave: function () { 600 | eventsData.dragleave = true; 601 | } 602 | }); 603 | form 604 | .fileupload('option', 'dropZone') 605 | .trigger($.Event('dragleave', eventObject)); 606 | expect(eventsData.dragleave).to.equal(true); 607 | }); 608 | 609 | it('drop', function () { 610 | var eventsData = {}; 611 | form.fileupload('option', { 612 | autoUpload: false, 613 | drop: function (e, data) { 614 | eventsData.drop = data; 615 | } 616 | }); 617 | form 618 | .fileupload('option', 'dropZone') 619 | .trigger($.Event('drop', eventObject)); 620 | expect(eventsData.drop.files).to.deep.equal(files); 621 | }); 622 | 623 | it('paste', function () { 624 | var eventsData = {}; 625 | form.fileupload('option', { 626 | autoUpload: false, 627 | pasteZone: document, 628 | paste: function (e, data) { 629 | eventsData.paste = data; 630 | } 631 | }); 632 | form 633 | .fileupload('option', 'pasteZone') 634 | .trigger($.Event('paste', eventObject)); 635 | expect(eventsData.paste.files).to.deep.equal(files); 636 | }); 637 | 638 | it('change', function () { 639 | var eventsData = {}; 640 | form.fileupload('option', { 641 | autoUpload: false, 642 | change: function () { 643 | eventsData.change = true; 644 | } 645 | }); 646 | form 647 | .fileupload('option', 'fileInput') 648 | .trigger($.Event('change', eventObject)); 649 | expect(eventsData.change).to.equal(true); 650 | }); 651 | }); 652 | 653 | describe('Options', function () { 654 | var form; 655 | 656 | beforeEach(function () { 657 | form = createFileuploadForm(); 658 | }); 659 | 660 | afterEach(function () { 661 | form.remove(); 662 | }); 663 | 664 | it('paramName', function (done) { 665 | form.fileupload({ 666 | send: function (e, data) { 667 | expect(data.paramName[0]).to.equal( 668 | form.fileupload('option', 'fileInput').prop('name') 669 | ); 670 | done(); 671 | return false; 672 | } 673 | }); 674 | form.fileupload('add', { files: [fileGIF] }); 675 | }); 676 | 677 | it('url', function (done) { 678 | form.fileupload({ 679 | send: function (e, data) { 680 | expect(data.url).to.equal(form.prop('action')); 681 | done(); 682 | return false; 683 | } 684 | }); 685 | form.fileupload('add', { files: [fileGIF] }); 686 | }); 687 | 688 | it('type', function (done) { 689 | form.fileupload({ 690 | type: 'PUT', 691 | send: function (e, data) { 692 | expect(data.type).to.equal('PUT'); 693 | done(); 694 | return false; 695 | } 696 | }); 697 | form.fileupload('add', { files: [fileGIF] }); 698 | }); 699 | 700 | it('replaceFileInput', function () { 701 | form.fileupload(); 702 | var fileInput = form.fileupload('option', 'fileInput'); 703 | fileInput.trigger($.Event('change', eventObject)); 704 | expect(form.fileupload('option', 'fileInput')[0]).to.not.equal( 705 | fileInput[0] 706 | ); 707 | form.fileupload('option', 'replaceFileInput', false); 708 | fileInput = form.fileupload('option', 'fileInput'); 709 | fileInput.trigger($.Event('change', eventObject)); 710 | expect(form.fileupload('option', 'fileInput')[0]).to.equal(fileInput[0]); 711 | }); 712 | 713 | it('forceIframeTransport', function (done) { 714 | form.fileupload({ 715 | forceIframeTransport: 'PUT', 716 | send: function (e, data) { 717 | expect(data.dataType.substr(0, 6)).to.equal('iframe'); 718 | done(); 719 | return false; 720 | } 721 | }); 722 | form.fileupload('add', { files: [fileGIF] }); 723 | }); 724 | 725 | it('singleFileUploads', function (done) { 726 | form.fileupload({ 727 | singleFileUploads: false, 728 | send: function (e, data) { 729 | expect(data.files).to.deep.equal(files); 730 | done(); 731 | return false; 732 | } 733 | }); 734 | form.fileupload('add', { files: files }); 735 | }); 736 | 737 | it('limitMultiFileUploads', function (done) { 738 | var completed = 0; 739 | form.fileupload({ 740 | singleFileUploads: false, 741 | limitMultiFileUploads: 2, 742 | send: function (e, data) { 743 | expect(data.files).to.deep.equal(files); 744 | completed++; 745 | if (completed < 2) return; 746 | done(); 747 | return false; 748 | } 749 | }); 750 | form.fileupload('add', { files: files.concat(files) }); 751 | }); 752 | 753 | it('limitMultiFileUploadSize', function (done) { 754 | var completed = 0; 755 | form.fileupload({ 756 | singleFileUploads: false, 757 | limitMultiFileUploadSize: files[0].size + files[1].size, 758 | limitMultiFileUploadSizeOverhead: 0, 759 | send: function (e, data) { 760 | expect(data.files).to.deep.equal(files); 761 | completed++; 762 | if (completed < 2) return; 763 | done(); 764 | return false; 765 | } 766 | }); 767 | form.fileupload('add', { files: files.concat(files) }); 768 | }); 769 | 770 | it('sequentialUploads', function (done) { 771 | this.slow(400); 772 | var completed = 0; 773 | var events = []; 774 | form.fileupload({ 775 | sequentialUploads: true, 776 | dataType: 'json', 777 | send: function () { 778 | events.push('send'); 779 | }, 780 | always: function () { 781 | events.push('complete'); 782 | completed++; 783 | }, 784 | stop: function () { 785 | if (completed === 4) { 786 | expect(events.join(',')).to.equal( 787 | [ 788 | 'send', 789 | 'complete', 790 | 'send', 791 | 'complete', 792 | 'send', 793 | 'complete', 794 | 'send', 795 | 'complete' 796 | ].join(',') 797 | ); 798 | done(); 799 | } 800 | } 801 | }); 802 | form.fileupload('add', { files: files.concat(files) }); 803 | }); 804 | 805 | it('limitConcurrentUploads', function (done) { 806 | this.slow(800); 807 | var completed = 0; 808 | var loadCount = 0; 809 | form.fileupload({ 810 | limitConcurrentUploads: 2, 811 | dataType: 'json', 812 | send: function () { 813 | loadCount++; 814 | expect(loadCount).to.be.at.most(2); 815 | }, 816 | always: function () { 817 | completed++; 818 | loadCount--; 819 | }, 820 | stop: function () { 821 | if (completed === 8) { 822 | done(); 823 | } 824 | } 825 | }); 826 | form.fileupload('add', { 827 | files: files.concat(files).concat(files).concat(files) 828 | }); 829 | }); 830 | 831 | it('multipart', function (done) { 832 | form.fileupload({ 833 | multipart: false, 834 | send: function (e, data) { 835 | expect(data.contentType).to.equal(fileGIF.type); 836 | expect(data.headers['Content-Disposition']).to.equal( 837 | 'attachment; filename="' + fileGIF.name + '"' 838 | ); 839 | done(); 840 | return false; 841 | } 842 | }); 843 | form.fileupload('add', { files: [fileGIF] }); 844 | }); 845 | 846 | it('uniqueFilenames', function (done) { 847 | form.fileupload({ 848 | uniqueFilenames: {}, 849 | send: function (e, data) { 850 | var formFiles = data.data.getAll('files[]'); 851 | expect(formFiles[0].name).to.equal(fileGIF.name); 852 | expect(formFiles[1].name).to.equal( 853 | fileGIF.name.replace('.gif', ' (1).gif') 854 | ); 855 | expect(formFiles[2].name).to.equal( 856 | fileGIF.name.replace('.gif', ' (2).gif') 857 | ); 858 | done(); 859 | return false; 860 | } 861 | }); 862 | form.fileupload('send', { files: [fileGIF, fileGIF, fileGIF] }); 863 | }); 864 | 865 | it('maxChunkSize', function (done) { 866 | this.slow(400); 867 | var events = []; 868 | form.fileupload({ 869 | maxChunkSize: 32, 870 | dataType: 'json', 871 | chunkbeforesend: function () { 872 | events.push('chunkbeforesend'); 873 | }, 874 | chunksend: function () { 875 | events.push('chunksend'); 876 | }, 877 | chunkdone: function () { 878 | events.push('chunkdone'); 879 | }, 880 | done: function (e, data) { 881 | var uploadedFile = data.result.files[0]; 882 | expect(uploadedFile.type).to.equal(fileGIF.type); 883 | expect(uploadedFile.size).to.equal(fileGIF.size); 884 | }, 885 | stop: function () { 886 | expect(events.join(',')).to.equal( 887 | [ 888 | 'chunkbeforesend', 889 | 'chunksend', 890 | 'chunkdone', 891 | 'chunkbeforesend', 892 | 'chunksend', 893 | 'chunkdone', 894 | 'chunkbeforesend', 895 | 'chunksend', 896 | 'chunkdone', 897 | 'chunkbeforesend', 898 | 'chunksend', 899 | 'chunkdone' 900 | ].join(',') 901 | ); 902 | done(); 903 | } 904 | }); 905 | form.fileupload('send', { files: [fileGIF] }); 906 | }); 907 | 908 | it('acceptFileTypes', function (done) { 909 | var processData; 910 | form.fileupload({ 911 | acceptFileTypes: /^image\/gif$/, 912 | singleFileUploads: false, 913 | processalways: function (e, data) { 914 | processData = data; 915 | }, 916 | processstop: function () { 917 | expect(processData.files[0].error).to.equal(); 918 | expect(processData.files[1].error).to.equal( 919 | form.fileupload('option').i18n('acceptFileTypes') 920 | ); 921 | done(); 922 | } 923 | }); 924 | form.fileupload('add', { files: files }); 925 | }); 926 | 927 | it('maxFileSize', function (done) { 928 | var processData; 929 | form.fileupload({ 930 | maxFileSize: 200, 931 | singleFileUploads: false, 932 | processalways: function (e, data) { 933 | processData = data; 934 | }, 935 | processstop: function () { 936 | expect(processData.files[0].error).to.equal(); 937 | expect(processData.files[1].error).to.equal( 938 | form.fileupload('option').i18n('maxFileSize') 939 | ); 940 | done(); 941 | } 942 | }); 943 | form.fileupload('add', { files: files }); 944 | }); 945 | 946 | it('minFileSize', function (done) { 947 | var processData; 948 | form.fileupload({ 949 | minFileSize: 200, 950 | singleFileUploads: false, 951 | processalways: function (e, data) { 952 | processData = data; 953 | }, 954 | processstop: function () { 955 | expect(processData.files[0].error).to.equal( 956 | form.fileupload('option').i18n('minFileSize') 957 | ); 958 | expect(processData.files[1].error).to.equal(); 959 | done(); 960 | } 961 | }); 962 | form.fileupload('add', { files: files }); 963 | }); 964 | 965 | it('maxNumberOfFiles', function (done) { 966 | var processData; 967 | form.fileupload({ 968 | maxNumberOfFiles: 2, 969 | getNumberOfFiles: function () { 970 | return 2; 971 | }, 972 | singleFileUploads: false, 973 | processalways: function (e, data) { 974 | processData = data; 975 | }, 976 | processstop: function () { 977 | expect(processData.files[0].error).to.equal( 978 | form.fileupload('option').i18n('maxNumberOfFiles') 979 | ); 980 | expect(processData.files[1].error).to.equal( 981 | form.fileupload('option').i18n('maxNumberOfFiles') 982 | ); 983 | done(); 984 | } 985 | }); 986 | form.fileupload('add', { files: files }); 987 | }); 988 | }); 989 | })(this.chai.expect, this.jQuery); 990 | -------------------------------------------------------------------------------- /test/vendor/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | #mocha .test .html-error { 140 | overflow: auto; 141 | color: black; 142 | display: block; 143 | float: left; 144 | clear: left; 145 | font: 12px/1.5 monaco, monospace; 146 | margin: 5px; 147 | padding: 15px; 148 | border: 1px solid #eee; 149 | max-width: 85%; /*(1)*/ 150 | max-width: -webkit-calc(100% - 42px); 151 | max-width: -moz-calc(100% - 42px); 152 | max-width: calc(100% - 42px); /*(2)*/ 153 | max-height: 300px; 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-box-shadow: 0 1px 3px #eee; 157 | -moz-box-shadow: 0 1px 3px #eee; 158 | box-shadow: 0 1px 3px #eee; 159 | -webkit-border-radius: 3px; 160 | -moz-border-radius: 3px; 161 | border-radius: 3px; 162 | } 163 | 164 | #mocha .test .html-error pre.error { 165 | border: none; 166 | -webkit-border-radius: 0; 167 | -moz-border-radius: 0; 168 | border-radius: 0; 169 | -webkit-box-shadow: 0; 170 | -moz-box-shadow: 0; 171 | box-shadow: 0; 172 | padding: 0; 173 | margin: 0; 174 | margin-top: 18px; 175 | max-height: none; 176 | } 177 | 178 | /** 179 | * (1): approximate for browsers not supporting calc 180 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 181 | * ^^ seriously 182 | */ 183 | #mocha .test pre { 184 | display: block; 185 | float: left; 186 | clear: left; 187 | font: 12px/1.5 monaco, monospace; 188 | margin: 5px; 189 | padding: 15px; 190 | border: 1px solid #eee; 191 | max-width: 85%; /*(1)*/ 192 | max-width: -webkit-calc(100% - 42px); 193 | max-width: -moz-calc(100% - 42px); 194 | max-width: calc(100% - 42px); /*(2)*/ 195 | word-wrap: break-word; 196 | border-bottom-color: #ddd; 197 | -webkit-box-shadow: 0 1px 3px #eee; 198 | -moz-box-shadow: 0 1px 3px #eee; 199 | box-shadow: 0 1px 3px #eee; 200 | -webkit-border-radius: 3px; 201 | -moz-border-radius: 3px; 202 | border-radius: 3px; 203 | } 204 | 205 | #mocha .test h2 { 206 | position: relative; 207 | } 208 | 209 | #mocha .test a.replay { 210 | position: absolute; 211 | top: 3px; 212 | right: 0; 213 | text-decoration: none; 214 | vertical-align: middle; 215 | display: block; 216 | width: 15px; 217 | height: 15px; 218 | line-height: 15px; 219 | text-align: center; 220 | background: #eee; 221 | font-size: 15px; 222 | -webkit-border-radius: 15px; 223 | -moz-border-radius: 15px; 224 | border-radius: 15px; 225 | -webkit-transition:opacity 200ms; 226 | -moz-transition:opacity 200ms; 227 | -o-transition:opacity 200ms; 228 | transition: opacity 200ms; 229 | opacity: 0.3; 230 | color: #888; 231 | } 232 | 233 | #mocha .test:hover a.replay { 234 | opacity: 1; 235 | } 236 | 237 | #mocha-report.pass .test.fail { 238 | display: none; 239 | } 240 | 241 | #mocha-report.fail .test.pass { 242 | display: none; 243 | } 244 | 245 | #mocha-report.pending .test.pass, 246 | #mocha-report.pending .test.fail { 247 | display: none; 248 | } 249 | #mocha-report.pending .test.pass.pending { 250 | display: block; 251 | } 252 | 253 | #mocha-error { 254 | color: #c00; 255 | font-size: 1.5em; 256 | font-weight: 100; 257 | letter-spacing: 1px; 258 | } 259 | 260 | #mocha-stats { 261 | position: fixed; 262 | top: 15px; 263 | right: 10px; 264 | font-size: 12px; 265 | margin: 0; 266 | color: #888; 267 | z-index: 1; 268 | } 269 | 270 | #mocha-stats .progress { 271 | float: right; 272 | padding-top: 0; 273 | 274 | /** 275 | * Set safe initial values, so mochas .progress does not inherit these 276 | * properties from Bootstrap .progress (which causes .progress height to 277 | * equal line height set in Bootstrap). 278 | */ 279 | height: auto; 280 | -webkit-box-shadow: none; 281 | -moz-box-shadow: none; 282 | box-shadow: none; 283 | background-color: initial; 284 | } 285 | 286 | #mocha-stats em { 287 | color: black; 288 | } 289 | 290 | #mocha-stats a { 291 | text-decoration: none; 292 | color: inherit; 293 | } 294 | 295 | #mocha-stats a:hover { 296 | border-bottom: 1px solid #eee; 297 | } 298 | 299 | #mocha-stats li { 300 | display: inline-block; 301 | margin: 0 5px; 302 | list-style: none; 303 | padding-top: 11px; 304 | } 305 | 306 | #mocha-stats canvas { 307 | width: 40px; 308 | height: 40px; 309 | } 310 | 311 | #mocha code .comment { color: #ddd; } 312 | #mocha code .init { color: #2f6fad; } 313 | #mocha code .string { color: #5890ad; } 314 | #mocha code .keyword { color: #8a6343; } 315 | #mocha code .number { color: #2f6fad; } 316 | 317 | @media screen and (max-device-width: 480px) { 318 | #mocha { 319 | margin: 60px 0px; 320 | } 321 | 322 | #mocha #stats { 323 | position: absolute; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /wdio/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 2019 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /wdio/.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | arrowParens: 'avoid', 5 | proseWrap: 'always', 6 | semi: false, 7 | singleQuote: true, 8 | trailingComma: 'none' 9 | } 10 | -------------------------------------------------------------------------------- /wdio/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2019 Sebastian Tschan, https://blueimp.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /wdio/assets/black+white-3x2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueimp/jQuery-File-Upload/0e92a4d4613d4ed5231ee0d8513519f2e04f99ba/wdio/assets/black+white-3x2.jpg -------------------------------------------------------------------------------- /wdio/assets/black+white-60x40.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueimp/jQuery-File-Upload/0e92a4d4613d4ed5231ee0d8513519f2e04f99ba/wdio/assets/black+white-60x40.gif -------------------------------------------------------------------------------- /wdio/conf/chrome.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable jsdoc/valid-types */ 4 | /** @type WebdriverIO.Config */ 5 | const config = { 6 | hostname: 'chromedriver', 7 | path: '/', 8 | capabilities: [ 9 | { 10 | // Set maxInstances to 1 if screen recordings are enabled: 11 | // maxInstances: 1, 12 | browserName: 'chrome', 13 | 'goog:chromeOptions': { 14 | // Disable headless mode if screen recordings are enabled: 15 | args: ['--headless', '--window-size=1440,900'] 16 | } 17 | } 18 | ], 19 | logLevel: 'warn', 20 | reporters: ['spec'], 21 | framework: 'mocha', 22 | mochaOpts: { 23 | timeout: 60000 24 | }, 25 | specs: ['test/specs/**/*.js'], 26 | maximizeWindow: true, 27 | screenshots: { 28 | saveOnFail: true 29 | }, 30 | videos: { 31 | enabled: false, 32 | resolution: '1440x900', 33 | startDelay: 500, 34 | stopDelay: 500 35 | }, 36 | assetsDir: '/home/webdriver/assets/', 37 | baseUrl: 'http://example' 38 | } 39 | 40 | exports.config = Object.assign({}, require('../hooks'), config) 41 | -------------------------------------------------------------------------------- /wdio/conf/firefox.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-disable jsdoc/valid-types */ 4 | /** @type WebdriverIO.Config */ 5 | const config = { 6 | hostname: 'geckodriver', 7 | capabilities: [ 8 | { 9 | // geckodriver supports no parallel sessions: 10 | maxInstances: 1, 11 | browserName: 'firefox', 12 | 'moz:firefoxOptions': { 13 | //args: ['-headless', '--window-size=1440,900'] 14 | } 15 | } 16 | ], 17 | videos: { 18 | enabled: true, 19 | resolution: '1440x900', 20 | startDelay: 500, 21 | stopDelay: 500 22 | } 23 | } 24 | 25 | exports.config = Object.assign({}, require('./chrome').config, config) 26 | -------------------------------------------------------------------------------- /wdio/hooks/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global browser, Promise */ 4 | 5 | const cmds = require('wdio-screen-commands') 6 | 7 | /* eslint-disable jsdoc/valid-types */ 8 | /** @type WebdriverIO.HookFunctionExtension */ 9 | const config = { 10 | before: async () => { 11 | // Add browser commands: 12 | browser.addCommand('saveScreenshotByName', cmds.saveScreenshotByName) 13 | browser.addCommand('saveAndDiffScreenshot', cmds.saveAndDiffScreenshot) 14 | // Add element commands: 15 | browser.addCommand('saveScreenshotByName', cmds.saveScreenshotByName, true) 16 | browser.addCommand( 17 | 'saveAndDiffScreenshot', 18 | cmds.saveAndDiffScreenshot, 19 | true 20 | ) 21 | if (browser.config.appium) 22 | await browser.updateSettings(browser.config.appium) 23 | if (browser.config.maximizeWindow) await browser.maximizeWindow() 24 | }, 25 | beforeTest: async test => { 26 | await cmds.startScreenRecording(test) 27 | }, 28 | afterTest: async (test, context, result) => { 29 | await Promise.all([ 30 | cmds.stopScreenRecording(test, result), 31 | cmds.saveScreenshotByTest(test, result) 32 | ]) 33 | } 34 | } 35 | 36 | module.exports = config 37 | -------------------------------------------------------------------------------- /wdio/reports/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/.gitignore 3 | -------------------------------------------------------------------------------- /wdio/test/pages/file-upload.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global browser, $, $$ */ 4 | /* eslint-disable class-methods-use-this */ 5 | 6 | class FileUpload { 7 | get fileinput() { 8 | return $('.fileinput-button input') 9 | } 10 | get start() { 11 | return $('.fileupload-buttonbar .start') 12 | } 13 | get toggle() { 14 | return $('.fileupload-buttonbar .toggle') 15 | } 16 | get remove() { 17 | return $('.fileupload-buttonbar .delete') 18 | } 19 | get processing() { 20 | return $$('.files .processing') 21 | } 22 | get uploads() { 23 | return $$('.files .template-upload') 24 | } 25 | get downloads() { 26 | return $$('.files .template-download') 27 | } 28 | get checked() { 29 | return $$('.files .toggle:checked') 30 | } 31 | /** 32 | * Opens the file upload form. 33 | * 34 | * @param {number} [timeout] Wait timeout 35 | */ 36 | async open(timeout) { 37 | await browser.url('/') 38 | await this.fileinput.waitForExist({ timeout }) 39 | } 40 | /** 41 | * Uploads files. 42 | * 43 | * @param {Array} files Files to upload 44 | * @param {number} [timeout] Wait timeout 45 | */ 46 | async upload(files, timeout) { 47 | await this.fileinput.addValue(files.join('\n')) 48 | await browser.waitUntil(async () => !(await this.processing.length), { 49 | timeout 50 | }) 51 | await this.start.click() 52 | await browser.waitUntil(async () => !!(await this.downloads.length), { 53 | timeout 54 | }) 55 | await browser.waitUntil(async () => !(await this.uploads.length), { 56 | timeout 57 | }) 58 | } 59 | /** 60 | * Deletes uploaded files. 61 | * 62 | * @param {number} [timeout] Wait timeout 63 | */ 64 | async delete(timeout) { 65 | await this.toggle.click() 66 | await browser.waitUntil( 67 | async () => (await this.downloads.length) === (await this.checked.length), 68 | { 69 | timeout 70 | } 71 | ) 72 | await this.remove.click() 73 | await browser.waitUntil(async () => !(await this.downloads.length), { 74 | timeout 75 | }) 76 | } 77 | } 78 | 79 | module.exports = new FileUpload() 80 | -------------------------------------------------------------------------------- /wdio/test/specs/01-file-upload.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global browser, describe, it */ 4 | 5 | const FileUpload = require('../pages/file-upload') 6 | const assetsDir = browser.config.assetsDir 7 | 8 | describe('File Upload', () => { 9 | if (!assetsDir) return 10 | 11 | it('uploads files', async () => { 12 | await FileUpload.open() 13 | await FileUpload.upload([ 14 | assetsDir + 'black+white-60x40.gif', 15 | assetsDir + 'black+white-3x2.jpg' 16 | ]) 17 | await browser.saveAndDiffScreenshot('Files uploaded') 18 | }) 19 | 20 | it('deletes files', async () => { 21 | await FileUpload.open() 22 | await FileUpload.delete() 23 | await browser.saveAndDiffScreenshot('Files deleted') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /wdio/wdio.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Default to the Chrome config: 4 | exports.config = require('./conf/chrome').config 5 | --------------------------------------------------------------------------------