├── .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 | ',
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 |
--------------------------------------------------------------------------------