Repository: FineUploader/fine-uploader Branch: master Commit: 057cc83a7e76 Files: 246 Total size: 2.3 MB Directory structure: gitextract_c09cz4qu/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── ATTRIBUTION.txt ├── LICENSE ├── Makefile ├── README.md ├── client/ │ ├── README.md │ ├── commonJs/ │ │ ├── all.js │ │ ├── azure.js │ │ ├── core/ │ │ │ ├── all.js │ │ │ ├── azure.js │ │ │ ├── index.js │ │ │ ├── s3.js │ │ │ └── traditional.js │ │ ├── dnd.js │ │ ├── jquery/ │ │ │ ├── azure.js │ │ │ ├── s3.js │ │ │ └── traditional.js │ │ ├── s3.js │ │ └── traditional.js │ ├── fine-uploader-gallery.css │ ├── fine-uploader-new.css │ ├── fine-uploader.css │ ├── html/ │ │ └── templates/ │ │ ├── default.html │ │ ├── gallery.html │ │ └── simple-thumbnails.html │ ├── js/ │ │ ├── ajax.requester.js │ │ ├── azure/ │ │ │ ├── azure.xhr.upload.handler.js │ │ │ ├── get-sas.js │ │ │ ├── jquery-plugin.js │ │ │ ├── rest/ │ │ │ │ ├── delete-blob.js │ │ │ │ ├── put-blob.js │ │ │ │ ├── put-block-list.js │ │ │ │ └── put-block.js │ │ │ ├── uploader.basic.js │ │ │ ├── uploader.js │ │ │ └── util.js │ │ ├── blob-proxy.js │ │ ├── button.js │ │ ├── deletefile.ajax.requester.js │ │ ├── dnd.js │ │ ├── error/ │ │ │ └── error.js │ │ ├── export.js │ │ ├── features.js │ │ ├── form-support.js │ │ ├── identify.js │ │ ├── iframe.xss.response.js │ │ ├── image-support/ │ │ │ ├── exif.js │ │ │ ├── image.js │ │ │ ├── megapix-image.js │ │ │ ├── scaler.js │ │ │ └── validation.image.js │ │ ├── jquery-dnd.js │ │ ├── jquery-plugin.js │ │ ├── non-traditional-common/ │ │ │ └── uploader.basic.api.js │ │ ├── paste.js │ │ ├── promise.js │ │ ├── s3/ │ │ │ ├── jquery-plugin.js │ │ │ ├── multipart.abort.ajax.requester.js │ │ │ ├── multipart.complete.ajax.requester.js │ │ │ ├── multipart.initiate.ajax.requester.js │ │ │ ├── request-signer.js │ │ │ ├── s3.form.upload.handler.js │ │ │ ├── s3.xhr.upload.handler.js │ │ │ ├── uploader.basic.js │ │ │ ├── uploader.js │ │ │ └── util.js │ │ ├── session.ajax.requester.js │ │ ├── session.js │ │ ├── templating.js │ │ ├── third-party/ │ │ │ ├── ExifRestorer.js │ │ │ └── crypto-js/ │ │ │ ├── core.js │ │ │ ├── enc-base64.js │ │ │ ├── hmac.js │ │ │ ├── lib-typedarrays.js │ │ │ ├── sha1.js │ │ │ └── sha256.js │ │ ├── total-progress.js │ │ ├── traditional/ │ │ │ ├── all-chunks-done.ajax.requester.js │ │ │ ├── traditional.form.upload.handler.js │ │ │ └── traditional.xhr.upload.handler.js │ │ ├── ui.handler.click.filebuttons.js │ │ ├── ui.handler.click.filename.js │ │ ├── ui.handler.edit.filename.js │ │ ├── ui.handler.events.js │ │ ├── ui.handler.focus.filenameinput.js │ │ ├── ui.handler.focusin.filenameinput.js │ │ ├── upload-data.js │ │ ├── upload-handler/ │ │ │ ├── form.upload.handler.js │ │ │ ├── upload.handler.controller.js │ │ │ ├── upload.handler.js │ │ │ └── xhr.upload.handler.js │ │ ├── uploader.api.js │ │ ├── uploader.basic.api.js │ │ ├── uploader.basic.js │ │ ├── uploader.js │ │ ├── uploadsuccess.ajax.requester.js │ │ ├── util.js │ │ ├── version.js │ │ └── window.receive.message.js │ └── typescript/ │ ├── fine-uploader.d.ts │ └── fine-uploader.test.ts ├── config/ │ └── karma.conf.js ├── docs/ │ ├── _static/ │ │ ├── css/ │ │ │ ├── main.css │ │ │ └── pygments.css │ │ └── js/ │ │ ├── main.js │ │ ├── navbar.js │ │ └── sidebar.js │ ├── _templates/ │ │ ├── api.html │ │ ├── base.html │ │ ├── feature.html │ │ ├── footer.html │ │ ├── layout.html │ │ ├── macros/ │ │ │ ├── alerts.html │ │ │ ├── code.html │ │ │ └── github.html │ │ └── navbar.html │ ├── api/ │ │ ├── events-s3.jmd │ │ ├── events.jmd │ │ ├── methods-azure.jmd │ │ ├── methods-s3.jmd │ │ ├── methods.jmd │ │ ├── options-azure.jmd │ │ ├── options-s3.jmd │ │ ├── options-ui.jmd │ │ ├── options.jmd │ │ └── qq.jmd │ ├── browser-support.jmd │ ├── endpoint_handlers/ │ │ ├── amazon-s3.jmd │ │ ├── azure.jmd │ │ └── traditional.jmd │ ├── faq.jmd │ ├── features/ │ │ ├── CORS.jmd │ │ ├── async-tasks-and-promises.jmd │ │ ├── azure.jmd │ │ ├── cancellable-uploads.jmd │ │ ├── chunking.jmd │ │ ├── concurrent-chunking.jmd │ │ ├── delete.jmd │ │ ├── drag-and-drop.jmd │ │ ├── extra-buttons.jmd │ │ ├── filename-edit.jmd │ │ ├── forms.jmd │ │ ├── handling-errors.jmd │ │ ├── modules.jmd │ │ ├── no-server-uploads.jmd │ │ ├── paste-to-upload.jmd │ │ ├── pause.jmd │ │ ├── progress-bars.jmd │ │ ├── request-parameters.jmd │ │ ├── resume.jmd │ │ ├── retry.jmd │ │ ├── s3.jmd │ │ ├── scaling.jmd │ │ ├── session.jmd │ │ ├── statistics-and-status-updates.jmd │ │ ├── styling.jmd │ │ ├── thumbnails.jmd │ │ ├── upload-files.jmd │ │ ├── upload-from-mobile-camera.jmd │ │ └── validation.jmd │ ├── index.jmd │ ├── integrating/ │ │ └── jquery.jmd │ ├── modes/ │ │ ├── core.jmd │ │ └── ui.jmd │ ├── quickstart/ │ │ ├── 01-getting-started.jmd │ │ ├── 02-setting_options-azure.jmd │ │ ├── 02-setting_options-s3.jmd │ │ ├── 02-setting_options.jmd │ │ ├── 03-setting_up_server-azure.jmd │ │ ├── 03-setting_up_server-s3.jmd │ │ └── 03-setting_up_server.jmd │ ├── upgrading-to-4.jmd │ └── upgrading-to-5.jmd ├── package.json └── test/ ├── dev/ │ ├── devenv.js │ ├── handlers/ │ │ ├── composer.json │ │ └── php.ini │ ├── index.html │ └── styles.css ├── static/ │ ├── local/ │ │ ├── blob-maker.js │ │ ├── client.js │ │ ├── formdata.js │ │ ├── helpme.js │ │ └── karma-runner.js │ └── third-party/ │ ├── assert/ │ │ └── assert.js │ ├── jquery/ │ │ └── jquery.js │ ├── jquery.simulate/ │ │ └── jquery.simulate.js │ ├── json2/ │ │ ├── README │ │ ├── cycle.js │ │ ├── json.js │ │ ├── json2.js │ │ ├── json_parse.js │ │ └── json_parse_state.js │ ├── mocha/ │ │ ├── css/ │ │ │ └── mocha.css │ │ └── js/ │ │ └── mocha.js │ ├── purl/ │ │ └── purl.js │ ├── q/ │ │ └── q-1.0.1.js │ └── sinon/ │ ├── event.js │ ├── fake_xml_http_request.js │ └── sinon.js └── unit/ ├── azure/ │ ├── chunked-uploads.js │ ├── delete-files.js │ └── simple-file-uploads.js ├── basic.js ├── button.js ├── chunked-uploads.js ├── concurrent-chunks.js ├── delete-file.js ├── dnd.js ├── exif.js ├── file-upload-params-and-headers.js ├── form-support.js ├── identify.js ├── iframe.xss.response.js ├── image.js ├── on-all-complete.js ├── promise.js ├── resources/ │ ├── empty.txt │ ├── sample.tif │ └── simpletext.txt ├── s3/ │ ├── cdn/ │ │ ├── generic-chunked.js │ │ └── generic-non-chunked.js │ ├── chunked-uploads.js │ ├── serverless-uploads.js │ ├── simple-file-uploads.js │ └── util.js ├── scaling.js ├── session.js ├── set-status.js ├── simple-file-uploads.js ├── submit-validate-cancel.js ├── templating.js ├── total-progress.js ├── ui.handler.click.filebuttons.js ├── ui.handler.click.filename.js ├── upload-data.js ├── uploader.basic.api.js ├── util.js ├── validation.image.js └── workarounds.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true # Matches multiple files with brace expansion notation # Set default charset [{*.js}] charset = utf-8 indent_style = space indent_size = 4 # Tab indentation (no size specified) [Makefile] indent_style = tab # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_style = space indent_size = 2 ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ## Type of issue - [ ] Bug report - [ ] Feature request ## Uploader type - [ ] Traditional - [ ] S3 - [ ] Azure ### Note: Support requests cannot be accepted due to lack of time.
Bug Report #### Fine Uploader version {example: 5.5.1} #### Browsers where the bug is reproducible {example: "Firefox" and "IE11"} #### Operating systems where the bug is reproducible {example: "iOS 9.1.0" and "Windows 8.1"} #### Exact steps required to reproduce the issue For example: 1. Select 3 files 2. Pause the 2nd file before it completes, but after it has started. 3. Attempt to resume the paused file. #### All relevant Fine Uploader-related code that you have written {simply copy and paste the JS used to control Fine Uploader browsers-ide} {also include your template HTML if related to a UI issue} #### Your Fine Uploader template markup (if using Fine Uploader UI and the issue is UI-related) {simply copy and paste your template markup} #### Detailed explanation of the problem {describe the bug here}
Feature Request #### Feature request details {why is this feature important, not just for you, but for many others?}
================================================ FILE: .github/PULL_REQUEST_TEMPLATE ================================================ ## Brief description of the changes {also describe what problem(s) these changes solve & reference any related issues/PRs} ## What browsers and operating systems have you tested these changes on? {example: Safari on iOS 9.1.0 and IE11 on Windows 8.1} ## Have you written unit tests? If not, explain why. {unit tests should accompany almost all PRs, unless the change is to documentation} ================================================ FILE: .gitignore ================================================ .* !.editorconfig *.ipr *~ .*.sw[a-z] *.iml *.iws !.github !.gitignore !.jshintrc !.jshintignore Thumbs.db _build/ _dist/ *.zip release/* master !.travis.yml hardcopy* selenium.log* root-server.PID test-resources-server.PID fine-uploader/ test/upload/* test/uploadsTemp/ test/coverage/* test/vendor/* test/uploads/* test/temp* test/_temp* test/_vendor* node_modules/ bin/ src npm-debug.log Vagrantfile test/dev/handlers/s3/composer.lock test/dev/handlers/traditional/files test/dev/handlers/traditional/chunks s3keys.php s3keys.sh test/dev/handlers/vendor/* test/dev/handlers/composer.lock test/dev/handlers/composer.phar ================================================ FILE: .jshintignore ================================================ client/js/third-party/* ================================================ FILE: .jshintrc ================================================ { // -------------------------------------------------------------------- // JSHint Configuration, Strict Edition // -------------------------------------------------------------------- // // This is a options template for [JSHint][1], using [JSHint example][2] // and [Ory Band's example][3] as basis and setting config values to // be most strict: // // * set all enforcing options to true // * set all relaxing options to false // * set all environment options to false, except the browser value // * set all JSLint legacy options to false // // [1]: http://www.jshint.com/ // [2]: https://github.com/jshint/node-jshint/blob/master/example/config.json // [3]: https://github.com/oryband/dotfiles/blob/master/jshintrc // // @author http://michael.haschke.biz/ // @license http://unlicense.org/ // == Enforcing Options =============================================== // // These options tell JSHint to be more strict towards your code. Use // them if you want to allow only a safe subset of JavaScript, very // useful when your codebase is shared with a big number of developers // with different skill levels. "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). "curly" : true, // Require {} for every new block or scope. "eqeqeq" : true, // Require triple equals i.e. `===`. "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` "latedef" : false, // Prohibit variable use before definition. "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. "noempty" : true, // Prohibit use of empty blocks. "nonew" : true, // Prohibit use of constructors for side-effects. "plusplus" : false, // Prohibit use of `++` & `--`. "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. "undef" : true, // Require all non-global variables be declared before they are used. "unused" : false, // Prohibit the use of defined, yet unused variables. "strict" : true, // Require `use strict` pragma in every file. "trailing" : true, // Prohibit trailing whitespaces. // == Relaxing Options ================================================ // // These options allow you to suppress certain types of warnings. Use // them only if you are absolutely positive that you know what you are // doing. "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. "debug" : false, // Allow debugger statements e.g. browser breakpoints. "eqnull" : true, // Tolerate use of `== null`. "esnext" : false, // Allow ES.next specific features such as `const` and `let`. "evil" : false, // Tolerate use of `eval`. "expr" : true, // Tolerate `ExpressionStatement` as Programs. "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). "iterator" : false, // Allow usage of __iterator__ property. "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. "laxcomma" : false, // Suppress warnings about comma-first coding style. "loopfunc" : false, // Allow functions to be defined within loops. "multistr" : false, // Tolerate multi-line strings. "onecase" : false, // Tolerate switches with just one case. "proto" : false, // Tolerate __proto__ property. This property is deprecated. "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. "scripturl" : false, // Tolerate script-targeted URLs. "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. // == Environments ==================================================== // // These options pre-define global variables that are exposed by // popular JavaScript libraries and runtime environments—such as // browser or node.js. "browser" : true, // Standard browser globals e.g. `window`, `document`. "couch" : false, // Enable globals exposed by CouchDB. "devel" : false, // Allow development statements e.g. `console.log();`. "dojo" : false, // Enable globals exposed by Dojo Toolkit. "jquery" : true, // Enable globals exposed by jQuery JavaScript library. "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. "node" : false, // Enable globals available when code is running inside of the NodeJS runtime environment. "nonstandard" : true, // Define non-standard but widely adopted globals such as escape and unescape. "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. // == JSLint Legacy =================================================== // // These options are legacy from JSLint. Aside from bug fixes they will // not be improved in any way and might be removed at any point. "nomen" : false, // Prohibit use of initial or trailing underbars in names. "onevar" : false, // Allow only one `var` statement per function. "passfail" : false, // Stop on first error. "white" : false, // Check against strict whitespace and indentation rules. // == Undocumented Options ============================================ // // While I've found these options in [example1][2] and [example2][3] // they are not described in the [JSHint Options documentation][4]. // // [4]: http://www.jshint.com/options/ "maxerr" : 100, // Maximum errors before stopping. "predef" : ["qq"], "quotmark" : "double", // Enforces consistencey of quotation marks. //"maxlen" : "80", // Enfore a maximum line length "indent" : 4 // Specify indentation spacing } ================================================ FILE: .travis.yml ================================================ addons: firefox: latest sudo: false language: python python: - 2.7 env: global: - DISPLAY=:99.0 - DOCS_GH_REF: github.com/FineUploader/docs.fineuploader.com # fineuploader-docs-bot access token has been moved to Travis-CI settings in the UI due to https://github.com/travis-ci/travis-ci/issues/7806 install: - . $HOME/.nvm/nvm.sh - nvm install 5.0.0 - nvm use 5.0.0 - npm install before_script: - sh -e /etc/init.d/xvfb start script: - npm test - if [ $TRAVIS_TEST_RESULT -eq 0 ]; then make docs-travis; fi ================================================ FILE: ATTRIBUTION.txt ================================================ Third-party credits (client/js/third-party/) MegaPixImage module Licensed under MIT (https://github.com/stomita/ios-imagefile-megapixel/blob/master/LICENSE) https://github.com/stomita/ios-imagefile-megapixel Copyright (c) 2012 Shinichi Tomita CryptoJS Licensed under MIT (https://code.google.com/p/crypto-js/wiki/License) https://code.google.com/p/crypto-js/ Copyright (c) 2009-2013 Jeff Mott ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2010-2012, Andrew Valums Copyright (c) 2012-2013, Andrew Valums and Raymond S. Nicholus, III Copyright (c) 2013-present, Widen Enterprises, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: clean _build publish start-test-resources-server test-resources-server.PID start-root-server root-server.PID version=$(shell node -pe "require('./package.json').version") dist-out-dir = _dist pub-dir = $(dist-out-dir)/$(version) # properly get npm-bin in cygwin (Eg. CYGWIN_NT-10.0) platform = $(shell uname -s) ifeq ($(findstring _NT,$(platform)),_NT) npm-bin = $(shell cygpath -u $(shell npm bin)) else npm-bin = $(shell npm bin) endif build-out-dir = _build src-dir = client js-src-dir = $(src-dir)/js js-3rdparty-src-dir = $(js-src-dir)/third-party test-dir = test unit-test-dir = $(test-dir)/unit export-file = $(js-src-dir)/export.js preamble = "// Fine Uploader $(version) - MIT licensed. http://fineuploader.com" cryptojs-files = \ $(js-3rdparty-src-dir)/crypto-js/core.js \ $(js-3rdparty-src-dir)/crypto-js/enc-base64.js \ $(js-3rdparty-src-dir)/crypto-js/hmac.js \ $(js-3rdparty-src-dir)/crypto-js/sha1.js \ $(js-3rdparty-src-dir)/crypto-js/sha256.js \ $(js-3rdparty-src-dir)/crypto-js/lib-typedarrays.js jquery-files = \ $(js-src-dir)/jquery-plugin.js \ $(js-src-dir)/jquery-dnd.js dnd-files-only = \ $(js-src-dir)/dnd.js dnd-files = \ $(js-src-dir)/util.js \ $(export-file) \ $(js-src-dir)/version.js \ $(js-src-dir)/features.js \ $(js-src-dir)/promise.js \ $(js-src-dir)/dnd.js core-files = \ $(js-src-dir)/util.js \ $(export-file) \ $(js-src-dir)/error/error.js \ $(js-src-dir)/version.js \ $(js-src-dir)/features.js \ $(js-src-dir)/promise.js \ $(js-src-dir)/blob-proxy.js \ $(js-src-dir)/button.js \ $(js-src-dir)/upload-data.js \ $(js-src-dir)/uploader.basic.api.js \ $(js-src-dir)/uploader.basic.js \ $(js-src-dir)/ajax.requester.js \ $(js-src-dir)/upload-handler/upload.handler.js \ $(js-src-dir)/upload-handler/upload.handler.controller.js \ $(js-src-dir)/window.receive.message.js \ $(js-src-dir)/upload-handler/form.upload.handler.js \ $(js-src-dir)/upload-handler/xhr.upload.handler.js \ $(js-src-dir)/deletefile.ajax.requester.js \ $(js-src-dir)/image-support/megapix-image.js \ $(js-src-dir)/image-support/image.js \ $(js-src-dir)/image-support/exif.js \ $(js-src-dir)/identify.js \ $(js-src-dir)/image-support/validation.image.js \ $(js-src-dir)/session.js \ $(js-src-dir)/session.ajax.requester.js \ $(js-src-dir)/image-support/scaler.js \ $(js-src-dir)/third-party/ExifRestorer.js \ $(js-src-dir)/total-progress.js \ $(js-src-dir)/paste.js \ $(js-src-dir)/form-support.js \ ui-files = \ $(dnd-files-only) \ $(js-src-dir)/uploader.api.js \ $(js-src-dir)/uploader.js \ $(js-src-dir)/templating.js \ $(js-src-dir)/ui.handler.events.js \ $(js-src-dir)/ui.handler.click.filebuttons.js \ $(js-src-dir)/ui.handler.click.filename.js \ $(js-src-dir)/ui.handler.focusin.filenameinput.js \ $(js-src-dir)/ui.handler.focus.filenameinput.js \ $(js-src-dir)/ui.handler.edit.filename.js traditional-files-only = \ $(js-src-dir)/traditional/traditional.form.upload.handler.js \ $(js-src-dir)/traditional/traditional.xhr.upload.handler.js \ $(js-src-dir)/traditional/all-chunks-done.ajax.requester.js \ traditional-files = \ $(core-files) \ $(traditional-files-only) traditional-jquery-files = \ $(jquery-files) \ $(traditional-files) traditional-ui-files = \ $(core-files) \ $(traditional-files-only) \ $(ui-files) traditional-ui-jquery-files = \ $(jquery-files) \ $(traditional-ui-files) s3-files-only = \ $(cryptojs-files) \ $(js-src-dir)/s3/util.js \ $(js-src-dir)/non-traditional-common/uploader.basic.api.js \ $(js-src-dir)/s3/uploader.basic.js \ $(js-src-dir)/s3/request-signer.js \ $(js-src-dir)/uploadsuccess.ajax.requester.js \ $(js-src-dir)/s3/multipart.initiate.ajax.requester.js \ $(js-src-dir)/s3/multipart.complete.ajax.requester.js \ $(js-src-dir)/s3/multipart.abort.ajax.requester.js \ $(js-src-dir)/s3/s3.xhr.upload.handler.js \ $(js-src-dir)/s3/s3.form.upload.handler.js s3-files = \ $(core-files) \ $(s3-files-only) s3-ui-files-only = \ $(js-src-dir)/s3/uploader.js s3-ui-files = \ $(core-files) \ $(s3-files-only) \ $(ui-files) \ $(s3-ui-files-only) \ s3-ui-jquery-files = \ $(jquery-files) \ $(js-src-dir)/s3/jquery-plugin.js \ $(s3-ui-files) azure-files-only = \ $(js-src-dir)/azure/util.js \ $(js-src-dir)/non-traditional-common/uploader.basic.api.js \ $(js-src-dir)/azure/uploader.basic.js \ $(js-src-dir)/azure/azure.xhr.upload.handler.js \ $(js-src-dir)/azure/get-sas.js \ $(js-src-dir)/uploadsuccess.ajax.requester.js \ $(js-src-dir)/azure/rest/delete-blob.js \ $(js-src-dir)/azure/rest/put-blob.js \ $(js-src-dir)/azure/rest/put-block.js \ $(js-src-dir)/azure/rest/put-block-list.js azure-files = \ $(core-files) \ $(azure-files-only) azure-ui-files-only = \ $(js-src-dir)/azure/uploader.js azure-ui-files = \ $(core-files) \ $(azure-files-only) \ $(ui-files) \ $(azure-ui-files-only) azure-ui-jquery-files = \ $(jquery-files) \ $(js-src-dir)/azure/jquery-plugin.js \ $(azure-ui-files) all-core-files = \ $(core-files) \ $(traditional-files-only) \ $(s3-files-only) \ $(azure-files-only) all-core-jquery-files = \ $(jquery-files) \ $(all-core-files) all-files = \ $(core-files) \ $(traditional-files-only) \ $(ui-files) \ $(s3-files-only) \ $(s3-ui-files-only) \ $(azure-files-only) \ $(azure-ui-files-only) all-jquery-files = \ $(jquery-files) \ $(all-files) clean: rm -rf $(build-out-dir) rm -rf $(dist-out-dir) lint: $(npm-bin)/jscs $(js-src-dir)/* $(npm-bin)/jshint $(js-src-dir)/* $(unit-test-dir)/* $(test-dir)/static/local/* _build: mkdir -p $@ cp -pR $(src-dir)/placeholders $@ cp -pR $(src-dir)/html/templates $@ cp LICENSE $@ cp $(src-dir)/*.css $@ cp $(src-dir)/*.gif $@ $(npm-bin)/cleancss --source-map $@/fine-uploader.css -o $@/fine-uploader.min.css $(npm-bin)/cleancss --source-map $@/fine-uploader-gallery.css -o $@/fine-uploader-gallery.min.css $(npm-bin)/cleancss --source-map $@/fine-uploader-new.css -o $@/fine-uploader-new.min.css uglify = $(npm-bin)/uglifyjs -b --preamble $(preamble) -e window:global -p relative --source-map-include-sources uglify-min = $(npm-bin)/uglifyjs -c -m --preamble $(preamble) -e window:global -p relative --source-map-include-sources build-dnd-standalone: _build $(uglify) $(dnd-files) -o $(build-out-dir)/dnd.js --source-map $(build-out-dir)/dnd.js.map build-dnd-standalone-min: _build $(uglify-min) $(dnd-files) -o $(build-out-dir)/dnd.min.js --source-map $(build-out-dir)/dnd.min.js.map build-core-traditional: _build $(uglify) $(traditional-files) -o $(build-out-dir)/fine-uploader.core.js --source-map $(build-out-dir)/fine-uploader.core.js.map build-core-traditional-min: _build $(uglify-min) $(traditional-files) -o $(build-out-dir)/fine-uploader.core.min.js --source-map $(build-out-dir)/fine-uploader.core.min.js.map build-ui-traditional: _build $(uglify) $(traditional-ui-files) -o $(build-out-dir)/fine-uploader.js --source-map $(build-out-dir)/fine-uploader.js.map build-ui-traditional-min: _build $(uglify-min) $(traditional-ui-files) -o $(build-out-dir)/fine-uploader.min.js --source-map $(build-out-dir)/fine-uploader.min.js.map build-ui-traditional-jquery: _build $(uglify) $(traditional-ui-jquery-files) -o $(build-out-dir)/jquery.fine-uploader.js --source-map $(build-out-dir)/jquery.fine-uploader.js.map build-ui-traditional-jquery-min: _build $(uglify-min) $(traditional-ui-jquery-files) -o $(build-out-dir)/jquery.fine-uploader.min.js --source-map $(build-out-dir)/jquery.fine-uploader.min.js.map build-core-s3: _build $(uglify) $(s3-files) -o $(build-out-dir)/s3.fine-uploader.core.js --source-map $(build-out-dir)/s3.fine-uploader.core.js.map build-core-s3-min: _build $(uglify-min) $(s3-files) -o $(build-out-dir)/s3.fine-uploader.core.min.js --source-map $(build-out-dir)/s3.fine-uploader.core.min.js.map build-ui-s3: _build $(uglify) $(s3-ui-files) -o $(build-out-dir)/s3.fine-uploader.js --source-map $(build-out-dir)/s3.fine-uploader.js.map build-ui-s3-min: _build $(uglify-min) $(s3-ui-jquery-files) -o $(build-out-dir)/s3.jquery.fine-uploader.min.js --source-map $(build-out-dir)/s3.jquery.fine-uploader.min.js.map build-ui-s3-jquery: _build $(uglify) $(s3-ui-jquery-files) -o $(build-out-dir)/s3.jquery.fine-uploader.js --source-map $(build-out-dir)/s3.jquery.fine-uploader.js.map build-ui-s3-jquery-min: _build $(uglify-min) $(s3-ui-files) -o $(build-out-dir)/s3.fine-uploader.min.js -e window:global --source-map $(build-out-dir)/s3.fine-uploader.min.js.map build-core-azure: _build $(uglify) $(azure-files) -o $(build-out-dir)/azure.fine-uploader.core.js --source-map $(build-out-dir)/azure.fine-uploader.core.js.map build-core-azure-min: _build $(uglify-min) $(azure-files) -o $(build-out-dir)/azure.fine-uploader.core.min.js -e window:global --source-map $(build-out-dir)/azure.fine-uploader.core.min.js.map build-ui-azure: _build $(uglify) $(azure-ui-files) -o $(build-out-dir)/azure.fine-uploader.js --source-map $(build-out-dir)/azure.fine-uploader.js.map build-ui-azure-min: _build $(uglify-min) $(azure-ui-files) -o $(build-out-dir)/azure.fine-uploader.min.js -e window:global --source-map $(build-out-dir)/azure.fine-uploader.min.js.map build-ui-azure-jquery: _build $(uglify) $(azure-ui-jquery-files) -o $(build-out-dir)/azure.jquery.fine-uploader.js --source-map $(build-out-dir)/azure.jquery.fine-uploader.js.map build-ui-azure-jquery-min: _build $(uglify-min) $(azure-ui-jquery-files) -o $(build-out-dir)/azure.jquery.fine-uploader.min.js -e window:global --source-map $(build-out-dir)/azure.jquery.fine-uploader.min.js.map build-all-core: _build $(uglify) $(all-core-files) -o $(build-out-dir)/all.fine-uploader.core.js --source-map $(build-out-dir)/all.fine-uploader.core.js.map build-all-core-min: _build $(uglify-min) $(all-core-files) -o $(build-out-dir)/all.fine-uploader.core.min.js -e window:global --source-map $(build-out-dir)/all.fine-uploader.core.min.js.map build-all-ui: _build $(uglify) $(all-files) -o $(build-out-dir)/all.fine-uploader.js --source-map $(build-out-dir)/all.fine-uploader.js.map build-all-ui-min: _build $(uglify-min) $(all-files) -o $(build-out-dir)/all.fine-uploader.min.js --source-map $(build-out-dir)/all.fine-uploader.min.js.map build: \ build-dnd-standalone \ build-dnd-standalone-min \ build-core-traditional \ build-core-traditional-min \ build-ui-traditional \ build-ui-traditional-min \ build-ui-traditional-jquery \ build-ui-traditional-jquery-min \ build-core-s3 \ build-core-s3-min \ build-ui-s3 \ build-ui-s3-min \ build-ui-s3-jquery \ build-ui-s3-jquery-min \ build-core-azure \ build-core-azure-min \ build-ui-azure \ build-ui-azure-min \ build-ui-azure-jquery \ build-ui-azure-jquery-min \ build-all-core \ build-all-core-min \ build-all-ui \ build-all-ui-min start-test-resources-server: test-resources-server.PID start-root-server: root-server.PID test-resources-server.PID: $(npm-bin)/static test/unit/resources -H '{"Access-Control-Allow-Origin": "*"}' -p 4000 & echo $$! > $@ root-server.PID: $(npm-bin)/static . -p 4001 & echo $$! > $@ stop-test-resources-server: test-resources-server.PID kill `cat $<` && rm $< stop-root-server: root-server.PID kill `cat $<` && rm $< test: $(MAKE) stop-test-resources-server $(MAKE) stop-root-server $(MAKE) start-test-resources-server $(MAKE) start-root-server $(MAKE) build-all-ui $(npm-bin)/karma start config/karma.conf.js $(MAKE) stop-test-resources-server $(MAKE) stop-root-server .PHONY: test zip: zip-traditional zip-s3 zip-azure zip-all common-zip-files = \ dnd*.* \ LICENSE \ placeholders/* \ templates/* \ *.gif \ fine-uploader*.css* zip-traditional: (cd $(build-out-dir) ; zip fine-uploader.zip $(common-zip-files) fine-uploader*.* jquery.fine-uploader*.*) zip-s3: (cd $(build-out-dir) ; zip s3.fine-uploader.zip $(common-zip-files) s3*.*) zip-azure: (cd $(build-out-dir) ; zip azure.fine-uploader.zip $(common-zip-files) azure*.*) zip-all: (cd $(build-out-dir) ; zip all.fine-uploader.zip $(common-zip-files) all*.*) setup-dist: mkdir -p $(pub-dir) cp LICENSE README.md package.json $(pub-dir) cp -pR $(src-dir)/commonJs/ $(pub-dir)/lib/ cp -pR $(src-dir)/typescript $(pub-dir)/ copy-build-to-dist: mkdir -p $(pub-dir)/$(PUB-SUBDIR) cp -pR $(build-out-dir)/placeholders $(build-out-dir)/templates $(pub-dir)/$(PUB-SUBDIR) cp $(build-out-dir)/*.gif $(pub-dir)/$(PUB-SUBDIR) ifneq (,$(findstring jquery,$(PUB-SUBDIR))) else cp $(build-out-dir)/$(PUB-SUBDIR).core.min* $(build-out-dir)/$(PUB-SUBDIR).core.js* $(pub-dir)/$(PUB-SUBDIR)/ endif cp $(build-out-dir)/$(PUB-SUBDIR).min* $(build-out-dir)/$(PUB-SUBDIR).js* $(pub-dir)/$(PUB-SUBDIR) cp $(build-out-dir)/fine-uploader*.css* $(pub-dir)/$(PUB-SUBDIR) copy-dnd: mkdir -p $(pub-dir)/dnd cp $(build-out-dir)/dnd*.* $(pub-dir)/dnd copy-traditional-dist: make copy-build-to-dist PUB-SUBDIR=fine-uploader cp $(js-src-dir)/iframe.xss.response.js $(pub-dir)/fine-uploader copy-traditional-jquery-dist: make copy-build-to-dist PUB-SUBDIR=jquery.fine-uploader cp $(js-src-dir)/iframe.xss.response.js $(pub-dir)/jquery.fine-uploader copy-s3-dist: make copy-build-to-dist PUB-SUBDIR=s3.fine-uploader copy-s3-jquery-dist: make copy-build-to-dist PUB-SUBDIR=s3.jquery.fine-uploader copy-azure-dist: make copy-build-to-dist PUB-SUBDIR=azure.fine-uploader copy-azure-jquery-dist: make copy-build-to-dist PUB-SUBDIR=azure.jquery.fine-uploader copy-all-dist: make copy-build-to-dist PUB-SUBDIR=all.fine-uploader docs: install-docfu git config --global user.email "fineuploader-docs-bot@raynicholus.com" git config --global user.name "fineuploader-docs-bot" docfu --$(type) "$(type-value)" "FineUploader/fine-uploader" "docfu-temp" git clone --depth 1 https://github.com/FineUploader/docs.fineuploader.com.git cp -pR docfu-temp/$(type) docs.fineuploader.com/ make maybe-update-root-docs (cd docs.fineuploader.com ; git add .) (cd docs.fineuploader.com ; git diff --cached --quiet || git commit -a -m "update docs for $(type) $(type-value)") @(cd docs.fineuploader.com ; git push https://$(DOCS_PUSH_ACCESS_TOKEN)@$(DOCS_GH_REF)) .PHONY: docs maybe-update-root-docs: ifndef TRAVIS_TAG ifeq ($(TRAVIS_BRANCH), master) cp -pR docs.fineuploader.com/branch/master/. docs.fineuploader.com/ endif endif .PHONY: maybe-update-root-docs docs-travis: ifneq ($(TRAVIS_PULL_REQUEST), false) @echo skipping docs build - not a non-PR or tag push else ifdef TRAVIS_TAG make docs type=tag type-value=$(TRAVIS_TAG) else make docs type=branch type-value=$(TRAVIS_BRANCH) endif .PHONY: docs-travis install-docfu: git clone --depth 1 -b 1.0.4 https://github.com/FineUploader/docfu (cd docfu ; python setup.py install) rm -rf docfu .PHONY: install-docfu tag-release: ifeq ($(simulate), true) @echo version is $(version) else git tag $(version) git push origin $(version) endif push-to-npm: ifeq ($(simulate), true) @echo not publishing - simulation mode else (cd $(pub-dir) ; npm publish) endif publish: \ clean \ build \ zip \ setup-dist \ copy-dnd \ copy-traditional-dist \ copy-traditional-jquery-dist \ copy-s3-dist \ copy-s3-jquery-dist \ copy-azure-dist \ copy-azure-jquery-dist \ copy-all-dist \ tag-release \ push-to-npm setup-dev: (cd test/dev/handlers; curl -sS https://getcomposer.org/installer | php; php composer.phar install) start-local-dev: (. test/dev/handlers/s3keys.sh; php -S 0.0.0.0:9090 -t . -c test/dev/handlers/php.ini) update-dev: (cd test/dev/handlers; php composer.phar update) rev-version: sed -i "" -e 's/$(version)/$(target)/g' client/js/version.js sed -i "" -e 's/$(version)/$(target)/g' package.json ================================================ FILE: README.md ================================================ **Fine Uploader is no longer maintained and the project has been effectively shut down. For more info, see https://github.com/FineUploader/fine-uploader/issues/2073.** [![Build Status](https://travis-ci.org/FineUploader/fine-uploader.svg?branch=master)](https://travis-ci.org/FineUploader/fine-uploader) [![npm](https://img.shields.io/npm/v/fine-uploader.svg)](https://www.npmjs.com/package/fine-uploader) [![CDNJS](https://img.shields.io/cdnjs/v/file-uploader.svg)](https://cdnjs.com/libraries/file-uploader) [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/fineuploader.svg?style=social&label=Follow%20%40FineUploader)](https://twitter.com/fineuploader) [**Documentation**](http://docs.fineuploader.com) | [**Examples**](http://fineuploader.com/demos) | [**Support**](../../issues) | [**Blog**](http://blog.fineuploader.com/) | [**Changelog**](../../releases) --- Fine Uploader is: - Cross-browser - Dependency-free - 100% JavaScript - 100% Free Open Source Software FineUploader is also simple to use. In the simplest case, you only need to include one JavaScript file. There are absolutely no other required external dependencies. For more information, please see the [**documentation**](http://docs.fineuploader.com). ## Contributing If you'd like to help and keep this project strong and relevant, you have several options. ### Help us pay the bills Fine Uploader is currently looking for a sponsor to pay the AWS bills (which have recently lapsed). These add up to about $40/month. Please open an issue if you are interesting in becoming a sponsor. We will happily list you as sponsor on the site and README. ### File a bug report If you see something that isn't quite right, whether it be in the code, or on the docs site, or even on FineUploader.com (which is hosted on GitHub), _please_ file a bug report. Be sure to make sure the [bug hasn't already been filed][issues] by someone else. If it has, feel free to upvote the issue and/or add your comments. ### Join the team Are you interested in working on a very popular JavaScript-based file upload library with countless users? If you're strong in JavaScript, HTML, and CSS, and have a desire to help push the FOSS movement forward, let us know! The project can always use more experts. ### Help spread the word Are you using Fine Uploader in your library or project? If so, let us know and we may add a link to your project or application _and_ your logo to FineUploader.com. If you care to write an article about Fine Uploader, we would be open to reading and publicizing it through our site, blog, or Twitter feed. ### Develop an integration library Are you using Fine Uploader inside of a larger framework (such as React, Angular2, Ember.js, etc)? If so, perhaps you've already written a library that wraps Fine Uploader and makes it simple to use Fine Uploader in this context. Let us know and it may make sense to either link to your library, or even move it into the FineUploader GitHub organization (with your approval, of course). We'd also love to see libraries that make it simple to pair Fine Uploader with other useful libraries, such as image editors and rich text editors. ### Contribute code The best way to contribute code is to open up a pull request that addresses one of the open [feature requests or bugs][issues]. In order to get started developing Fine Uploader, read this entire section to get the project up and running on your local development machine. This section describes how you can build and test Fine Uploader locally. You may use these instructions to build a copy for yourself, or to contribute changes back to the library. #### Setup You must have Node.js instaled locally (any version should be fine), _and_ you must have Unix-like environment to work with. Linux, FreeBSD/OS X, Cygwin, and Windows 10 bash all _should_ be acceptable environments. Please open up a new issue if you have trouble building. The build process is centered around a single Makefile, so GNU Make is required as well (though most if not all Unix-like OSes should already have this installed). Finally, you will need a git client. To pull down the project & build dependencies: 1. Download the project repository: `git clone https://github.com/FineUploader/fine-uploader.git`. 2. Install all project development dependencies: `npm install`. #### Generating build artifacts - To build all build artifacts for all endpoint types: `make build`. You can speed this process up a bit by using the parallel recipes feature of Make: `make build -j`. If you would like to build only a specific endpoint type, see the Makefile for the appropriate recipe. The build output will be created in the `_build` directory. - To build zip files for all endpoint types: `make zip`. To build a zip for only a specific endpoint type, see the Makefile for the appropriate recipe. The zip files will be included alongside the build output in the `_build` directory. - To rev the version number: `make rev-version target=NEW_VERSION`, where `NEW_VERSION` is the semver-compatible target version identifier. #### Running tests To build, run the tests & linter: `npm test` (you'll need Firefox installed locally). #### Commiting new code and changes - Follow the [Angular.js commit guidelines][angular-commit]. - Follow the [Git Flow][git-flow] branching strategy. [angular-commit]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit [git-flow]: http://nvie.com/posts/a-successful-git-branching-model/ [issues]: https://github.com/FineUploader/fine-uploader/issues ================================================ FILE: client/README.md ================================================ Do not add files from this directory into your project. Please visit the downloads page for zips that contain combined and version-stamped javascript and css files, along with all required resources. # Download To download a pre-packaged version of Fine Uploader visit: http://fineuploader.com/downloads.html # Build For instructions on building your own packaged version of Fine Uploader visit: http://docs.fineuploader.com/contributing.htm ================================================ FILE: client/commonJs/all.js ================================================ "use strict"; module.exports = require("../all.fine-uploader/all.fine-uploader"); ================================================ FILE: client/commonJs/azure.js ================================================ "use strict"; module.exports = require("../azure.fine-uploader/azure.fine-uploader"); ================================================ FILE: client/commonJs/core/all.js ================================================ "use strict"; module.exports = require("../../all.fine-uploader/all.fine-uploader.core"); ================================================ FILE: client/commonJs/core/azure.js ================================================ "use strict"; module.exports = require("../../azure.fine-uploader/azure.fine-uploader.core"); ================================================ FILE: client/commonJs/core/index.js ================================================ "use strict"; module.exports = require("../../fine-uploader/fine-uploader.core"); ================================================ FILE: client/commonJs/core/s3.js ================================================ "use strict"; module.exports = require("../../s3.fine-uploader/s3.fine-uploader.core"); ================================================ FILE: client/commonJs/core/traditional.js ================================================ "use strict"; module.exports = require("../../fine-uploader/fine-uploader.core"); ================================================ FILE: client/commonJs/dnd.js ================================================ "use strict"; module.exports = require("../dnd/dnd"); ================================================ FILE: client/commonJs/jquery/azure.js ================================================ "use strict"; module.exports = require("../../azure.jquery.fine-uploader/azure.jquery.fine-uploader"); ================================================ FILE: client/commonJs/jquery/s3.js ================================================ "use strict"; module.exports = require("../../s3.jquery.fine-uploader/s3.jquery.fine-uploader"); ================================================ FILE: client/commonJs/jquery/traditional.js ================================================ "use strict"; module.exports = require("../../jquery.fine-uploader/jquery.fine-uploader"); ================================================ FILE: client/commonJs/s3.js ================================================ "use strict"; module.exports = require("../s3.fine-uploader/s3.fine-uploader"); ================================================ FILE: client/commonJs/traditional.js ================================================ "use strict"; module.exports = require("../fine-uploader/fine-uploader"); ================================================ FILE: client/fine-uploader-gallery.css ================================================ /* --------------------------------------- /* Fine Uploader Gallery View Styles /* --------------------------------------- /* Buttons ------------------------------------------ */ .qq-gallery .qq-btn { float: right; border: none; padding: 0; margin: 0; box-shadow: none; } /* Upload Button ------------------------------------------ */ .qq-gallery .qq-upload-button { display: inline; width: 105px; padding: 7px 10px; float: left; text-align: center; background: #00ABC7; color: #FFFFFF; border-radius: 2px; border: 1px solid #37B7CC; box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, 1px 0 1px rgba(255, 255, 255, 0.07) inset, 0 1px 0 rgba(0, 0, 0, 0.36), 0 -2px 12px rgba(0, 0, 0, 0.08) inset } .qq-gallery .qq-upload-button-hover { background: #33B6CC; } .qq-gallery .qq-upload-button-focus { outline: 1px dotted #000000; } /* Drop Zone ------------------------------------------ */ .qq-gallery.qq-uploader { position: relative; min-height: 200px; max-height: 490px; overflow-y: hidden; width: inherit; border-radius: 6px; border: 1px dashed #CCCCCC; background-color: #FAFAFA; padding: 20px; } .qq-gallery.qq-uploader:before { content: attr(qq-drop-area-text) " "; position: absolute; font-size: 200%; left: 0; width: 100%; text-align: center; top: 45%; opacity: 0.25; filter: alpha(opacity=25); } .qq-gallery .qq-upload-drop-area, .qq-upload-extra-drop-area { position: absolute; top: 0; left: 0; width: 100%; height: 100%; min-height: 30px; z-index: 2; background: #F9F9F9; border-radius: 4px; text-align: center; } .qq-gallery .qq-upload-drop-area span { display: block; position: absolute; top: 50%; width: 100%; margin-top: -8px; font-size: 16px; } .qq-gallery .qq-upload-extra-drop-area { position: relative; margin-top: 50px; font-size: 16px; padding-top: 30px; height: 20px; min-height: 40px; } .qq-gallery .qq-upload-drop-area-active { background: #FDFDFD; border-radius: 4px; } .qq-gallery .qq-upload-list { margin: 0; padding: 10px 0 0; list-style: none; max-height: 450px; overflow-y: auto; clear: both; box-shadow: none; } /* Uploaded Elements ------------------------------------------ */ .qq-gallery .qq-upload-list li { display: inline-block; position: relative; max-width: 120px; margin: 0 25px 25px 0; padding: 0; line-height: 16px; font-size: 13px; color: #424242; background-color: #FFFFFF; border-radius: 2px; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22); vertical-align: top; /* to ensure consistent size of tiles - may need to change if qq-max-size attr on preview img changes */ height: 186px; } .qq-gallery .qq-upload-spinner, .qq-gallery .qq-upload-size, .qq-gallery .qq-upload-retry, .qq-gallery .qq-upload-failed-text, .qq-gallery .qq-upload-delete, .qq-gallery .qq-upload-pause, .qq-gallery .qq-upload-continue { display: inline; } .qq-gallery .qq-upload-retry:hover, .qq-gallery .qq-upload-delete:hover, .qq-gallery .qq-upload-pause:hover, .qq-gallery .qq-upload-continue:hover { background-color: transparent; } .qq-gallery .qq-upload-delete, .qq-gallery .qq-upload-pause, .qq-gallery .qq-upload-continue, .qq-gallery .qq-upload-cancel { cursor: pointer; } .qq-gallery .qq-upload-delete, .qq-gallery .qq-upload-pause, .qq-gallery .qq-upload-continue { border:none; background: none; color: #00A0BA; font-size: 12px; padding: 0; } /* to ensure consistent size of tiles - only display status text before auto-retry or after failure */ .qq-gallery .qq-upload-status-text { color: #333333; font-size: 12px; padding-left: 3px; padding-top: 2px; width: inherit; display: none; width: 108px; } .qq-gallery .qq-upload-fail .qq-upload-status-text { text-overflow: ellipsis; white-space: nowrap; overflow-x: hidden; display: block; } .qq-gallery .qq-upload-retrying .qq-upload-status-text { display: inline-block; } .qq-gallery .qq-upload-retrying .qq-progress-bar-container { display: none; } .qq-gallery .qq-upload-cancel { background-color: #525252; color: #F7F7F7; font-weight: bold; font-family: Arial, Helvetica, sans-serif; border-radius: 12px; border: none; height: 22px; width: 22px; padding: 4px; position: absolute; right: -5px; top: -6px; margin: 0; line-height: 17px; } .qq-gallery .qq-upload-cancel:hover { background-color: #525252; } .qq-gallery .qq-upload-retry { cursor: pointer; position: absolute; top: 30px; left: 50%; margin-left: -31px; box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, 1px 0 1px rgba(255, 255, 255, 0.07) inset, 0 4px 4px rgba(0, 0, 0, 0.5), 0 -2px 12px rgba(0, 0, 0, 0.08) inset; padding: 3px 4px; border: 1px solid #d2ddc7; border-radius: 2px; color: inherit; background-color: #EBF6E0; z-index: 1; } .qq-gallery .qq-upload-retry:hover { background-color: #f7ffec; } .qq-gallery .qq-file-info { padding: 10px 6px 4px; margin-top: -3px; border-radius: 0 0 2px 2px; text-align: left; overflow: hidden; } .qq-gallery .qq-file-info .qq-file-name { position: relative; } .qq-gallery .qq-upload-file { display: block; margin-right: 0; margin-bottom: 3px; width: auto; /* to ensure consistent size of tiles - constrain text to single line */ text-overflow: ellipsis; white-space: nowrap; overflow-x: hidden; } .qq-gallery .qq-upload-spinner { display: inline-block; background: url("loading.gif"); position: absolute; left: 50%; margin-left: -7px; top: 53px; width: 15px; height: 15px; vertical-align: text-bottom; } .qq-gallery .qq-drop-processing { display: block; } .qq-gallery .qq-drop-processing-spinner { display: inline-block; background: url("processing.gif"); width: 24px; height: 24px; vertical-align: text-bottom; } .qq-gallery .qq-upload-failed-text { display: none; font-style: italic; font-weight: bold; } .qq-gallery .qq-upload-failed-icon { display:none; width:15px; height:15px; vertical-align:text-bottom; } .qq-gallery .qq-upload-fail .qq-upload-failed-text { display: inline; } .qq-gallery .qq-upload-retrying .qq-upload-failed-text { display: inline; } .qq-gallery .qq-upload-list li.qq-upload-success { background-color: #F2F7ED; } .qq-gallery .qq-upload-list li.qq-upload-fail { background-color: #F5EDED; box-shadow: 0 0 1px 0 red; border: 0; } .qq-gallery .qq-progress-bar { display: block; background: #00abc7; width: 0%; height: 15px; border-radius: 6px; margin-bottom: 3px; } .qq-gallery .qq-total-progress-bar { height: 25px; border-radius: 9px; } .qq-gallery .qq-total-progress-bar-container { margin-left: 9px; display: inline; float: right; width: 500px; } .qq-gallery .qq-upload-size { float: left; font-size: 11px; color: #929292; margin-bottom: 3px; margin-right: 0; display: inline-block; } .qq-gallery INPUT.qq-edit-filename { position: absolute; opacity: 0; filter: alpha(opacity=0); z-index: -1; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; } .qq-gallery .qq-upload-file.qq-editable { cursor: pointer; margin-right: 20px; } .qq-gallery .qq-edit-filename-icon.qq-editable { display: inline-block; cursor: pointer; position: absolute; right: 0; top: 0; } .qq-gallery INPUT.qq-edit-filename.qq-editing { position: static; height: 28px; width: 90px; width: -moz-available; padding: 0 8px; margin-bottom: 3px; border: 1px solid #ccc; border-radius: 2px; font-size: 13px; opacity: 1; filter: alpha(opacity=100); -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; } .qq-gallery .qq-edit-filename-icon { display: none; background: url("edit.gif"); width: 15px; height: 15px; vertical-align: text-bottom; } .qq-gallery .qq-delete-icon { background: url("trash.gif"); width: 15px; height: 15px; vertical-align: sub; display: inline-block; } .qq-gallery .qq-retry-icon { background: url("retry.gif"); width: 15px; height: 15px; vertical-align: sub; display: inline-block; float: none; } .qq-gallery .qq-continue-icon { background: url("continue.gif"); width: 15px; height: 15px; vertical-align: sub; display: inline-block; } .qq-gallery .qq-pause-icon { background: url("pause.gif"); width: 15px; height: 15px; vertical-align: sub; display: inline-block; } .qq-gallery .qq-hide { display: none; } /* Thumbnail ------------------------------------------ */ .qq-gallery .qq-in-progress .qq-thumbnail-wrapper { /* makes the spinner on top of the thumbnail more visible */ opacity: 0.5; filter: alpha(opacity=50); } .qq-gallery .qq-thumbnail-wrapper { overflow: hidden; position: relative; /* to ensure consistent size of tiles - should match qq-max-size attribute value on qq-thumbnail-selector IMG element */ height: 120px; width: 120px; } .qq-gallery .qq-thumbnail-selector { border-radius: 2px 2px 0 0; bottom: 0; /* we will override this in the :root thumbnail selector (to help center the preview) for everything other than IE8 */ top: 0; /* center the thumb horizontally in the tile */ margin:auto; display: block; } /* hack to ensure we don't try to center preview in IE8, since -ms-filter doesn't mimic translateY as expected in all cases */ :root *> .qq-gallery .qq-thumbnail-selector { /* vertically center preview image on tile */ position: relative; top: 50%; transform: translateY(-50%); -moz-transform: translateY(-50%); -ms-transform: translateY(-50%); -webkit-transform: translateY(-50%); } /* element styles */ .qq-gallery.qq-uploader DIALOG { display: none; } .qq-gallery.qq-uploader DIALOG[open] { display: block; } .qq-gallery.qq-uploader DIALOG { display: none; } .qq-gallery.qq-uploader DIALOG[open] { display: block; } .qq-gallery.qq-uploader DIALOG .qq-dialog-buttons { text-align: center; padding-top: 10px; } .qq-gallery.qq-uploader DIALOG .qq-dialog-buttons BUTTON { margin-left: 5px; margin-right: 5px; } .qq-gallery.qq-uploader DIALOG .qq-dialog-message-selector { padding-bottom: 10px; } .qq-gallery .qq-uploader DIALOG::backdrop { background-color: rgba(0, 0, 0, 0.7); } ================================================ FILE: client/fine-uploader-new.css ================================================ /* --------------------------------------- /* Fine Uploader Styles /* --------------------------------------- /* Buttons ------------------------------------------ */ .qq-btn { box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, 1px 0 1px rgba(255, 255, 255, 0.07) inset, 0 1px 0 rgba(0, 0, 0, 0.36), 0 -2px 12px rgba(0, 0, 0, 0.08) inset; padding: 3px 4px; border: 1px solid #CCCCCC; border-radius: 2px; color: inherit; background-color: #FFFFFF; } .qq-upload-delete, .qq-upload-pause, .qq-upload-continue { display: inline; } .qq-upload-delete { background-color: #e65c47; color: #FAFAFA; border-color: #dc523d; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55); } .qq-upload-delete:hover { background-color: #f56b56; } .qq-upload-cancel { background-color: #F5D7D7; border-color: #e6c8c8; } .qq-upload-cancel:hover { background-color: #ffe1e1; } .qq-upload-retry { background-color: #EBF6E0; border-color: #d2ddc7; } .qq-upload-retry:hover { background-color: #f7ffec; } .qq-upload-pause, .qq-upload-continue { background-color: #00ABC7; color: #FAFAFA; border-color: #2dadc2; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55); } .qq-upload-pause:hover, .qq-upload-continue:hover { background-color: #0fbad6; } /* Upload Button ------------------------------------------ */ .qq-upload-button { display: inline; width: 105px; margin-bottom: 10px; padding: 7px 10px; text-align: center; float: left; background: #00ABC7; color: #FFFFFF; border-radius: 2px; border: 1px solid #2dadc2; box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, 1px 0 1px rgba(255, 255, 255, 0.07) inset, 0 1px 0 rgba(0, 0, 0, 0.36), 0 -2px 12px rgba(0, 0, 0, 0.08) inset; } .qq-upload-button-hover { background: #33B6CC; } .qq-upload-button-focus { outline: 1px dotted #000000; } /* Drop Zone ------------------------------------------ */ .qq-uploader { position: relative; min-height: 200px; max-height: 490px; overflow-y: hidden; width: inherit; border-radius: 6px; background-color: #FDFDFD; border: 1px dashed #CCCCCC; padding: 20px; } .qq-uploader:before { content: attr(qq-drop-area-text) " "; position: absolute; font-size: 200%; left: 0; width: 100%; text-align: center; top: 45%; opacity: 0.25; } .qq-upload-drop-area, .qq-upload-extra-drop-area { position: absolute; top: 0; left: 0; width: 100%; height: 100%; min-height: 30px; z-index: 2; background: #F9F9F9; border-radius: 4px; border: 1px dashed #CCCCCC; text-align: center; } .qq-upload-drop-area span { display: block; position: absolute; top: 50%; width: 100%; margin-top: -8px; font-size: 16px; } .qq-upload-extra-drop-area { position: relative; margin-top: 50px; font-size: 16px; padding-top: 30px; height: 20px; min-height: 40px; } .qq-upload-drop-area-active { background: #FDFDFD; border-radius: 4px; border: 1px dashed #CCCCCC; } .qq-upload-list { margin: 0; padding: 0; list-style: none; max-height: 450px; overflow-y: auto; box-shadow: 0px 1px 0px rgba(15, 15, 50, 0.14); clear: both; } /* Uploaded Elements ------------------------------------------ */ .qq-upload-list li { margin: 0; padding: 9px; line-height: 15px; font-size: 16px; color: #424242; background-color: #F6F6F6; border-top: 1px solid #FFFFFF; border-bottom: 1px solid #DDDDDD; } .qq-upload-list li:first-child { border-top: none; } .qq-upload-list li:last-child { border-bottom: none; } .qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-failed-text, .qq-upload-delete, .qq-upload-pause, .qq-upload-continue { margin-right: 12px; display: inline; } .qq-upload-file { vertical-align: middle; display: inline-block; width: 300px; text-overflow: ellipsis; white-space: nowrap; overflow-x: hidden; height: 18px; } .qq-upload-spinner { display: inline-block; background: url("loading.gif"); width: 15px; height: 15px; vertical-align: text-bottom; } .qq-drop-processing { display: block; } .qq-drop-processing-spinner { display: inline-block; background: url("processing.gif"); width: 24px; height: 24px; vertical-align: text-bottom; } .qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-delete, .qq-upload-pause, .qq-upload-continue { font-size: 12px; font-weight: normal; cursor: pointer; vertical-align: middle; } .qq-upload-status-text { font-size: 14px; font-weight: bold; display: block; } .qq-upload-failed-text { display: none; font-style: italic; font-weight: bold; } .qq-upload-failed-icon { display:none; width:15px; height:15px; vertical-align:text-bottom; } .qq-upload-fail .qq-upload-failed-text { display: inline; } .qq-upload-retrying .qq-upload-failed-text { display: inline; } .qq-upload-list li.qq-upload-success { background-color: #EBF6E0; color: #424242; border-bottom: 1px solid #D3DED1; border-top: 1px solid #F7FFF5; } .qq-upload-list li.qq-upload-fail { background-color: #F5D7D7; color: #424242; border-bottom: 1px solid #DECACA; border-top: 1px solid #FCE6E6; } .qq-progress-bar { display: block; display: block; background: #00abc7; width: 0%; height: 15px; border-radius: 6px; margin-bottom: 3px; } .qq-total-progress-bar { height: 25px; border-radius: 9px; } .qq-total-progress-bar-container { margin-left: 9px; display: inline; float: right; width: 500px; } INPUT.qq-edit-filename { position: absolute; opacity: 0; filter: alpha(opacity=0); z-index: -1; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; } .qq-upload-file.qq-editable { cursor: pointer; margin-right: 4px; } .qq-edit-filename-icon.qq-editable { display: inline-block; cursor: pointer; } INPUT.qq-edit-filename.qq-editing { position: static; height: 28px; padding: 0 8px; margin-right: 10px; margin-bottom: -5px; border: 1px solid #ccc; border-radius: 2px; font-size: 16px; opacity: 1; filter: alpha(opacity=100); -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; } .qq-edit-filename-icon { display: none; background: url("edit.gif"); width: 15px; height: 15px; vertical-align: text-bottom; margin-right: 16px; } .qq-hide { display: none; } /* Thumbnail ------------------------------------------ */ .qq-thumbnail-selector { vertical-align: middle; margin-right: 12px; } /* element styles */ .qq-uploader DIALOG { display: none; } .qq-uploader DIALOG[open] { display: block; } .qq-uploader DIALOG { display: none; } .qq-uploader DIALOG[open] { display: block; } .qq-uploader DIALOG .qq-dialog-buttons { text-align: center; padding-top: 10px; } .qq-uploader DIALOG .qq-dialog-buttons BUTTON { margin-left: 5px; margin-right: 5px; } .qq-uploader DIALOG .qq-dialog-message-selector { padding-bottom: 10px; } .qq-uploader DIALOG::backdrop { background-color: rgba(0, 0, 0, 0.7); } ================================================ FILE: client/fine-uploader.css ================================================ .qq-uploader { position: relative; width: 100%; } .qq-upload-button { display: block; width: 105px; padding: 7px 0; text-align: center; background: #880000; border-bottom: 1px solid #DDD; color: #FFF; } .qq-upload-button-hover { background: #CC0000; } .qq-upload-button-focus { outline: 1px dotted #000000; } .qq-upload-drop-area, .qq-upload-extra-drop-area { position: absolute; top: 0; left: 0; width: 100%; height: 100%; min-height: 30px; z-index: 2; background: #FF9797; text-align: center; } .qq-upload-drop-area span { display: block; position: absolute; top: 50%; width: 100%; margin-top: -8px; font-size: 16px; } .qq-upload-extra-drop-area { position: relative; margin-top: 50px; font-size: 16px; padding-top: 30px; height: 20px; min-height: 40px; } .qq-upload-drop-area-active { background: #FF7171; } .qq-upload-list { margin: 0; padding: 0; list-style: none; } .qq-upload-list li { margin: 0; padding: 9px; line-height: 15px; font-size: 16px; background-color: #FFF0BD; } .qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-failed-text, .qq-upload-delete, .qq-upload-pause, .qq-upload-continue { margin-right: 12px; display: inline; } .qq-upload-file { } .qq-upload-spinner { display: inline-block; background: url("loading.gif"); width: 15px; height: 15px; vertical-align: text-bottom; } .qq-drop-processing { display: block; } .qq-drop-processing-spinner { display: inline-block; background: url("processing.gif"); width: 24px; height: 24px; vertical-align: text-bottom; } .qq-upload-delete, .qq-upload-pause, .qq-upload-continue { display: inline; } .qq-upload-retry, .qq-upload-delete, .qq-upload-cancel, .qq-upload-pause, .qq-upload-continue { color: #000000; } .qq-upload-size, .qq-upload-cancel, .qq-upload-retry, .qq-upload-delete, .qq-upload-pause, .qq-upload-continue { font-size: 12px; font-weight: normal; } .qq-upload-failed-text { display: none; font-style: italic; font-weight: bold; } .qq-upload-failed-icon { display:none; width:15px; height:15px; vertical-align:text-bottom; } .qq-upload-fail .qq-upload-failed-text { display: inline; } .qq-upload-retrying .qq-upload-failed-text { display: inline; color: #D60000; } .qq-upload-list li.qq-upload-success { background-color: #5DA30C; color: #FFFFFF; } .qq-upload-list li.qq-upload-fail { background-color: #D60000; color: #FFFFFF; } .qq-progress-bar { display: block; background: -moz-linear-gradient(top, rgba(30,87,153,1) 0%, rgba(41,137,216,1) 50%, rgba(32,124,202,1) 51%, rgba(125,185,232,1) 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(30,87,153,1)), color-stop(50%,rgba(41,137,216,1)), color-stop(51%,rgba(32,124,202,1)), color-stop(100%,rgba(125,185,232,1))); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* IE10+ */ background: linear-gradient(to bottom, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* W3C */ width: 0%; height: 15px; border-radius: 6px; margin-bottom: 3px; } .qq-total-progress-bar { height: 25px; border-radius: 9px; } .qq-total-progress-bar-container { margin: 9px; } INPUT.qq-edit-filename { position: absolute; opacity: 0; filter: alpha(opacity=0); z-index: -1; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; } .qq-upload-file.qq-editable { cursor: pointer; } .qq-edit-filename-icon.qq-editable { display: inline-block; cursor: pointer; } INPUT.qq-edit-filename.qq-editing { position: static; margin-top: -5px; margin-right: 10px; margin-bottom: -5px; opacity: 1; filter: alpha(opacity=100); -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; } .qq-edit-filename-icon { display: none; background: url("edit.gif"); width: 15px; height: 15px; vertical-align: text-bottom; margin-right: 5px; } .qq-hide { display: none; } /* element styles */ .qq-uploader DIALOG { display: none; } .qq-uploader DIALOG[open] { display: block; } .qq-uploader DIALOG { display: none; } .qq-uploader DIALOG[open] { display: block; } .qq-uploader DIALOG .qq-dialog-buttons { text-align: center; padding-top: 10px; } .qq-uploader DIALOG .qq-dialog-buttons BUTTON { margin-left: 5px; margin-right: 5px; } .qq-uploader DIALOG .qq-dialog-message-selector { padding-bottom: 10px; } .qq-uploader DIALOG::backdrop { background-color: rgba(0, 0, 0, 0.7); } ================================================ FILE: client/html/templates/default.html ================================================ ================================================ FILE: client/html/templates/gallery.html ================================================ ================================================ FILE: client/html/templates/simple-thumbnails.html ================================================ ================================================ FILE: client/js/ajax.requester.js ================================================ /*globals qq, XDomainRequest*/ /** Generic class for sending non-upload ajax requests and handling the associated responses **/ qq.AjaxRequester = function(o) { "use strict"; var log, shouldParamsBeInQueryString, queue = [], requestData = {}, options = { acceptHeader: null, validMethods: ["PATCH", "POST", "PUT"], method: "POST", contentType: "application/x-www-form-urlencoded", maxConnections: 3, customHeaders: {}, endpointStore: {}, paramsStore: {}, mandatedParams: {}, allowXRequestedWithAndCacheControl: true, successfulResponseCodes: { DELETE: [200, 202, 204], PATCH: [200, 201, 202, 203, 204], POST: [200, 201, 202, 203, 204], PUT: [200, 201, 202, 203, 204], GET: [200] }, cors: { expected: false, sendCredentials: false }, log: function(str, level) {}, onSend: function(id) {}, onComplete: function(id, xhrOrXdr, isError) {}, onProgress: null }; qq.extend(options, o); log = options.log; if (qq.indexOf(options.validMethods, options.method) < 0) { throw new Error("'" + options.method + "' is not a supported method for this type of request!"); } // [Simple methods](http://www.w3.org/TR/cors/#simple-method) // are defined by the W3C in the CORS spec as a list of methods that, in part, // make a CORS request eligible to be exempt from preflighting. function isSimpleMethod() { return qq.indexOf(["GET", "POST", "HEAD"], options.method) >= 0; } // [Simple headers](http://www.w3.org/TR/cors/#simple-header) // are defined by the W3C in the CORS spec as a list of headers that, in part, // make a CORS request eligible to be exempt from preflighting. function containsNonSimpleHeaders(headers) { var containsNonSimple = false; qq.each(containsNonSimple, function(idx, header) { if (qq.indexOf(["Accept", "Accept-Language", "Content-Language", "Content-Type"], header) < 0) { containsNonSimple = true; return false; } }); return containsNonSimple; } function isXdr(xhr) { //The `withCredentials` test is a commonly accepted way to determine if XHR supports CORS. return options.cors.expected && xhr.withCredentials === undefined; } // Returns either a new `XMLHttpRequest` or `XDomainRequest` instance. function getCorsAjaxTransport() { var xhrOrXdr; if (window.XMLHttpRequest || window.ActiveXObject) { xhrOrXdr = qq.createXhrInstance(); if (xhrOrXdr.withCredentials === undefined) { xhrOrXdr = new XDomainRequest(); // Workaround for XDR bug in IE9 - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment xhrOrXdr.onload = function() {}; xhrOrXdr.onerror = function() {}; xhrOrXdr.ontimeout = function() {}; xhrOrXdr.onprogress = function() {}; } } return xhrOrXdr; } // Returns either a new XHR/XDR instance, or an existing one for the associated `File` or `Blob`. function getXhrOrXdr(id, suppliedXhr) { var xhrOrXdr = requestData[id] && requestData[id].xhr; if (!xhrOrXdr) { if (suppliedXhr) { xhrOrXdr = suppliedXhr; } else { if (options.cors.expected) { xhrOrXdr = getCorsAjaxTransport(); } else { xhrOrXdr = qq.createXhrInstance(); } } requestData[id].xhr = xhrOrXdr; } return xhrOrXdr; } // Removes element from queue, sends next request function dequeue(id) { var i = qq.indexOf(queue, id), max = options.maxConnections, nextId; delete requestData[id]; queue.splice(i, 1); if (queue.length >= max && i < max) { nextId = queue[max - 1]; sendRequest(nextId); } } function onComplete(id, xdrError) { var xhr = getXhrOrXdr(id), method = options.method, isError = xdrError === true; dequeue(id); if (isError) { log(method + " request for " + id + " has failed", "error"); } else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) { isError = true; log(method + " request for " + id + " has failed - response code " + xhr.status, "error"); } options.onComplete(id, xhr, isError); } function getParams(id) { var onDemandParams = requestData[id].additionalParams, mandatedParams = options.mandatedParams, params; if (options.paramsStore.get) { params = options.paramsStore.get(id); } if (onDemandParams) { qq.each(onDemandParams, function(name, val) { params = params || {}; params[name] = val; }); } if (mandatedParams) { qq.each(mandatedParams, function(name, val) { params = params || {}; params[name] = val; }); } return params; } function sendRequest(id, optXhr) { var xhr = getXhrOrXdr(id, optXhr), method = options.method, params = getParams(id), payload = requestData[id].payload, url; options.onSend(id); url = createUrl(id, params, requestData[id].additionalQueryParams); // XDR and XHR status detection APIs differ a bit. if (isXdr(xhr)) { xhr.onload = getXdrLoadHandler(id); xhr.onerror = getXdrErrorHandler(id); } else { xhr.onreadystatechange = getXhrReadyStateChangeHandler(id); } registerForUploadProgress(id); // The last parameter is assumed to be ignored if we are actually using `XDomainRequest`. xhr.open(method, url, true); // Instruct the transport to send cookies along with the CORS request, // unless we are using `XDomainRequest`, which is not capable of this. if (options.cors.expected && options.cors.sendCredentials && !isXdr(xhr)) { xhr.withCredentials = true; } setHeaders(id); log("Sending " + method + " request for " + id); if (payload) { xhr.send(payload); } else if (shouldParamsBeInQueryString || !params) { xhr.send(); } else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/x-www-form-urlencoded") >= 0) { xhr.send(qq.obj2url(params, "")); } else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/json") >= 0) { xhr.send(JSON.stringify(params)); } else { xhr.send(params); } return xhr; } function createUrl(id, params, additionalQueryParams) { var endpoint = options.endpointStore.get(id), addToPath = requestData[id].addToPath; /*jshint -W116,-W041 */ if (addToPath != undefined) { endpoint += "/" + addToPath; } if (shouldParamsBeInQueryString && params) { endpoint = qq.obj2url(params, endpoint); } if (additionalQueryParams) { endpoint = qq.obj2url(additionalQueryParams, endpoint); } return endpoint; } // Invoked by the UA to indicate a number of possible states that describe // a live `XMLHttpRequest` transport. function getXhrReadyStateChangeHandler(id) { return function() { if (getXhrOrXdr(id).readyState === 4) { onComplete(id); } }; } function registerForUploadProgress(id) { var onProgress = options.onProgress; if (onProgress) { getXhrOrXdr(id).upload.onprogress = function(e) { if (e.lengthComputable) { onProgress(id, e.loaded, e.total); } }; } } // This will be called by IE to indicate **success** for an associated // `XDomainRequest` transported request. function getXdrLoadHandler(id) { return function() { onComplete(id); }; } // This will be called by IE to indicate **failure** for an associated // `XDomainRequest` transported request. function getXdrErrorHandler(id) { return function() { onComplete(id, true); }; } function setHeaders(id) { var xhr = getXhrOrXdr(id), customHeaders = options.customHeaders, onDemandHeaders = requestData[id].additionalHeaders || {}, method = options.method, allHeaders = {}; // If XDomainRequest is being used, we can't set headers, so just ignore this block. if (!isXdr(xhr)) { options.acceptHeader && xhr.setRequestHeader("Accept", options.acceptHeader); // Only attempt to add X-Requested-With & Cache-Control if permitted if (options.allowXRequestedWithAndCacheControl) { // Do not add X-Requested-With & Cache-Control if this is a cross-origin request // OR the cross-origin request contains a non-simple method or header. // This is done to ensure a preflight is not triggered exclusively based on the // addition of these 2 non-simple headers. if (!options.cors.expected || (!isSimpleMethod() || containsNonSimpleHeaders(customHeaders))) { xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.setRequestHeader("Cache-Control", "no-cache"); } } if (options.contentType && (method === "POST" || method === "PUT")) { xhr.setRequestHeader("Content-Type", options.contentType); } qq.extend(allHeaders, qq.isFunction(customHeaders) ? customHeaders(id) : customHeaders); qq.extend(allHeaders, onDemandHeaders); qq.each(allHeaders, function(name, val) { xhr.setRequestHeader(name, val); }); } } function isResponseSuccessful(responseCode) { return qq.indexOf(options.successfulResponseCodes[options.method], responseCode) >= 0; } function prepareToSend(id, optXhr, addToPath, additionalParams, additionalQueryParams, additionalHeaders, payload) { requestData[id] = { addToPath: addToPath, additionalParams: additionalParams, additionalQueryParams: additionalQueryParams, additionalHeaders: additionalHeaders, payload: payload }; var len = queue.push(id); // if too many active connections, wait... if (len <= options.maxConnections) { return sendRequest(id, optXhr); } } shouldParamsBeInQueryString = options.method === "GET" || options.method === "DELETE"; qq.extend(this, { // Start the process of sending the request. The ID refers to the file associated with the request. initTransport: function(id) { var path, params, headers, payload, cacheBuster, additionalQueryParams; return { // Optionally specify the end of the endpoint path for the request. withPath: function(appendToPath) { path = appendToPath; return this; }, // Optionally specify additional parameters to send along with the request. // These will be added to the query string for GET/DELETE requests or the payload // for POST/PUT requests. The Content-Type of the request will be used to determine // how these parameters should be formatted as well. withParams: function(additionalParams) { params = additionalParams; return this; }, withQueryParams: function(_additionalQueryParams_) { additionalQueryParams = _additionalQueryParams_; return this; }, // Optionally specify additional headers to send along with the request. withHeaders: function(additionalHeaders) { headers = additionalHeaders; return this; }, // Optionally specify a payload/body for the request. withPayload: function(thePayload) { payload = thePayload; return this; }, // Appends a cache buster (timestamp) to the request URL as a query parameter (only if GET or DELETE) withCacheBuster: function() { cacheBuster = true; return this; }, // Send the constructed request. send: function(optXhr) { if (cacheBuster && qq.indexOf(["GET", "DELETE"], options.method) >= 0) { params.qqtimestamp = new Date().getTime(); } return prepareToSend(id, optXhr, path, params, additionalQueryParams, headers, payload); } }; }, canceled: function(id) { dequeue(id); } }); }; ================================================ FILE: client/js/azure/azure.xhr.upload.handler.js ================================================ /*globals qq */ /** * Upload handler used by the upload to Azure module that depends on File API support, and, therefore, makes use of * `XMLHttpRequest` level 2 to upload `File`s and `Blob`s directly to Azure Blob Storage containers via the * associated Azure API. * * @param spec Options passed from the base handler * @param proxy Callbacks & methods used to query for or push out data/changes */ // TODO l18n for error messages returned to UI qq.azure.XhrUploadHandler = function(spec, proxy) { "use strict"; var handler = this, log = proxy.log, cors = spec.cors, endpointStore = spec.endpointStore, paramsStore = spec.paramsStore, signature = spec.signature, filenameParam = spec.filenameParam, minFileSizeForChunking = spec.chunking.minFileSize, deleteBlob = spec.deleteBlob, onGetBlobName = spec.onGetBlobName, getName = proxy.getName, getSize = proxy.getSize, getBlobMetadata = function(id) { var params = paramsStore.get(id); params[filenameParam] = getName(id); return params; }, api = { putBlob: new qq.azure.PutBlob({ getBlobMetadata: getBlobMetadata, log: log }), putBlock: new qq.azure.PutBlock({ log: log }), putBlockList: new qq.azure.PutBlockList({ getBlobMetadata: getBlobMetadata, log: log }), getSasForPutBlobOrBlock: new qq.azure.GetSas({ cors: cors, customHeaders: signature.customHeaders, endpointStore: { get: function() { return signature.endpoint; } }, log: log, restRequestVerb: "PUT" }) }; function combineChunks(id) { var promise = new qq.Promise(); getSignedUrl(id).then(function(sasUri) { var mimeType = handler._getMimeType(id), blockIdEntries = handler._getPersistableData(id).blockIdEntries; api.putBlockList.send(id, sasUri, blockIdEntries, mimeType, function(xhr) { handler._registerXhr(id, null, xhr, api.putBlockList); }) .then(function(xhr) { log("Success combining chunks for id " + id); promise.success({}, xhr); }, function(xhr) { log("Attempt to combine chunks failed for id " + id, "error"); handleFailure(xhr, promise); }); }, promise.failure); return promise; } function determineBlobUrl(id) { var containerUrl = endpointStore.get(id), promise = new qq.Promise(), getBlobNameSuccess = function(blobName) { handler._setThirdPartyFileId(id, blobName); promise.success(containerUrl + "/" + blobName); }, getBlobNameFailure = function(reason) { promise.failure(reason); }; onGetBlobName(id).then(getBlobNameSuccess, getBlobNameFailure); return promise; } function getSignedUrl(id, optChunkIdx) { // We may have multiple SAS requests in progress for the same file, so we must include the chunk idx // as part of the ID when communicating with the SAS ajax requester to avoid collisions. var getSasId = optChunkIdx == null ? id : id + "." + optChunkIdx, promise = new qq.Promise(), getSasSuccess = function(sasUri) { log("GET SAS request succeeded."); promise.success(sasUri); }, getSasFailure = function(reason, getSasXhr) { log("GET SAS request failed: " + reason, "error"); promise.failure({error: "Problem communicating with local server"}, getSasXhr); }, determineBlobUrlSuccess = function(blobUrl) { api.getSasForPutBlobOrBlock.request(getSasId, blobUrl).then( getSasSuccess, getSasFailure ); }, determineBlobUrlFailure = function(reason) { log(qq.format("Failed to determine blob name for ID {} - {}", id, reason), "error"); promise.failure({error: reason}); }; determineBlobUrl(id).then(determineBlobUrlSuccess, determineBlobUrlFailure); return promise; } function handleFailure(xhr, promise) { var azureError = qq.azure.util.parseAzureError(xhr.responseText, log), errorMsg = "Problem sending file to Azure"; promise.failure({error: errorMsg, azureError: azureError && azureError.message, reset: xhr.status === 403 }); } qq.extend(this, { uploadChunk: function(params) { var chunkIdx = params.chunkIdx; var id = params.id; var promise = new qq.Promise(); getSignedUrl(id, chunkIdx).then( function(sasUri) { var xhr = handler._createXhr(id, chunkIdx), chunkData = handler._getChunkData(id, chunkIdx); handler._registerProgressHandler(id, chunkIdx, chunkData.size); handler._registerXhr(id, chunkIdx, xhr, api.putBlock); // We may have multiple put block requests in progress for the same file, so we must include the chunk idx // as part of the ID when communicating with the put block ajax requester to avoid collisions. api.putBlock.upload(id + "." + chunkIdx, xhr, sasUri, chunkIdx, chunkData.blob).then( function(blockIdEntry) { if (!handler._getPersistableData(id).blockIdEntries) { handler._getPersistableData(id).blockIdEntries = []; } handler._getPersistableData(id).blockIdEntries.push(blockIdEntry); log("Put Block call succeeded for " + id); promise.success({}, xhr); }, function() { log(qq.format("Put Block call failed for ID {} on part {}", id, chunkIdx), "error"); handleFailure(xhr, promise); } ); }, promise.failure ); return promise; }, uploadFile: function(id) { var promise = new qq.Promise(), fileOrBlob = handler.getFile(id); getSignedUrl(id).then(function(sasUri) { var xhr = handler._createXhr(id); handler._registerProgressHandler(id); api.putBlob.upload(id, xhr, sasUri, fileOrBlob).then( function() { log("Put Blob call succeeded for " + id); promise.success({}, xhr); }, function() { log("Put Blob call failed for " + id, "error"); handleFailure(xhr, promise); } ); }, promise.failure); return promise; } }); qq.extend(this, new qq.XhrUploadHandler({ options: qq.extend({namespace: "azure"}, spec), proxy: qq.extend({getEndpoint: spec.endpointStore.get}, proxy) } )); qq.override(this, function(super_) { return { expunge: function(id) { var relatedToCancel = handler._wasCanceled(id), chunkingData = handler._getPersistableData(id), blockIdEntries = (chunkingData && chunkingData.blockIdEntries) || []; if (relatedToCancel && blockIdEntries.length > 0) { deleteBlob(id); } super_.expunge(id); }, finalizeChunks: function(id) { return combineChunks(id); }, _shouldChunkThisFile: function(id) { var maybePossible = super_._shouldChunkThisFile(id); return maybePossible && getSize(id) >= minFileSizeForChunking; } }; }); }; ================================================ FILE: client/js/azure/get-sas.js ================================================ /* globals qq */ /** * Sends a GET request to the integrator's server, which should return a Shared Access Signature URI used to * make a specific request on a Blob via the Azure REST API. */ qq.azure.GetSas = function(o) { "use strict"; var requester, options = { cors: { expected: false, sendCredentials: false }, customHeaders: {}, restRequestVerb: "PUT", endpointStore: null, log: function(str, level) {} }, requestPromises = {}; qq.extend(options, o); function sasResponseReceived(id, xhr, isError) { var promise = requestPromises[id]; if (isError) { promise.failure("Received response code " + xhr.status, xhr); } else { if (xhr.responseText.length) { promise.success(xhr.responseText); } else { promise.failure("Empty response.", xhr); } } delete requestPromises[id]; } requester = qq.extend(this, new qq.AjaxRequester({ acceptHeader: "application/json", validMethods: ["GET"], method: "GET", successfulResponseCodes: { GET: [200] }, contentType: null, customHeaders: options.customHeaders, endpointStore: options.endpointStore, cors: options.cors, log: options.log, onComplete: sasResponseReceived })); qq.extend(this, { request: function(id, blobUri) { var requestPromise = new qq.Promise(), restVerb = options.restRequestVerb; options.log(qq.format("Submitting GET SAS request for a {} REST request related to file ID {}.", restVerb, id)); requestPromises[id] = requestPromise; requester.initTransport(id) .withParams({ bloburi: blobUri, _method: restVerb }) .withCacheBuster() .send(); return requestPromise; } }); }; ================================================ FILE: client/js/azure/jquery-plugin.js ================================================ /*globals jQuery*/ /** * Simply an alias for the `fineUploader` plug-in wrapper, but hides the required `endpointType` option from the * integrator. I thought it may be confusing to convey to the integrator that, when using Fine Uploader in Azure mode, * you need to specify an `endpointType` with a value of "azure", and perhaps an `uploaderType` with a value of "basic" if * you want to use basic mode when uploading directly to Azure as well. So, you can use this plug-in alias and not worry * about the `endpointType` option at all. */ (function($) { "use strict"; $.fn.fineUploaderAzure = function(optionsOrCommand) { if (typeof optionsOrCommand === "object") { // This option is used to tell the plug-in wrapper to instantiate the appropriate Azure-namespace modules. optionsOrCommand.endpointType = "azure"; } return $.fn.fineUploader.apply(this, arguments); }; }(jQuery)); ================================================ FILE: client/js/azure/rest/delete-blob.js ================================================ /* globals qq */ /** * Implements the Delete Blob Azure REST API call. http://msdn.microsoft.com/en-us/library/windowsazure/dd179413.aspx. */ qq.azure.DeleteBlob = function(o) { "use strict"; var requester, method = "DELETE", options = { endpointStore: {}, onDelete: function(id) {}, onDeleteComplete: function(id, xhr, isError) {}, log: function(str, level) {} }; qq.extend(options, o); requester = qq.extend(this, new qq.AjaxRequester({ validMethods: [method], method: method, successfulResponseCodes: (function() { var codes = {}; codes[method] = [202]; return codes; }()), contentType: null, endpointStore: options.endpointStore, allowXRequestedWithAndCacheControl: false, cors: { expected: true }, log: options.log, onSend: options.onDelete, onComplete: options.onDeleteComplete })); qq.extend(this, { method: method, send: function(id) { options.log("Submitting Delete Blob request for " + id); return requester.initTransport(id) .send(); } }); }; ================================================ FILE: client/js/azure/rest/put-blob.js ================================================ /* globals qq */ /** * Implements the Put Blob Azure REST API call. http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx. */ qq.azure.PutBlob = function(o) { "use strict"; var requester, method = "PUT", options = { getBlobMetadata: function(id) {}, log: function(str, level) {} }, endpoints = {}, promises = {}, endpointHandler = { get: function(id) { return endpoints[id]; } }; qq.extend(options, o); requester = qq.extend(this, new qq.AjaxRequester({ validMethods: [method], method: method, successfulResponseCodes: (function() { var codes = {}; codes[method] = [201]; return codes; }()), contentType: null, customHeaders: function(id) { var params = options.getBlobMetadata(id), headers = qq.azure.util.getParamsAsHeaders(params); headers["x-ms-blob-type"] = "BlockBlob"; return headers; }, endpointStore: endpointHandler, allowXRequestedWithAndCacheControl: false, cors: { expected: true }, log: options.log, onComplete: function(id, xhr, isError) { var promise = promises[id]; delete endpoints[id]; delete promises[id]; if (isError) { promise.failure(); } else { promise.success(); } } })); qq.extend(this, { method: method, upload: function(id, xhr, url, file) { var promise = new qq.Promise(); options.log("Submitting Put Blob request for " + id); promises[id] = promise; endpoints[id] = url; requester.initTransport(id) .withPayload(file) .withHeaders({"Content-Type": file.type}) .send(xhr); return promise; } }); }; ================================================ FILE: client/js/azure/rest/put-block-list.js ================================================ /* globals qq */ /** * Implements the Put Block List Azure REST API call. http://msdn.microsoft.com/en-us/library/windowsazure/dd179467.aspx. */ qq.azure.PutBlockList = function(o) { "use strict"; var requester, method = "PUT", promises = {}, options = { getBlobMetadata: function(id) {}, log: function(str, level) {} }, endpoints = {}, endpointHandler = { get: function(id) { return endpoints[id]; } }; qq.extend(options, o); requester = qq.extend(this, new qq.AjaxRequester({ validMethods: [method], method: method, successfulResponseCodes: (function() { var codes = {}; codes[method] = [201]; return codes; }()), customHeaders: function(id) { var params = options.getBlobMetadata(id); return qq.azure.util.getParamsAsHeaders(params); }, contentType: "text/plain", endpointStore: endpointHandler, allowXRequestedWithAndCacheControl: false, cors: { expected: true }, log: options.log, onSend: function() {}, onComplete: function(id, xhr, isError) { var promise = promises[id]; delete endpoints[id]; delete promises[id]; if (isError) { promise.failure(xhr); } else { promise.success(xhr); } } })); function createRequestBody(blockIdEntries) { var doc = document.implementation.createDocument(null, "BlockList", null); // If we don't sort the block ID entries by part number, the file will be combined incorrectly by Azure blockIdEntries.sort(function(a, b) { return a.part - b.part; }); // Construct an XML document for each pair of etag/part values that correspond to part uploads. qq.each(blockIdEntries, function(idx, blockIdEntry) { var latestEl = doc.createElement("Latest"), latestTextEl = doc.createTextNode(blockIdEntry.id); latestEl.appendChild(latestTextEl); qq(doc).children()[0].appendChild(latestEl); }); // Turn the resulting XML document into a string fit for transport. return new XMLSerializer().serializeToString(doc); } qq.extend(this, { method: method, send: function(id, sasUri, blockIdEntries, fileMimeType, registerXhrCallback) { var promise = new qq.Promise(), blockIdsXml = createRequestBody(blockIdEntries), xhr; promises[id] = promise; options.log(qq.format("Submitting Put Block List request for {}", id)); endpoints[id] = qq.format("{}&comp=blocklist", sasUri); xhr = requester.initTransport(id) .withPayload(blockIdsXml) .withHeaders({"x-ms-blob-content-type": fileMimeType}) .send(); registerXhrCallback(xhr); return promise; } }); }; ================================================ FILE: client/js/azure/rest/put-block.js ================================================ /* globals qq */ /** * Implements the Put Block Azure REST API call. http://msdn.microsoft.com/en-us/library/windowsazure/dd135726.aspx. */ qq.azure.PutBlock = function(o) { "use strict"; var requester, method = "PUT", blockIdEntries = {}, promises = {}, options = { log: function(str, level) {} }, endpoints = {}, endpointHandler = { get: function(id) { return endpoints[id]; } }; qq.extend(options, o); requester = qq.extend(this, new qq.AjaxRequester({ validMethods: [method], method: method, successfulResponseCodes: (function() { var codes = {}; codes[method] = [201]; return codes; }()), contentType: null, endpointStore: endpointHandler, allowXRequestedWithAndCacheControl: false, cors: { expected: true }, log: options.log, onComplete: function(id, xhr, isError) { var promise = promises[id], blockIdEntry = blockIdEntries[id]; delete endpoints[id]; delete promises[id]; delete blockIdEntries[id]; if (isError) { promise.failure(); } else { promise.success(blockIdEntry); } } })); function createBlockId(partNum) { var digits = 5, zeros = new Array(digits + 1).join("0"), paddedPartNum = (zeros + partNum).slice(-digits); return btoa(paddedPartNum); } qq.extend(this, { method: method, upload: function(id, xhr, sasUri, partNum, blob) { var promise = new qq.Promise(), blockId = createBlockId(partNum); promises[id] = promise; options.log(qq.format("Submitting Put Block request for {} = part {}", id, partNum)); endpoints[id] = qq.format("{}&comp=block&blockid={}", sasUri, encodeURIComponent(blockId)); blockIdEntries[id] = {part: partNum, id: blockId}; requester.initTransport(id) .withPayload(blob) .send(xhr); return promise; } }); }; ================================================ FILE: client/js/azure/uploader.basic.js ================================================ /*globals qq */ /** * This defines FineUploaderBasic mode w/ support for uploading to Azure, which provides all the basic * functionality of Fine Uploader Basic as well as code to handle uploads directly to Azure. * Some inherited options and API methods have a special meaning in the context of the Azure uploader. */ (function() { "use strict"; qq.azure.FineUploaderBasic = function(o) { if (!qq.supportedFeatures.ajaxUploading) { throw new qq.Error("Uploading directly to Azure is not possible in this browser."); } var options = { signature: { endpoint: null, customHeaders: {} }, // 'uuid', 'filename', or a function which may be promissory blobProperties: { name: "uuid" }, uploadSuccess: { endpoint: null, method: "POST", // In addition to the default params sent by Fine Uploader params: {}, customHeaders: {} }, chunking: { // If this is increased, Azure may respond with a 413 partSize: 4000000, // Don't chunk files less than this size minFileSize: 4000001 } }; // Replace any default options with user defined ones qq.extend(options, o, true); // Call base module qq.FineUploaderBasic.call(this, options); this._uploadSuccessParamsStore = this._createStore(this._options.uploadSuccess.params); this._uploadSuccessEndpointStore = this._createStore(this._options.uploadSuccess.endpoint); // This will hold callbacks for failed uploadSuccess requests that will be invoked on retry. // Indexed by file ID. this._failedSuccessRequestCallbacks = {}; // Holds blob names for file representations constructed from a session request. this._cannedBlobNames = {}; }; // Inherit basic public & private API methods. qq.extend(qq.azure.FineUploaderBasic.prototype, qq.basePublicApi); qq.extend(qq.azure.FineUploaderBasic.prototype, qq.basePrivateApi); qq.extend(qq.azure.FineUploaderBasic.prototype, qq.nonTraditionalBasePublicApi); qq.extend(qq.azure.FineUploaderBasic.prototype, qq.nonTraditionalBasePrivateApi); // Define public & private API methods for this module. qq.extend(qq.azure.FineUploaderBasic.prototype, { getBlobName: function(id) { /* jshint eqnull:true */ if (this._cannedBlobNames[id] == null) { return this._handler.getThirdPartyFileId(id); } return this._cannedBlobNames[id]; }, _getEndpointSpecificParams: function(id) { return { blob: this.getBlobName(id), uuid: this.getUuid(id), name: this.getName(id), container: this._endpointStore.get(id) }; }, _createUploadHandler: function() { return qq.FineUploaderBasic.prototype._createUploadHandler.call(this, { signature: this._options.signature, onGetBlobName: qq.bind(this._determineBlobName, this), deleteBlob: qq.bind(this._deleteBlob, this, true) }, "azure"); }, _determineBlobName: function(id) { var self = this, blobNameOptionValue = this._options.blobProperties.name, uuid = this.getUuid(id), filename = this.getName(id), fileExtension = qq.getExtension(filename), blobNameToUse = uuid; if (qq.isString(blobNameOptionValue)) { switch (blobNameOptionValue) { case "uuid": if (fileExtension !== undefined) { blobNameToUse += "." + fileExtension; } return new qq.Promise().success(blobNameToUse); case "filename": return new qq.Promise().success(filename); default: return new qq.Promise.failure("Invalid blobName option value - " + blobNameOptionValue); } } else { return blobNameOptionValue.call(this, id); } }, _addCannedFile: function(sessionData) { var id; /* jshint eqnull:true */ if (sessionData.blobName == null) { throw new qq.Error("Did not find blob name property in server session response. This is required!"); } else { id = qq.FineUploaderBasic.prototype._addCannedFile.apply(this, arguments); this._cannedBlobNames[id] = sessionData.blobName; } return id; }, _deleteBlob: function(relatedToCancel, id) { var self = this, deleteBlobSasUri = {}, blobUriStore = { get: function(id) { return self._endpointStore.get(id) + "/" + self.getBlobName(id); } }, deleteFileEndpointStore = { get: function(id) { return deleteBlobSasUri[id]; } }, getSasSuccess = function(id, sasUri) { deleteBlobSasUri[id] = sasUri; deleteBlob.send(id); }, getSasFailure = function(id, reason, xhr) { if (relatedToCancel) { self.log("Will cancel upload, but cannot remove uncommitted parts from Azure due to issue retrieving SAS", "error"); qq.FineUploaderBasic.prototype._onCancel.call(self, id, self.getName(id)); } else { self._onDeleteComplete(id, xhr, true); self._options.callbacks.onDeleteComplete(id, xhr, true); } }, deleteBlob = new qq.azure.DeleteBlob({ endpointStore: deleteFileEndpointStore, log: qq.bind(self.log, self), onDelete: function(id) { self._onDelete(id); self._options.callbacks.onDelete(id); }, onDeleteComplete: function(id, xhrOrXdr, isError) { delete deleteBlobSasUri[id]; if (isError) { if (relatedToCancel) { self.log("Will cancel upload, but failed to remove uncommitted parts from Azure.", "error"); } else { qq.azure.util.parseAzureError(xhrOrXdr.responseText, qq.bind(self.log, self)); } } if (relatedToCancel) { qq.FineUploaderBasic.prototype._onCancel.call(self, id, self.getName(id)); self.log("Deleted uncommitted blob chunks for " + id); } else { self._onDeleteComplete(id, xhrOrXdr, isError); self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError); } } }), getSas = new qq.azure.GetSas({ cors: this._options.cors, customHeaders: this._options.signature.customHeaders, endpointStore: { get: function() { return self._options.signature.endpoint; } }, restRequestVerb: deleteBlob.method, log: qq.bind(self.log, self) }); getSas.request(id, blobUriStore.get(id)).then( qq.bind(getSasSuccess, self, id), qq.bind(getSasFailure, self, id)); }, _createDeleteHandler: function() { var self = this; return { sendDelete: function(id, uuid) { self._deleteBlob(false, id); } }; } }); }()); ================================================ FILE: client/js/azure/uploader.js ================================================ /*globals qq */ /** * This defines FineUploader mode w/ support for uploading to Azure, which provides all the basic * functionality of Fine Uploader as well as code to handle uploads directly to Azure. * This module inherits all logic from UI & core mode and adds some UI-related logic * specific to the upload-to-Azure workflow. Some inherited options and API methods have a special meaning * in the context of the Azure uploader. */ (function() { "use strict"; qq.azure.FineUploader = function(o) { var options = { failedUploadTextDisplay: { mode: "custom" } }; // Replace any default options with user defined ones qq.extend(options, o, true); // Inherit instance data from FineUploader, which should in turn inherit from azure.FineUploaderBasic. qq.FineUploader.call(this, options, "azure"); }; // Inherit the API methods from FineUploaderBasicS3 qq.extend(qq.azure.FineUploader.prototype, qq.azure.FineUploaderBasic.prototype); // Inherit public and private API methods related to UI qq.extend(qq.azure.FineUploader.prototype, qq.uiPublicApi); qq.extend(qq.azure.FineUploader.prototype, qq.uiPrivateApi); // Define public & private API methods for this module. qq.extend(qq.azure.FineUploader.prototype, { }); }()); ================================================ FILE: client/js/azure/util.js ================================================ /*globals qq */ qq.azure = qq.azure || {}; qq.azure.util = qq.azure.util || (function() { "use strict"; return { AZURE_PARAM_PREFIX: "x-ms-meta-", /** Test if a request header is actually a known Azure parameter. See: https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx * * @param name Name of the Request Header parameter. * @returns {Boolean} Test result. */ _paramNameMatchesAzureParameter: function(name) { switch (name) { case "Cache-Control": case "Content-Disposition": case "Content-Encoding": case "Content-MD5": case "x-ms-blob-content-encoding": case "x-ms-blob-content-disposition": case "x-ms-blob-content-md5": case "x-ms-blob-cache-control": return true; default: return false; } }, /** Create Prefixed request headers which are appropriate for Azure. * * If the request header is appropriate for Azure (e.g. Cache-Control) then it should be * passed along without a metadata prefix. For all other request header parameter names, * qq.azure.util.AZURE_PARAM_PREFIX should be prepended. * * @param name Name of the Request Header parameter to construct a (possibly) prefixed name. * @returns {String} A valid Request Header parameter name. */ _getPrefixedParamName: function(name) { if (qq.azure.util._paramNameMatchesAzureParameter(name)) { return name; } else { return qq.azure.util.AZURE_PARAM_PREFIX + name; } }, getParamsAsHeaders: function(params) { var headers = {}; qq.each(params, function(name, val) { var headerName = qq.azure.util._getPrefixedParamName(name), value = null; if (qq.isFunction(val)) { value = String(val()); } else if (qq.isObject(val)) { qq.extend(headers, qq.azure.util.getParamsAsHeaders(val)); } else { value = String(val); } if (value !== null) { if (qq.azure.util._paramNameMatchesAzureParameter(name)) { headers[headerName] = value; } else { headers[headerName] = encodeURIComponent(value); } } }); return headers; }, parseAzureError: function(responseText, log) { var domParser = new DOMParser(), responseDoc = domParser.parseFromString(responseText, "application/xml"), errorTag = responseDoc.getElementsByTagName("Error")[0], errorDetails = {}, codeTag, messageTag; log("Received error response: " + responseText, "error"); if (errorTag) { messageTag = errorTag.getElementsByTagName("Message")[0]; if (messageTag) { errorDetails.message = messageTag.textContent; } codeTag = errorTag.getElementsByTagName("Code")[0]; if (codeTag) { errorDetails.code = codeTag.textContent; } log("Parsed Azure error: " + JSON.stringify(errorDetails), "error"); return errorDetails; } } }; }()); ================================================ FILE: client/js/blob-proxy.js ================================================ /* globals qq */ /** * Placeholder for a Blob that will be generated on-demand. * * @param referenceBlob Parent of the generated blob * @param onCreate Function to invoke when the blob must be created. Must be promissory. * @constructor */ qq.BlobProxy = function(referenceBlob, onCreate) { "use strict"; qq.extend(this, { referenceBlob: referenceBlob, create: function() { return onCreate(referenceBlob); } }); }; ================================================ FILE: client/js/button.js ================================================ /*globals qq*/ /** * This module represents an upload or "Select File(s)" button. It's job is to embed an opaque `` * element as a child of a provided "container" element. This "container" element (`options.element`) is used to provide * a custom style for the `` element. The ability to change the style of the container element is also * provided here by adding CSS classes to the container on hover/focus. * * TODO Eliminate the mouseover and mouseout event handlers since the :hover CSS pseudo-class should now be * available on all supported browsers. * * @param o Options to override the default values */ qq.UploadButton = function(o) { "use strict"; var self = this, disposeSupport = new qq.DisposeSupport(), options = { // Corresponds to the `accept` attribute on the associated `` acceptFiles: null, // "Container" element element: null, focusClass: "qq-upload-button-focus", // A true value allows folders to be selected, if supported by the UA folders: false, // **This option will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers hoverClass: "qq-upload-button-hover", ios8BrowserCrashWorkaround: false, // If true adds `multiple` attribute to `` multiple: false, // `name` attribute of `` name: "qqfile", // Called when the browser invokes the onchange handler on the `` onChange: function(input) {}, title: null }, input, buttonId; // Overrides any of the default option values with any option values passed in during construction. qq.extend(options, o); buttonId = qq.getUniqueId(); // Embed an opaque `` element as a child of `options.element`. function createInput() { var input = document.createElement("input"); input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId); input.setAttribute("title", options.title); self.setMultiple(options.multiple, input); if (options.folders && qq.supportedFeatures.folderSelection) { // selecting directories is only possible in Chrome now, via a vendor-specific prefixed attribute input.setAttribute("webkitdirectory", ""); } if (options.acceptFiles) { input.setAttribute("accept", options.acceptFiles); } input.setAttribute("type", "file"); input.setAttribute("name", options.name); qq(input).css({ position: "absolute", // in Opera only 'browse' button // is clickable and it is located at // the right side of the input right: 0, top: 0, fontFamily: "Arial", // It's especially important to make this an arbitrarily large value // to ensure the rendered input button in IE takes up the entire // space of the container element. Otherwise, the left side of the // button will require a double-click to invoke the file chooser. // In other browsers, this might cause other issues, so a large font-size // is only used in IE. There is a bug in IE8 where the opacity style is ignored // in some cases when the font-size is large. So, this workaround is not applied // to IE8. fontSize: qq.ie() && !qq.ie8() ? "3500px" : "118px", margin: 0, padding: 0, cursor: "pointer", opacity: 0 }); // Setting the file input's height to 100% in IE7 causes // most of the visible button to be unclickable. !qq.ie7() && qq(input).css({height: "100%"}); options.element.appendChild(input); disposeSupport.attach(input, "change", function() { options.onChange(input); }); // **These event handlers will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers disposeSupport.attach(input, "mouseover", function() { qq(options.element).addClass(options.hoverClass); }); disposeSupport.attach(input, "mouseout", function() { qq(options.element).removeClass(options.hoverClass); }); disposeSupport.attach(input, "focus", function() { qq(options.element).addClass(options.focusClass); }); disposeSupport.attach(input, "blur", function() { qq(options.element).removeClass(options.focusClass); }); return input; } // Make button suitable container for input qq(options.element).css({ position: "relative", overflow: "hidden", // Make sure browse button is in the right side in Internet Explorer direction: "ltr" }); // Exposed API qq.extend(this, { getInput: function() { return input; }, getButtonId: function() { return buttonId; }, setMultiple: function(isMultiple, optInput) { var input = optInput || this.getInput(); // Temporary workaround for bug in in iOS8 UIWebView that causes the browser to crash // before the file chooser appears if the file input doesn't contain a multiple attribute. // See #1283. if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) { input.setAttribute("multiple", ""); } else { if (isMultiple) { input.setAttribute("multiple", ""); } else { input.removeAttribute("multiple"); } } }, setAcceptFiles: function(acceptFiles) { if (acceptFiles !== options.acceptFiles) { input.setAttribute("accept", acceptFiles); } }, reset: function() { if (input.parentNode) { qq(input).remove(); } qq(options.element).removeClass(options.focusClass); input = null; input = createInput(); } }); input = createInput(); }; qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id"; ================================================ FILE: client/js/deletefile.ajax.requester.js ================================================ /*globals qq, XMLHttpRequest*/ qq.DeleteFileAjaxRequester = function(o) { "use strict"; var requester, options = { method: "DELETE", uuidParamName: "qquuid", endpointStore: {}, maxConnections: 3, customHeaders: function(id) {return {};}, paramsStore: {}, cors: { expected: false, sendCredentials: false }, log: function(str, level) {}, onDelete: function(id) {}, onDeleteComplete: function(id, xhrOrXdr, isError) {} }; qq.extend(options, o); function getMandatedParams() { if (options.method.toUpperCase() === "POST") { return { _method: "DELETE" }; } return {}; } requester = qq.extend(this, new qq.AjaxRequester({ acceptHeader: "application/json", validMethods: ["POST", "DELETE"], method: options.method, endpointStore: options.endpointStore, paramsStore: options.paramsStore, mandatedParams: getMandatedParams(), maxConnections: options.maxConnections, customHeaders: function(id) { return options.customHeaders.get(id); }, log: options.log, onSend: options.onDelete, onComplete: options.onDeleteComplete, cors: options.cors })); qq.extend(this, { sendDelete: function(id, uuid, additionalMandatedParams) { var additionalOptions = additionalMandatedParams || {}; options.log("Submitting delete file request for " + id); if (options.method === "DELETE") { requester.initTransport(id) .withPath(uuid) .withParams(additionalOptions) .send(); } else { additionalOptions[options.uuidParamName] = uuid; requester.initTransport(id) .withParams(additionalOptions) .send(); } } }); }; ================================================ FILE: client/js/dnd.js ================================================ /*globals qq, document, CustomEvent*/ qq.DragAndDrop = function(o) { "use strict"; var options, HIDE_ZONES_EVENT_NAME = "qq-hidezones", HIDE_BEFORE_ENTER_ATTR = "qq-hide-dropzone", uploadDropZones = [], droppedFiles = [], disposeSupport = new qq.DisposeSupport(); options = { dropZoneElements: [], allowMultipleItems: true, classes: { dropActive: null }, callbacks: new qq.DragAndDrop.callbacks() }; qq.extend(options, o, true); function uploadDroppedFiles(files, uploadDropZone) { // We need to convert the `FileList` to an actual `Array` to avoid iteration issues var filesAsArray = Array.prototype.slice.call(files); options.callbacks.dropLog("Grabbed " + files.length + " dropped files."); uploadDropZone.dropDisabled(false); options.callbacks.processingDroppedFilesComplete(filesAsArray, uploadDropZone.getElement()); } function traverseFileTree(entry) { var parseEntryPromise = new qq.Promise(); if (entry.isFile) { entry.file(function(file) { file.qqPath = extractDirectoryPath(entry); droppedFiles.push(file); parseEntryPromise.success(); }, function(fileError) { options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error"); parseEntryPromise.failure(); }); } else if (entry.isDirectory) { getFilesInDirectory(entry).then( function allEntriesRead(entries) { var entriesLeft = entries.length; qq.each(entries, function(idx, entry) { traverseFileTree(entry).done(function() { entriesLeft -= 1; if (entriesLeft === 0) { parseEntryPromise.success(); } }); }); if (!entries.length) { parseEntryPromise.success(); } }, function readFailure(fileError) { options.callbacks.dropLog("Problem parsing '" + entry.fullPath + "'. FileError code " + fileError.code + ".", "error"); parseEntryPromise.failure(); } ); } return parseEntryPromise; } function extractDirectoryPath(entry) { var name = entry.name, fullPath = entry.fullPath, indexOfNameInFullPath = fullPath.lastIndexOf(name); // remove file name from full path string fullPath = fullPath.substr(0, indexOfNameInFullPath); // remove leading slash in full path string if (fullPath.charAt(0) === "/") { fullPath = fullPath.substr(1); } return fullPath; } // Promissory. Guaranteed to read all files in the root of the passed directory. function getFilesInDirectory(entry, reader, accumEntries, existingPromise) { var promise = existingPromise || new qq.Promise(), dirReader = reader || entry.createReader(); dirReader.readEntries( function readSuccess(entries) { var newEntries = accumEntries ? accumEntries.concat(entries) : entries; if (entries.length) { setTimeout(function() { // prevent stack overflow, however unlikely getFilesInDirectory(entry, dirReader, newEntries, promise); }, 0); } else { promise.success(newEntries); } }, promise.failure ); return promise; } function handleDataTransfer(dataTransfer, uploadDropZone) { var pendingFolderPromises = [], handleDataTransferPromise = new qq.Promise(); options.callbacks.processingDroppedFiles(); uploadDropZone.dropDisabled(true); if (dataTransfer.files.length > 1 && !options.allowMultipleItems) { options.callbacks.processingDroppedFilesComplete([]); options.callbacks.dropError("tooManyFilesError", ""); uploadDropZone.dropDisabled(false); handleDataTransferPromise.failure(); } else { droppedFiles = []; if (qq.isFolderDropSupported(dataTransfer)) { qq.each(dataTransfer.items, function(idx, item) { var entry = item.webkitGetAsEntry(); if (entry) { //due to a bug in Chrome's File System API impl - #149735 if (entry.isFile) { droppedFiles.push(item.getAsFile()); } else { pendingFolderPromises.push(traverseFileTree(entry).done(function() { pendingFolderPromises.pop(); if (pendingFolderPromises.length === 0) { handleDataTransferPromise.success(); } })); } } }); } else { droppedFiles = dataTransfer.files; } if (pendingFolderPromises.length === 0) { handleDataTransferPromise.success(); } } return handleDataTransferPromise; } function setupDropzone(dropArea) { var dropZone = new qq.UploadDropZone({ HIDE_ZONES_EVENT_NAME: HIDE_ZONES_EVENT_NAME, element: dropArea, onEnter: function(e) { qq(dropArea).addClass(options.classes.dropActive); options.callbacks.dragEnter(); e.stopPropagation(); }, onLeaveNotDescendants: function(e) { qq(dropArea).removeClass(options.classes.dropActive); options.callbacks.dragLeave(); }, onDrop: function(e) { handleDataTransfer(e.dataTransfer, dropZone).then( function() { uploadDroppedFiles(droppedFiles, dropZone); }, function() { options.callbacks.dropLog("Drop event DataTransfer parsing failed. No files will be uploaded.", "error"); } ); } }); disposeSupport.addDisposer(function() { dropZone.dispose(); }); qq(dropArea).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropArea).hide(); uploadDropZones.push(dropZone); return dropZone; } function isFileDrag(dragEvent) { var fileDrag; qq.each(dragEvent.dataTransfer.types, function(key, val) { if (val === "Files") { fileDrag = true; return false; } }); return fileDrag; } // Attempt to determine when the file has left the document. It is not always possible to detect this // in all cases, but it is generally possible in all browsers, with a few exceptions. // // Exceptions: // * IE10+ & Safari: We can't detect a file leaving the document if the Explorer window housing the file // overlays the browser window. // * IE10+: If the file is dragged out of the window too quickly, IE does not set the expected values of the // event's X & Y properties. function leavingDocumentOut(e) { if (qq.safari()) { return e.x < 0 || e.y < 0; } return e.x === 0 && e.y === 0; } function setupDragDrop() { var dropZones = options.dropZoneElements, maybeHideDropZones = function() { setTimeout(function() { qq.each(dropZones, function(idx, dropZone) { qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR) && qq(dropZone).hide(); qq(dropZone).removeClass(options.classes.dropActive); }); }, 10); }; qq.each(dropZones, function(idx, dropZone) { var uploadDropZone = setupDropzone(dropZone); // IE <= 9 does not support the File API used for drag+drop uploads if (dropZones.length && qq.supportedFeatures.fileDrop) { disposeSupport.attach(document, "dragenter", function(e) { if (!uploadDropZone.dropDisabled() && isFileDrag(e)) { qq.each(dropZones, function(idx, dropZone) { // We can't apply styles to non-HTMLElements, since they lack the `style` property. // Also, if the drop zone isn't initially hidden, let's not mess with `style.display`. if (dropZone instanceof HTMLElement && qq(dropZone).hasAttribute(HIDE_BEFORE_ENTER_ATTR)) { qq(dropZone).css({display: "block"}); } }); } }); } }); disposeSupport.attach(document, "dragleave", function(e) { if (leavingDocumentOut(e)) { maybeHideDropZones(); } }); // Just in case we were not able to detect when a dragged file has left the document, // hide all relevant drop zones the next time the mouse enters the document. // Note that mouse events such as this one are not fired during drag operations. disposeSupport.attach(qq(document).children()[0], "mouseenter", function(e) { maybeHideDropZones(); }); disposeSupport.attach(document, "drop", function(e) { if (isFileDrag(e)) { e.preventDefault(); maybeHideDropZones(); } }); disposeSupport.attach(document, HIDE_ZONES_EVENT_NAME, maybeHideDropZones); } setupDragDrop(); qq.extend(this, { setupExtraDropzone: function(element) { options.dropZoneElements.push(element); setupDropzone(element); }, removeDropzone: function(element) { var i, dzs = options.dropZoneElements; for (i in dzs) { if (dzs[i] === element) { return dzs.splice(i, 1); } } }, dispose: function() { disposeSupport.dispose(); qq.each(uploadDropZones, function(idx, dropZone) { dropZone.dispose(); }); } }); this._testing = {}; this._testing.extractDirectoryPath = extractDirectoryPath; }; qq.DragAndDrop.callbacks = function() { "use strict"; return { dragEnter: function () {}, dragLeave: function () {}, processingDroppedFiles: function() {}, processingDroppedFilesComplete: function(files, targetEl) {}, dropError: function(code, errorSpecifics) { qq.log("Drag & drop error code '" + code + " with these specifics: '" + errorSpecifics + "'", "error"); }, dropLog: function(message, level) { qq.log(message, level); } }; }; qq.UploadDropZone = function(o) { "use strict"; var disposeSupport = new qq.DisposeSupport(), options, element, preventDrop, dropOutsideDisabled; options = { element: null, onEnter: function(e) {}, onLeave: function(e) {}, // is not fired when leaving element by hovering descendants onLeaveNotDescendants: function(e) {}, onDrop: function(e) {} }; qq.extend(options, o); element = options.element; function dragoverShouldBeCanceled() { return qq.safari() || (qq.firefox() && qq.windows()); } function disableDropOutside(e) { // run only once for all instances if (!dropOutsideDisabled) { // for these cases we need to catch onDrop to reset dropArea if (dragoverShouldBeCanceled) { disposeSupport.attach(document, "dragover", function(e) { e.preventDefault(); }); } else { disposeSupport.attach(document, "dragover", function(e) { if (e.dataTransfer) { e.dataTransfer.dropEffect = "none"; e.preventDefault(); } }); } dropOutsideDisabled = true; } } function isValidFileDrag(e) { // e.dataTransfer currently causing IE errors // IE9 does NOT support file API, so drag-and-drop is not possible if (!qq.supportedFeatures.fileDrop) { return false; } var effectTest, dt = e.dataTransfer, // do not check dt.types.contains in webkit, because it crashes safari 4 isSafari = qq.safari(); // dt.effectAllowed is none in Safari 5 // dt.effectAllowed crashes IE 11 & 10 when files have been dragged from // the filesystem effectTest = qq.ie() && qq.supportedFeatures.fileDrop ? true : dt.effectAllowed !== "none"; return dt && effectTest && ( (dt.files && dt.files.length) || // Valid for drop events with files (!isSafari && dt.types.contains && dt.types.contains("Files")) || // Valid in Chrome/Firefox (dt.types.includes && dt.types.includes("Files")) // Valid in IE ); } function isOrSetDropDisabled(isDisabled) { if (isDisabled !== undefined) { preventDrop = isDisabled; } return preventDrop; } function triggerHidezonesEvent() { var hideZonesEvent; function triggerUsingOldApi() { hideZonesEvent = document.createEvent("Event"); hideZonesEvent.initEvent(options.HIDE_ZONES_EVENT_NAME, true, true); } if (window.CustomEvent) { try { hideZonesEvent = new CustomEvent(options.HIDE_ZONES_EVENT_NAME); } catch (err) { triggerUsingOldApi(); } } else { triggerUsingOldApi(); } document.dispatchEvent(hideZonesEvent); } function attachEvents() { disposeSupport.attach(element, "dragover", function(e) { if (!isValidFileDrag(e)) { return; } // dt.effectAllowed crashes IE 11 & 10 when files have been dragged from // the filesystem var effect = qq.ie() && qq.supportedFeatures.fileDrop ? null : e.dataTransfer.effectAllowed; if (effect === "move" || effect === "linkMove") { e.dataTransfer.dropEffect = "move"; // for FF (only move allowed) } else { e.dataTransfer.dropEffect = "copy"; // for Chrome } e.stopPropagation(); e.preventDefault(); }); disposeSupport.attach(element, "dragenter", function(e) { if (!isOrSetDropDisabled()) { if (!isValidFileDrag(e)) { return; } options.onEnter(e); } }); disposeSupport.attach(element, "dragleave", function(e) { if (!isValidFileDrag(e)) { return; } options.onLeave(e); var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); // do not fire when moving a mouse over a descendant if (qq(this).contains(relatedTarget)) { return; } options.onLeaveNotDescendants(e); }); disposeSupport.attach(element, "drop", function(e) { if (!isOrSetDropDisabled()) { if (!isValidFileDrag(e)) { return; } e.preventDefault(); e.stopPropagation(); options.onDrop(e); triggerHidezonesEvent(); } }); } disableDropOutside(); attachEvents(); qq.extend(this, { dropDisabled: function(isDisabled) { return isOrSetDropDisabled(isDisabled); }, dispose: function() { disposeSupport.dispose(); }, getElement: function() { return element; } }); this._testing = {}; this._testing.isValidFileDrag = isValidFileDrag; }; ================================================ FILE: client/js/error/error.js ================================================ /* globals qq */ /** * Fine Uploader top-level Error container. Inherits from `Error`. */ (function() { "use strict"; qq.Error = function(message) { this.message = "[Fine Uploader " + qq.version + "] " + message; }; qq.Error.prototype = new Error(); }()); ================================================ FILE: client/js/export.js ================================================ /* globals define, module, global, qq */ (function() { "use strict"; if (typeof define === "function" && define.amd) { define(function() { return qq; }); } else if (typeof module !== "undefined" && module.exports) { module.exports = qq; } else { global.qq = qq; } }()); ================================================ FILE: client/js/features.js ================================================ /* globals qq */ qq.supportedFeatures = (function() { "use strict"; var supportsUploading, supportsUploadingBlobs, supportsFileDrop, supportsAjaxFileUploading, supportsFolderDrop, supportsChunking, supportsResume, supportsUploadViaPaste, supportsUploadCors, supportsDeleteFileXdr, supportsDeleteFileCorsXhr, supportsDeleteFileCors, supportsFolderSelection, supportsImagePreviews, supportsUploadProgress; function testSupportsFileInputElement() { var supported = true, tempInput; try { tempInput = document.createElement("input"); tempInput.type = "file"; qq(tempInput).hide(); if (tempInput.disabled) { supported = false; } } catch (ex) { supported = false; } return supported; } //only way to test for complete Clipboard API support at this time function isChrome14OrHigher() { return (qq.chrome() || qq.opera()) && navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined; } //Ensure we can send cross-origin `XMLHttpRequest`s function isCrossOriginXhrSupported() { if (window.XMLHttpRequest) { var xhr = qq.createXhrInstance(); //Commonly accepted test for XHR CORS support. return xhr.withCredentials !== undefined; } return false; } //Test for (terrible) cross-origin ajax transport fallback for IE9 and IE8 function isXdrSupported() { return window.XDomainRequest !== undefined; } // CORS Ajax requests are supported if it is either possible to send credentialed `XMLHttpRequest`s, // or if `XDomainRequest` is an available alternative. function isCrossOriginAjaxSupported() { if (isCrossOriginXhrSupported()) { return true; } return isXdrSupported(); } function isFolderSelectionSupported() { // We know that folder selection is only supported in Chrome via this proprietary attribute for now return document.createElement("input").webkitdirectory !== undefined; } function isLocalStorageSupported() { try { return !!window.localStorage && // unpatched versions of IE10/11 have buggy impls of localStorage where setItem is a string qq.isFunction(window.localStorage.setItem); } catch (error) { // probably caught a security exception, so no localStorage for you return false; } } function isDragAndDropSupported() { var span = document.createElement("span"); return ("draggable" in span || ("ondragstart" in span && "ondrop" in span)) && !qq.android() && !qq.ios(); } supportsUploading = testSupportsFileInputElement(); supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported(); supportsUploadingBlobs = supportsAjaxFileUploading && !qq.androidStock(); supportsFileDrop = supportsAjaxFileUploading && isDragAndDropSupported(); // adapted from https://stackoverflow.com/a/23278460/486979 supportsFolderDrop = supportsFileDrop && (function() { var input = document.createElement("input"); input.type = "file"; return !!("webkitdirectory" in (input || document.querySelectorAll("input[type=file]")[0])); }()); supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported(); supportsResume = supportsAjaxFileUploading && supportsChunking && isLocalStorageSupported(); supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher(); supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading); supportsDeleteFileCorsXhr = isCrossOriginXhrSupported(); supportsDeleteFileXdr = isXdrSupported(); supportsDeleteFileCors = isCrossOriginAjaxSupported(); supportsFolderSelection = isFolderSelectionSupported(); supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined; supportsUploadProgress = (function() { if (supportsAjaxFileUploading) { return !qq.androidStock() && !qq.iosChrome(); } return false; }()); return { ajaxUploading: supportsAjaxFileUploading, blobUploading: supportsUploadingBlobs, canDetermineSize: supportsAjaxFileUploading, chunking: supportsChunking, deleteFileCors: supportsDeleteFileCors, deleteFileCorsXdr: supportsDeleteFileXdr, //NOTE: will also return true in IE10, where XDR is also supported deleteFileCorsXhr: supportsDeleteFileCorsXhr, dialogElement: !!window.HTMLDialogElement, fileDrop: supportsFileDrop, folderDrop: supportsFolderDrop, folderSelection: supportsFolderSelection, imagePreviews: supportsImagePreviews, imageValidation: supportsImagePreviews, itemSizeValidation: supportsAjaxFileUploading, pause: supportsChunking, progressBar: supportsUploadProgress, resume: supportsResume, scaling: supportsImagePreviews && supportsUploadingBlobs, tiffPreviews: qq.safari(), // Not the best solution, but simple and probably accurate enough (for now) unlimitedScaledImageSize: !qq.ios(), // false simply indicates that there is some known limit uploading: supportsUploading, uploadCors: supportsUploadCors, uploadCustomHeaders: supportsAjaxFileUploading, uploadNonMultipart: supportsAjaxFileUploading, uploadViaPaste: supportsUploadViaPaste }; }()); ================================================ FILE: client/js/form-support.js ================================================ /* globals qq */ /** * Module that handles support for existing forms. * * @param options Options passed from the integrator-supplied options related to form support. * @param startUpload Callback to invoke when files "stored" should be uploaded. * @param log Proxy for the logger * @constructor */ qq.FormSupport = function(options, startUpload, log) { "use strict"; var self = this, interceptSubmit = options.interceptSubmit, formEl = options.element, autoUpload = options.autoUpload; // Available on the public API associated with this module. qq.extend(this, { // To be used by the caller to determine if the endpoint will be determined by some processing // that occurs in this module, such as if the form has an action attribute. // Ignore if `attachToForm === false`. newEndpoint: null, // To be used by the caller to determine if auto uploading should be allowed. // Ignore if `attachToForm === false`. newAutoUpload: autoUpload, // true if a form was detected and is being tracked by this module attachedToForm: false, // Returns an object with names and values for all valid form elements associated with the attached form. getFormInputsAsObject: function() { /* jshint eqnull:true */ if (formEl == null) { return null; } return self._form2Obj(formEl); } }); // If the form contains an action attribute, this should be the new upload endpoint. function determineNewEndpoint(formEl) { if (formEl.getAttribute("action")) { self.newEndpoint = formEl.getAttribute("action"); } } // Return true only if the form is valid, or if we cannot make this determination. // If the form is invalid, ensure invalid field(s) are highlighted in the UI. function validateForm(formEl, nativeSubmit) { if (formEl.checkValidity && !formEl.checkValidity()) { log("Form did not pass validation checks - will not upload.", "error"); nativeSubmit(); } else { return true; } } // Intercept form submit attempts, unless the integrator has told us not to do this. function maybeUploadOnSubmit(formEl) { var nativeSubmit = formEl.submit; // Intercept and squelch submit events. qq(formEl).attach("submit", function(event) { event = event || window.event; if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } validateForm(formEl, nativeSubmit) && startUpload(); }); // The form's `submit()` function may be called instead (i.e. via jQuery.submit()). // Intercept that too. formEl.submit = function() { validateForm(formEl, nativeSubmit) && startUpload(); }; } // If the element value passed from the uploader is a string, assume it is an element ID - select it. // The rest of the code in this module depends on this being an HTMLElement. function determineFormEl(formEl) { if (formEl) { if (qq.isString(formEl)) { formEl = document.getElementById(formEl); } if (formEl) { log("Attaching to form element."); determineNewEndpoint(formEl); interceptSubmit && maybeUploadOnSubmit(formEl); } } return formEl; } formEl = determineFormEl(formEl); this.attachedToForm = !!formEl; }; qq.extend(qq.FormSupport.prototype, { // Converts all relevant form fields to key/value pairs. This is meant to mimic the data a browser will // construct from a given form when the form is submitted. _form2Obj: function(form) { "use strict"; var obj = {}, notIrrelevantType = function(type) { var irrelevantTypes = [ "button", "image", "reset", "submit" ]; return qq.indexOf(irrelevantTypes, type.toLowerCase()) < 0; }, radioOrCheckbox = function(type) { return qq.indexOf(["checkbox", "radio"], type.toLowerCase()) >= 0; }, ignoreValue = function(el) { if (radioOrCheckbox(el.type) && !el.checked) { return true; } return el.disabled && el.type.toLowerCase() !== "hidden"; }, selectValue = function(select) { var value = null; qq.each(qq(select).children(), function(idx, child) { if (child.tagName.toLowerCase() === "option" && child.selected) { value = child.value; return false; } }); return value; }; qq.each(form.elements, function(idx, el) { if ((qq.isInput(el, true) || el.tagName.toLowerCase() === "textarea") && notIrrelevantType(el.type) && !ignoreValue(el)) { obj[el.name] = el.value; } else if (el.tagName.toLowerCase() === "select" && !ignoreValue(el)) { var value = selectValue(el); if (value !== null) { obj[el.name] = value; } } }); return obj; } }); ================================================ FILE: client/js/identify.js ================================================ /*globals qq */ qq.Identify = function(fileOrBlob, log) { "use strict"; function isIdentifiable(magicBytes, questionableBytes) { var identifiable = false, magicBytesEntries = [].concat(magicBytes); qq.each(magicBytesEntries, function(idx, magicBytesArrayEntry) { if (questionableBytes.indexOf(magicBytesArrayEntry) === 0) { identifiable = true; return false; } }); return identifiable; } qq.extend(this, { /** * Determines if a Blob can be displayed natively in the current browser. This is done by reading magic * bytes in the beginning of the file, so this is an asynchronous operation. Before we attempt to read the * file, we will examine the blob's type attribute to save CPU cycles. * * @returns {qq.Promise} Promise that is fulfilled when identification is complete. * If successful, the MIME string is passed to the success handler. */ isPreviewable: function() { var self = this, identifier = new qq.Promise(), previewable = false, name = fileOrBlob.name === undefined ? "blob" : fileOrBlob.name; log(qq.format("Attempting to determine if {} can be rendered in this browser", name)); log("First pass: check type attribute of blob object."); if (this.isPreviewableSync()) { log("Second pass: check for magic bytes in file header."); qq.readBlobToHex(fileOrBlob, 0, 4).then(function(hex) { qq.each(self.PREVIEWABLE_MIME_TYPES, function(mime, bytes) { if (isIdentifiable(bytes, hex)) { // Safari is the only supported browser that can deal with TIFFs natively, // so, if this is a TIFF and the UA isn't Safari, declare this file "non-previewable". if (mime !== "image/tiff" || qq.supportedFeatures.tiffPreviews) { previewable = true; identifier.success(mime); } return false; } }); log(qq.format("'{}' is {} able to be rendered in this browser", name, previewable ? "" : "NOT")); if (!previewable) { identifier.failure(); } }, function() { log("Error reading file w/ name '" + name + "'. Not able to be rendered in this browser."); identifier.failure(); }); } else { identifier.failure(); } return identifier; }, /** * Determines if a Blob can be displayed natively in the current browser. This is done by checking the * blob's type attribute. This is a synchronous operation, useful for situations where an asynchronous operation * would be challenging to support. Note that the blob's type property is not as accurate as reading the * file's magic bytes. * * @returns {Boolean} true if the blob can be rendered in the current browser */ isPreviewableSync: function() { var fileMime = fileOrBlob.type, // Assumption: This will only ever be executed in browsers that support `Object.keys`. isRecognizedImage = qq.indexOf(Object.keys(this.PREVIEWABLE_MIME_TYPES), fileMime) >= 0, previewable = false, name = fileOrBlob.name === undefined ? "blob" : fileOrBlob.name; if (isRecognizedImage) { if (fileMime === "image/tiff") { previewable = qq.supportedFeatures.tiffPreviews; } else { previewable = true; } } !previewable && log(name + " is not previewable in this browser per the blob's type attr"); return previewable; } }); }; qq.Identify.prototype.PREVIEWABLE_MIME_TYPES = { "image/jpeg": "ffd8ff", "image/gif": "474946", "image/png": "89504e", "image/bmp": "424d", "image/tiff": ["49492a00", "4d4d002a"] }; ================================================ FILE: client/js/iframe.xss.response.js ================================================ (function() { "use strict"; var match = /(\{.*\})/.exec(document.body.innerHTML); if (match) { parent.postMessage(match[1], "*"); } }()); ================================================ FILE: client/js/image-support/exif.js ================================================ /*globals qq */ /** * EXIF image data parser. Currently only parses the Orientation tag value, * but this may be expanded to other tags in the future. * * @param fileOrBlob Attempt to parse EXIF data in this `Blob` * @constructor */ qq.Exif = function(fileOrBlob, log) { "use strict"; // Orientation is the only tag parsed here at this time. var TAG_IDS = [274], TAG_INFO = { 274: { name: "Orientation", bytes: 2 } }; // Convert a little endian (hex string) to big endian (decimal). function parseLittleEndian(hex) { var result = 0, pow = 0; while (hex.length > 0) { result += parseInt(hex.substring(0, 2), 16) * Math.pow(2, pow); hex = hex.substring(2, hex.length); pow += 8; } return result; } // Find the byte offset, of Application Segment 1 (EXIF). // External callers need not supply any arguments. function seekToApp1(offset, promise) { var theOffset = offset, thePromise = promise; if (theOffset === undefined) { theOffset = 2; thePromise = new qq.Promise(); } qq.readBlobToHex(fileOrBlob, theOffset, 4).then(function(hex) { var match = /^ffe([0-9])/.exec(hex), segmentLength; if (match) { if (match[1] !== "1") { segmentLength = parseInt(hex.slice(4, 8), 16); seekToApp1(theOffset + segmentLength + 2, thePromise); } else { thePromise.success(theOffset); } } else { thePromise.failure("No EXIF header to be found!"); } }); return thePromise; } // Find the byte offset of Application Segment 1 (EXIF) for valid JPEGs only. function getApp1Offset() { var promise = new qq.Promise(); qq.readBlobToHex(fileOrBlob, 0, 6).then(function(hex) { if (hex.indexOf("ffd8") !== 0) { promise.failure("Not a valid JPEG!"); } else { seekToApp1().then(function(offset) { promise.success(offset); }, function(error) { promise.failure(error); }); } }); return promise; } // Determine the byte ordering of the EXIF header. function isLittleEndian(app1Start) { var promise = new qq.Promise(); qq.readBlobToHex(fileOrBlob, app1Start + 10, 2).then(function(hex) { promise.success(hex === "4949"); }); return promise; } // Determine the number of directory entries in the EXIF header. function getDirEntryCount(app1Start, littleEndian) { var promise = new qq.Promise(); qq.readBlobToHex(fileOrBlob, app1Start + 18, 2).then(function(hex) { if (littleEndian) { return promise.success(parseLittleEndian(hex)); } else { promise.success(parseInt(hex, 16)); } }); return promise; } // Get the IFD portion of the EXIF header as a hex string. function getIfd(app1Start, dirEntries) { var offset = app1Start + 20, bytes = dirEntries * 12; return qq.readBlobToHex(fileOrBlob, offset, bytes); } // Obtain an array of all directory entries (as hex strings) in the EXIF header. function getDirEntries(ifdHex) { var entries = [], offset = 0; while (offset + 24 <= ifdHex.length) { entries.push(ifdHex.slice(offset, offset + 24)); offset += 24; } return entries; } // Obtain values for all relevant tags and return them. function getTagValues(littleEndian, dirEntries) { var TAG_VAL_OFFSET = 16, tagsToFind = qq.extend([], TAG_IDS), vals = {}; qq.each(dirEntries, function(idx, entry) { var idHex = entry.slice(0, 4), id = littleEndian ? parseLittleEndian(idHex) : parseInt(idHex, 16), tagsToFindIdx = tagsToFind.indexOf(id), tagValHex, tagName, tagValLength; if (tagsToFindIdx >= 0) { tagName = TAG_INFO[id].name; tagValLength = TAG_INFO[id].bytes; tagValHex = entry.slice(TAG_VAL_OFFSET, TAG_VAL_OFFSET + (tagValLength * 2)); vals[tagName] = littleEndian ? parseLittleEndian(tagValHex) : parseInt(tagValHex, 16); tagsToFind.splice(tagsToFindIdx, 1); } if (tagsToFind.length === 0) { return false; } }); return vals; } qq.extend(this, { /** * Attempt to parse the EXIF header for the `Blob` associated with this instance. * * @returns {qq.Promise} To be fulfilled when the parsing is complete. * If successful, the parsed EXIF header as an object will be included. */ parse: function() { var parser = new qq.Promise(), onParseFailure = function(message) { log(qq.format("EXIF header parse failed: '{}' ", message)); parser.failure(message); }; getApp1Offset().then(function(app1Offset) { log(qq.format("Moving forward with EXIF header parsing for '{}'", fileOrBlob.name === undefined ? "blob" : fileOrBlob.name)); isLittleEndian(app1Offset).then(function(littleEndian) { log(qq.format("EXIF Byte order is {} endian", littleEndian ? "little" : "big")); getDirEntryCount(app1Offset, littleEndian).then(function(dirEntryCount) { log(qq.format("Found {} APP1 directory entries", dirEntryCount)); getIfd(app1Offset, dirEntryCount).then(function(ifdHex) { var dirEntries = getDirEntries(ifdHex), tagValues = getTagValues(littleEndian, dirEntries); log("Successfully parsed some EXIF tags"); parser.success(tagValues); }, onParseFailure); }, onParseFailure); }, onParseFailure); }, onParseFailure); return parser; } }); /**/ this._testing = {}; this._testing.parseLittleEndian = parseLittleEndian; /**/ }; ================================================ FILE: client/js/image-support/image.js ================================================ /*globals qq */ /** * Draws a thumbnail of a Blob/File/URL onto an or . * * @constructor */ qq.ImageGenerator = function(log) { "use strict"; function isImg(el) { return el.tagName.toLowerCase() === "img"; } function isCanvas(el) { return el.tagName.toLowerCase() === "canvas"; } function isImgCorsSupported() { return new Image().crossOrigin !== undefined; } function isCanvasSupported() { var canvas = document.createElement("canvas"); return canvas.getContext && canvas.getContext("2d"); } // This is only meant to determine the MIME type of a renderable image file. // It is used to ensure images drawn from a URL that have transparent backgrounds // are rendered correctly, among other things. function determineMimeOfFileName(nameWithPath) { /*jshint -W015 */ var pathSegments = nameWithPath.split("/"), name = pathSegments[pathSegments.length - 1].split("?")[0], extension = qq.getExtension(name); extension = extension && extension.toLowerCase(); switch (extension) { case "jpeg": case "jpg": return "image/jpeg"; case "png": return "image/png"; case "bmp": return "image/bmp"; case "gif": return "image/gif"; case "tiff": case "tif": return "image/tiff"; } } // This will likely not work correctly in IE8 and older. // It's only used as part of a formula to determine // if a canvas can be used to scale a server-hosted thumbnail. // If canvas isn't supported by the UA (IE8 and older) // this method should not even be called. function isCrossOrigin(url) { var targetAnchor = document.createElement("a"), targetProtocol, targetHostname, targetPort; targetAnchor.href = url; targetProtocol = targetAnchor.protocol; targetPort = targetAnchor.port; targetHostname = targetAnchor.hostname; if (targetProtocol.toLowerCase() !== window.location.protocol.toLowerCase()) { return true; } if (targetHostname.toLowerCase() !== window.location.hostname.toLowerCase()) { return true; } // IE doesn't take ports into consideration when determining if two endpoints are same origin. if (targetPort !== window.location.port && !qq.ie()) { return true; } return false; } function registerImgLoadListeners(img, promise) { img.onload = function() { img.onload = null; img.onerror = null; promise.success(img); }; img.onerror = function() { img.onload = null; img.onerror = null; log("Problem drawing thumbnail!", "error"); promise.failure(img, "Problem drawing thumbnail!"); }; } function registerCanvasDrawImageListener(canvas, promise) { // The image is drawn on the canvas by a third-party library, // and we want to know when this is completed. Since the library // may invoke drawImage many times in a loop, we need to be called // back when the image is fully rendered. So, we are expecting the // code that draws this image to follow a convention that involves a // function attached to the canvas instance be invoked when it is done. canvas.qqImageRendered = function() { promise.success(canvas); }; } // Fulfills a `qq.Promise` when an image has been drawn onto the target, // whether that is a or an . The attempt is considered a // failure if the target is not an or a , or if the drawing // attempt was not successful. function registerThumbnailRenderedListener(imgOrCanvas, promise) { var registered = isImg(imgOrCanvas) || isCanvas(imgOrCanvas); if (isImg(imgOrCanvas)) { registerImgLoadListeners(imgOrCanvas, promise); } else if (isCanvas(imgOrCanvas)) { registerCanvasDrawImageListener(imgOrCanvas, promise); } else { promise.failure(imgOrCanvas); log(qq.format("Element container of type {} is not supported!", imgOrCanvas.tagName), "error"); } return registered; } // Draw a preview iff the current UA can natively display it. // Also rotate the image if necessary. function draw(fileOrBlob, container, options) { var drawPreview = new qq.Promise(), identifier = new qq.Identify(fileOrBlob, log), maxSize = options.maxSize, // jshint eqnull:true orient = options.orient == null ? true : options.orient, megapixErrorHandler = function() { container.onerror = null; container.onload = null; log("Could not render preview, file may be too large!", "error"); drawPreview.failure(container, "Browser cannot render image!"); }; identifier.isPreviewable().then( function(mime) { // If options explicitly specify that Orientation is not desired, // replace the orient task with a dummy promise that "succeeds" immediately. var dummyExif = { parse: function() { return new qq.Promise().success(); } }, exif = orient ? new qq.Exif(fileOrBlob, log) : dummyExif, mpImg = new qq.MegaPixImage(fileOrBlob, megapixErrorHandler); if (registerThumbnailRenderedListener(container, drawPreview)) { exif.parse().then( function(exif) { var orientation = exif && exif.Orientation; mpImg.render(container, { maxWidth: maxSize, maxHeight: maxSize, orientation: orientation, mime: mime, resize: options.customResizeFunction }); }, function(failureMsg) { log(qq.format("EXIF data could not be parsed ({}). Assuming orientation = 1.", failureMsg)); mpImg.render(container, { maxWidth: maxSize, maxHeight: maxSize, mime: mime, resize: options.customResizeFunction }); } ); } }, function() { log("Not previewable"); drawPreview.failure(container, "Not previewable"); } ); return drawPreview; } function drawOnCanvasOrImgFromUrl(url, canvasOrImg, draw, maxSize, customResizeFunction) { var tempImg = new Image(), tempImgRender = new qq.Promise(); registerThumbnailRenderedListener(tempImg, tempImgRender); if (isCrossOrigin(url)) { tempImg.crossOrigin = "anonymous"; } tempImg.src = url; tempImgRender.then( function rendered() { registerThumbnailRenderedListener(canvasOrImg, draw); var mpImg = new qq.MegaPixImage(tempImg); mpImg.render(canvasOrImg, { maxWidth: maxSize, maxHeight: maxSize, mime: determineMimeOfFileName(url), resize: customResizeFunction }); }, draw.failure ); } function drawOnImgFromUrlWithCssScaling(url, img, draw, maxSize) { registerThumbnailRenderedListener(img, draw); // NOTE: The fact that maxWidth/height is set on the thumbnail for scaled images // that must drop back to CSS is known and exploited by the templating module. // In this module, we pre-render "waiting" thumbs for all files immediately after they // are submitted, and we must be sure to pass any style associated with the "waiting" preview. qq(img).css({ maxWidth: maxSize + "px", maxHeight: maxSize + "px" }); img.src = url; } // Draw a (server-hosted) thumbnail given a URL. // This will optionally scale the thumbnail as well. // It attempts to use to scale, but will fall back // to max-width and max-height style properties if the UA // doesn't support canvas or if the images is cross-domain and // the UA doesn't support the crossorigin attribute on img tags, // which is required to scale a cross-origin image using & // then export it back to an . function drawFromUrl(url, container, options) { var draw = new qq.Promise(), scale = options.scale, maxSize = scale ? options.maxSize : null; // container is an img, scaling needed if (scale && isImg(container)) { // Iff canvas is available in this UA, try to use it for scaling. // Otherwise, fall back to CSS scaling if (isCanvasSupported()) { // Attempt to use for image scaling, // but we must fall back to scaling via CSS/styles // if this is a cross-origin image and the UA doesn't support CORS. if (isCrossOrigin(url) && !isImgCorsSupported()) { drawOnImgFromUrlWithCssScaling(url, container, draw, maxSize); } else { drawOnCanvasOrImgFromUrl(url, container, draw, maxSize); } } else { drawOnImgFromUrlWithCssScaling(url, container, draw, maxSize); } } // container is a canvas, scaling optional else if (isCanvas(container)) { drawOnCanvasOrImgFromUrl(url, container, draw, maxSize); } // container is an img & no scaling: just set the src attr to the passed url else if (registerThumbnailRenderedListener(container, draw)) { container.src = url; } return draw; } qq.extend(this, { /** * Generate a thumbnail. Depending on the arguments, this may either result in * a client-side rendering of an image (if a `Blob` is supplied) or a server-generated * image that may optionally be scaled client-side using or CSS/styles (as a fallback). * * @param fileBlobOrUrl a `File`, `Blob`, or a URL pointing to the image * @param container or to contain the preview * @param options possible properties include `maxSize` (int), `orient` (bool - default true), resize` (bool - default true), and `customResizeFunction`. * @returns qq.Promise fulfilled when the preview has been drawn, or the attempt has failed */ generate: function(fileBlobOrUrl, container, options) { if (qq.isString(fileBlobOrUrl)) { log("Attempting to update thumbnail based on server response."); return drawFromUrl(fileBlobOrUrl, container, options || {}); } else { log("Attempting to draw client-side image preview."); return draw(fileBlobOrUrl, container, options || {}); } } }); /**/ this._testing = {}; this._testing.isImg = isImg; this._testing.isCanvas = isCanvas; this._testing.isCrossOrigin = isCrossOrigin; this._testing.determineMimeOfFileName = determineMimeOfFileName; /**/ }; ================================================ FILE: client/js/image-support/megapix-image.js ================================================ /*global qq, define */ /*jshint strict:false,bitwise:false,nonew:false,asi:true,-W064,-W116,-W089 */ /** * Mega pixel image rendering library for iOS6+ * * Fixes iOS6+'s image file rendering issue for large size image (over mega-pixel), * which causes unexpected subsampling when drawing it in canvas. * By using this library, you can safely render the image with proper stretching. * * Copyright (c) 2012 Shinichi Tomita * Released under the MIT license * * Heavily modified by Widen for Fine Uploader */ (function() { /** * Detect subsampling in loaded image. * In iOS, larger images than 2M pixels may be subsampled in rendering. */ function detectSubsampling(img) { var iw = img.naturalWidth, ih = img.naturalHeight, canvas = document.createElement("canvas"), ctx; if (iw * ih > 1024 * 1024) { // subsampling may happen over megapixel image canvas.width = canvas.height = 1; ctx = canvas.getContext("2d"); ctx.drawImage(img, -iw + 1, 0); // subsampled image becomes half smaller in rendering size. // check alpha channel value to confirm image is covering edge pixel or not. // if alpha value is 0 image is not covering, hence subsampled. return ctx.getImageData(0, 0, 1, 1).data[3] === 0; } else { return false; } } /** * Detecting vertical squash in loaded image. * Fixes a bug which squash image vertically while drawing into canvas for some images. */ function detectVerticalSquash(img, iw, ih) { var canvas = document.createElement("canvas"), sy = 0, ey = ih, py = ih, ctx, data, alpha, ratio; canvas.width = 1; canvas.height = ih; ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); data = ctx.getImageData(0, 0, 1, ih).data; // search image edge pixel position in case it is squashed vertically. while (py > sy) { alpha = data[(py - 1) * 4 + 3]; if (alpha === 0) { ey = py; } else { sy = py; } py = (ey + sy) >> 1; } ratio = (py / ih); return (ratio === 0) ? 1 : ratio; } /** * Rendering image element (with resizing) and get its data URL */ function renderImageToDataURL(img, blob, options, doSquash) { var canvas = document.createElement("canvas"), mime = options.mime || "image/jpeg", promise = new qq.Promise(); renderImageToCanvas(img, blob, canvas, options, doSquash) .then(function() { promise.success( canvas.toDataURL(mime, options.quality || 0.8) ); }); return promise; } function maybeCalculateDownsampledDimensions(spec) { var maxPixels = 5241000; //iOS specific value if (!qq.ios()) { throw new qq.Error("Downsampled dimensions can only be reliably calculated for iOS!"); } if (spec.origHeight * spec.origWidth > maxPixels) { return { newHeight: Math.round(Math.sqrt(maxPixels * (spec.origHeight / spec.origWidth))), newWidth: Math.round(Math.sqrt(maxPixels * (spec.origWidth / spec.origHeight))) }; } } /** * Rendering image element (with resizing) into the canvas element */ function renderImageToCanvas(img, blob, canvas, options, doSquash) { var iw = img.naturalWidth, ih = img.naturalHeight, width = options.width, height = options.height, ctx = canvas.getContext("2d"), promise = new qq.Promise(), modifiedDimensions; ctx.save(); if (options.resize) { return renderImageToCanvasWithCustomResizer({ blob: blob, canvas: canvas, image: img, imageHeight: ih, imageWidth: iw, orientation: options.orientation, resize: options.resize, targetHeight: height, targetWidth: width }); } if (!qq.supportedFeatures.unlimitedScaledImageSize) { modifiedDimensions = maybeCalculateDownsampledDimensions({ origWidth: width, origHeight: height }); if (modifiedDimensions) { qq.log(qq.format("Had to reduce dimensions due to device limitations from {}w / {}h to {}w / {}h", width, height, modifiedDimensions.newWidth, modifiedDimensions.newHeight), "warn"); width = modifiedDimensions.newWidth; height = modifiedDimensions.newHeight; } } transformCoordinate(canvas, width, height, options.orientation); // Fine Uploader specific: Save some CPU cycles if not using iOS // Assumption: This logic is only needed to overcome iOS image sampling issues if (qq.ios()) { (function() { if (detectSubsampling(img)) { iw /= 2; ih /= 2; } var d = 1024, // size of tiling canvas tmpCanvas = document.createElement("canvas"), vertSquashRatio = doSquash ? detectVerticalSquash(img, iw, ih) : 1, dw = Math.ceil(d * width / iw), dh = Math.ceil(d * height / ih / vertSquashRatio), sy = 0, dy = 0, tmpCtx, sx, dx; tmpCanvas.width = tmpCanvas.height = d; tmpCtx = tmpCanvas.getContext("2d"); while (sy < ih) { sx = 0; dx = 0; while (sx < iw) { tmpCtx.clearRect(0, 0, d, d); tmpCtx.drawImage(img, -sx, -sy); ctx.drawImage(tmpCanvas, 0, 0, d, d, dx, dy, dw, dh); sx += d; dx += dw; } sy += d; dy += dh; } ctx.restore(); tmpCanvas = tmpCtx = null; }()); } else { ctx.drawImage(img, 0, 0, width, height); } canvas.qqImageRendered && canvas.qqImageRendered(); promise.success(); return promise; } function renderImageToCanvasWithCustomResizer(resizeInfo) { var blob = resizeInfo.blob, image = resizeInfo.image, imageHeight = resizeInfo.imageHeight, imageWidth = resizeInfo.imageWidth, orientation = resizeInfo.orientation, promise = new qq.Promise(), resize = resizeInfo.resize, sourceCanvas = document.createElement("canvas"), sourceCanvasContext = sourceCanvas.getContext("2d"), targetCanvas = resizeInfo.canvas, targetHeight = resizeInfo.targetHeight, targetWidth = resizeInfo.targetWidth; transformCoordinate(sourceCanvas, imageWidth, imageHeight, orientation); targetCanvas.height = targetHeight; targetCanvas.width = targetWidth; sourceCanvasContext.drawImage(image, 0, 0); resize({ blob: blob, height: targetHeight, image: image, sourceCanvas: sourceCanvas, targetCanvas: targetCanvas, width: targetWidth }) .then( function success() { targetCanvas.qqImageRendered && targetCanvas.qqImageRendered(); promise.success(); }, promise.failure ); return promise; } /** * Transform canvas coordination according to specified frame size and orientation * Orientation value is from EXIF tag */ function transformCoordinate(canvas, width, height, orientation) { switch (orientation) { case 5: case 6: case 7: case 8: canvas.width = height; canvas.height = width; break; default: canvas.width = width; canvas.height = height; } var ctx = canvas.getContext("2d"); switch (orientation) { case 2: // horizontal flip ctx.translate(width, 0); ctx.scale(-1, 1); break; case 3: // 180 rotate left ctx.translate(width, height); ctx.rotate(Math.PI); break; case 4: // vertical flip ctx.translate(0, height); ctx.scale(1, -1); break; case 5: // vertical flip + 90 rotate right ctx.rotate(0.5 * Math.PI); ctx.scale(1, -1); break; case 6: // 90 rotate right ctx.rotate(0.5 * Math.PI); ctx.translate(0, -height); break; case 7: // horizontal flip + 90 rotate right ctx.rotate(0.5 * Math.PI); ctx.translate(width, -height); ctx.scale(-1, 1); break; case 8: // 90 rotate left ctx.rotate(-0.5 * Math.PI); ctx.translate(-width, 0); break; default: break; } } /** * MegaPixImage class */ function MegaPixImage(srcImage, errorCallback) { var self = this; if (window.Blob && srcImage instanceof Blob) { (function() { var img = new Image(), URL = window.URL && window.URL.createObjectURL ? window.URL : window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL : null; if (!URL) { throw Error("No createObjectURL function found to create blob url"); } img.src = URL.createObjectURL(srcImage); self.blob = srcImage; srcImage = img; }()); } if (!srcImage.naturalWidth && !srcImage.naturalHeight) { srcImage.onload = function() { var listeners = self.imageLoadListeners; if (listeners) { self.imageLoadListeners = null; // IE11 doesn't reliably report actual image dimensions immediately after onload for small files, // so let's push this to the end of the UI thread queue. setTimeout(function() { for (var i = 0, len = listeners.length; i < len; i++) { listeners[i](); } }, 0); } }; srcImage.onerror = errorCallback; this.imageLoadListeners = []; } this.srcImage = srcImage; } /** * Rendering megapix image into specified target element */ MegaPixImage.prototype.render = function(target, options) { options = options || {}; var self = this, imgWidth = this.srcImage.naturalWidth, imgHeight = this.srcImage.naturalHeight, width = options.width, height = options.height, maxWidth = options.maxWidth, maxHeight = options.maxHeight, doSquash = !this.blob || this.blob.type === "image/jpeg", tagName = target.tagName.toLowerCase(), opt; if (this.imageLoadListeners) { this.imageLoadListeners.push(function() { self.render(target, options); }); return; } if (width && !height) { height = (imgHeight * width / imgWidth) << 0; } else if (height && !width) { width = (imgWidth * height / imgHeight) << 0; } else { width = imgWidth; height = imgHeight; } if (maxWidth && width > maxWidth) { width = maxWidth; height = (imgHeight * width / imgWidth) << 0; } if (maxHeight && height > maxHeight) { height = maxHeight; width = (imgWidth * height / imgHeight) << 0; } opt = { width: width, height: height }, qq.each(options, function(optionsKey, optionsValue) { opt[optionsKey] = optionsValue; }); if (tagName === "img") { (function() { var oldTargetSrc = target.src; renderImageToDataURL(self.srcImage, self.blob, opt, doSquash) .then(function(dataUri) { target.src = dataUri; oldTargetSrc === target.src && target.onload && target.onload(); }); }()); } else if (tagName === "canvas") { renderImageToCanvas(this.srcImage, this.blob, target, opt, doSquash); } if (typeof this.onrender === "function") { this.onrender(target); } }; qq.MegaPixImage = MegaPixImage; })(); ================================================ FILE: client/js/image-support/scaler.js ================================================ /* globals qq, ExifRestorer */ /** * Controls generation of scaled images based on a reference image encapsulated in a `File` or `Blob`. * Scaled images are generated and converted to blobs on-demand. * Multiple scaled images per reference image with varying sizes and other properties are supported. * * @param spec Information about the scaled images to generate. * @param log Logger instance * @constructor */ qq.Scaler = function(spec, log) { "use strict"; var self = this, customResizeFunction = spec.customResizer, includeOriginal = spec.sendOriginal, orient = spec.orient, defaultType = spec.defaultType, defaultQuality = spec.defaultQuality / 100, failedToScaleText = spec.failureText, includeExif = spec.includeExif, sizes = this._getSortedSizes(spec.sizes); // Revealed API for instances of this module qq.extend(this, { // If no targeted sizes have been declared or if this browser doesn't support // client-side image preview generation, there is no scaling to do. enabled: qq.supportedFeatures.scaling && sizes.length > 0, getFileRecords: function(originalFileUuid, originalFileName, originalBlobOrBlobData) { var self = this, records = [], originalBlob = originalBlobOrBlobData.blob ? originalBlobOrBlobData.blob : originalBlobOrBlobData, identifier = new qq.Identify(originalBlob, log); // If the reference file cannot be rendered natively, we can't create scaled versions. if (identifier.isPreviewableSync()) { // Create records for each scaled version & add them to the records array, smallest first. qq.each(sizes, function(idx, sizeRecord) { var outputType = self._determineOutputType({ defaultType: defaultType, requestedType: sizeRecord.type, refType: originalBlob.type }); records.push({ uuid: qq.getUniqueId(), name: self._getName(originalFileName, { name: sizeRecord.name, type: outputType, refType: originalBlob.type }), blob: new qq.BlobProxy(originalBlob, qq.bind(self._generateScaledImage, self, { customResizeFunction: customResizeFunction, maxSize: sizeRecord.maxSize, orient: orient, type: outputType, quality: defaultQuality, failedText: failedToScaleText, includeExif: includeExif, log: log })) }); }); records.push({ uuid: originalFileUuid, name: originalFileName, size: originalBlob.size, blob: includeOriginal ? originalBlob : null }); } else { records.push({ uuid: originalFileUuid, name: originalFileName, size: originalBlob.size, blob: originalBlob }); } return records; }, handleNewFile: function(file, name, uuid, size, fileList, batchId, uuidParamName, api) { var self = this, buttonId = file.qqButtonId || (file.blob && file.blob.qqButtonId), scaledIds = [], originalId = null, addFileToHandler = api.addFileToHandler, uploadData = api.uploadData, paramsStore = api.paramsStore, proxyGroupId = qq.getUniqueId(); qq.each(self.getFileRecords(uuid, name, file), function(idx, record) { var blobSize = record.size, id; if (record.blob instanceof qq.BlobProxy) { blobSize = -1; } id = uploadData.addFile({ uuid: record.uuid, name: record.name, size: blobSize, batchId: batchId, proxyGroupId: proxyGroupId }); if (record.blob instanceof qq.BlobProxy) { scaledIds.push(id); } else { originalId = id; } if (record.blob) { addFileToHandler(id, record.blob); fileList.push({id: id, file: record.blob}); } else { uploadData.setStatus(id, qq.status.REJECTED); } }); // If we are potentially uploading an original file and some scaled versions, // ensure the scaled versions include reference's to the parent's UUID and size // in their associated upload requests. if (originalId !== null) { qq.each(scaledIds, function(idx, scaledId) { var params = { qqparentuuid: uploadData.retrieve({id: originalId}).uuid, qqparentsize: uploadData.retrieve({id: originalId}).size }; // Make sure the UUID for each scaled image is sent with the upload request, // to be consistent (since we may need to ensure it is sent for the original file as well). params[uuidParamName] = uploadData.retrieve({id: scaledId}).uuid; uploadData.setParentId(scaledId, originalId); paramsStore.addReadOnly(scaledId, params); }); // If any scaled images are tied to this parent image, be SURE we send its UUID as an upload request // parameter as well. if (scaledIds.length) { (function() { var param = {}; param[uuidParamName] = uploadData.retrieve({id: originalId}).uuid; paramsStore.addReadOnly(originalId, param); }()); } } } }); }; qq.extend(qq.Scaler.prototype, { scaleImage: function(id, specs, api) { "use strict"; if (!qq.supportedFeatures.scaling) { throw new qq.Error("Scaling is not supported in this browser!"); } var scalingEffort = new qq.Promise(), log = api.log, file = api.getFile(id), uploadData = api.uploadData.retrieve({id: id}), name = uploadData && uploadData.name, uuid = uploadData && uploadData.uuid, scalingOptions = { customResizer: specs.customResizer, sendOriginal: false, orient: specs.orient, defaultType: specs.type || null, defaultQuality: specs.quality, failedToScaleText: "Unable to scale", sizes: [{name: "", maxSize: specs.maxSize}] }, scaler = new qq.Scaler(scalingOptions, log); if (!qq.Scaler || !qq.supportedFeatures.imagePreviews || !file) { scalingEffort.failure(); log("Could not generate requested scaled image for " + id + ". " + "Scaling is either not possible in this browser, or the file could not be located.", "error"); } else { (qq.bind(function() { // Assumption: There will never be more than one record var record = scaler.getFileRecords(uuid, name, file)[0]; if (record && record.blob instanceof qq.BlobProxy) { record.blob.create().then(scalingEffort.success, scalingEffort.failure); } else { log(id + " is not a scalable image!", "error"); scalingEffort.failure(); } }, this)()); } return scalingEffort; }, // NOTE: We cannot reliably determine at this time if the UA supports a specific MIME type for the target format. // image/jpeg and image/png are the only safe choices at this time. _determineOutputType: function(spec) { "use strict"; var requestedType = spec.requestedType, defaultType = spec.defaultType, referenceType = spec.refType; // If a default type and requested type have not been specified, this should be a // JPEG if the original type is a JPEG, otherwise, a PNG. if (!defaultType && !requestedType) { if (referenceType !== "image/jpeg") { return "image/png"; } return referenceType; } // A specified default type is used when a requested type is not specified. if (!requestedType) { return defaultType; } // If requested type is specified, use it, as long as this recognized type is supported by the current UA if (qq.indexOf(Object.keys(qq.Identify.prototype.PREVIEWABLE_MIME_TYPES), requestedType) >= 0) { if (requestedType === "image/tiff") { return qq.supportedFeatures.tiffPreviews ? requestedType : defaultType; } return requestedType; } return defaultType; }, // Get a file name for a generated scaled file record, based on the provided scaled image description _getName: function(originalName, scaledVersionProperties) { "use strict"; var startOfExt = originalName.lastIndexOf("."), versionType = scaledVersionProperties.type || "image/png", referenceType = scaledVersionProperties.refType, scaledName = "", scaledExt = qq.getExtension(originalName), nameAppendage = ""; if (scaledVersionProperties.name && scaledVersionProperties.name.trim().length) { nameAppendage = " (" + scaledVersionProperties.name + ")"; } if (startOfExt >= 0) { scaledName = originalName.substr(0, startOfExt); if (referenceType !== versionType) { scaledExt = versionType.split("/")[1]; } scaledName += nameAppendage + "." + scaledExt; } else { scaledName = originalName + nameAppendage; } return scaledName; }, // We want the smallest scaled file to be uploaded first _getSortedSizes: function(sizes) { "use strict"; sizes = qq.extend([], sizes); return sizes.sort(function(a, b) { if (a.maxSize > b.maxSize) { return 1; } if (a.maxSize < b.maxSize) { return -1; } return 0; }); }, _generateScaledImage: function(spec, sourceFile) { "use strict"; var self = this, customResizeFunction = spec.customResizeFunction, log = spec.log, maxSize = spec.maxSize, orient = spec.orient, type = spec.type, quality = spec.quality, failedText = spec.failedText, includeExif = spec.includeExif && sourceFile.type === "image/jpeg" && type === "image/jpeg", scalingEffort = new qq.Promise(), imageGenerator = new qq.ImageGenerator(log), canvas = document.createElement("canvas"); log("Attempting to generate scaled version for " + sourceFile.name); imageGenerator.generate(sourceFile, canvas, {maxSize: maxSize, orient: orient, customResizeFunction: customResizeFunction}).then(function() { var scaledImageDataUri = canvas.toDataURL(type, quality), signalSuccess = function() { log("Success generating scaled version for " + sourceFile.name); var blob = qq.dataUriToBlob(scaledImageDataUri); scalingEffort.success(blob); }; if (includeExif) { self._insertExifHeader(sourceFile, scaledImageDataUri, log).then(function(scaledImageDataUriWithExif) { scaledImageDataUri = scaledImageDataUriWithExif; signalSuccess(); }, function() { log("Problem inserting EXIF header into scaled image. Using scaled image w/out EXIF data.", "error"); signalSuccess(); }); } else { signalSuccess(); } }, function() { log("Failed attempt to generate scaled version for " + sourceFile.name, "error"); scalingEffort.failure(failedText); }); return scalingEffort; }, // Attempt to insert the original image's EXIF header into a scaled version. _insertExifHeader: function(originalImage, scaledImageDataUri, log) { "use strict"; var reader = new FileReader(), insertionEffort = new qq.Promise(), originalImageDataUri = ""; reader.onload = function() { originalImageDataUri = reader.result; insertionEffort.success(qq.ExifRestorer.restore(originalImageDataUri, scaledImageDataUri)); }; reader.onerror = function() { log("Problem reading " + originalImage.name + " during attempt to transfer EXIF data to scaled version.", "error"); insertionEffort.failure(); }; reader.readAsDataURL(originalImage); return insertionEffort; }, _dataUriToBlob: function(dataUri) { "use strict"; var byteString, mimeString, arrayBuffer, intArray; // convert base64 to raw binary data held in a string if (dataUri.split(",")[0].indexOf("base64") >= 0) { byteString = atob(dataUri.split(",")[1]); } else { byteString = decodeURI(dataUri.split(",")[1]); } // extract the MIME mimeString = dataUri.split(",")[0] .split(":")[1] .split(";")[0]; // write the bytes of the binary string to an ArrayBuffer arrayBuffer = new ArrayBuffer(byteString.length); intArray = new Uint8Array(arrayBuffer); qq.each(byteString, function(idx, character) { intArray[idx] = character.charCodeAt(0); }); return this._createBlob(arrayBuffer, mimeString); }, _createBlob: function(data, mime) { "use strict"; var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder, blobBuilder = BlobBuilder && new BlobBuilder(); if (blobBuilder) { blobBuilder.append(data); return blobBuilder.getBlob(mime); } else { return new Blob([data], {type: mime}); } } }); ================================================ FILE: client/js/image-support/validation.image.js ================================================ /*globals qq*/ /** * Attempts to validate an image, wherever possible. * * @param blob File or Blob representing a user-selecting image. * @param log Uses this to post log messages to the console. * @constructor */ qq.ImageValidation = function(blob, log) { "use strict"; /** * @param limits Object with possible image-related limits to enforce. * @returns {boolean} true if at least one of the limits has a non-zero value */ function hasNonZeroLimits(limits) { var atLeastOne = false; qq.each(limits, function(limit, value) { if (value > 0) { atLeastOne = true; return false; } }); return atLeastOne; } /** * @returns {qq.Promise} The promise is a failure if we can't obtain the width & height. * Otherwise, `success` is called on the returned promise with an object containing * `width` and `height` properties. */ function getWidthHeight() { var sizeDetermination = new qq.Promise(); new qq.Identify(blob, log).isPreviewable().then(function() { var image = new Image(), url = window.URL && window.URL.createObjectURL ? window.URL : window.webkitURL && window.webkitURL.createObjectURL ? window.webkitURL : null; if (url) { image.onerror = function() { log("Cannot determine dimensions for image. May be too large.", "error"); sizeDetermination.failure(); }; image.onload = function() { sizeDetermination.success({ width: this.width, height: this.height }); }; image.src = url.createObjectURL(blob); } else { log("No createObjectURL function available to generate image URL!", "error"); sizeDetermination.failure(); } }, sizeDetermination.failure); return sizeDetermination; } /** * * @param limits Object with possible image-related limits to enforce. * @param dimensions Object containing `width` & `height` properties for the image to test. * @returns {String || undefined} The name of the failing limit. Undefined if no failing limits. */ function getFailingLimit(limits, dimensions) { var failingLimit; qq.each(limits, function(limitName, limitValue) { if (limitValue > 0) { var limitMatcher = /(max|min)(Width|Height)/.exec(limitName), dimensionPropName = limitMatcher[2].charAt(0).toLowerCase() + limitMatcher[2].slice(1), actualValue = dimensions[dimensionPropName]; /*jshint -W015*/ switch (limitMatcher[1]) { case "min": if (actualValue < limitValue) { failingLimit = limitName; return false; } break; case "max": if (actualValue > limitValue) { failingLimit = limitName; return false; } break; } } }); return failingLimit; } /** * Validate the associated blob. * * @param limits * @returns {qq.Promise} `success` is called on the promise is the image is valid or * if the blob is not an image, or if the image is not verifiable. * Otherwise, `failure` with the name of the failing limit. */ this.validate = function(limits) { var validationEffort = new qq.Promise(); log("Attempting to validate image."); if (hasNonZeroLimits(limits)) { getWidthHeight().then(function(dimensions) { var failingLimit = getFailingLimit(limits, dimensions); if (failingLimit) { validationEffort.failure(failingLimit); } else { validationEffort.success(); } }, validationEffort.success); } else { validationEffort.success(); } return validationEffort; }; }; ================================================ FILE: client/js/jquery-dnd.js ================================================ /*globals jQuery, qq*/ (function($) { "use strict"; var rootDataKey = "fineUploaderDnd", $el; function init(options) { if (!options) { options = {}; } options.dropZoneElements = [$el]; var xformedOpts = transformVariables(options); addCallbacks(xformedOpts); dnd(new qq.DragAndDrop(xformedOpts)); return $el; } function dataStore(key, val) { var data = $el.data(rootDataKey); if (val) { if (data === undefined) { data = {}; } data[key] = val; $el.data(rootDataKey, data); } else { if (data === undefined) { return null; } return data[key]; } } function dnd(instanceToStore) { return dataStore("dndInstance", instanceToStore); } function addCallbacks(transformedOpts) { var callbacks = transformedOpts.callbacks = {}; $.each(new qq.DragAndDrop.callbacks(), function(prop, func) { var name = prop, $callbackEl; $callbackEl = $el; callbacks[prop] = function() { var args = Array.prototype.slice.call(arguments), jqueryHandlerResult = $callbackEl.triggerHandler(name, args); return jqueryHandlerResult; }; }); } //transform jQuery objects into HTMLElements, and pass along all other option properties function transformVariables(source, dest) { var xformed, arrayVals; if (dest === undefined) { xformed = {}; } else { xformed = dest; } $.each(source, function(prop, val) { if (val instanceof $) { xformed[prop] = val[0]; } else if ($.isPlainObject(val)) { xformed[prop] = {}; transformVariables(val, xformed[prop]); } else if ($.isArray(val)) { arrayVals = []; $.each(val, function(idx, arrayVal) { if (arrayVal instanceof $) { $.merge(arrayVals, arrayVal); } else { arrayVals.push(arrayVal); } }); xformed[prop] = arrayVals; } else { xformed[prop] = val; } }); if (dest === undefined) { return xformed; } } function isValidCommand(command) { return $.type(command) === "string" && command === "dispose" && dnd()[command] !== undefined; } function delegateCommand(command) { var xformedArgs = [], origArgs = Array.prototype.slice.call(arguments, 1); transformVariables(origArgs, xformedArgs); return dnd()[command].apply(dnd(), xformedArgs); } $.fn.fineUploaderDnd = function(optionsOrCommand) { var self = this, selfArgs = arguments, retVals = []; this.each(function(index, el) { $el = $(el); if (dnd() && isValidCommand(optionsOrCommand)) { retVals.push(delegateCommand.apply(self, selfArgs)); if (self.length === 1) { return false; } } else if (typeof optionsOrCommand === "object" || !optionsOrCommand) { init.apply(self, selfArgs); } else { $.error("Method " + optionsOrCommand + " does not exist in Fine Uploader's DnD module."); } }); if (retVals.length === 1) { return retVals[0]; } else if (retVals.length > 1) { return retVals; } return this; }; }(jQuery)); ================================================ FILE: client/js/jquery-plugin.js ================================================ /*globals jQuery, qq*/ (function($) { "use strict"; var $el, pluginOptions = ["uploaderType", "endpointType"]; function init(options) { var xformedOpts = transformVariables(options || {}), newUploaderInstance = getNewUploaderInstance(xformedOpts); uploader(newUploaderInstance); addCallbacks(xformedOpts, newUploaderInstance); return $el; } function getNewUploaderInstance(params) { var uploaderType = pluginOption("uploaderType"), namespace = pluginOption("endpointType"); // If the integrator has defined a specific type of uploader to load, use that, otherwise assume `qq.FineUploader` if (uploaderType) { // We can determine the correct constructor function to invoke by combining "FineUploader" // with the upper camel cased `uploaderType` value. uploaderType = uploaderType.charAt(0).toUpperCase() + uploaderType.slice(1).toLowerCase(); if (namespace) { return new qq[namespace]["FineUploader" + uploaderType](params); } return new qq["FineUploader" + uploaderType](params); } else { if (namespace) { return new qq[namespace].FineUploader(params); } return new qq.FineUploader(params); } } function dataStore(key, val) { var data = $el.data("fineuploader"); if (val) { if (data === undefined) { data = {}; } data[key] = val; $el.data("fineuploader", data); } else { if (data === undefined) { return null; } return data[key]; } } //the underlying Fine Uploader instance is stored in jQuery's data stored, associated with the element // tied to this instance of the plug-in function uploader(instanceToStore) { return dataStore("uploader", instanceToStore); } function pluginOption(option, optionVal) { return dataStore(option, optionVal); } // Implement all callbacks defined in Fine Uploader as functions that trigger appropriately names events and // return the result of executing the bound handler back to Fine Uploader function addCallbacks(transformedOpts, newUploaderInstance) { var callbacks = transformedOpts.callbacks = {}; $.each(newUploaderInstance._options.callbacks, function(prop, nonJqueryCallback) { var name, callbackEventTarget; name = /^on(\w+)/.exec(prop)[1]; name = name.substring(0, 1).toLowerCase() + name.substring(1); callbackEventTarget = $el; callbacks[prop] = function() { var originalArgs = Array.prototype.slice.call(arguments), transformedArgs = [], nonJqueryCallbackRetVal, jqueryEventCallbackRetVal; $.each(originalArgs, function(idx, arg) { transformedArgs.push(maybeWrapInJquery(arg)); }); nonJqueryCallbackRetVal = nonJqueryCallback.apply(this, originalArgs); try { jqueryEventCallbackRetVal = callbackEventTarget.triggerHandler(name, transformedArgs); } catch (error) { qq.log("Caught error in Fine Uploader jQuery event handler: " + error.message, "error"); } /*jshint -W116*/ if (nonJqueryCallbackRetVal != null) { return nonJqueryCallbackRetVal; } return jqueryEventCallbackRetVal; }; }); newUploaderInstance._options.callbacks = callbacks; } //transform jQuery objects into HTMLElements, and pass along all other option properties function transformVariables(source, dest) { var xformed, arrayVals; if (dest === undefined) { if (source.uploaderType !== "basic") { xformed = { element: $el[0] }; } else { xformed = {}; } } else { xformed = dest; } $.each(source, function(prop, val) { if ($.inArray(prop, pluginOptions) >= 0) { pluginOption(prop, val); } else if (val instanceof $) { xformed[prop] = val[0]; } else if ($.isPlainObject(val)) { xformed[prop] = {}; transformVariables(val, xformed[prop]); } else if ($.isArray(val)) { arrayVals = []; $.each(val, function(idx, arrayVal) { var arrayObjDest = {}; if (arrayVal instanceof $) { $.merge(arrayVals, arrayVal); } else if ($.isPlainObject(arrayVal)) { transformVariables(arrayVal, arrayObjDest); arrayVals.push(arrayObjDest); } else { arrayVals.push(arrayVal); } }); xformed[prop] = arrayVals; } else { xformed[prop] = val; } }); if (dest === undefined) { return xformed; } } function isValidCommand(command) { return $.type(command) === "string" && !command.match(/^_/) && //enforce private methods convention uploader()[command] !== undefined; } // Assuming we have already verified that this is a valid command, call the associated function in the underlying // Fine Uploader instance (passing along the arguments from the caller) and return the result of the call back to the caller function delegateCommand(command) { var xformedArgs = [], origArgs = Array.prototype.slice.call(arguments, 1), retVal; transformVariables(origArgs, xformedArgs); retVal = uploader()[command].apply(uploader(), xformedArgs); return maybeWrapInJquery(retVal); } // If the value is an `HTMLElement` or `HTMLDocument`, wrap it in a `jQuery` object function maybeWrapInJquery(val) { var transformedVal = val; // If the command is returning an `HTMLElement` or `HTMLDocument`, wrap it in a `jQuery` object /*jshint -W116*/ if (val != null && typeof val === "object" && (val.nodeType === 1 || val.nodeType === 9) && val.cloneNode) { transformedVal = $(val); } return transformedVal; } $.fn.fineUploader = function(optionsOrCommand) { var self = this, selfArgs = arguments, retVals = []; this.each(function(index, el) { $el = $(el); if (uploader() && isValidCommand(optionsOrCommand)) { retVals.push(delegateCommand.apply(self, selfArgs)); if (self.length === 1) { return false; } } else if (typeof optionsOrCommand === "object" || !optionsOrCommand) { init.apply(self, selfArgs); } else { $.error("Method " + optionsOrCommand + " does not exist on jQuery.fineUploader"); } }); if (retVals.length === 1) { return retVals[0]; } else if (retVals.length > 1) { return retVals; } return this; }; }(jQuery)); ================================================ FILE: client/js/non-traditional-common/uploader.basic.api.js ================================================ /*globals qq*/ /** * Defines the public API for non-traditional FineUploaderBasic mode. */ (function() { "use strict"; qq.nonTraditionalBasePublicApi = { setUploadSuccessParams: function(params, id) { this._uploadSuccessParamsStore.set(params, id); }, setUploadSuccessEndpoint: function(endpoint, id) { this._uploadSuccessEndpointStore.set(endpoint, id); } }; qq.nonTraditionalBasePrivateApi = { /** * When the upload has completed, if it is successful, send a request to the `successEndpoint` (if defined). * This will hold up the call to the `onComplete` callback until we have determined success of the upload * according to the local server, if a `successEndpoint` has been defined by the integrator. * * @param id ID of the completed upload * @param name Name of the associated item * @param result Object created from the server's parsed JSON response. * @param xhr Associated XmlHttpRequest, if this was used to send the request. * @returns {boolean || qq.Promise} true/false if success can be determined immediately, otherwise a `qq.Promise` * if we need to ask the server. * @private */ _onComplete: function(id, name, result, xhr) { var success = result.success ? true : false, self = this, onCompleteArgs = arguments, successEndpoint = this._uploadSuccessEndpointStore.get(id), successCustomHeaders = this._options.uploadSuccess.customHeaders, successMethod = this._options.uploadSuccess.method, cors = this._options.cors, promise = new qq.Promise(), uploadSuccessParams = this._uploadSuccessParamsStore.get(id), fileParams = this._paramsStore.get(id), // If we are waiting for confirmation from the local server, and have received it, // include properties from the local server response in the `response` parameter // sent to the `onComplete` callback, delegate to the parent `_onComplete`, and // fulfill the associated promise. onSuccessFromServer = function(successRequestResult) { delete self._failedSuccessRequestCallbacks[id]; qq.extend(result, successRequestResult); qq.FineUploaderBasic.prototype._onComplete.apply(self, onCompleteArgs); promise.success(successRequestResult); }, // If the upload success request fails, attempt to re-send the success request (via the core retry code). // The entire upload may be restarted if the server returns a "reset" property with a value of true as well. onFailureFromServer = function(successRequestResult) { var callback = submitSuccessRequest; qq.extend(result, successRequestResult); if (result && result.reset) { callback = null; } if (!callback) { delete self._failedSuccessRequestCallbacks[id]; } else { self._failedSuccessRequestCallbacks[id] = callback; } if (!self._onAutoRetry(id, name, result, xhr, callback)) { qq.FineUploaderBasic.prototype._onComplete.apply(self, onCompleteArgs); promise.failure(successRequestResult); } }, submitSuccessRequest, successAjaxRequester; // Ask the local server if the file sent is ok. if (success && successEndpoint) { successAjaxRequester = new qq.UploadSuccessAjaxRequester({ endpoint: successEndpoint, method: successMethod, customHeaders: successCustomHeaders, cors: cors, log: qq.bind(this.log, this) }); // combine custom params and default params qq.extend(uploadSuccessParams, self._getEndpointSpecificParams(id, result, xhr), true); // include any params associated with the file fileParams && qq.extend(uploadSuccessParams, fileParams, true); submitSuccessRequest = qq.bind(function() { successAjaxRequester.sendSuccessRequest(id, uploadSuccessParams) .then(onSuccessFromServer, onFailureFromServer); }, self); submitSuccessRequest(); return promise; } // If we are not asking the local server about the file, just delegate to the parent `_onComplete`. return qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments); }, // If the failure occurred on an upload success request (and a reset was not ordered), try to resend that instead. _manualRetry: function(id) { var successRequestCallback = this._failedSuccessRequestCallbacks[id]; return qq.FineUploaderBasic.prototype._manualRetry.call(this, id, successRequestCallback); } }; }()); ================================================ FILE: client/js/paste.js ================================================ /*globals qq*/ qq.PasteSupport = function(o) { "use strict"; var options, detachPasteHandler; options = { targetElement: null, callbacks: { log: function(message, level) {}, pasteReceived: function(blob) {} } }; function isImage(item) { return item.type && item.type.indexOf("image/") === 0; } function registerPasteHandler() { detachPasteHandler = qq(options.targetElement).attach("paste", function(event) { var clipboardData = event.clipboardData; if (clipboardData) { qq.each(clipboardData.items, function(idx, item) { if (isImage(item)) { var blob = item.getAsFile(); options.callbacks.pasteReceived(blob); } }); } }); } function unregisterPasteHandler() { if (detachPasteHandler) { detachPasteHandler(); } } qq.extend(options, o); registerPasteHandler(); qq.extend(this, { reset: function() { unregisterPasteHandler(); } }); }; ================================================ FILE: client/js/promise.js ================================================ /*globals qq*/ // Is the passed object a promise instance? qq.isGenericPromise = function(maybePromise) { "use strict"; return !!(maybePromise && maybePromise.then && qq.isFunction(maybePromise.then)); }; qq.Promise = function() { "use strict"; var successArgs, failureArgs, successCallbacks = [], failureCallbacks = [], doneCallbacks = [], state = 0; qq.extend(this, { then: function(onSuccess, onFailure) { if (state === 0) { if (onSuccess) { successCallbacks.push(onSuccess); } if (onFailure) { failureCallbacks.push(onFailure); } } else if (state === -1) { onFailure && onFailure.apply(null, failureArgs); } else if (onSuccess) { onSuccess.apply(null, successArgs); } return this; }, done: function(callback) { if (state === 0) { doneCallbacks.push(callback); } else { callback.apply(null, failureArgs === undefined ? successArgs : failureArgs); } return this; }, success: function() { state = 1; successArgs = arguments; if (successCallbacks.length) { qq.each(successCallbacks, function(idx, callback) { callback.apply(null, successArgs); }); } if (doneCallbacks.length) { qq.each(doneCallbacks, function(idx, callback) { callback.apply(null, successArgs); }); } return this; }, failure: function() { state = -1; failureArgs = arguments; if (failureCallbacks.length) { qq.each(failureCallbacks, function(idx, callback) { callback.apply(null, failureArgs); }); } if (doneCallbacks.length) { qq.each(doneCallbacks, function(idx, callback) { callback.apply(null, failureArgs); }); } return this; } }); }; ================================================ FILE: client/js/s3/jquery-plugin.js ================================================ /*globals jQuery*/ /** * Simply an alias for the `fineUploader` plug-in wrapper, but hides the required `endpointType` option from the * integrator. I thought it may be confusing to convey to the integrator that, when using Fine Uploader in S3 mode, * you need to specify an `endpointType` with a value of S3, and perhaps an `uploaderType` with a value of "basic" if * you want to use basic mode when uploading directly to S3 as well. So, you can use this plug-in alias and not worry * about the `endpointType` option at all. */ (function($) { "use strict"; $.fn.fineUploaderS3 = function(optionsOrCommand) { if (typeof optionsOrCommand === "object") { // This option is used to tell the plug-in wrapper to instantiate the appropriate S3-namespace modules. optionsOrCommand.endpointType = "s3"; } return $.fn.fineUploader.apply(this, arguments); }; }(jQuery)); ================================================ FILE: client/js/s3/multipart.abort.ajax.requester.js ================================================ /*globals qq */ /** * Ajax requester used to send an ["Abort Multipart Upload"](http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadAbort.html) * request to S3 via the REST API. * @param o * @constructor */ qq.s3.AbortMultipartAjaxRequester = function(o) { "use strict"; var requester, options = { method: "DELETE", endpointStore: null, signatureSpec: null, maxConnections: 3, getBucket: function(id) {}, getHost: function(id) {}, getKey: function(id) {}, log: function(str, level) {} }, getSignatureAjaxRequester; qq.extend(options, o); // Transport for requesting signatures (for the "Complete" requests) from the local server getSignatureAjaxRequester = new qq.s3.RequestSigner({ endpointStore: options.endpointStore, signatureSpec: options.signatureSpec, cors: options.cors, log: options.log }); /** * Attach all required headers (including Authorization) to the "Abort" request. This is a promissory function * that will fulfill the associated promise once all headers have been attached or when an error has occurred that * prevents headers from being attached. * * @param id Associated file ID * @param uploadId ID of the associated upload, according to AWS * @returns {qq.Promise} */ function getHeaders(id, uploadId) { var headers = {}, promise = new qq.Promise(), bucket = options.getBucket(id), host = options.getHost(id), signatureConstructor = getSignatureAjaxRequester.constructStringToSign (getSignatureAjaxRequester.REQUEST_TYPE.MULTIPART_ABORT, bucket, host, options.getKey(id)) .withUploadId(uploadId); // Ask the local server to sign the request. Use this signature to form the Authorization header. getSignatureAjaxRequester.getSignature(id, {signatureConstructor: signatureConstructor}).then(promise.success, promise.failure); return promise; } /** * Called by the base ajax requester when the response has been received. We definitively determine here if the * "Abort MPU" request has been a success or not. * * @param id ID associated with the file. * @param xhr `XMLHttpRequest` object containing the response, among other things. * @param isError A boolean indicating success or failure according to the base ajax requester (primarily based on status code). */ function handleAbortRequestComplete(id, xhr, isError) { var domParser = new DOMParser(), responseDoc = domParser.parseFromString(xhr.responseText, "application/xml"), errorEls = responseDoc.getElementsByTagName("Error"), awsErrorMsg; options.log(qq.format("Abort response status {}, body = {}", xhr.status, xhr.responseText)); // If the base requester has determine this a failure, give up. if (isError) { options.log(qq.format("Abort Multipart Upload request for {} failed with status {}.", id, xhr.status), "error"); } else { // Make sure the correct bucket and key has been specified in the XML response from AWS. if (errorEls.length) { isError = true; awsErrorMsg = responseDoc.getElementsByTagName("Message")[0].textContent; options.log(qq.format("Failed to Abort Multipart Upload request for {}. Error: {}", id, awsErrorMsg), "error"); } else { options.log(qq.format("Abort MPU request succeeded for file ID {}.", id)); } } } requester = qq.extend(this, new qq.AjaxRequester({ validMethods: ["DELETE"], method: options.method, contentType: null, endpointStore: options.endpointStore, maxConnections: options.maxConnections, allowXRequestedWithAndCacheControl: false, //These headers are not necessary & would break some installations if added log: options.log, onComplete: handleAbortRequestComplete, successfulResponseCodes: { DELETE: [204] } })); qq.extend(this, { /** * Sends the "Abort" request. * * @param id ID associated with the file. * @param uploadId AWS uploadId for this file */ send: function(id, uploadId) { getHeaders(id, uploadId).then(function(headers, endOfUrl) { options.log("Submitting S3 Abort multipart upload request for " + id); requester.initTransport(id) .withPath(endOfUrl) .withHeaders(headers) .send(); }); } }); }; ================================================ FILE: client/js/s3/multipart.complete.ajax.requester.js ================================================ /*globals qq*/ /** * Ajax requester used to send an ["Complete Multipart Upload"](http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html) * request to S3 via the REST API. * * @param o Options passed by the creator, to overwrite any default option values. * @constructor */ qq.s3.CompleteMultipartAjaxRequester = function(o) { "use strict"; var requester, pendingCompleteRequests = {}, options = { method: "POST", contentType: "text/xml", endpointStore: null, signatureSpec: null, maxConnections: 3, getBucket: function(id) {}, getHost: function(id) {}, getKey: function(id) {}, log: function(str, level) {} }, getSignatureAjaxRequester; qq.extend(options, o); // Transport for requesting signatures (for the "Complete" requests) from the local server getSignatureAjaxRequester = new qq.s3.RequestSigner({ endpointStore: options.endpointStore, signatureSpec: options.signatureSpec, cors: options.cors, log: options.log }); /** * Attach all required headers (including Authorization) to the "Complete" request. This is a promissory function * that will fulfill the associated promise once all headers have been attached or when an error has occurred that * prevents headers from being attached. * * @returns {qq.Promise} */ function getHeaders(id, uploadId, body) { var headers = {}, promise = new qq.Promise(), bucket = options.getBucket(id), host = options.getHost(id), signatureConstructor = getSignatureAjaxRequester.constructStringToSign (getSignatureAjaxRequester.REQUEST_TYPE.MULTIPART_COMPLETE, bucket, host, options.getKey(id)) .withUploadId(uploadId) .withContent(body) .withContentType("application/xml; charset=UTF-8"); // Ask the local server to sign the request. Use this signature to form the Authorization header. getSignatureAjaxRequester.getSignature(id, {signatureConstructor: signatureConstructor}).then(promise.success, promise.failure); return promise; } /** * Called by the base ajax requester when the response has been received. We definitively determine here if the * "Complete MPU" request has been a success or not. * * @param id ID associated with the file. * @param xhr `XMLHttpRequest` object containing the response, among other things. * @param isError A boolean indicating success or failure according to the base ajax requester (primarily based on status code). */ function handleCompleteRequestComplete(id, xhr, isError) { var promise = pendingCompleteRequests[id], domParser = new DOMParser(), bucket = options.getBucket(id), key = options.getKey(id), responseDoc = domParser.parseFromString(xhr.responseText, "application/xml"), bucketEls = responseDoc.getElementsByTagName("Bucket"), keyEls = responseDoc.getElementsByTagName("Key"); delete pendingCompleteRequests[id]; options.log(qq.format("Complete response status {}, body = {}", xhr.status, xhr.responseText)); // If the base requester has determine this a failure, give up. if (isError) { options.log(qq.format("Complete Multipart Upload request for {} failed with status {}.", id, xhr.status), "error"); } else { // Make sure the correct bucket and key has been specified in the XML response from AWS. if (bucketEls.length && keyEls.length) { if (bucketEls[0].textContent !== bucket) { isError = true; options.log(qq.format("Wrong bucket in response to Complete Multipart Upload request for {}.", id), "error"); } // TODO Compare key name from response w/ expected key name if AWS ever fixes the encoding of key names in this response. } else { isError = true; options.log(qq.format("Missing bucket and/or key in response to Complete Multipart Upload request for {}.", id), "error"); } } if (isError) { promise.failure("Problem combining the file parts!", xhr); } else { promise.success({}, xhr); } } /** * @param etagEntries Array of objects containing `etag` values and their associated `part` numbers. * @returns {string} XML string containing the body to send with the "Complete" request */ function getCompleteRequestBody(etagEntries) { var doc = document.implementation.createDocument(null, "CompleteMultipartUpload", null); // The entries MUST be sorted by part number, per the AWS API spec. etagEntries.sort(function(a, b) { return a.part - b.part; }); // Construct an XML document for each pair of etag/part values that correspond to part uploads. qq.each(etagEntries, function(idx, etagEntry) { var part = etagEntry.part, etag = etagEntry.etag, partEl = doc.createElement("Part"), partNumEl = doc.createElement("PartNumber"), partNumTextEl = doc.createTextNode(part), etagTextEl = doc.createTextNode(etag), etagEl = doc.createElement("ETag"); etagEl.appendChild(etagTextEl); partNumEl.appendChild(partNumTextEl); partEl.appendChild(partNumEl); partEl.appendChild(etagEl); qq(doc).children()[0].appendChild(partEl); }); // Turn the resulting XML document into a string fit for transport. return new XMLSerializer().serializeToString(doc); } requester = qq.extend(this, new qq.AjaxRequester({ method: options.method, contentType: "application/xml; charset=UTF-8", endpointStore: options.endpointStore, maxConnections: options.maxConnections, allowXRequestedWithAndCacheControl: false, //These headers are not necessary & would break some installations if added log: options.log, onComplete: handleCompleteRequestComplete, successfulResponseCodes: { POST: [200] } })); qq.extend(this, { /** * Sends the "Complete" request and fulfills the returned promise when the success of this request is known. * * @param id ID associated with the file. * @param uploadId AWS uploadId for this file * @param etagEntries Array of objects containing `etag` values and their associated `part` numbers. * @returns {qq.Promise} */ send: function(id, uploadId, etagEntries) { var promise = new qq.Promise(), body = getCompleteRequestBody(etagEntries); getHeaders(id, uploadId, body).then(function(headers, endOfUrl) { options.log("Submitting S3 complete multipart upload request for " + id); pendingCompleteRequests[id] = promise; delete headers["Content-Type"]; requester.initTransport(id) .withPath(endOfUrl) .withHeaders(headers) .withPayload(body) .send(); }, promise.failure); return promise; } }); }; ================================================ FILE: client/js/s3/multipart.initiate.ajax.requester.js ================================================ /*globals qq*/ /** * Ajax requester used to send an ["Initiate Multipart Upload"](http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadInitiate.html) * request to S3 via the REST API. * * @param o Options from the caller - will override the defaults. * @constructor */ qq.s3.InitiateMultipartAjaxRequester = function(o) { "use strict"; var requester, pendingInitiateRequests = {}, options = { filenameParam: "qqfilename", method: "POST", endpointStore: null, paramsStore: null, signatureSpec: null, aclStore: null, reducedRedundancy: false, serverSideEncryption: false, maxConnections: 3, getContentType: function(id) {}, getBucket: function(id) {}, getHost: function(id) {}, getKey: function(id) {}, getName: function(id) {}, log: function(str, level) {} }, getSignatureAjaxRequester; qq.extend(options, o); getSignatureAjaxRequester = new qq.s3.RequestSigner({ endpointStore: options.endpointStore, signatureSpec: options.signatureSpec, cors: options.cors, log: options.log }); /** * Determine all headers for the "Initiate MPU" request, including the "Authorization" header, which must be determined * by the local server. This is a promissory function. If the server responds with a signature, the headers * (including the Authorization header) will be passed into the success method of the promise. Otherwise, the failure * method on the promise will be called. * * @param id Associated file ID * @returns {qq.Promise} */ function getHeaders(id) { var bucket = options.getBucket(id), host = options.getHost(id), headers = {}, promise = new qq.Promise(), key = options.getKey(id), signatureConstructor; headers["x-amz-acl"] = options.aclStore.get(id); if (options.reducedRedundancy) { headers[qq.s3.util.REDUCED_REDUNDANCY_PARAM_NAME] = qq.s3.util.REDUCED_REDUNDANCY_PARAM_VALUE; } if (options.serverSideEncryption) { headers[qq.s3.util.SERVER_SIDE_ENCRYPTION_PARAM_NAME] = qq.s3.util.SERVER_SIDE_ENCRYPTION_PARAM_VALUE; } headers[qq.s3.util.AWS_PARAM_PREFIX + options.filenameParam] = encodeURIComponent(options.getName(id)); qq.each(options.paramsStore.get(id), function(name, val) { if (qq.indexOf(qq.s3.util.UNPREFIXED_PARAM_NAMES, name) >= 0) { headers[name] = val; } else { headers[qq.s3.util.AWS_PARAM_PREFIX + name] = encodeURIComponent(val); } }); signatureConstructor = getSignatureAjaxRequester.constructStringToSign (getSignatureAjaxRequester.REQUEST_TYPE.MULTIPART_INITIATE, bucket, host, key) .withContentType(options.getContentType(id)) .withHeaders(headers); // Ask the local server to sign the request. Use this signature to form the Authorization header. getSignatureAjaxRequester.getSignature(id, {signatureConstructor: signatureConstructor}).then(promise.success, promise.failure); return promise; } /** * Called by the base ajax requester when the response has been received. We definitively determine here if the * "Initiate MPU" request has been a success or not. * * @param id ID associated with the file. * @param xhr `XMLHttpRequest` object containing the response, among other things. * @param isError A boolean indicating success or failure according to the base ajax requester (primarily based on status code). */ function handleInitiateRequestComplete(id, xhr, isError) { var promise = pendingInitiateRequests[id], domParser = new DOMParser(), responseDoc = domParser.parseFromString(xhr.responseText, "application/xml"), uploadIdElements, messageElements, uploadId, errorMessage, status; delete pendingInitiateRequests[id]; // The base ajax requester may declare the request to be a failure based on status code. if (isError) { status = xhr.status; messageElements = responseDoc.getElementsByTagName("Message"); if (messageElements.length > 0) { errorMessage = messageElements[0].textContent; } } // If the base ajax requester has not declared this a failure, make sure we can retrieve the uploadId from the response. else { uploadIdElements = responseDoc.getElementsByTagName("UploadId"); if (uploadIdElements.length > 0) { uploadId = uploadIdElements[0].textContent; } else { errorMessage = "Upload ID missing from request"; } } // Either fail the promise (passing a descriptive error message) or declare it a success (passing the upload ID) if (uploadId === undefined) { if (errorMessage) { options.log(qq.format("Specific problem detected initiating multipart upload request for {}: '{}'.", id, errorMessage), "error"); } else { options.log(qq.format("Unexplained error with initiate multipart upload request for {}. Status code {}.", id, status), "error"); } promise.failure("Problem initiating upload request.", xhr); } else { options.log(qq.format("Initiate multipart upload request successful for {}. Upload ID is {}", id, uploadId)); promise.success(uploadId, xhr); } } requester = qq.extend(this, new qq.AjaxRequester({ method: options.method, contentType: null, endpointStore: options.endpointStore, maxConnections: options.maxConnections, allowXRequestedWithAndCacheControl: false, //These headers are not necessary & would break some installations if added log: options.log, onComplete: handleInitiateRequestComplete, successfulResponseCodes: { POST: [200] } })); qq.extend(this, { /** * Sends the "Initiate MPU" request to AWS via the REST API. First, though, we must get a signature from the * local server for the request. If all is successful, the uploadId from AWS will be passed into the promise's * success handler. Otherwise, an error message will ultimately be passed into the failure method. * * @param id The ID associated with the file * @returns {qq.Promise} */ send: function(id) { var promise = new qq.Promise(); getHeaders(id).then(function(headers, endOfUrl) { options.log("Submitting S3 initiate multipart upload request for " + id); pendingInitiateRequests[id] = promise; requester.initTransport(id) .withPath(endOfUrl) .withHeaders(headers) .send(); }, promise.failure); return promise; } }); }; ================================================ FILE: client/js/s3/request-signer.js ================================================ /* globals qq, CryptoJS */ // IE 10 does not support Uint8ClampedArray. We don't need it, but CryptoJS attempts to reference it // inside a conditional via an instanceof check, which breaks S3 v4 signatures for chunked uploads. if (!window.Uint8ClampedArray) { window.Uint8ClampedArray = function() {}; } /** * Handles signature determination for HTML Form Upload requests and Multipart Uploader requests (via the S3 REST API). * * If the S3 requests are to be signed server side, this module will send a POST request to the server in an attempt * to solicit signatures for various S3-related requests. This module also parses the response and attempts * to determine if the effort was successful. * * If the S3 requests are to be signed client-side, without the help of a server, this module will utilize CryptoJS to * sign the requests directly in the browser and send them off to S3. * * @param o Options associated with all such requests * @returns {{getSignature: Function}} API method used to initiate the signature request. * @constructor */ qq.s3.RequestSigner = function(o) { "use strict"; var requester, thisSignatureRequester = this, pendingSignatures = {}, options = { expectingPolicy: false, method: "POST", signatureSpec: { drift: 0, credentialsProvider: {}, endpoint: null, customHeaders: {}, version: 2 }, maxConnections: 3, endpointStore: {}, paramsStore: {}, cors: { expected: false, sendCredentials: false }, log: function(str, level) {} }, credentialsProvider, generateHeaders = function(signatureConstructor, signature, promise) { var headers = signatureConstructor.getHeaders(); if (options.signatureSpec.version === 4) { headers.Authorization = qq.s3.util.V4_ALGORITHM_PARAM_VALUE + " Credential=" + options.signatureSpec.credentialsProvider.get().accessKey + "/" + qq.s3.util.getCredentialsDate(signatureConstructor.getRequestDate()) + "/" + options.signatureSpec.region + "/" + "s3/aws4_request," + "SignedHeaders=" + signatureConstructor.getSignedHeaders() + "," + "Signature=" + signature; } else { headers.Authorization = "AWS " + options.signatureSpec.credentialsProvider.get().accessKey + ":" + signature; } promise.success(headers, signatureConstructor.getEndOfUrl()); }, v2 = { getStringToSign: function(signatureSpec) { return qq.format("{}\n{}\n{}\n\n{}/{}/{}", signatureSpec.method, signatureSpec.contentMd5 || "", signatureSpec.contentType || "", signatureSpec.headersStr || "\n", signatureSpec.bucket, signatureSpec.endOfUrl); }, signApiRequest: function(signatureConstructor, headersStr, signatureEffort) { var headersWordArray = qq.CryptoJS.enc.Utf8.parse(headersStr), headersHmacSha1 = qq.CryptoJS.HmacSHA1(headersWordArray, credentialsProvider.get().secretKey), headersHmacSha1Base64 = qq.CryptoJS.enc.Base64.stringify(headersHmacSha1); generateHeaders(signatureConstructor, headersHmacSha1Base64, signatureEffort); }, signPolicy: function(policy, signatureEffort, updatedAccessKey, updatedSessionToken) { var policyStr = JSON.stringify(policy), policyWordArray = qq.CryptoJS.enc.Utf8.parse(policyStr), base64Policy = qq.CryptoJS.enc.Base64.stringify(policyWordArray), policyHmacSha1 = qq.CryptoJS.HmacSHA1(base64Policy, credentialsProvider.get().secretKey), policyHmacSha1Base64 = qq.CryptoJS.enc.Base64.stringify(policyHmacSha1); signatureEffort.success({ policy: base64Policy, signature: policyHmacSha1Base64 }, updatedAccessKey, updatedSessionToken); } }, v4 = { getCanonicalQueryString: function(endOfUri) { var queryParamIdx = endOfUri.indexOf("?"), canonicalQueryString = "", encodedQueryParams, encodedQueryParamNames, queryStrings; if (queryParamIdx >= 0) { encodedQueryParams = {}; queryStrings = endOfUri.substr(queryParamIdx + 1).split("&"); qq.each(queryStrings, function(idx, queryString) { var nameAndVal = queryString.split("="), paramVal = nameAndVal[1]; if (paramVal == null) { paramVal = ""; } encodedQueryParams[encodeURIComponent(nameAndVal[0])] = encodeURIComponent(paramVal); }); encodedQueryParamNames = Object.keys(encodedQueryParams).sort(); encodedQueryParamNames.forEach(function(encodedQueryParamName, idx) { canonicalQueryString += encodedQueryParamName + "=" + encodedQueryParams[encodedQueryParamName]; if (idx < encodedQueryParamNames.length - 1) { canonicalQueryString += "&"; } }); } return canonicalQueryString; }, getCanonicalRequest: function(signatureSpec) { return qq.format("{}\n{}\n{}\n{}\n{}\n{}", signatureSpec.method, v4.getCanonicalUri(signatureSpec.endOfUrl), v4.getCanonicalQueryString(signatureSpec.endOfUrl), signatureSpec.headersStr || "\n", v4.getSignedHeaders(signatureSpec.headerNames), signatureSpec.hashedContent); }, getCanonicalUri: function(endOfUri) { var path = endOfUri, queryParamIdx = endOfUri.indexOf("?"); if (queryParamIdx > 0) { path = endOfUri.substr(0, queryParamIdx); } return "/" + path; }, getEncodedHashedPayload: function(body) { var promise = new qq.Promise(), reader; if (qq.isBlob(body)) { // TODO hash blob in webworker if this becomes a notable perf issue reader = new FileReader(); reader.onloadend = function(e) { if (e.target.readyState === FileReader.DONE) { if (e.target.error) { promise.failure(e.target.error); } else { var wordArray = qq.CryptoJS.lib.WordArray.create(e.target.result); promise.success(qq.CryptoJS.SHA256(wordArray).toString()); } } }; reader.readAsArrayBuffer(body); } else { body = body || ""; promise.success(qq.CryptoJS.SHA256(body).toString()); } return promise; }, getScope: function(date, region) { return qq.s3.util.getCredentialsDate(date) + "/" + region + "/s3/aws4_request"; }, getStringToSign: function(signatureSpec) { var canonicalRequest = v4.getCanonicalRequest(signatureSpec), date = qq.s3.util.getV4PolicyDate(signatureSpec.date, signatureSpec.drift), hashedRequest = qq.CryptoJS.SHA256(canonicalRequest).toString(), scope = v4.getScope(signatureSpec.date, options.signatureSpec.region), stringToSignTemplate = "AWS4-HMAC-SHA256\n{}\n{}\n{}"; return { hashed: qq.format(stringToSignTemplate, date, scope, hashedRequest), raw: qq.format(stringToSignTemplate, date, scope, canonicalRequest) }; }, getSignedHeaders: function(headerNames) { var signedHeaders = ""; headerNames.forEach(function(headerName, idx) { signedHeaders += headerName.toLowerCase(); if (idx < headerNames.length - 1) { signedHeaders += ";"; } }); return signedHeaders; }, signApiRequest: function(signatureConstructor, headersStr, signatureEffort) { var secretKey = credentialsProvider.get().secretKey, headersPattern = /.+\n.+\n(\d+)\/(.+)\/s3\/.+\n(.+)/, matches = headersPattern.exec(headersStr), dateKey, dateRegionKey, dateRegionServiceKey, signingKey; dateKey = qq.CryptoJS.HmacSHA256(matches[1], "AWS4" + secretKey); dateRegionKey = qq.CryptoJS.HmacSHA256(matches[2], dateKey); dateRegionServiceKey = qq.CryptoJS.HmacSHA256("s3", dateRegionKey); signingKey = qq.CryptoJS.HmacSHA256("aws4_request", dateRegionServiceKey); generateHeaders(signatureConstructor, qq.CryptoJS.HmacSHA256(headersStr, signingKey), signatureEffort); }, signPolicy: function(policy, signatureEffort, updatedAccessKey, updatedSessionToken) { var policyStr = JSON.stringify(policy), policyWordArray = qq.CryptoJS.enc.Utf8.parse(policyStr), base64Policy = qq.CryptoJS.enc.Base64.stringify(policyWordArray), secretKey = credentialsProvider.get().secretKey, credentialPattern = /.+\/(.+)\/(.+)\/s3\/aws4_request/, credentialCondition = (function() { var credential = null; qq.each(policy.conditions, function(key, condition) { var val = condition["x-amz-credential"]; if (val) { credential = val; return false; } }); return credential; }()), matches, dateKey, dateRegionKey, dateRegionServiceKey, signingKey; matches = credentialPattern.exec(credentialCondition); dateKey = qq.CryptoJS.HmacSHA256(matches[1], "AWS4" + secretKey); dateRegionKey = qq.CryptoJS.HmacSHA256(matches[2], dateKey); dateRegionServiceKey = qq.CryptoJS.HmacSHA256("s3", dateRegionKey); signingKey = qq.CryptoJS.HmacSHA256("aws4_request", dateRegionServiceKey); signatureEffort.success({ policy: base64Policy, signature: qq.CryptoJS.HmacSHA256(base64Policy, signingKey).toString() }, updatedAccessKey, updatedSessionToken); } }; qq.extend(options, o, true); credentialsProvider = options.signatureSpec.credentialsProvider; function handleSignatureReceived(id, xhrOrXdr, isError) { var responseJson = xhrOrXdr.responseText, pendingSignatureData = pendingSignatures[id], promise = pendingSignatureData.promise, signatureConstructor = pendingSignatureData.signatureConstructor, errorMessage, response; delete pendingSignatures[id]; // Attempt to parse what we would expect to be a JSON response if (responseJson) { try { response = qq.parseJson(responseJson); } catch (error) { options.log("Error attempting to parse signature response: " + error, "error"); } } // If the response is parsable and contains an `error` property, use it as the error message if (response && response.error) { isError = true; errorMessage = response.error; } // If we have received a parsable response, and it has an `invalid` property, // the policy document or request headers may have been tampered with client-side. else if (response && response.invalid) { isError = true; errorMessage = "Invalid policy document or request headers!"; } // Make sure the response contains policy & signature properties else if (response) { if (options.expectingPolicy && !response.policy) { isError = true; errorMessage = "Response does not include the base64 encoded policy!"; } else if (!response.signature) { isError = true; errorMessage = "Response does not include the signature!"; } } // Something unknown went wrong else { isError = true; errorMessage = "Received an empty or invalid response from the server!"; } if (isError) { if (errorMessage) { options.log(errorMessage, "error"); } promise.failure(errorMessage); } else if (signatureConstructor) { generateHeaders(signatureConstructor, response.signature, promise); } else { promise.success(response); } } function getStringToSignArtifacts(id, version, requestInfo) { var promise = new qq.Promise(), method = "POST", headerNames = [], headersStr = "", now = new Date(), endOfUrl, signatureSpec, toSign, generateStringToSign = function(requestInfo) { var contentMd5, headerIndexesToRemove = []; qq.each(requestInfo.headers, function(name) { headerNames.push(name); }); headerNames.sort(); qq.each(headerNames, function(idx, headerName) { if (qq.indexOf(qq.s3.util.UNSIGNABLE_REST_HEADER_NAMES, headerName) < 0) { headersStr += headerName.toLowerCase() + ":" + requestInfo.headers[headerName].trim() + "\n"; } else if (headerName === "Content-MD5") { contentMd5 = requestInfo.headers[headerName]; } else { headerIndexesToRemove.unshift(idx); } }); qq.each(headerIndexesToRemove, function(idx, headerIdx) { headerNames.splice(headerIdx, 1); }); signatureSpec = { bucket: requestInfo.bucket, contentMd5: contentMd5, contentType: requestInfo.contentType, date: now, drift: options.signatureSpec.drift, endOfUrl: endOfUrl, hashedContent: requestInfo.hashedContent, headerNames: headerNames, headersStr: headersStr, method: method }; toSign = version === 2 ? v2.getStringToSign(signatureSpec) : v4.getStringToSign(signatureSpec); return { date: now, endOfUrl: endOfUrl, signedHeaders: version === 4 ? v4.getSignedHeaders(signatureSpec.headerNames) : null, toSign: version === 4 ? toSign.hashed : toSign, toSignRaw: version === 4 ? toSign.raw : toSign }; }; /*jshint indent:false */ switch (requestInfo.type) { case thisSignatureRequester.REQUEST_TYPE.MULTIPART_ABORT: method = "DELETE"; endOfUrl = qq.format("uploadId={}", requestInfo.uploadId); break; case thisSignatureRequester.REQUEST_TYPE.MULTIPART_INITIATE: endOfUrl = "uploads"; break; case thisSignatureRequester.REQUEST_TYPE.MULTIPART_COMPLETE: endOfUrl = qq.format("uploadId={}", requestInfo.uploadId); break; case thisSignatureRequester.REQUEST_TYPE.MULTIPART_UPLOAD: method = "PUT"; endOfUrl = qq.format("partNumber={}&uploadId={}", requestInfo.partNum, requestInfo.uploadId); break; } endOfUrl = requestInfo.key + "?" + endOfUrl; if (version === 4) { v4.getEncodedHashedPayload(requestInfo.content).then(function(hashedContent) { requestInfo.headers["x-amz-content-sha256"] = hashedContent; requestInfo.headers.Host = requestInfo.host; requestInfo.headers["x-amz-date"] = qq.s3.util.getV4PolicyDate(now, options.signatureSpec.drift); requestInfo.hashedContent = hashedContent; promise.success(generateStringToSign(requestInfo)); }, function (err) { promise.failure(err); }); } else { promise.success(generateStringToSign(requestInfo)); } return promise; } function determineSignatureClientSide(id, toBeSigned, signatureEffort, updatedAccessKey, updatedSessionToken) { var updatedHeaders; // REST API request if (toBeSigned.signatureConstructor) { if (updatedSessionToken) { updatedHeaders = toBeSigned.signatureConstructor.getHeaders(); updatedHeaders[qq.s3.util.SESSION_TOKEN_PARAM_NAME] = updatedSessionToken; toBeSigned.signatureConstructor.withHeaders(updatedHeaders); } toBeSigned.signatureConstructor.getToSign(id).then(function(signatureArtifacts) { signApiRequest(toBeSigned.signatureConstructor, signatureArtifacts.stringToSign, signatureEffort); }, function (err) { signatureEffort.failure(err); }); } // Form upload (w/ policy document) else { updatedSessionToken && qq.s3.util.refreshPolicyCredentials(toBeSigned, updatedSessionToken); signPolicy(toBeSigned, signatureEffort, updatedAccessKey, updatedSessionToken); } } function signPolicy(policy, signatureEffort, updatedAccessKey, updatedSessionToken) { if (options.signatureSpec.version === 4) { v4.signPolicy(policy, signatureEffort, updatedAccessKey, updatedSessionToken); } else { v2.signPolicy(policy, signatureEffort, updatedAccessKey, updatedSessionToken); } } function signApiRequest(signatureConstructor, headersStr, signatureEffort) { if (options.signatureSpec.version === 4) { v4.signApiRequest(signatureConstructor, headersStr, signatureEffort); } else { v2.signApiRequest(signatureConstructor, headersStr, signatureEffort); } } requester = qq.extend(this, new qq.AjaxRequester({ acceptHeader: "application/json", method: options.method, contentType: "application/json; charset=utf-8", endpointStore: { get: function() { return options.signatureSpec.endpoint; } }, paramsStore: options.paramsStore, maxConnections: options.maxConnections, customHeaders: options.signatureSpec.customHeaders, log: options.log, onComplete: handleSignatureReceived, cors: options.cors })); qq.extend(this, { /** * On success, an object containing the parsed JSON response will be passed into the success handler if the * request succeeds. Otherwise an error message will be passed into the failure method. * * @param id File ID. * @param toBeSigned an Object that holds the item(s) to be signed * @returns {qq.Promise} A promise that is fulfilled when the response has been received. */ getSignature: function(id, toBeSigned) { var params = toBeSigned, signatureConstructor = toBeSigned.signatureConstructor, signatureEffort = new qq.Promise(), queryParams; if (options.signatureSpec.version === 4) { queryParams = {v4: true}; } if (credentialsProvider.get().secretKey && qq.CryptoJS) { if (credentialsProvider.get().expiration.getTime() > Date.now()) { determineSignatureClientSide(id, toBeSigned, signatureEffort); } // If credentials are expired, ask for new ones before attempting to sign request else { credentialsProvider.onExpired().then(function() { determineSignatureClientSide(id, toBeSigned, signatureEffort, credentialsProvider.get().accessKey, credentialsProvider.get().sessionToken); }, function(errorMsg) { options.log("Attempt to update expired credentials apparently failed! Unable to sign request. ", "error"); signatureEffort.failure("Unable to sign request - expired credentials."); }); } } else { options.log("Submitting S3 signature request for " + id); if (signatureConstructor) { signatureConstructor.getToSign(id).then(function(signatureArtifacts) { params = {headers: signatureArtifacts.stringToSignRaw}; requester.initTransport(id) .withParams(params) .withQueryParams(queryParams) .send(); }, function (err) { options.log("Failed to construct signature. ", "error"); signatureEffort.failure("Failed to construct signature."); }); } else { requester.initTransport(id) .withParams(params) .withQueryParams(queryParams) .send(); } pendingSignatures[id] = { promise: signatureEffort, signatureConstructor: signatureConstructor }; } return signatureEffort; }, constructStringToSign: function(type, bucket, host, key) { var headers = {}, uploadId, content, contentType, partNum, artifacts; return { withHeaders: function(theHeaders) { headers = theHeaders; return this; }, withUploadId: function(theUploadId) { uploadId = theUploadId; return this; }, withContent: function(theContent) { content = theContent; return this; }, withContentType: function(theContentType) { contentType = theContentType; return this; }, withPartNum: function(thePartNum) { partNum = thePartNum; return this; }, getToSign: function(id) { var sessionToken = credentialsProvider.get().sessionToken, promise = new qq.Promise(), adjustedDate = new Date(Date.now() + options.signatureSpec.drift); headers["x-amz-date"] = adjustedDate.toUTCString(); if (sessionToken) { headers[qq.s3.util.SESSION_TOKEN_PARAM_NAME] = sessionToken; } getStringToSignArtifacts(id, options.signatureSpec.version, { bucket: bucket, content: content, contentType: contentType, headers: headers, host: host, key: key, partNum: partNum, type: type, uploadId: uploadId }).then(function(_artifacts_) { artifacts = _artifacts_; promise.success({ headers: (function() { if (contentType) { headers["Content-Type"] = contentType; } delete headers.Host; // we don't want this to be set on the XHR-initiated request return headers; }()), date: artifacts.date, endOfUrl: artifacts.endOfUrl, signedHeaders: artifacts.signedHeaders, stringToSign: artifacts.toSign, stringToSignRaw: artifacts.toSignRaw }); }, function (err) { promise.failure(err); }); return promise; }, getHeaders: function() { return qq.extend({}, headers); }, getEndOfUrl: function() { return artifacts && artifacts.endOfUrl; }, getRequestDate: function() { return artifacts && artifacts.date; }, getSignedHeaders: function() { return artifacts && artifacts.signedHeaders; } }; } }); }; qq.s3.RequestSigner.prototype.REQUEST_TYPE = { MULTIPART_INITIATE: "multipart_initiate", MULTIPART_COMPLETE: "multipart_complete", MULTIPART_ABORT: "multipart_abort", MULTIPART_UPLOAD: "multipart_upload" }; ================================================ FILE: client/js/s3/s3.form.upload.handler.js ================================================ /*globals qq */ /** * Upload handler used by the upload to S3 module that assumes the current user agent does not have any support for the * File API, and, therefore, makes use of iframes and forms to submit the files directly to S3 buckets via the associated * AWS API. * * @param options Options passed from the base handler * @param proxy Callbacks & methods used to query for or push out data/changes */ qq.s3.FormUploadHandler = function(options, proxy) { "use strict"; var handler = this, clockDrift = options.clockDrift, onUuidChanged = proxy.onUuidChanged, getName = proxy.getName, getUuid = proxy.getUuid, log = proxy.log, onGetBucket = options.getBucket, onGetKeyName = options.getKeyName, filenameParam = options.filenameParam, paramsStore = options.paramsStore, endpointStore = options.endpointStore, aclStore = options.aclStore, reducedRedundancy = options.objectProperties.reducedRedundancy, region = options.objectProperties.region, serverSideEncryption = options.objectProperties.serverSideEncryption, validation = options.validation, signature = options.signature, successRedirectUrl = options.iframeSupport.localBlankPagePath, credentialsProvider = options.signature.credentialsProvider, getSignatureAjaxRequester = new qq.s3.RequestSigner({ signatureSpec: signature, cors: options.cors, log: log }); if (successRedirectUrl === undefined) { throw new Error("successRedirectEndpoint MUST be defined if you intend to use browsers that do not support the File API!"); } /** * Attempt to parse the contents of an iframe after receiving a response from the server. If the contents cannot be * read (perhaps due to a security error) it is safe to assume that the upload was not successful since Amazon should * have redirected to a known endpoint that should provide a parseable response. * * @param id ID of the associated file * @param iframe target of the form submit * @returns {boolean} true if the contents can be read, false otherwise */ function isValidResponse(id, iframe) { var response, endpoint = options.endpointStore.get(id), bucket = handler._getFileState(id).bucket, doc, innerHtml, responseData; //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases try { // iframe.contentWindow.document - for IE<7 doc = iframe.contentDocument || iframe.contentWindow.document; innerHtml = doc.body.innerHTML; responseData = qq.s3.util.parseIframeResponse(iframe); if (responseData.bucket === bucket && responseData.key === qq.s3.util.encodeQueryStringParam(handler.getThirdPartyFileId(id))) { return true; } log("Response from AWS included an unexpected bucket or key name.", "error"); } catch (error) { log("Error when attempting to parse form upload response (" + error.message + ")", "error"); } return false; } function generateAwsParams(id) { /*jshint -W040 */ var customParams = paramsStore.get(id); customParams[filenameParam] = getName(id); return qq.s3.util.generateAwsParams({ endpoint: endpointStore.get(id), clockDrift: clockDrift, params: customParams, bucket: handler._getFileState(id).bucket, key: handler.getThirdPartyFileId(id), accessKey: credentialsProvider.get().accessKey, sessionToken: credentialsProvider.get().sessionToken, acl: aclStore.get(id), minFileSize: validation.minSizeLimit, maxFileSize: validation.maxSizeLimit, successRedirectUrl: successRedirectUrl, reducedRedundancy: reducedRedundancy, region: region, serverSideEncryption: serverSideEncryption, signatureVersion: signature.version, log: log }, qq.bind(getSignatureAjaxRequester.getSignature, this, id)); } /** * Creates form, that will be submitted to iframe */ function createForm(id, iframe) { var promise = new qq.Promise(), method = "POST", endpoint = options.endpointStore.get(id), fileName = getName(id); generateAwsParams(id).then(function(params) { var form = handler._initFormForUpload({ method: method, endpoint: endpoint, params: params, paramsInBody: true, targetName: iframe.name }); promise.success(form); }, function(errorMessage) { promise.failure(errorMessage); handleFinishedUpload(id, iframe, fileName, {error: errorMessage}); }); return promise; } function handleUpload(id) { var iframe = handler._createIframe(id), input = handler.getInput(id), promise = new qq.Promise(); createForm(id, iframe).then(function(form) { form.appendChild(input); // Register a callback when the response comes in from S3 handler._attachLoadEvent(iframe, function(response) { log("iframe loaded"); // If the common response handler has determined success or failure immediately if (response) { // If there is something fundamentally wrong with the response (such as iframe content is not accessible) if (response.success === false) { log("Amazon likely rejected the upload request", "error"); promise.failure(response); } } // The generic response (iframe onload) handler was not able to make a determination regarding the success of the request else { response = {}; response.success = isValidResponse(id, iframe); // If the more specific response handle detected a problem with the response from S3 if (response.success === false) { log("A success response was received by Amazon, but it was invalid in some way.", "error"); promise.failure(response); } else { qq.extend(response, qq.s3.util.parseIframeResponse(iframe)); promise.success(response); } } handleFinishedUpload(id, iframe); }); log("Sending upload request for " + id); form.submit(); qq(form).remove(); }, promise.failure); return promise; } function handleFinishedUpload(id, iframe) { handler._detachLoadEvent(id); iframe && qq(iframe).remove(); } qq.extend(this, new qq.FormUploadHandler({ options: { isCors: false, inputName: "file" }, proxy: { onCancel: options.onCancel, onUuidChanged: onUuidChanged, getName: getName, getUuid: getUuid, log: log } })); qq.extend(this, { uploadFile: function(id) { var name = getName(id), promise = new qq.Promise(); if (handler.getThirdPartyFileId(id)) { if (handler._getFileState(id).bucket) { handleUpload(id).then(promise.success, promise.failure); } else { onGetBucket(id).then(function(bucket) { handler._getFileState(id).bucket = bucket; handleUpload(id).then(promise.success, promise.failure); }); } } else { // The S3 uploader module will either calculate the key or ask the server for it // and will call us back once it is known. onGetKeyName(id, name).then(function(key) { onGetBucket(id).then(function(bucket) { handler._getFileState(id).bucket = bucket; handler._setThirdPartyFileId(id, key); handleUpload(id).then(promise.success, promise.failure); }, function(errorReason) { promise.failure({error: errorReason}); }); }, function(errorReason) { promise.failure({error: errorReason}); }); } return promise; } }); }; ================================================ FILE: client/js/s3/s3.xhr.upload.handler.js ================================================ /*globals qq */ /** * Upload handler used by the upload to S3 module that depends on File API support, and, therefore, makes use of * `XMLHttpRequest` level 2 to upload `File`s and `Blob`s directly to S3 buckets via the associated AWS API. * * If chunking is supported and enabled, the S3 Multipart Upload REST API is utilized. * * @param spec Options passed from the base handler * @param proxy Callbacks & methods used to query for or push out data/changes */ qq.s3.XhrUploadHandler = function(spec, proxy) { "use strict"; var getName = proxy.getName, log = proxy.log, clockDrift = spec.clockDrift, expectedStatus = 200, onGetBucket = spec.getBucket, onGetHost = spec.getHost, onGetKeyName = spec.getKeyName, filenameParam = spec.filenameParam, paramsStore = spec.paramsStore, endpointStore = spec.endpointStore, aclStore = spec.aclStore, reducedRedundancy = spec.objectProperties.reducedRedundancy, region = spec.objectProperties.region, serverSideEncryption = spec.objectProperties.serverSideEncryption, validation = spec.validation, signature = qq.extend({region: region, drift: clockDrift}, spec.signature), handler = this, credentialsProvider = spec.signature.credentialsProvider, chunked = { // Sends a "Complete Multipart Upload" request and then signals completion of the upload // when the response to this request has been parsed. combine: function(id) { var uploadId = handler._getPersistableData(id).uploadId, etagMap = handler._getPersistableData(id).etags, result = new qq.Promise(); requesters.completeMultipart.send(id, uploadId, etagMap).then( result.success, function failure(reason, xhr) { result.failure(upload.done(id, xhr).response, xhr); } ); return result; }, // The last step in handling a chunked upload. This is called after each chunk has been sent. // The request may be successful, or not. If it was successful, we must extract the "ETag" element // in the XML response and store that along with the associated part number. // We need these items to "Complete" the multipart upload after all chunks have been successfully sent. done: function(id, xhr, chunkIdx) { var response = upload.response.parse(id, xhr), etag; if (response.success) { etag = xhr.getResponseHeader("ETag"); if (!handler._getPersistableData(id).etags) { handler._getPersistableData(id).etags = []; } handler._getPersistableData(id).etags.push({part: chunkIdx + 1, etag: etag}); } }, /** * Determines headers that must be attached to the chunked (Multipart Upload) request. One of these headers is an * Authorization value, which must be determined by asking the local server to sign the request first. So, this * function returns a promise. Once all headers are determined, the `success` method of the promise is called with * the headers object. If there was some problem determining the headers, we delegate to the caller's `failure` * callback. * * @param id File ID * @param chunkIdx Index of the chunk to PUT * @returns {qq.Promise} */ initHeaders: function(id, chunkIdx, blob) { var headers = {}, bucket = upload.bucket.getName(id), host = upload.host.getName(id), key = upload.key.urlSafe(id), promise = new qq.Promise(), signatureConstructor = requesters.restSignature.constructStringToSign (requesters.restSignature.REQUEST_TYPE.MULTIPART_UPLOAD, bucket, host, key) .withPartNum(chunkIdx + 1) .withContent(blob) .withUploadId(handler._getPersistableData(id).uploadId); // Ask the local server to sign the request. Use this signature to form the Authorization header. requesters.restSignature.getSignature(id + "." + chunkIdx, {signatureConstructor: signatureConstructor}).then(promise.success, promise.failure); return promise; }, put: function(id, chunkIdx) { var xhr = handler._createXhr(id, chunkIdx), chunkData = handler._getChunkData(id, chunkIdx), domain = spec.endpointStore.get(id), promise = new qq.Promise(); // Add appropriate headers to the multipart upload request. // Once these have been determined (asynchronously) attach the headers and send the chunk. chunked.initHeaders(id, chunkIdx, chunkData.blob).then(function(headers, endOfUrl) { if (xhr._cancelled) { log(qq.format("Upload of item {}.{} cancelled. Upload will not start after successful signature request.", id, chunkIdx)); promise.failure({error: "Chunk upload cancelled"}); } else { var url = domain + "/" + endOfUrl; handler._registerProgressHandler(id, chunkIdx, chunkData.size); upload.track(id, xhr, chunkIdx).then(promise.success, promise.failure); xhr.open("PUT", url, true); var hasContentType = false; qq.each(headers, function(name, val) { if (name === "Content-Type") { hasContentType = true; } xhr.setRequestHeader(name, val); }); // Workaround for IE Edge if (!hasContentType) { xhr.setRequestHeader("Content-Type", ""); } xhr.send(chunkData.blob); } }, function() { promise.failure({error: "Problem signing the chunk!"}, xhr); }); return promise; }, send: function(id, chunkIdx) { var promise = new qq.Promise(); chunked.setup(id).then( // The "Initiate" request succeeded. We are ready to send the first chunk. function() { chunked.put(id, chunkIdx).then(promise.success, promise.failure); }, // We were unable to initiate the chunked upload process. function(errorMessage, xhr) { promise.failure({error: errorMessage}, xhr); } ); return promise; }, /** * Sends an "Initiate Multipart Upload" request to S3 via the REST API, but only if the MPU has not already been * initiated. * * @param id Associated file ID * @returns {qq.Promise} A promise that is fulfilled when the initiate request has been sent and the response has been parsed. */ setup: function(id) { var promise = new qq.Promise(), uploadId = handler._getPersistableData(id).uploadId, uploadIdPromise = new qq.Promise(); if (!uploadId) { handler._getPersistableData(id).uploadId = uploadIdPromise; requesters.initiateMultipart.send(id).then( function(uploadId) { handler._getPersistableData(id).uploadId = uploadId; uploadIdPromise.success(uploadId); promise.success(uploadId); }, function(errorMsg, xhr) { handler._getPersistableData(id).uploadId = null; promise.failure(errorMsg, xhr); uploadIdPromise.failure(errorMsg, xhr); } ); } else if (uploadId instanceof qq.Promise) { uploadId.then(function(uploadId) { promise.success(uploadId); }); } else { promise.success(uploadId); } return promise; } }, requesters = { abortMultipart: new qq.s3.AbortMultipartAjaxRequester({ endpointStore: endpointStore, signatureSpec: signature, cors: spec.cors, log: log, getBucket: function(id) { return upload.bucket.getName(id); }, getHost: function(id) { return upload.host.getName(id); }, getKey: function(id) { return upload.key.urlSafe(id); } }), completeMultipart: new qq.s3.CompleteMultipartAjaxRequester({ endpointStore: endpointStore, signatureSpec: signature, cors: spec.cors, log: log, getBucket: function(id) { return upload.bucket.getName(id); }, getHost: function(id) { return upload.host.getName(id); }, getKey: function(id) { return upload.key.urlSafe(id); } }), initiateMultipart: new qq.s3.InitiateMultipartAjaxRequester({ filenameParam: filenameParam, endpointStore: endpointStore, paramsStore: paramsStore, signatureSpec: signature, aclStore: aclStore, reducedRedundancy: reducedRedundancy, serverSideEncryption: serverSideEncryption, cors: spec.cors, log: log, getContentType: function(id) { return handler._getMimeType(id); }, getBucket: function(id) { return upload.bucket.getName(id); }, getHost: function(id) { return upload.host.getName(id); }, getKey: function(id) { return upload.key.urlSafe(id); }, getName: function(id) { return getName(id); } }), policySignature: new qq.s3.RequestSigner({ expectingPolicy: true, signatureSpec: signature, cors: spec.cors, log: log }), restSignature: new qq.s3.RequestSigner({ endpointStore: endpointStore, signatureSpec: signature, cors: spec.cors, log: log }) }, simple = { /** * Used for simple (non-chunked) uploads to determine the parameters to send along with the request. Part of this * process involves asking the local server to sign the request, so this function returns a promise. The promise * is fulfilled when all parameters are determined, or when we determine that all parameters cannot be calculated * due to some error. * * @param id File ID * @returns {qq.Promise} */ initParams: function(id) { /*jshint -W040 */ var customParams = paramsStore.get(id); customParams[filenameParam] = getName(id); return qq.s3.util.generateAwsParams({ endpoint: endpointStore.get(id), clockDrift: clockDrift, params: customParams, type: handler._getMimeType(id), bucket: upload.bucket.getName(id), key: handler.getThirdPartyFileId(id), accessKey: credentialsProvider.get().accessKey, sessionToken: credentialsProvider.get().sessionToken, acl: aclStore.get(id), expectedStatus: expectedStatus, minFileSize: validation.minSizeLimit, maxFileSize: validation.maxSizeLimit, reducedRedundancy: reducedRedundancy, region: region, serverSideEncryption: serverSideEncryption, signatureVersion: signature.version, log: log }, qq.bind(requesters.policySignature.getSignature, this, id)); }, send: function(id) { var promise = new qq.Promise(), xhr = handler._createXhr(id), fileOrBlob = handler.getFile(id); handler._registerProgressHandler(id); upload.track(id, xhr).then(promise.success, promise.failure); // Delegate to a function the sets up the XHR request and notifies us when it is ready to be sent, along w/ the payload. simple.setup(id, xhr, fileOrBlob).then(function(toSend) { log("Sending upload request for " + id); xhr.send(toSend); }, promise.failure); return promise; }, /** * Starts the upload process by delegating to an async function that determine parameters to be attached to the * request. If all params can be determined, we are called back with the params and the caller of this function is * informed by invoking the `success` method on the promise returned by this function, passing the payload of the * request. If some error occurs here, we delegate to a function that signals a failure for this upload attempt. * * Note that this is only used by the simple (non-chunked) upload process. * * @param id File ID * @param xhr XMLHttpRequest to use for the upload * @param fileOrBlob `File` or `Blob` to send * @returns {qq.Promise} */ setup: function(id, xhr, fileOrBlob) { var formData = new FormData(), endpoint = endpointStore.get(id), url = endpoint, promise = new qq.Promise(); simple.initParams(id).then( // Success - all params determined function(awsParams) { xhr.open("POST", url, true); qq.obj2FormData(awsParams, formData); // AWS requires the file field be named "file". formData.append("file", fileOrBlob); promise.success(formData); }, // Failure - we couldn't determine some params (likely the signature) function(errorMessage) { promise.failure({error: errorMessage}); } ); return promise; } }, upload = { /** * Note that this is called when an upload has reached a termination point, * regardless of success/failure. For example, it is called when we have * encountered an error during the upload or when the file may have uploaded successfully. * * @param id file ID */ bucket: { promise: function(id) { var promise = new qq.Promise(), cachedBucket = handler._getFileState(id).bucket; if (cachedBucket) { promise.success(cachedBucket); } else { onGetBucket(id).then(function(bucket) { handler._getFileState(id).bucket = bucket; promise.success(bucket); }, promise.failure); } return promise; }, getName: function(id) { return handler._getFileState(id).bucket; } }, host: { promise: function(id) { var promise = new qq.Promise(), cachedHost = handler._getFileState(id).host; if (cachedHost) { promise.success(cachedHost); } else { onGetHost(id).then(function(host) { handler._getFileState(id).host = host; promise.success(host); }, promise.failure); } return promise; }, getName: function(id) { return handler._getFileState(id).host; } }, done: function(id, xhr) { var response = upload.response.parse(id, xhr), isError = response.success !== true; if (isError && upload.response.shouldReset(response.code)) { log("This is an unrecoverable error, we must restart the upload entirely on the next retry attempt.", "error"); response.reset = true; } return { success: !isError, response: response }; }, key: { promise: function(id) { var promise = new qq.Promise(), key = handler.getThirdPartyFileId(id); /* jshint eqnull:true */ if (key == null) { handler._setThirdPartyFileId(id, promise); onGetKeyName(id, getName(id)).then( function(keyName) { handler._setThirdPartyFileId(id, keyName); promise.success(keyName); }, function(errorReason) { handler._setThirdPartyFileId(id, null); promise.failure(errorReason); } ); } else if (qq.isGenericPromise(key)) { key.then(promise.success, promise.failure); } else { promise.success(key); } return promise; }, urlSafe: function(id) { var encodedKey = handler.getThirdPartyFileId(id); return qq.s3.util.uriEscapePath(encodedKey); } }, response: { parse: function(id, xhr) { var response = {}, parsedErrorProps; try { log(qq.format("Received response status {} with body: {}", xhr.status, xhr.responseText)); if (xhr.status === expectedStatus) { response.success = true; } else { parsedErrorProps = upload.response.parseError(xhr.responseText); if (parsedErrorProps) { response.error = parsedErrorProps.message; response.code = parsedErrorProps.code; } } } catch (error) { log("Error when attempting to parse xhr response text (" + error.message + ")", "error"); } return response; }, /** * This parses an XML response by extracting the "Message" and "Code" elements that accompany AWS error responses. * * @param awsResponseXml XML response from AWS * @returns {object} Object w/ `code` and `message` properties, or undefined if we couldn't find error info in the XML document. */ parseError: function(awsResponseXml) { var parser = new DOMParser(), parsedDoc = parser.parseFromString(awsResponseXml, "application/xml"), errorEls = parsedDoc.getElementsByTagName("Error"), errorDetails = {}, codeEls, messageEls; if (errorEls.length) { codeEls = parsedDoc.getElementsByTagName("Code"); messageEls = parsedDoc.getElementsByTagName("Message"); if (messageEls.length) { errorDetails.message = messageEls[0].textContent; } if (codeEls.length) { errorDetails.code = codeEls[0].textContent; } return errorDetails; } }, // Determine if the upload should be restarted on the next retry attempt // based on the error code returned in the response from AWS. shouldReset: function(errorCode) { /*jshint -W014 */ return errorCode === "EntityTooSmall" || errorCode === "InvalidPart" || errorCode === "InvalidPartOrder" || errorCode === "NoSuchUpload"; } }, start: function(params) { var id = params.id; var optChunkIdx = params.chunkIdx; var promise = new qq.Promise(); upload.key.promise(id).then(function() { upload.bucket.promise(id).then(function() { upload.host.promise(id).then(function() { /* jshint eqnull:true */ if (optChunkIdx == null) { simple.send(id).then(promise.success, promise.failure); } else { chunked.send(id, optChunkIdx).then(promise.success, promise.failure); } }); }); }, function(errorReason) { promise.failure({error: errorReason}); }); return promise; }, track: function(id, xhr, optChunkIdx) { var promise = new qq.Promise(); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { var result; /* jshint eqnull:true */ if (optChunkIdx == null) { result = upload.done(id, xhr); promise[result.success ? "success" : "failure"](result.response, xhr); } else { chunked.done(id, xhr, optChunkIdx); result = upload.done(id, xhr); promise[result.success ? "success" : "failure"](result.response, xhr); } } }; return promise; } }; qq.extend(this, { uploadChunk: upload.start, uploadFile: function(id) { return upload.start({ id: id }); } }); qq.extend(this, new qq.XhrUploadHandler({ options: qq.extend({namespace: "s3"}, spec), proxy: qq.extend({getEndpoint: spec.endpointStore.get}, proxy) })); qq.override(this, function(super_) { return { expunge: function(id) { var uploadId = handler._getPersistableData(id) && handler._getPersistableData(id).uploadId, existedInLocalStorage = handler._maybeDeletePersistedChunkData(id); if (uploadId !== undefined && existedInLocalStorage) { requesters.abortMultipart.send(id, uploadId); } super_.expunge(id); }, finalizeChunks: function(id) { return chunked.combine(id); }, _getLocalStorageId: function(id) { var baseStorageId = super_._getLocalStorageId(id), bucketName = upload.bucket.getName(id); return baseStorageId + "-" + bucketName; } }; }); }; ================================================ FILE: client/js/s3/uploader.basic.js ================================================ /*globals qq */ /** * This defines FineUploaderBasic mode w/ support for uploading to S3, which provides all the basic * functionality of Fine Uploader Basic as well as code to handle uploads directly to S3. * Some inherited options and API methods have a special meaning in the context of the S3 uploader. */ (function() { "use strict"; qq.s3.FineUploaderBasic = function(o) { var options = { request: { // public key (required for server-side signing, ignored if `credentials` have been provided) accessKey: null, // padding, in milliseconds, to add to the x-amz-date header & the policy expiration date clockDrift: 0 }, objectProperties: { acl: "private", // string or a function which may be promissory bucket: qq.bind(function(id) { return qq.s3.util.getBucket(this.getEndpoint(id)); }, this), // string or a function which may be promissory - only used for V4 multipart uploads host: qq.bind(function(id) { return (/(?:http|https):\/\/(.+)(?:\/.+)?/).exec(this._endpointStore.get(id))[1]; }, this), // 'uuid', 'filename', or a function which may be promissory key: "uuid", reducedRedundancy: false, // Defined at http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region region: "us-east-1", serverSideEncryption: false }, credentials: { // Public key (required). accessKey: null, // Private key (required). secretKey: null, // Expiration date for the credentials (required). May be an ISO string or a `Date`. expiration: null, // Temporary credentials session token. // Only required for temporary credentials obtained via AssumeRoleWithWebIdentity. sessionToken: null }, // All but `version` are ignored if `credentials` is provided. signature: { customHeaders: {}, endpoint: null, version: 2 }, uploadSuccess: { endpoint: null, method: "POST", // In addition to the default params sent by Fine Uploader params: {}, customHeaders: {} }, // required if non-File-API browsers, such as IE9 and older, are used iframeSupport: { localBlankPagePath: null }, chunking: { // minimum part size is 5 MiB when uploading to S3 partSize: 5242880 }, cors: { allowXdr: true }, callbacks: { onCredentialsExpired: function() {} } }; // Replace any default options with user defined ones qq.extend(options, o, true); if (!this.setCredentials(options.credentials, true)) { this._currentCredentials.accessKey = options.request.accessKey; } this._aclStore = this._createStore(options.objectProperties.acl); // Call base module qq.FineUploaderBasic.call(this, options); this._uploadSuccessParamsStore = this._createStore(this._options.uploadSuccess.params); this._uploadSuccessEndpointStore = this._createStore(this._options.uploadSuccess.endpoint); // This will hold callbacks for failed uploadSuccess requests that will be invoked on retry. // Indexed by file ID. this._failedSuccessRequestCallbacks = {}; // Holds S3 keys for file representations constructed from a session request. this._cannedKeys = {}; // Holds S3 buckets for file representations constructed from a session request. this._cannedBuckets = {}; this._buckets = {}; this._hosts = {}; }; // Inherit basic public & private API methods. qq.extend(qq.s3.FineUploaderBasic.prototype, qq.basePublicApi); qq.extend(qq.s3.FineUploaderBasic.prototype, qq.basePrivateApi); qq.extend(qq.s3.FineUploaderBasic.prototype, qq.nonTraditionalBasePublicApi); qq.extend(qq.s3.FineUploaderBasic.prototype, qq.nonTraditionalBasePrivateApi); // Define public & private API methods for this module. qq.extend(qq.s3.FineUploaderBasic.prototype, { getBucket: function(id) { if (this._cannedBuckets[id] == null) { return this._buckets[id]; } return this._cannedBuckets[id]; }, /** * @param id File ID * @returns {*} Key name associated w/ the file, if one exists */ getKey: function(id) { /* jshint eqnull:true */ if (this._cannedKeys[id] == null) { return this._handler.getThirdPartyFileId(id); } return this._cannedKeys[id]; }, /** * Override the parent's reset function to cleanup various S3-related items. */ reset: function() { qq.FineUploaderBasic.prototype.reset.call(this); this._failedSuccessRequestCallbacks = []; this._buckets = {}; this._hosts = {}; }, setCredentials: function(credentials, ignoreEmpty) { if (credentials && credentials.secretKey) { if (!credentials.accessKey) { throw new qq.Error("Invalid credentials: no accessKey"); } else if (!credentials.expiration) { throw new qq.Error("Invalid credentials: no expiration"); } else { this._currentCredentials = qq.extend({}, credentials); // Ensure expiration is a `Date`. If initially a string, assuming it is in ISO format. if (qq.isString(credentials.expiration)) { this._currentCredentials.expiration = new Date(credentials.expiration); } } return true; } else if (!ignoreEmpty) { throw new qq.Error("Invalid credentials parameter!"); } else { this._currentCredentials = {}; } }, setAcl: function(acl, id) { this._aclStore.set(acl, id); }, /** * Ensures the parent's upload handler creator passes any additional S3-specific options to the handler as well * as information required to instantiate the specific handler based on the current browser's capabilities. * * @returns {qq.UploadHandlerController} * @private */ _createUploadHandler: function() { var self = this, additionalOptions = { aclStore: this._aclStore, getBucket: qq.bind(this._determineBucket, this), getHost: qq.bind(this._determineHost, this), getKeyName: qq.bind(this._determineKeyName, this), iframeSupport: this._options.iframeSupport, objectProperties: this._options.objectProperties, signature: this._options.signature, clockDrift: this._options.request.clockDrift, // pass size limit validation values to include in the request so AWS enforces this server-side validation: { minSizeLimit: this._options.validation.minSizeLimit, maxSizeLimit: this._options.validation.sizeLimit } }; // We assume HTTP if it is missing from the start of the endpoint string. qq.override(this._endpointStore, function(super_) { return { get: function(id) { var endpoint = super_.get(id); if (endpoint.indexOf("http") < 0) { return "http://" + endpoint; } return endpoint; } }; }); // Some param names should be lower case to avoid signature mismatches qq.override(this._paramsStore, function(super_) { return { get: function(id) { var oldParams = super_.get(id), modifiedParams = {}; qq.each(oldParams, function(name, val) { var paramName = name; if (qq.indexOf(qq.s3.util.CASE_SENSITIVE_PARAM_NAMES, paramName) < 0) { paramName = paramName.toLowerCase(); } modifiedParams[paramName] = qq.isFunction(val) ? val() : val; }); return modifiedParams; } }; }); additionalOptions.signature.credentialsProvider = { get: function() { return self._currentCredentials; }, onExpired: function() { var updateCredentials = new qq.Promise(), callbackRetVal = self._options.callbacks.onCredentialsExpired(); if (qq.isGenericPromise(callbackRetVal)) { callbackRetVal.then(function(credentials) { try { self.setCredentials(credentials); updateCredentials.success(); } catch (error) { self.log("Invalid credentials returned from onCredentialsExpired callback! (" + error.message + ")", "error"); updateCredentials.failure("onCredentialsExpired did not return valid credentials."); } }, function(errorMsg) { self.log("onCredentialsExpired callback indicated failure! (" + errorMsg + ")", "error"); updateCredentials.failure("onCredentialsExpired callback failed."); }); } else { self.log("onCredentialsExpired callback did not return a promise!", "error"); updateCredentials.failure("Unexpected return value for onCredentialsExpired."); } return updateCredentials; } }; return qq.FineUploaderBasic.prototype._createUploadHandler.call(this, additionalOptions, "s3"); }, _determineObjectPropertyValue: function(id, property) { var maybe = this._options.objectProperties[property], promise = new qq.Promise(), self = this; if (qq.isFunction(maybe)) { maybe = maybe(id); if (qq.isGenericPromise(maybe)) { promise = maybe; } else { promise.success(maybe); } } else if (qq.isString(maybe)) { promise.success(maybe); } promise.then( function success(value) { self["_" + property + "s"][id] = value; }, function failure(errorMsg) { qq.log("Problem determining " + property + " for ID " + id + " (" + errorMsg + ")", "error"); } ); return promise; }, _determineBucket: function(id) { return this._determineObjectPropertyValue(id, "bucket"); }, _determineHost: function(id) { return this._determineObjectPropertyValue(id, "host"); }, /** * Determine the file's key name and passes it to the caller via a promissory callback. This also may * delegate to an integrator-defined function that determines the file's key name on demand, * which also may be promissory. * * @param id ID of the file * @param filename Name of the file * @returns {qq.Promise} A promise that will be fulfilled when the key name has been determined (and will be passed to the caller via the success callback). * @private */ _determineKeyName: function(id, filename) { /*jshint -W015*/ var promise = new qq.Promise(), keynameLogic = this._options.objectProperties.key, extension = qq.getExtension(filename), onGetKeynameFailure = promise.failure, onGetKeynameSuccess = function(keyname, extension) { var keynameToUse = keyname; if (extension !== undefined) { keynameToUse += "." + extension; } promise.success(keynameToUse); }; switch (keynameLogic) { case "uuid": onGetKeynameSuccess(this.getUuid(id), extension); break; case "filename": onGetKeynameSuccess(filename); break; default: if (qq.isFunction(keynameLogic)) { this._handleKeynameFunction(keynameLogic, id, onGetKeynameSuccess, onGetKeynameFailure); } else { this.log(keynameLogic + " is not a valid value for the s3.keyname option!", "error"); onGetKeynameFailure(); } } return promise; }, /** * Called by the internal onUpload handler if the integrator has supplied a function to determine * the file's key name. The integrator's function may be promissory. We also need to fulfill * the promise contract associated with the caller as well. * * @param keynameFunc Integrator-supplied function that must be executed to determine the key name. May be promissory. * @param id ID of the associated file * @param successCallback Invoke this if key name retrieval is successful, passing in the key name. * @param failureCallback Invoke this if key name retrieval was unsuccessful. * @private */ _handleKeynameFunction: function(keynameFunc, id, successCallback, failureCallback) { var self = this, onSuccess = function(keyname) { successCallback(keyname); }, onFailure = function(reason) { self.log(qq.format("Failed to retrieve key name for {}. Reason: {}", id, reason || "null"), "error"); failureCallback(reason); }, keyname = keynameFunc.call(this, id); if (qq.isGenericPromise(keyname)) { keyname.then(onSuccess, onFailure); } /*jshint -W116*/ else if (keyname == null) { onFailure(); } else { onSuccess(keyname); } }, _getEndpointSpecificParams: function(id, response, maybeXhr) { var params = { key: this.getKey(id), uuid: this.getUuid(id), name: this.getName(id), bucket: this.getBucket(id) }; if (maybeXhr && maybeXhr.getResponseHeader("ETag")) { params.etag = maybeXhr.getResponseHeader("ETag"); } else if (response.etag) { params.etag = response.etag; } return params; }, // Hooks into the base internal `_onSubmitDelete` to add key and bucket params to the delete file request. _onSubmitDelete: function(id, onSuccessCallback) { var additionalMandatedParams = { key: this.getKey(id), bucket: this.getBucket(id) }; return qq.FineUploaderBasic.prototype._onSubmitDelete.call(this, id, onSuccessCallback, additionalMandatedParams); }, _addCannedFile: function(sessionData) { var id; /* jshint eqnull:true */ if (sessionData.s3Key == null) { throw new qq.Error("Did not find s3Key property in server session response. This is required!"); } else { id = qq.FineUploaderBasic.prototype._addCannedFile.apply(this, arguments); this._cannedKeys[id] = sessionData.s3Key; this._cannedBuckets[id] = sessionData.s3Bucket; } return id; } }); }()); ================================================ FILE: client/js/s3/uploader.js ================================================ /*globals qq */ /** * This defines FineUploader mode w/ support for uploading to S3, which provides all the basic * functionality of Fine Uploader as well as code to handle uploads directly to S3. * This module inherits all logic from FineUploader mode and FineUploaderBasicS3 mode and adds some UI-related logic * specific to the upload-to-S3 workflow. Some inherited options and API methods have a special meaning * in the context of the S3 uploader. */ (function() { "use strict"; qq.s3.FineUploader = function(o) { var options = { failedUploadTextDisplay: { mode: "custom" } }; // Replace any default options with user defined ones qq.extend(options, o, true); // Inherit instance data from FineUploader, which should in turn inherit from s3.FineUploaderBasic. qq.FineUploader.call(this, options, "s3"); if (!qq.supportedFeatures.ajaxUploading && options.iframeSupport.localBlankPagePath === undefined) { this._options.element.innerHTML = "
You MUST set the localBlankPagePath property " + "of the iframeSupport option since this browser does not support the File API!
"; } }; // Inherit the API methods from FineUploaderBasicS3 qq.extend(qq.s3.FineUploader.prototype, qq.s3.FineUploaderBasic.prototype); // Inherit public and private API methods related to UI qq.extend(qq.s3.FineUploader.prototype, qq.uiPublicApi); qq.extend(qq.s3.FineUploader.prototype, qq.uiPrivateApi); }()); ================================================ FILE: client/js/s3/util.js ================================================ /*globals qq */ qq.s3 = qq.s3 || {}; qq.s3.util = qq.s3.util || (function() { "use strict"; return { ALGORITHM_PARAM_NAME: "x-amz-algorithm", AWS_PARAM_PREFIX: "x-amz-meta-", CREDENTIAL_PARAM_NAME: "x-amz-credential", DATE_PARAM_NAME: "x-amz-date", REDUCED_REDUNDANCY_PARAM_NAME: "x-amz-storage-class", REDUCED_REDUNDANCY_PARAM_VALUE: "REDUCED_REDUNDANCY", SERVER_SIDE_ENCRYPTION_PARAM_NAME: "x-amz-server-side-encryption", SERVER_SIDE_ENCRYPTION_PARAM_VALUE: "AES256", SESSION_TOKEN_PARAM_NAME: "x-amz-security-token", V4_ALGORITHM_PARAM_VALUE: "AWS4-HMAC-SHA256", V4_SIGNATURE_PARAM_NAME: "x-amz-signature", CASE_SENSITIVE_PARAM_NAMES: [ "Cache-Control", "Content-Disposition", "Content-Encoding", "Content-MD5" ], UNSIGNABLE_REST_HEADER_NAMES: [ "Cache-Control", "Content-Disposition", "Content-Encoding", "Content-MD5" ], UNPREFIXED_PARAM_NAMES: [ "Cache-Control", "Content-Disposition", "Content-Encoding", "Content-MD5", "x-amz-server-side-encryption", "x-amz-server-side-encryption-aws-kms-key-id", "x-amz-server-side-encryption-customer-algorithm", "x-amz-server-side-encryption-customer-key", "x-amz-server-side-encryption-customer-key-MD5" ], /** * This allows for the region to be specified in the bucket's endpoint URL, or not. * * Examples of some valid endpoints are: * http://foo.s3.amazonaws.com * https://foo.s3.amazonaws.com * http://foo.s3-ap-northeast-1.amazonaws.com * foo.s3.amazonaws.com * http://foo.bar.com * http://s3.amazonaws.com/foo.bar.com * ...etc * * @param endpoint The bucket's URL. * @returns {String || undefined} The bucket name, or undefined if the URL cannot be parsed. */ getBucket: function(endpoint) { var patterns = [ //bucket in domain /^(?:https?:\/\/)?([a-z0-9.\-_]+)\.s3(?:-[a-z0-9\-]+)?\.amazonaws\.com/i, //bucket in path /^(?:https?:\/\/)?s3(?:-[a-z0-9\-]+)?\.amazonaws\.com\/([a-z0-9.\-_]+)/i, //custom domain /^(?:https?:\/\/)?([a-z0-9.\-_]+)/i ], bucket; qq.each(patterns, function(idx, pattern) { var match = pattern.exec(endpoint); if (match) { bucket = match[1]; return false; } }); return bucket; }, /** Create Prefixed request headers which are appropriate for S3. * * If the request header is appropriate for S3 (e.g. Cache-Control) then pass * it along without a metadata prefix. For all other request header parameter names, * apply qq.s3.util.AWS_PARAM_PREFIX before the name. * See: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html */ _getPrefixedParamName: function(name) { if (qq.indexOf(qq.s3.util.UNPREFIXED_PARAM_NAMES, name) >= 0) { return name; } return qq.s3.util.AWS_PARAM_PREFIX + name; }, /** * Create a policy document to be signed and sent along with the S3 upload request. * * @param spec Object with properties use to construct the policy document. * @returns {Object} Policy doc. */ getPolicy: function(spec) { var policy = {}, conditions = [], bucket = spec.bucket, date = spec.date, drift = spec.clockDrift, key = spec.key, accessKey = spec.accessKey, acl = spec.acl, type = spec.type, expectedStatus = spec.expectedStatus, sessionToken = spec.sessionToken, params = spec.params, successRedirectUrl = qq.s3.util.getSuccessRedirectAbsoluteUrl(spec.successRedirectUrl), minFileSize = spec.minFileSize, maxFileSize = spec.maxFileSize, reducedRedundancy = spec.reducedRedundancy, region = spec.region, serverSideEncryption = spec.serverSideEncryption, signatureVersion = spec.signatureVersion; policy.expiration = qq.s3.util.getPolicyExpirationDate(date, drift); conditions.push({acl: acl}); conditions.push({bucket: bucket}); if (type) { conditions.push({"Content-Type": type}); } // jscs:disable requireCamelCaseOrUpperCaseIdentifiers if (expectedStatus) { conditions.push({success_action_status: expectedStatus.toString()}); } if (successRedirectUrl) { conditions.push({success_action_redirect: successRedirectUrl}); } // jscs:enable if (reducedRedundancy) { conditions.push({}); conditions[conditions.length - 1][qq.s3.util.REDUCED_REDUNDANCY_PARAM_NAME] = qq.s3.util.REDUCED_REDUNDANCY_PARAM_VALUE; } if (sessionToken) { conditions.push({}); conditions[conditions.length - 1][qq.s3.util.SESSION_TOKEN_PARAM_NAME] = sessionToken; } if (serverSideEncryption) { conditions.push({}); conditions[conditions.length - 1][qq.s3.util.SERVER_SIDE_ENCRYPTION_PARAM_NAME] = qq.s3.util.SERVER_SIDE_ENCRYPTION_PARAM_VALUE; } if (signatureVersion === 2) { conditions.push({key: key}); } else if (signatureVersion === 4) { conditions.push({}); conditions[conditions.length - 1][qq.s3.util.ALGORITHM_PARAM_NAME] = qq.s3.util.V4_ALGORITHM_PARAM_VALUE; conditions.push({}); conditions[conditions.length - 1].key = key; conditions.push({}); conditions[conditions.length - 1][qq.s3.util.CREDENTIAL_PARAM_NAME] = qq.s3.util.getV4CredentialsString({date: date, key: accessKey, region: region}); conditions.push({}); conditions[conditions.length - 1][qq.s3.util.DATE_PARAM_NAME] = qq.s3.util.getV4PolicyDate(date, drift); } // user metadata qq.each(params, function(name, val) { var awsParamName = qq.s3.util._getPrefixedParamName(name), param = {}; if (qq.indexOf(qq.s3.util.UNPREFIXED_PARAM_NAMES, awsParamName) >= 0) { param[awsParamName] = val; } else { param[awsParamName] = encodeURIComponent(val); } conditions.push(param); }); policy.conditions = conditions; qq.s3.util.enforceSizeLimits(policy, minFileSize, maxFileSize); return policy; }, /** * Update a previously constructed policy document with updated credentials. Currently, this only requires we * update the session token. This is only relevant if requests are being signed client-side. * * @param policy Live policy document * @param newSessionToken Updated session token. */ refreshPolicyCredentials: function(policy, newSessionToken) { var sessionTokenFound = false; qq.each(policy.conditions, function(oldCondIdx, oldCondObj) { qq.each(oldCondObj, function(oldCondName, oldCondVal) { if (oldCondName === qq.s3.util.SESSION_TOKEN_PARAM_NAME) { oldCondObj[oldCondName] = newSessionToken; sessionTokenFound = true; } }); }); if (!sessionTokenFound) { policy.conditions.push({}); policy.conditions[policy.conditions.length - 1][qq.s3.util.SESSION_TOKEN_PARAM_NAME] = newSessionToken; } }, /** * Generates all parameters to be passed along with the S3 upload request. This includes invoking a callback * that is expected to asynchronously retrieve a signature for the policy document. Note that the server * signing the request should reject a "tainted" policy document that includes unexpected values, since it is * still possible for a malicious user to tamper with these values during policy document generation, * before it is sent to the server for signing. * * @param spec Object with properties: `params`, `type`, `key`, `accessKey`, `acl`, `expectedStatus`, `successRedirectUrl`, * `reducedRedundancy`, `region`, `serverSideEncryption`, `version`, and `log()`, along with any options associated with `qq.s3.util.getPolicy()`. * @returns {qq.Promise} Promise that will be fulfilled once all parameters have been determined. */ generateAwsParams: function(spec, signPolicyCallback) { var awsParams = {}, customParams = spec.params, promise = new qq.Promise(), sessionToken = spec.sessionToken, drift = spec.clockDrift, type = spec.type, key = spec.key, accessKey = spec.accessKey, acl = spec.acl, expectedStatus = spec.expectedStatus, successRedirectUrl = qq.s3.util.getSuccessRedirectAbsoluteUrl(spec.successRedirectUrl), reducedRedundancy = spec.reducedRedundancy, region = spec.region, serverSideEncryption = spec.serverSideEncryption, signatureVersion = spec.signatureVersion, now = new Date(), log = spec.log, policyJson; spec.date = now; policyJson = qq.s3.util.getPolicy(spec); awsParams.key = key; if (type) { awsParams["Content-Type"] = type; } // jscs:disable requireCamelCaseOrUpperCaseIdentifiers if (expectedStatus) { awsParams.success_action_status = expectedStatus; } if (successRedirectUrl) { awsParams.success_action_redirect = successRedirectUrl; } // jscs:enable if (reducedRedundancy) { awsParams[qq.s3.util.REDUCED_REDUNDANCY_PARAM_NAME] = qq.s3.util.REDUCED_REDUNDANCY_PARAM_VALUE; } if (serverSideEncryption) { awsParams[qq.s3.util.SERVER_SIDE_ENCRYPTION_PARAM_NAME] = qq.s3.util.SERVER_SIDE_ENCRYPTION_PARAM_VALUE; } if (sessionToken) { awsParams[qq.s3.util.SESSION_TOKEN_PARAM_NAME] = sessionToken; } awsParams.acl = acl; // Custom (user-supplied) params must be prefixed with the value of `qq.s3.util.AWS_PARAM_PREFIX`. // Params such as Cache-Control or Content-Disposition will not be prefixed. // Prefixed param values will be URI encoded as well. qq.each(customParams, function(name, val) { var awsParamName = qq.s3.util._getPrefixedParamName(name); if (qq.indexOf(qq.s3.util.UNPREFIXED_PARAM_NAMES, awsParamName) >= 0) { awsParams[awsParamName] = val; } else { awsParams[awsParamName] = encodeURIComponent(val); } }); if (signatureVersion === 2) { awsParams.AWSAccessKeyId = accessKey; } else if (signatureVersion === 4) { awsParams[qq.s3.util.ALGORITHM_PARAM_NAME] = qq.s3.util.V4_ALGORITHM_PARAM_VALUE; awsParams[qq.s3.util.CREDENTIAL_PARAM_NAME] = qq.s3.util.getV4CredentialsString({date: now, key: accessKey, region: region}); awsParams[qq.s3.util.DATE_PARAM_NAME] = qq.s3.util.getV4PolicyDate(now, drift); } // Invoke a promissory callback that should provide us with a base64-encoded policy doc and an // HMAC signature for the policy doc. signPolicyCallback(policyJson).then( function(policyAndSignature, updatedAccessKey, updatedSessionToken) { awsParams.policy = policyAndSignature.policy; if (spec.signatureVersion === 2) { awsParams.signature = policyAndSignature.signature; if (updatedAccessKey) { awsParams.AWSAccessKeyId = updatedAccessKey; } } else if (spec.signatureVersion === 4) { awsParams[qq.s3.util.V4_SIGNATURE_PARAM_NAME] = policyAndSignature.signature; } if (updatedSessionToken) { awsParams[qq.s3.util.SESSION_TOKEN_PARAM_NAME] = updatedSessionToken; } promise.success(awsParams); }, function(errorMessage) { errorMessage = errorMessage || "Can't continue further with request to S3 as we did not receive " + "a valid signature and policy from the server."; log("Policy signing failed. " + errorMessage, "error"); promise.failure(errorMessage); } ); return promise; }, /** * Add a condition to an existing S3 upload request policy document used to ensure AWS enforces any size * restrictions placed on files server-side. This is important to do, in case users mess with the client-side * checks already in place. * * @param policy Policy document as an `Object`, with a `conditions` property already attached * @param minSize Minimum acceptable size, in bytes * @param maxSize Maximum acceptable size, in bytes (0 = unlimited) */ enforceSizeLimits: function(policy, minSize, maxSize) { var adjustedMinSize = minSize < 0 ? 0 : minSize, // Adjust a maxSize of 0 to the largest possible integer, since we must specify a high and a low in the request adjustedMaxSize = maxSize <= 0 ? 9007199254740992 : maxSize; if (minSize > 0 || maxSize > 0) { policy.conditions.push(["content-length-range", adjustedMinSize.toString(), adjustedMaxSize.toString()]); } }, getPolicyExpirationDate: function(date, drift) { var adjustedDate = new Date(date.getTime() + drift); return qq.s3.util.getPolicyDate(adjustedDate, 5); }, getCredentialsDate: function(date) { return date.getUTCFullYear() + "" + ("0" + (date.getUTCMonth() + 1)).slice(-2) + ("0" + date.getUTCDate()).slice(-2); }, getPolicyDate: function(date, _minutesToAdd_) { var minutesToAdd = _minutesToAdd_ || 0, pad, r; /*jshint -W014 */ // Is this going to be a problem if we encounter this moments before 2 AM just before daylight savings time ends? date.setMinutes(date.getMinutes() + (minutesToAdd || 0)); if (Date.prototype.toISOString) { return date.toISOString(); } else { pad = function(number) { r = String(number); if (r.length === 1) { r = "0" + r; } return r; }; return date.getUTCFullYear() + "-" + pad(date.getUTCMonth() + 1) + "-" + pad(date.getUTCDate()) + "T" + pad(date.getUTCHours()) + ":" + pad(date.getUTCMinutes()) + ":" + pad(date.getUTCSeconds()) + "." + String((date.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5) + "Z"; } }, /** * Looks at a response from S3 contained in an iframe and parses the query string in an attempt to identify * the associated resource. * * @param iframe Iframe containing response * @returns {{bucket: *, key: *, etag: *}} */ parseIframeResponse: function(iframe) { var doc = iframe.contentDocument || iframe.contentWindow.document, queryString = doc.location.search, match = /bucket=(.+)&key=(.+)&etag=(.+)/.exec(queryString); if (match) { return { bucket: match[1], key: match[2], etag: match[3].replace(/%22/g, "") }; } }, /** * @param successRedirectUrl Relative or absolute location of success redirect page * @returns {*|string} undefined if the parameter is undefined, otherwise the absolute location of the success redirect page */ getSuccessRedirectAbsoluteUrl: function(successRedirectUrl) { if (successRedirectUrl) { var targetAnchorContainer = document.createElement("div"), targetAnchor; if (qq.ie7()) { // Note that we must make use of `innerHTML` for IE7 only instead of simply creating an anchor via // `document.createElement('a')` and setting the `href` attribute. The latter approach does not allow us to // obtain an absolute URL in IE7 if the `endpoint` is a relative URL. targetAnchorContainer.innerHTML = ""; targetAnchor = targetAnchorContainer.firstChild; return targetAnchor.href; } else { // IE8 and IE9 do not seem to derive an absolute URL from a relative URL using the `innerHTML` // approach above, so we'll just create an anchor this way and set it's `href` attribute. // Due to yet another quirk in IE8 and IE9, we have to set the `href` equal to itself // in order to ensure relative URLs will be properly parsed. targetAnchor = document.createElement("a"); targetAnchor.href = successRedirectUrl; targetAnchor.href = targetAnchor.href; return targetAnchor.href; } } }, getV4CredentialsString: function(spec) { return spec.key + "/" + qq.s3.util.getCredentialsDate(spec.date) + "/" + spec.region + "/s3/aws4_request"; }, getV4PolicyDate: function(date, drift) { var adjustedDate = new Date(date.getTime() + drift); return qq.s3.util.getCredentialsDate(adjustedDate) + "T" + ("0" + adjustedDate.getUTCHours()).slice(-2) + ("0" + adjustedDate.getUTCMinutes()).slice(-2) + ("0" + adjustedDate.getUTCSeconds()).slice(-2) + "Z"; }, // AWS employs a strict interpretation of [RFC 3986](http://tools.ietf.org/html/rfc3986#page-12). // So, we must ensure all reserved characters listed in the spec are percent-encoded, // and spaces are replaced with "+". encodeQueryStringParam: function(param) { var percentEncoded = encodeURIComponent(param); // %-encode characters not handled by `encodeURIComponent` (to follow RFC 3986) percentEncoded = percentEncoded.replace(/[!'()]/g, escape); // %-encode characters not handled by `escape` (to follow RFC 3986) percentEncoded = percentEncoded.replace(/\*/g, "%2A"); // replace percent-encoded spaces with a "+" return percentEncoded.replace(/%20/g, "+"); }, /** * Escapes url part as for AWS requirements * AWS uriEscapePath function pulled from aws-sdk-js licensed under Apache 2.0 - http://github.com/aws/aws-sdk-js */ uriEscape: function(string) { var output = encodeURIComponent(string); output = output.replace(/[^A-Za-z0-9_.~\-%]+/g, escape); output = output.replace(/[*]/g, function(ch) { return "%" + ch.charCodeAt(0).toString(16).toUpperCase(); }); return output; }, /** * Escapes a path as for AWS requirement * AWS uriEscapePath function pulled from aws-sdk-js licensed under Apache 2.0 - http://github.com/aws/aws-sdk-js */ uriEscapePath: function(path) { var parts = []; qq.each(path.split("/"), function(idx, item) { parts.push(qq.s3.util.uriEscape(item)); }); return parts.join("/"); } }; }()); ================================================ FILE: client/js/session.ajax.requester.js ================================================ /*globals qq, XMLHttpRequest*/ /** * Thin module used to send GET requests to the server, expecting information about session * data used to initialize an uploader instance. * * @param spec Various options used to influence the associated request. * @constructor */ qq.SessionAjaxRequester = function(spec) { "use strict"; var requester, options = { endpoint: null, customHeaders: {}, params: {}, cors: { expected: false, sendCredentials: false }, onComplete: function(response, success, xhrOrXdr) {}, log: function(str, level) {} }; qq.extend(options, spec); function onComplete(id, xhrOrXdr, isError) { var response = null; /* jshint eqnull:true */ if (xhrOrXdr.responseText != null) { try { response = qq.parseJson(xhrOrXdr.responseText); } catch (err) { options.log("Problem parsing session response: " + err.message, "error"); isError = true; } } options.onComplete(response, !isError, xhrOrXdr); } requester = qq.extend(this, new qq.AjaxRequester({ acceptHeader: "application/json", validMethods: ["GET"], method: "GET", endpointStore: { get: function() { return options.endpoint; } }, customHeaders: options.customHeaders, log: options.log, onComplete: onComplete, cors: options.cors })); qq.extend(this, { queryServer: function() { var params = qq.extend({}, options.params); options.log("Session query request."); requester.initTransport("sessionRefresh") .withParams(params) .withCacheBuster() .send(); } }); }; ================================================ FILE: client/js/session.js ================================================ /* globals qq */ /** * Module used to control populating the initial list of files. * * @constructor */ qq.Session = function(spec) { "use strict"; var options = { endpoint: null, params: {}, customHeaders: {}, cors: {}, addFileRecord: function(sessionData) {}, log: function(message, level) {} }; qq.extend(options, spec, true); function isJsonResponseValid(response) { if (qq.isArray(response)) { return true; } options.log("Session response is not an array.", "error"); } function handleFileItems(fileItems, success, xhrOrXdr, promise) { var someItemsIgnored = false; success = success && isJsonResponseValid(fileItems); if (success) { qq.each(fileItems, function(idx, fileItem) { /* jshint eqnull:true */ if (fileItem.uuid == null) { someItemsIgnored = true; options.log(qq.format("Session response item {} did not include a valid UUID - ignoring.", idx), "error"); } else if (fileItem.name == null) { someItemsIgnored = true; options.log(qq.format("Session response item {} did not include a valid name - ignoring.", idx), "error"); } else { try { options.addFileRecord(fileItem); return true; } catch (err) { someItemsIgnored = true; options.log(err.message, "error"); } } return false; }); } promise[success && !someItemsIgnored ? "success" : "failure"](fileItems, xhrOrXdr); } // Initiate a call to the server that will be used to populate the initial file list. // Returns a `qq.Promise`. this.refresh = function() { /*jshint indent:false */ var refreshEffort = new qq.Promise(), refreshCompleteCallback = function(response, success, xhrOrXdr) { handleFileItems(response, success, xhrOrXdr, refreshEffort); }, requesterOptions = qq.extend({}, options), requester = new qq.SessionAjaxRequester( qq.extend(requesterOptions, {onComplete: refreshCompleteCallback}) ); requester.queryServer(); return refreshEffort; }; }; ================================================ FILE: client/js/templating.js ================================================ /* globals qq */ /* jshint -W065 */ /** * Module responsible for rendering all Fine Uploader UI templates. This module also asserts at least * a limited amount of control over the template elements after they are added to the DOM. * Wherever possible, this module asserts total control over template elements present in the DOM. * * @param spec Specification object used to control various templating behaviors * @constructor */ qq.Templating = function(spec) { "use strict"; var FILE_ID_ATTR = "qq-file-id", FILE_CLASS_PREFIX = "qq-file-id-", THUMBNAIL_MAX_SIZE_ATTR = "qq-max-size", THUMBNAIL_SERVER_SCALE_ATTR = "qq-server-scale", // This variable is duplicated in the DnD module since it can function as a standalone as well HIDE_DROPZONE_ATTR = "qq-hide-dropzone", DROPZPONE_TEXT_ATTR = "qq-drop-area-text", IN_PROGRESS_CLASS = "qq-in-progress", HIDDEN_FOREVER_CLASS = "qq-hidden-forever", fileBatch = { content: document.createDocumentFragment(), map: {} }, isCancelDisabled = false, generatedThumbnails = 0, thumbnailQueueMonitorRunning = false, thumbGenerationQueue = [], thumbnailMaxSize = -1, options = { log: null, limits: { maxThumbs: 0, timeBetweenThumbs: 750 }, templateIdOrEl: "qq-template", containerEl: null, fileContainerEl: null, button: null, imageGenerator: null, classes: { hide: "qq-hide", editable: "qq-editable" }, placeholders: { waitUntilUpdate: false, thumbnailNotAvailable: null, waitingForThumbnail: null }, text: { paused: "Paused" } }, selectorClasses = { button: "qq-upload-button-selector", alertDialog: "qq-alert-dialog-selector", dialogCancelButton: "qq-cancel-button-selector", confirmDialog: "qq-confirm-dialog-selector", dialogMessage: "qq-dialog-message-selector", dialogOkButton: "qq-ok-button-selector", promptDialog: "qq-prompt-dialog-selector", uploader: "qq-uploader-selector", drop: "qq-upload-drop-area-selector", list: "qq-upload-list-selector", progressBarContainer: "qq-progress-bar-container-selector", progressBar: "qq-progress-bar-selector", totalProgressBarContainer: "qq-total-progress-bar-container-selector", totalProgressBar: "qq-total-progress-bar-selector", file: "qq-upload-file-selector", spinner: "qq-upload-spinner-selector", size: "qq-upload-size-selector", cancel: "qq-upload-cancel-selector", pause: "qq-upload-pause-selector", continueButton: "qq-upload-continue-selector", deleteButton: "qq-upload-delete-selector", retry: "qq-upload-retry-selector", statusText: "qq-upload-status-text-selector", editFilenameInput: "qq-edit-filename-selector", editNameIcon: "qq-edit-filename-icon-selector", dropText: "qq-upload-drop-area-text-selector", dropProcessing: "qq-drop-processing-selector", dropProcessingSpinner: "qq-drop-processing-spinner-selector", thumbnail: "qq-thumbnail-selector" }, previewGeneration = {}, cachedThumbnailNotAvailableImg = new qq.Promise(), cachedWaitingForThumbnailImg = new qq.Promise(), log, isEditElementsExist, isRetryElementExist, templateDom, container, fileList, showThumbnails, serverScale, // During initialization of the templating module we should cache any // placeholder images so we can quickly swap them into the file list on demand. // Any placeholder images that cannot be loaded/found are simply ignored. cacheThumbnailPlaceholders = function() { var notAvailableUrl = options.placeholders.thumbnailNotAvailable, waitingUrl = options.placeholders.waitingForThumbnail, spec = { maxSize: thumbnailMaxSize, scale: serverScale }; if (showThumbnails) { if (notAvailableUrl) { options.imageGenerator.generate(notAvailableUrl, new Image(), spec).then( function(updatedImg) { cachedThumbnailNotAvailableImg.success(updatedImg); }, function() { cachedThumbnailNotAvailableImg.failure(); log("Problem loading 'not available' placeholder image at " + notAvailableUrl, "error"); } ); } else { cachedThumbnailNotAvailableImg.failure(); } if (waitingUrl) { options.imageGenerator.generate(waitingUrl, new Image(), spec).then( function(updatedImg) { cachedWaitingForThumbnailImg.success(updatedImg); }, function() { cachedWaitingForThumbnailImg.failure(); log("Problem loading 'waiting for thumbnail' placeholder image at " + waitingUrl, "error"); } ); } else { cachedWaitingForThumbnailImg.failure(); } } }, // Displays a "waiting for thumbnail" type placeholder image // iff we were able to load it during initialization of the templating module. displayWaitingImg = function(thumbnail) { var waitingImgPlacement = new qq.Promise(); cachedWaitingForThumbnailImg.then(function(img) { maybeScalePlaceholderViaCss(img, thumbnail); /* jshint eqnull:true */ if (!thumbnail.src) { thumbnail.src = img.src; thumbnail.onload = function() { thumbnail.onload = null; show(thumbnail); waitingImgPlacement.success(); }; } else { waitingImgPlacement.success(); } }, function() { // In some browsers (such as IE9 and older) an img w/out a src attribute // are displayed as "broken" images, so we should just hide the img tag // if we aren't going to display the "waiting" placeholder. hide(thumbnail); waitingImgPlacement.success(); }); return waitingImgPlacement; }, generateNewPreview = function(id, blob, spec) { var thumbnail = getThumbnail(id); log("Generating new thumbnail for " + id); blob.qqThumbnailId = id; return options.imageGenerator.generate(blob, thumbnail, spec).then( function() { generatedThumbnails++; show(thumbnail); previewGeneration[id].success(); }, function() { previewGeneration[id].failure(); // Display the "not available" placeholder img only if we are // not expecting a thumbnail at a later point, such as in a server response. if (!options.placeholders.waitUntilUpdate) { maybeSetDisplayNotAvailableImg(id, thumbnail); } }); }, generateNextQueuedPreview = function() { if (thumbGenerationQueue.length) { thumbnailQueueMonitorRunning = true; var queuedThumbRequest = thumbGenerationQueue.shift(); if (queuedThumbRequest.update) { processUpdateQueuedPreviewRequest(queuedThumbRequest); } else { processNewQueuedPreviewRequest(queuedThumbRequest); } } else { thumbnailQueueMonitorRunning = false; } }, getCancel = function(id) { return getTemplateEl(getFile(id), selectorClasses.cancel); }, getContinue = function(id) { return getTemplateEl(getFile(id), selectorClasses.continueButton); }, getDialog = function(type) { return getTemplateEl(container, selectorClasses[type + "Dialog"]); }, getDelete = function(id) { return getTemplateEl(getFile(id), selectorClasses.deleteButton); }, getDropProcessing = function() { return getTemplateEl(container, selectorClasses.dropProcessing); }, getEditIcon = function(id) { return getTemplateEl(getFile(id), selectorClasses.editNameIcon); }, getFile = function(id) { return fileBatch.map[id] || qq(fileList).getFirstByClass(FILE_CLASS_PREFIX + id); }, getFilename = function(id) { return getTemplateEl(getFile(id), selectorClasses.file); }, getPause = function(id) { return getTemplateEl(getFile(id), selectorClasses.pause); }, getProgress = function(id) { /* jshint eqnull:true */ // Total progress bar if (id == null) { return getTemplateEl(container, selectorClasses.totalProgressBarContainer) || getTemplateEl(container, selectorClasses.totalProgressBar); } // Per-file progress bar return getTemplateEl(getFile(id), selectorClasses.progressBarContainer) || getTemplateEl(getFile(id), selectorClasses.progressBar); }, getRetry = function(id) { return getTemplateEl(getFile(id), selectorClasses.retry); }, getSize = function(id) { return getTemplateEl(getFile(id), selectorClasses.size); }, getSpinner = function(id) { return getTemplateEl(getFile(id), selectorClasses.spinner); }, getTemplateEl = function(context, cssClass) { return context && qq(context).getFirstByClass(cssClass); }, getThumbnail = function(id) { return showThumbnails && getTemplateEl(getFile(id), selectorClasses.thumbnail); }, hide = function(el) { el && qq(el).addClass(options.classes.hide); }, // Ensures a placeholder image does not exceed any max size specified // via `style` attribute properties iff was not used to scale // the placeholder AND the target doesn't already have these `style` attribute properties set. maybeScalePlaceholderViaCss = function(placeholder, thumbnail) { var maxWidth = placeholder.style.maxWidth, maxHeight = placeholder.style.maxHeight; if (maxHeight && maxWidth && !thumbnail.style.maxWidth && !thumbnail.style.maxHeight) { qq(thumbnail).css({ maxWidth: maxWidth, maxHeight: maxHeight }); } }, // Displays a "thumbnail not available" type placeholder image // iff we were able to load this placeholder during initialization // of the templating module or after preview generation has failed. maybeSetDisplayNotAvailableImg = function(id, thumbnail) { var previewing = previewGeneration[id] || new qq.Promise().failure(), notAvailableImgPlacement = new qq.Promise(); cachedThumbnailNotAvailableImg.then(function(img) { previewing.then( function() { notAvailableImgPlacement.success(); }, function() { maybeScalePlaceholderViaCss(img, thumbnail); thumbnail.onload = function() { thumbnail.onload = null; notAvailableImgPlacement.success(); }; thumbnail.src = img.src; show(thumbnail); } ); }); return notAvailableImgPlacement; }, /** * Grabs the HTML from the script tag holding the template markup. This function will also adjust * some internally-tracked state variables based on the contents of the template. * The template is filtered so that irrelevant elements (such as the drop zone if DnD is not supported) * are omitted from the DOM. Useful errors will be thrown if the template cannot be parsed. * * @returns {{template: *, fileTemplate: *}} HTML for the top-level file items templates */ parseAndGetTemplate = function() { var scriptEl, scriptHtml, fileListNode, tempTemplateEl, fileListEl, defaultButton, dropArea, thumbnail, dropProcessing, dropTextEl, uploaderEl; log("Parsing template"); /*jshint -W116*/ if (options.templateIdOrEl == null) { throw new Error("You MUST specify either a template element or ID!"); } // Grab the contents of the script tag holding the template. if (qq.isString(options.templateIdOrEl)) { scriptEl = document.getElementById(options.templateIdOrEl); if (scriptEl === null) { throw new Error(qq.format("Cannot find template script at ID '{}'!", options.templateIdOrEl)); } scriptHtml = scriptEl.innerHTML; } else { if (options.templateIdOrEl.innerHTML === undefined) { throw new Error("You have specified an invalid value for the template option! " + "It must be an ID or an Element."); } scriptHtml = options.templateIdOrEl.innerHTML; } scriptHtml = qq.trimStr(scriptHtml); tempTemplateEl = document.createElement("div"); tempTemplateEl.appendChild(qq.toElement(scriptHtml)); uploaderEl = qq(tempTemplateEl).getFirstByClass(selectorClasses.uploader); // Don't include the default template button in the DOM // if an alternate button container has been specified. if (options.button) { defaultButton = qq(tempTemplateEl).getFirstByClass(selectorClasses.button); if (defaultButton) { qq(defaultButton).remove(); } } // Omit the drop processing element from the DOM if DnD is not supported by the UA, // or the drag and drop module is not found. // NOTE: We are consciously not removing the drop zone if the UA doesn't support DnD // to support layouts where the drop zone is also a container for visible elements, // such as the file list. if (!qq.DragAndDrop || !qq.supportedFeatures.fileDrop) { dropProcessing = qq(tempTemplateEl).getFirstByClass(selectorClasses.dropProcessing); if (dropProcessing) { qq(dropProcessing).remove(); } } dropArea = qq(tempTemplateEl).getFirstByClass(selectorClasses.drop); // If DnD is not available then remove // it from the DOM as well. if (dropArea && !qq.DragAndDrop) { log("DnD module unavailable.", "info"); qq(dropArea).remove(); } if (!qq.supportedFeatures.fileDrop) { // don't display any "drop files to upload" background text uploaderEl.removeAttribute(DROPZPONE_TEXT_ATTR); if (dropArea && qq(dropArea).hasAttribute(HIDE_DROPZONE_ATTR)) { // If there is a drop area defined in the template, and the current UA doesn't support DnD, // and the drop area is marked as "hide before enter", ensure it is hidden as the DnD module // will not do this (since we will not be loading the DnD module) qq(dropArea).css({ display: "none" }); } } else if (qq(uploaderEl).hasAttribute(DROPZPONE_TEXT_ATTR) && dropArea) { dropTextEl = qq(dropArea).getFirstByClass(selectorClasses.dropText); dropTextEl && qq(dropTextEl).remove(); } // Ensure the `showThumbnails` flag is only set if the thumbnail element // is present in the template AND the current UA is capable of generating client-side previews. thumbnail = qq(tempTemplateEl).getFirstByClass(selectorClasses.thumbnail); if (!showThumbnails) { thumbnail && qq(thumbnail).remove(); } else if (thumbnail) { thumbnailMaxSize = parseInt(thumbnail.getAttribute(THUMBNAIL_MAX_SIZE_ATTR)); // Only enforce max size if the attr value is non-zero thumbnailMaxSize = thumbnailMaxSize > 0 ? thumbnailMaxSize : null; serverScale = qq(thumbnail).hasAttribute(THUMBNAIL_SERVER_SCALE_ATTR); } showThumbnails = showThumbnails && thumbnail; isEditElementsExist = qq(tempTemplateEl).getByClass(selectorClasses.editFilenameInput).length > 0; isRetryElementExist = qq(tempTemplateEl).getByClass(selectorClasses.retry).length > 0; fileListNode = qq(tempTemplateEl).getFirstByClass(selectorClasses.list); /*jshint -W116*/ if (fileListNode == null) { throw new Error("Could not find the file list container in the template!"); } fileListEl = fileListNode.children[0].cloneNode(true); fileListNode.innerHTML = ""; // We must call `createElement` in IE8 in order to target and hide any via CSS if (tempTemplateEl.getElementsByTagName("DIALOG").length) { document.createElement("dialog"); } log("Template parsing complete"); return { template: tempTemplateEl, fileTemplate: fileListEl }; }, prependFile = function(el, index, fileList) { var parentEl = fileList, beforeEl = parentEl.firstChild; if (index > 0) { beforeEl = qq(parentEl).children()[index].nextSibling; } parentEl.insertBefore(el, beforeEl); }, processNewQueuedPreviewRequest = function(queuedThumbRequest) { var id = queuedThumbRequest.id, optFileOrBlob = queuedThumbRequest.optFileOrBlob, relatedThumbnailId = optFileOrBlob && optFileOrBlob.qqThumbnailId, thumbnail = getThumbnail(id), spec = { customResizeFunction: queuedThumbRequest.customResizeFunction, maxSize: thumbnailMaxSize, orient: true, scale: true }; if (qq.supportedFeatures.imagePreviews) { if (thumbnail) { if (options.limits.maxThumbs && options.limits.maxThumbs <= generatedThumbnails) { maybeSetDisplayNotAvailableImg(id, thumbnail); generateNextQueuedPreview(); } else { displayWaitingImg(thumbnail).done(function() { previewGeneration[id] = new qq.Promise(); previewGeneration[id].done(function() { setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs); }); /* jshint eqnull: true */ // If we've already generated an for this file, use the one that exists, // don't waste resources generating a new one. if (relatedThumbnailId != null) { useCachedPreview(id, relatedThumbnailId); } else { generateNewPreview(id, optFileOrBlob, spec); } }); } } // File element in template may have been removed, so move on to next item in queue else { generateNextQueuedPreview(); } } else if (thumbnail) { displayWaitingImg(thumbnail); generateNextQueuedPreview(); } }, processUpdateQueuedPreviewRequest = function(queuedThumbRequest) { var id = queuedThumbRequest.id, thumbnailUrl = queuedThumbRequest.thumbnailUrl, showWaitingImg = queuedThumbRequest.showWaitingImg, thumbnail = getThumbnail(id), spec = { customResizeFunction: queuedThumbRequest.customResizeFunction, scale: serverScale, maxSize: thumbnailMaxSize }; if (thumbnail) { if (thumbnailUrl) { if (options.limits.maxThumbs && options.limits.maxThumbs <= generatedThumbnails) { maybeSetDisplayNotAvailableImg(id, thumbnail); generateNextQueuedPreview(); } else { if (showWaitingImg) { displayWaitingImg(thumbnail); } return options.imageGenerator.generate(thumbnailUrl, thumbnail, spec).then( function() { show(thumbnail); generatedThumbnails++; setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs); }, function() { maybeSetDisplayNotAvailableImg(id, thumbnail); setTimeout(generateNextQueuedPreview, options.limits.timeBetweenThumbs); } ); } } else { maybeSetDisplayNotAvailableImg(id, thumbnail); generateNextQueuedPreview(); } } }, setProgressBarWidth = function(id, percent) { var bar = getProgress(id), /* jshint eqnull:true */ progressBarSelector = id == null ? selectorClasses.totalProgressBar : selectorClasses.progressBar; if (bar && !qq(bar).hasClass(progressBarSelector)) { bar = qq(bar).getFirstByClass(progressBarSelector); } if (bar) { qq(bar).css({width: percent + "%"}); bar.setAttribute("aria-valuenow", percent); } }, show = function(el) { el && qq(el).removeClass(options.classes.hide); }, useCachedPreview = function(targetThumbnailId, cachedThumbnailId) { var targetThumbnail = getThumbnail(targetThumbnailId), cachedThumbnail = getThumbnail(cachedThumbnailId); log(qq.format("ID {} is the same file as ID {}. Will use generated thumbnail from ID {} instead.", targetThumbnailId, cachedThumbnailId, cachedThumbnailId)); // Generation of the related thumbnail may still be in progress, so, wait until it is done. previewGeneration[cachedThumbnailId].then(function() { generatedThumbnails++; previewGeneration[targetThumbnailId].success(); log(qq.format("Now using previously generated thumbnail created for ID {} on ID {}.", cachedThumbnailId, targetThumbnailId)); targetThumbnail.src = cachedThumbnail.src; show(targetThumbnail); }, function() { previewGeneration[targetThumbnailId].failure(); if (!options.placeholders.waitUntilUpdate) { maybeSetDisplayNotAvailableImg(targetThumbnailId, targetThumbnail); } }); }; qq.extend(options, spec); log = options.log; // No need to worry about conserving CPU or memory on older browsers, // since there is no ability to preview, and thumbnail display is primitive and quick. if (!qq.supportedFeatures.imagePreviews) { options.limits.timeBetweenThumbs = 0; options.limits.maxThumbs = 0; } container = options.containerEl; showThumbnails = options.imageGenerator !== undefined; templateDom = parseAndGetTemplate(); cacheThumbnailPlaceholders(); qq.extend(this, { render: function() { log("Rendering template in DOM."); generatedThumbnails = 0; container.appendChild(templateDom.template.cloneNode(true)); hide(getDropProcessing()); this.hideTotalProgress(); fileList = options.fileContainerEl || getTemplateEl(container, selectorClasses.list); log("Template rendering complete"); }, renderFailure: function(message) { var cantRenderEl = qq.toElement(message); container.innerHTML = ""; container.appendChild(cantRenderEl); }, reset: function() { container.innerHTML = ""; this.render(); }, clearFiles: function() { fileList.innerHTML = ""; }, disableCancel: function() { isCancelDisabled = true; }, addFile: function(id, name, prependInfo, hideForever, batch) { var fileEl = templateDom.fileTemplate.cloneNode(true), fileNameEl = getTemplateEl(fileEl, selectorClasses.file), uploaderEl = getTemplateEl(container, selectorClasses.uploader), fileContainer = batch ? fileBatch.content : fileList, thumb; if (batch) { fileBatch.map[id] = fileEl; } qq(fileEl).addClass(FILE_CLASS_PREFIX + id); uploaderEl.removeAttribute(DROPZPONE_TEXT_ATTR); if (fileNameEl) { qq(fileNameEl).setText(name); fileNameEl.setAttribute("title", name); } fileEl.setAttribute(FILE_ID_ATTR, id); if (prependInfo) { prependFile(fileEl, prependInfo.index, fileContainer); } else { fileContainer.appendChild(fileEl); } if (hideForever) { fileEl.style.display = "none"; qq(fileEl).addClass(HIDDEN_FOREVER_CLASS); } else { hide(getProgress(id)); hide(getSize(id)); hide(getDelete(id)); hide(getRetry(id)); hide(getPause(id)); hide(getContinue(id)); if (isCancelDisabled) { this.hideCancel(id); } thumb = getThumbnail(id); if (thumb && !thumb.src) { cachedWaitingForThumbnailImg.then(function(waitingImg) { thumb.src = waitingImg.src; if (waitingImg.style.maxHeight && waitingImg.style.maxWidth) { qq(thumb).css({ maxHeight: waitingImg.style.maxHeight, maxWidth: waitingImg.style.maxWidth }); } show(thumb); }); } } }, addFileToCache: function(id, name, prependInfo, hideForever) { this.addFile(id, name, prependInfo, hideForever, true); }, addCacheToDom: function() { fileList.appendChild(fileBatch.content); fileBatch.content = document.createDocumentFragment(); fileBatch.map = {}; }, removeFile: function(id) { qq(getFile(id)).remove(); }, getFileId: function(el) { var currentNode = el; if (currentNode) { /*jshint -W116*/ while (currentNode.getAttribute(FILE_ID_ATTR) == null) { currentNode = currentNode.parentNode; } return parseInt(currentNode.getAttribute(FILE_ID_ATTR)); } }, getFileList: function() { return fileList; }, markFilenameEditable: function(id) { var filename = getFilename(id); filename && qq(filename).addClass(options.classes.editable); }, updateFilename: function(id, name) { var filenameEl = getFilename(id); if (filenameEl) { qq(filenameEl).setText(name); filenameEl.setAttribute("title", name); } }, hideFilename: function(id) { hide(getFilename(id)); }, showFilename: function(id) { show(getFilename(id)); }, isFileName: function(el) { return qq(el).hasClass(selectorClasses.file); }, getButton: function() { return options.button || getTemplateEl(container, selectorClasses.button); }, hideDropProcessing: function() { hide(getDropProcessing()); }, showDropProcessing: function() { show(getDropProcessing()); }, getDropZone: function() { return getTemplateEl(container, selectorClasses.drop); }, isEditFilenamePossible: function() { return isEditElementsExist; }, hideRetry: function(id) { hide(getRetry(id)); }, isRetryPossible: function() { return isRetryElementExist; }, showRetry: function(id) { show(getRetry(id)); }, getFileContainer: function(id) { return getFile(id); }, showEditIcon: function(id) { var icon = getEditIcon(id); icon && qq(icon).addClass(options.classes.editable); }, isHiddenForever: function(id) { return qq(getFile(id)).hasClass(HIDDEN_FOREVER_CLASS); }, hideEditIcon: function(id) { var icon = getEditIcon(id); icon && qq(icon).removeClass(options.classes.editable); }, isEditIcon: function(el) { return qq(el).hasClass(selectorClasses.editNameIcon, true); }, getEditInput: function(id) { return getTemplateEl(getFile(id), selectorClasses.editFilenameInput); }, isEditInput: function(el) { return qq(el).hasClass(selectorClasses.editFilenameInput, true); }, updateProgress: function(id, loaded, total) { var bar = getProgress(id), percent; if (bar && total > 0) { percent = Math.round(loaded / total * 100); if (percent === 100) { hide(bar); } else { show(bar); } setProgressBarWidth(id, percent); } }, updateTotalProgress: function(loaded, total) { this.updateProgress(null, loaded, total); }, hideProgress: function(id) { var bar = getProgress(id); bar && hide(bar); }, hideTotalProgress: function() { this.hideProgress(); }, resetProgress: function(id) { setProgressBarWidth(id, 0); this.hideTotalProgress(id); }, resetTotalProgress: function() { this.resetProgress(); }, showCancel: function(id) { if (!isCancelDisabled) { var cancel = getCancel(id); cancel && qq(cancel).removeClass(options.classes.hide); } }, hideCancel: function(id) { hide(getCancel(id)); }, isCancel: function(el) { return qq(el).hasClass(selectorClasses.cancel, true); }, allowPause: function(id) { show(getPause(id)); hide(getContinue(id)); }, uploadPaused: function(id) { this.setStatusText(id, options.text.paused); this.allowContinueButton(id); hide(getSpinner(id)); }, hidePause: function(id) { hide(getPause(id)); }, isPause: function(el) { return qq(el).hasClass(selectorClasses.pause, true); }, isContinueButton: function(el) { return qq(el).hasClass(selectorClasses.continueButton, true); }, allowContinueButton: function(id) { show(getContinue(id)); hide(getPause(id)); }, uploadContinued: function(id) { this.setStatusText(id, ""); this.allowPause(id); show(getSpinner(id)); }, showDeleteButton: function(id) { show(getDelete(id)); }, hideDeleteButton: function(id) { hide(getDelete(id)); }, isDeleteButton: function(el) { return qq(el).hasClass(selectorClasses.deleteButton, true); }, isRetry: function(el) { return qq(el).hasClass(selectorClasses.retry, true); }, updateSize: function(id, text) { var size = getSize(id); if (size) { show(size); qq(size).setText(text); } }, setStatusText: function(id, text) { var textEl = getTemplateEl(getFile(id), selectorClasses.statusText); if (textEl) { /*jshint -W116*/ if (text == null) { qq(textEl).clearText(); } else { qq(textEl).setText(text); } } }, hideSpinner: function(id) { qq(getFile(id)).removeClass(IN_PROGRESS_CLASS); hide(getSpinner(id)); }, showSpinner: function(id) { qq(getFile(id)).addClass(IN_PROGRESS_CLASS); show(getSpinner(id)); }, generatePreview: function(id, optFileOrBlob, customResizeFunction) { if (!this.isHiddenForever(id)) { thumbGenerationQueue.push({id: id, customResizeFunction: customResizeFunction, optFileOrBlob: optFileOrBlob}); !thumbnailQueueMonitorRunning && generateNextQueuedPreview(); } }, updateThumbnail: function(id, thumbnailUrl, showWaitingImg, customResizeFunction) { if (!this.isHiddenForever(id)) { thumbGenerationQueue.push({customResizeFunction: customResizeFunction, update: true, id: id, thumbnailUrl: thumbnailUrl, showWaitingImg: showWaitingImg}); !thumbnailQueueMonitorRunning && generateNextQueuedPreview(); } }, hasDialog: function(type) { return qq.supportedFeatures.dialogElement && !!getDialog(type); }, showDialog: function(type, message, defaultValue) { var dialog = getDialog(type), messageEl = getTemplateEl(dialog, selectorClasses.dialogMessage), inputEl = dialog.getElementsByTagName("INPUT")[0], cancelBtn = getTemplateEl(dialog, selectorClasses.dialogCancelButton), okBtn = getTemplateEl(dialog, selectorClasses.dialogOkButton), promise = new qq.Promise(), closeHandler = function() { cancelBtn.removeEventListener("click", cancelClickHandler); okBtn && okBtn.removeEventListener("click", okClickHandler); promise.failure(); }, cancelClickHandler = function() { cancelBtn.removeEventListener("click", cancelClickHandler); dialog.close(); }, okClickHandler = function() { dialog.removeEventListener("close", closeHandler); okBtn.removeEventListener("click", okClickHandler); dialog.close(); promise.success(inputEl && inputEl.value); }; dialog.addEventListener("close", closeHandler); cancelBtn.addEventListener("click", cancelClickHandler); okBtn && okBtn.addEventListener("click", okClickHandler); if (inputEl) { inputEl.value = defaultValue; } messageEl.textContent = message; dialog.showModal(); return promise; } }); }; ================================================ FILE: client/js/third-party/ExifRestorer.js ================================================ //Based on MinifyJpeg //http://elicon.blog57.fc2.com/blog-entry-206.html qq.ExifRestorer = (function() { var ExifRestorer = {}; ExifRestorer.KEY_STR = "ABCDEFGHIJKLMNOP" + "QRSTUVWXYZabcdef" + "ghijklmnopqrstuv" + "wxyz0123456789+/" + "="; ExifRestorer.encode64 = function(input) { var output = "", chr1, chr2, chr3 = "", enc1, enc2, enc3, enc4 = "", i = 0; do { chr1 = input[i++]; chr2 = input[i++]; chr3 = input[i++]; enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + this.KEY_STR.charAt(enc1) + this.KEY_STR.charAt(enc2) + this.KEY_STR.charAt(enc3) + this.KEY_STR.charAt(enc4); chr1 = chr2 = chr3 = ""; enc1 = enc2 = enc3 = enc4 = ""; } while (i < input.length); return output; }; ExifRestorer.restore = function(origFileBase64, resizedFileBase64) { var expectedBase64Header = "data:image/jpeg;base64,"; if (!origFileBase64.match(expectedBase64Header)) { return resizedFileBase64; } var rawImage = this.decode64(origFileBase64.replace(expectedBase64Header, "")); var segments = this.slice2Segments(rawImage); var image = this.exifManipulation(resizedFileBase64, segments); return expectedBase64Header + this.encode64(image); }; ExifRestorer.exifManipulation = function(resizedFileBase64, segments) { var exifArray = this.getExifArray(segments), newImageArray = this.insertExif(resizedFileBase64, exifArray), aBuffer = new Uint8Array(newImageArray); return aBuffer; }; ExifRestorer.getExifArray = function(segments) { var seg; for (var x = 0; x < segments.length; x++) { seg = segments[x]; if (seg[0] == 255 & seg[1] == 225) //(ff e1) { return seg; } } return []; }; ExifRestorer.insertExif = function(resizedFileBase64, exifArray) { var imageData = resizedFileBase64.replace("data:image/jpeg;base64,", ""), buf = this.decode64(imageData), separatePoint = buf.indexOf(255,3), mae = buf.slice(0, separatePoint), ato = buf.slice(separatePoint), array = mae; array = array.concat(exifArray); array = array.concat(ato); return array; }; ExifRestorer.slice2Segments = function(rawImageArray) { var head = 0, segments = []; while (1) { if (rawImageArray[head] == 255 & rawImageArray[head + 1] == 218){break;} if (rawImageArray[head] == 255 & rawImageArray[head + 1] == 216) { head += 2; } else { var length = rawImageArray[head + 2] * 256 + rawImageArray[head + 3], endPoint = head + length + 2, seg = rawImageArray.slice(head, endPoint); segments.push(seg); head = endPoint; } if (head > rawImageArray.length){break;} } return segments; }; ExifRestorer.decode64 = function(input) { var output = "", chr1, chr2, chr3 = "", enc1, enc2, enc3, enc4 = "", i = 0, buf = []; // remove all characters that are not A-Z, a-z, 0-9, +, /, or = var base64test = /[^A-Za-z0-9\+\/\=]/g; if (base64test.exec(input)) { throw new Error("There were invalid base64 characters in the input text. " + "Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='"); } input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); do { enc1 = this.KEY_STR.indexOf(input.charAt(i++)); enc2 = this.KEY_STR.indexOf(input.charAt(i++)); enc3 = this.KEY_STR.indexOf(input.charAt(i++)); enc4 = this.KEY_STR.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; buf.push(chr1); if (enc3 != 64) { buf.push(chr2); } if (enc4 != 64) { buf.push(chr3); } chr1 = chr2 = chr3 = ""; enc1 = enc2 = enc3 = enc4 = ""; } while (i < input.length); return buf; }; return ExifRestorer; })(); ================================================ FILE: client/js/third-party/crypto-js/core.js ================================================ /* CryptoJS v3.1.2 code.google.com/p/crypto-js (c) 2009-2013 by Jeff Mott. All rights reserved. code.google.com/p/crypto-js/wiki/License */ /** * CryptoJS core components. */ qq.CryptoJS = (function (Math, undefined) { /** * CryptoJS namespace. */ var C = {}; /** * Library namespace. */ var C_lib = C.lib = {}; /** * Base object for prototypal inheritance. */ var Base = C_lib.Base = (function () { function F() {} return { /** * Creates a new object that inherits from this object. * * @param {Object} overrides Properties to copy into the new object. * * @return {Object} The new object. * * @static * * @example * * var MyType = CryptoJS.lib.Base.extend({ * field: 'value', * * method: function () { * } * }); */ extend: function (overrides) { // Spawn F.prototype = this; var subtype = new F(); // Augment if (overrides) { subtype.mixIn(overrides); } // Create default initializer if (!subtype.hasOwnProperty('init')) { subtype.init = function () { subtype.$super.init.apply(this, arguments); }; } // Initializer's prototype is the subtype object subtype.init.prototype = subtype; // Reference supertype subtype.$super = this; return subtype; }, /** * Extends this object and runs the init method. * Arguments to create() will be passed to init(). * * @return {Object} The new object. * * @static * * @example * * var instance = MyType.create(); */ create: function () { var instance = this.extend(); instance.init.apply(instance, arguments); return instance; }, /** * Initializes a newly created object. * Override this method to add some logic when your objects are created. * * @example * * var MyType = CryptoJS.lib.Base.extend({ * init: function () { * // ... * } * }); */ init: function () { }, /** * Copies properties into this object. * * @param {Object} properties The properties to mix in. * * @example * * MyType.mixIn({ * field: 'value' * }); */ mixIn: function (properties) { for (var propertyName in properties) { if (properties.hasOwnProperty(propertyName)) { this[propertyName] = properties[propertyName]; } } // IE won't copy toString using the loop above if (properties.hasOwnProperty('toString')) { this.toString = properties.toString; } }, /** * Creates a copy of this object. * * @return {Object} The clone. * * @example * * var clone = instance.clone(); */ clone: function () { return this.init.prototype.extend(this); } }; }()); /** * An array of 32-bit words. * * @property {Array} words The array of 32-bit words. * @property {number} sigBytes The number of significant bytes in this word array. */ var WordArray = C_lib.WordArray = Base.extend({ /** * Initializes a newly created word array. * * @param {Array} words (Optional) An array of 32-bit words. * @param {number} sigBytes (Optional) The number of significant bytes in the words. * * @example * * var wordArray = CryptoJS.lib.WordArray.create(); * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); */ init: function (words, sigBytes) { words = this.words = words || []; if (sigBytes != undefined) { this.sigBytes = sigBytes; } else { this.sigBytes = words.length * 4; } }, /** * Converts this word array to a string. * * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex * * @return {string} The stringified word array. * * @example * * var string = wordArray + ''; * var string = wordArray.toString(); * var string = wordArray.toString(CryptoJS.enc.Utf8); */ toString: function (encoder) { return (encoder || Hex).stringify(this); }, /** * Concatenates a word array to this word array. * * @param {WordArray} wordArray The word array to append. * * @return {WordArray} This word array. * * @example * * wordArray1.concat(wordArray2); */ concat: function (wordArray) { // Shortcuts var thisWords = this.words; var thatWords = wordArray.words; var thisSigBytes = this.sigBytes; var thatSigBytes = wordArray.sigBytes; // Clamp excess bits this.clamp(); // Concat if (thisSigBytes % 4) { // Copy one byte at a time for (var i = 0; i < thatSigBytes; i++) { var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); } } else if (thatWords.length > 0xffff) { // Copy one word at a time for (var i = 0; i < thatSigBytes; i += 4) { thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2]; } } else { // Copy all words at once thisWords.push.apply(thisWords, thatWords); } this.sigBytes += thatSigBytes; // Chainable return this; }, /** * Removes insignificant bits. * * @example * * wordArray.clamp(); */ clamp: function () { // Shortcuts var words = this.words; var sigBytes = this.sigBytes; // Clamp words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); words.length = Math.ceil(sigBytes / 4); }, /** * Creates a copy of this word array. * * @return {WordArray} The clone. * * @example * * var clone = wordArray.clone(); */ clone: function () { var clone = Base.clone.call(this); clone.words = this.words.slice(0); return clone; }, /** * Creates a word array filled with random bytes. * * @param {number} nBytes The number of random bytes to generate. * * @return {WordArray} The random word array. * * @static * * @example * * var wordArray = CryptoJS.lib.WordArray.random(16); */ random: function (nBytes) { var words = []; for (var i = 0; i < nBytes; i += 4) { words.push((Math.random() * 0x100000000) | 0); } return new WordArray.init(words, nBytes); } }); /** * Encoder namespace. */ var C_enc = C.enc = {}; /** * Hex encoding strategy. */ var Hex = C_enc.Hex = { /** * Converts a word array to a hex string. * * @param {WordArray} wordArray The word array. * * @return {string} The hex string. * * @static * * @example * * var hexString = CryptoJS.enc.Hex.stringify(wordArray); */ stringify: function (wordArray) { // Shortcuts var words = wordArray.words; var sigBytes = wordArray.sigBytes; // Convert var hexChars = []; for (var i = 0; i < sigBytes; i++) { var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; hexChars.push((bite >>> 4).toString(16)); hexChars.push((bite & 0x0f).toString(16)); } return hexChars.join(''); }, /** * Converts a hex string to a word array. * * @param {string} hexStr The hex string. * * @return {WordArray} The word array. * * @static * * @example * * var wordArray = CryptoJS.enc.Hex.parse(hexString); */ parse: function (hexStr) { // Shortcut var hexStrLength = hexStr.length; // Convert var words = []; for (var i = 0; i < hexStrLength; i += 2) { words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); } return new WordArray.init(words, hexStrLength / 2); } }; /** * Latin1 encoding strategy. */ var Latin1 = C_enc.Latin1 = { /** * Converts a word array to a Latin1 string. * * @param {WordArray} wordArray The word array. * * @return {string} The Latin1 string. * * @static * * @example * * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); */ stringify: function (wordArray) { // Shortcuts var words = wordArray.words; var sigBytes = wordArray.sigBytes; // Convert var latin1Chars = []; for (var i = 0; i < sigBytes; i++) { var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; latin1Chars.push(String.fromCharCode(bite)); } return latin1Chars.join(''); }, /** * Converts a Latin1 string to a word array. * * @param {string} latin1Str The Latin1 string. * * @return {WordArray} The word array. * * @static * * @example * * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); */ parse: function (latin1Str) { // Shortcut var latin1StrLength = latin1Str.length; // Convert var words = []; for (var i = 0; i < latin1StrLength; i++) { words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); } return new WordArray.init(words, latin1StrLength); } }; /** * UTF-8 encoding strategy. */ var Utf8 = C_enc.Utf8 = { /** * Converts a word array to a UTF-8 string. * * @param {WordArray} wordArray The word array. * * @return {string} The UTF-8 string. * * @static * * @example * * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); */ stringify: function (wordArray) { try { return decodeURIComponent(escape(Latin1.stringify(wordArray))); } catch (e) { throw new Error('Malformed UTF-8 data'); } }, /** * Converts a UTF-8 string to a word array. * * @param {string} utf8Str The UTF-8 string. * * @return {WordArray} The word array. * * @static * * @example * * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); */ parse: function (utf8Str) { return Latin1.parse(unescape(encodeURIComponent(utf8Str))); } }; /** * Abstract buffered block algorithm template. * * The property blockSize must be implemented in a concrete subtype. * * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0 */ var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({ /** * Resets this block algorithm's data buffer to its initial state. * * @example * * bufferedBlockAlgorithm.reset(); */ reset: function () { // Initial values this._data = new WordArray.init(); this._nDataBytes = 0; }, /** * Adds new data to this block algorithm's buffer. * * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8. * * @example * * bufferedBlockAlgorithm._append('data'); * bufferedBlockAlgorithm._append(wordArray); */ _append: function (data) { // Convert string to WordArray, else assume WordArray already if (typeof data == 'string') { data = Utf8.parse(data); } // Append this._data.concat(data); this._nDataBytes += data.sigBytes; }, /** * Processes available data blocks. * * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. * * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. * * @return {WordArray} The processed data. * * @example * * var processedData = bufferedBlockAlgorithm._process(); * var processedData = bufferedBlockAlgorithm._process(!!'flush'); */ _process: function (doFlush) { // Shortcuts var data = this._data; var dataWords = data.words; var dataSigBytes = data.sigBytes; var blockSize = this.blockSize; var blockSizeBytes = blockSize * 4; // Count blocks ready var nBlocksReady = dataSigBytes / blockSizeBytes; if (doFlush) { // Round up to include partial blocks nBlocksReady = Math.ceil(nBlocksReady); } else { // Round down to include only full blocks, // less the number of blocks that must remain in the buffer nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); } // Count words ready var nWordsReady = nBlocksReady * blockSize; // Count bytes ready var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); // Process blocks if (nWordsReady) { for (var offset = 0; offset < nWordsReady; offset += blockSize) { // Perform concrete-algorithm logic this._doProcessBlock(dataWords, offset); } // Remove processed words var processedWords = dataWords.splice(0, nWordsReady); data.sigBytes -= nBytesReady; } // Return processed words return new WordArray.init(processedWords, nBytesReady); }, /** * Creates a copy of this object. * * @return {Object} The clone. * * @example * * var clone = bufferedBlockAlgorithm.clone(); */ clone: function () { var clone = Base.clone.call(this); clone._data = this._data.clone(); return clone; }, _minBufferSize: 0 }); /** * Abstract hasher template. * * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits) */ var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({ /** * Configuration options. */ cfg: Base.extend(), /** * Initializes a newly created hasher. * * @param {Object} cfg (Optional) The configuration options to use for this hash computation. * * @example * * var hasher = CryptoJS.algo.SHA256.create(); */ init: function (cfg) { // Apply config defaults this.cfg = this.cfg.extend(cfg); // Set initial values this.reset(); }, /** * Resets this hasher to its initial state. * * @example * * hasher.reset(); */ reset: function () { // Reset data buffer BufferedBlockAlgorithm.reset.call(this); // Perform concrete-hasher logic this._doReset(); }, /** * Updates this hasher with a message. * * @param {WordArray|string} messageUpdate The message to append. * * @return {Hasher} This hasher. * * @example * * hasher.update('message'); * hasher.update(wordArray); */ update: function (messageUpdate) { // Append this._append(messageUpdate); // Update the hash this._process(); // Chainable return this; }, /** * Finalizes the hash computation. * Note that the finalize operation is effectively a destructive, read-once operation. * * @param {WordArray|string} messageUpdate (Optional) A final message update. * * @return {WordArray} The hash. * * @example * * var hash = hasher.finalize(); * var hash = hasher.finalize('message'); * var hash = hasher.finalize(wordArray); */ finalize: function (messageUpdate) { // Final message update if (messageUpdate) { this._append(messageUpdate); } // Perform concrete-hasher logic var hash = this._doFinalize(); return hash; }, blockSize: 512/32, /** * Creates a shortcut function to a hasher's object interface. * * @param {Hasher} hasher The hasher to create a helper for. * * @return {Function} The shortcut function. * * @static * * @example * * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); */ _createHelper: function (hasher) { return function (message, cfg) { return new hasher.init(cfg).finalize(message); }; }, /** * Creates a shortcut function to the HMAC's object interface. * * @param {Hasher} hasher The hasher to use in this HMAC helper. * * @return {Function} The shortcut function. * * @static * * @example * * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); */ _createHmacHelper: function (hasher) { return function (message, key) { return new C_algo.HMAC.init(hasher, key).finalize(message); }; } }); /** * Algorithm namespace. */ var C_algo = C.algo = {}; return C; }(Math)); ================================================ FILE: client/js/third-party/crypto-js/enc-base64.js ================================================ /* CryptoJS v3.1.2 code.google.com/p/crypto-js (c) 2009-2013 by Jeff Mott. All rights reserved. code.google.com/p/crypto-js/wiki/License */ (function () { // Shortcuts var C = qq.CryptoJS; var C_lib = C.lib; var WordArray = C_lib.WordArray; var C_enc = C.enc; /** * Base64 encoding strategy. */ var Base64 = C_enc.Base64 = { /** * Converts a word array to a Base64 string. * * @param {WordArray} wordArray The word array. * * @return {string} The Base64 string. * * @static * * @example * * var base64String = CryptoJS.enc.Base64.stringify(wordArray); */ stringify: function (wordArray) { // Shortcuts var words = wordArray.words; var sigBytes = wordArray.sigBytes; var map = this._map; // Clamp excess bits wordArray.clamp(); // Convert var base64Chars = []; for (var i = 0; i < sigBytes; i += 3) { var byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; var triplet = (byte1 << 16) | (byte2 << 8) | byte3; for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) { base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); } } // Add padding var paddingChar = map.charAt(64); if (paddingChar) { while (base64Chars.length % 4) { base64Chars.push(paddingChar); } } return base64Chars.join(''); }, /** * Converts a Base64 string to a word array. * * @param {string} base64Str The Base64 string. * * @return {WordArray} The word array. * * @static * * @example * * var wordArray = CryptoJS.enc.Base64.parse(base64String); */ parse: function (base64Str) { // Shortcuts var base64StrLength = base64Str.length; var map = this._map; // Ignore padding var paddingChar = map.charAt(64); if (paddingChar) { var paddingIndex = base64Str.indexOf(paddingChar); if (paddingIndex != -1) { base64StrLength = paddingIndex; } } // Convert var words = []; var nBytes = 0; for (var i = 0; i < base64StrLength; i++) { if (i % 4) { var bits1 = map.indexOf(base64Str.charAt(i - 1)) << ((i % 4) * 2); var bits2 = map.indexOf(base64Str.charAt(i)) >>> (6 - (i % 4) * 2); words[nBytes >>> 2] |= (bits1 | bits2) << (24 - (nBytes % 4) * 8); nBytes++; } } return WordArray.create(words, nBytes); }, _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' }; }()); ================================================ FILE: client/js/third-party/crypto-js/hmac.js ================================================ /* CryptoJS v3.1.2 code.google.com/p/crypto-js (c) 2009-2013 by Jeff Mott. All rights reserved. code.google.com/p/crypto-js/wiki/License */ (function () { // Shortcuts var C = qq.CryptoJS; var C_lib = C.lib; var Base = C_lib.Base; var C_enc = C.enc; var Utf8 = C_enc.Utf8; var C_algo = C.algo; /** * HMAC algorithm. */ var HMAC = C_algo.HMAC = Base.extend({ /** * Initializes a newly created HMAC. * * @param {Hasher} hasher The hash algorithm to use. * @param {WordArray|string} key The secret key. * * @example * * var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key); */ init: function (hasher, key) { // Init hasher hasher = this._hasher = new hasher.init(); // Convert string to WordArray, else assume WordArray already if (typeof key == 'string') { key = Utf8.parse(key); } // Shortcuts var hasherBlockSize = hasher.blockSize; var hasherBlockSizeBytes = hasherBlockSize * 4; // Allow arbitrary length keys if (key.sigBytes > hasherBlockSizeBytes) { key = hasher.finalize(key); } // Clamp excess bits key.clamp(); // Clone key for inner and outer pads var oKey = this._oKey = key.clone(); var iKey = this._iKey = key.clone(); // Shortcuts var oKeyWords = oKey.words; var iKeyWords = iKey.words; // XOR keys with pad constants for (var i = 0; i < hasherBlockSize; i++) { oKeyWords[i] ^= 0x5c5c5c5c; iKeyWords[i] ^= 0x36363636; } oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes; // Set initial values this.reset(); }, /** * Resets this HMAC to its initial state. * * @example * * hmacHasher.reset(); */ reset: function () { // Shortcut var hasher = this._hasher; // Reset hasher.reset(); hasher.update(this._iKey); }, /** * Updates this HMAC with a message. * * @param {WordArray|string} messageUpdate The message to append. * * @return {HMAC} This HMAC instance. * * @example * * hmacHasher.update('message'); * hmacHasher.update(wordArray); */ update: function (messageUpdate) { this._hasher.update(messageUpdate); // Chainable return this; }, /** * Finalizes the HMAC computation. * Note that the finalize operation is effectively a destructive, read-once operation. * * @param {WordArray|string} messageUpdate (Optional) A final message update. * * @return {WordArray} The HMAC. * * @example * * var hmac = hmacHasher.finalize(); * var hmac = hmacHasher.finalize('message'); * var hmac = hmacHasher.finalize(wordArray); */ finalize: function (messageUpdate) { // Shortcut var hasher = this._hasher; // Compute HMAC var innerHash = hasher.finalize(messageUpdate); hasher.reset(); var hmac = hasher.finalize(this._oKey.clone().concat(innerHash)); return hmac; } }); }()); ================================================ FILE: client/js/third-party/crypto-js/lib-typedarrays.js ================================================ /* CryptoJS v3.1.2 code.google.com/p/crypto-js (c) 2009-2013 by Jeff Mott. All rights reserved. code.google.com/p/crypto-js/wiki/License */ (function () { // Check if typed arrays are supported if (typeof ArrayBuffer != 'function') { return; } // Shortcuts var C = qq.CryptoJS; var C_lib = C.lib; var WordArray = C_lib.WordArray; // Reference original init var superInit = WordArray.init; // Augment WordArray.init to handle typed arrays var subInit = WordArray.init = function (typedArray) { // Convert buffers to uint8 if (typedArray instanceof ArrayBuffer) { typedArray = new Uint8Array(typedArray); } // Convert other array views to uint8 if ( typedArray instanceof Int8Array || typedArray instanceof Uint8ClampedArray || typedArray instanceof Int16Array || typedArray instanceof Uint16Array || typedArray instanceof Int32Array || typedArray instanceof Uint32Array || typedArray instanceof Float32Array || typedArray instanceof Float64Array ) { typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); } // Handle Uint8Array if (typedArray instanceof Uint8Array) { // Shortcut var typedArrayByteLength = typedArray.byteLength; // Extract bytes var words = []; for (var i = 0; i < typedArrayByteLength; i++) { words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8); } // Initialize this word array superInit.call(this, words, typedArrayByteLength); } else { // Else call normal init superInit.apply(this, arguments); } }; subInit.prototype = WordArray; }()); ================================================ FILE: client/js/third-party/crypto-js/sha1.js ================================================ /* CryptoJS v3.1.2 code.google.com/p/crypto-js (c) 2009-2013 by Jeff Mott. All rights reserved. code.google.com/p/crypto-js/wiki/License */ (function () { // Shortcuts var C = qq.CryptoJS; var C_lib = C.lib; var WordArray = C_lib.WordArray; var Hasher = C_lib.Hasher; var C_algo = C.algo; // Reusable object var W = []; /** * SHA-1 hash algorithm. */ var SHA1 = C_algo.SHA1 = Hasher.extend({ _doReset: function () { this._hash = new WordArray.init([ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 ]); }, _doProcessBlock: function (M, offset) { // Shortcut var H = this._hash.words; // Working variables var a = H[0]; var b = H[1]; var c = H[2]; var d = H[3]; var e = H[4]; // Computation for (var i = 0; i < 80; i++) { if (i < 16) { W[i] = M[offset + i] | 0; } else { var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; W[i] = (n << 1) | (n >>> 31); } var t = ((a << 5) | (a >>> 27)) + e + W[i]; if (i < 20) { t += ((b & c) | (~b & d)) + 0x5a827999; } else if (i < 40) { t += (b ^ c ^ d) + 0x6ed9eba1; } else if (i < 60) { t += ((b & c) | (b & d) | (c & d)) - 0x70e44324; } else /* if (i < 80) */ { t += (b ^ c ^ d) - 0x359d3e2a; } e = d; d = c; c = (b << 30) | (b >>> 2); b = a; a = t; } // Intermediate hash value H[0] = (H[0] + a) | 0; H[1] = (H[1] + b) | 0; H[2] = (H[2] + c) | 0; H[3] = (H[3] + d) | 0; H[4] = (H[4] + e) | 0; }, _doFinalize: function () { // Shortcuts var data = this._data; var dataWords = data.words; var nBitsTotal = this._nDataBytes * 8; var nBitsLeft = data.sigBytes * 8; // Add padding dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; data.sigBytes = dataWords.length * 4; // Hash final blocks this._process(); // Return final computed hash return this._hash; }, clone: function () { var clone = Hasher.clone.call(this); clone._hash = this._hash.clone(); return clone; } }); /** * Shortcut function to the hasher's object interface. * * @param {WordArray|string} message The message to hash. * * @return {WordArray} The hash. * * @static * * @example * * var hash = CryptoJS.SHA1('message'); * var hash = CryptoJS.SHA1(wordArray); */ C.SHA1 = Hasher._createHelper(SHA1); /** * Shortcut function to the HMAC's object interface. * * @param {WordArray|string} message The message to hash. * @param {WordArray|string} key The secret key. * * @return {WordArray} The HMAC. * * @static * * @example * * var hmac = CryptoJS.HmacSHA1(message, key); */ C.HmacSHA1 = Hasher._createHmacHelper(SHA1); }()); ================================================ FILE: client/js/third-party/crypto-js/sha256.js ================================================ /* CryptoJS v3.1.2 code.google.com/p/crypto-js (c) 2009-2013 by Jeff Mott. All rights reserved. code.google.com/p/crypto-js/wiki/License */ (function (Math) { // Shortcuts var C = qq.CryptoJS; var C_lib = C.lib; var WordArray = C_lib.WordArray; var Hasher = C_lib.Hasher; var C_algo = C.algo; // Initialization and round constants tables var H = []; var K = []; // Compute constants (function () { function isPrime(n) { var sqrtN = Math.sqrt(n); for (var factor = 2; factor <= sqrtN; factor++) { if (!(n % factor)) { return false; } } return true; } function getFractionalBits(n) { return ((n - (n | 0)) * 0x100000000) | 0; } var n = 2; var nPrime = 0; while (nPrime < 64) { if (isPrime(n)) { if (nPrime < 8) { H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2)); } K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3)); nPrime++; } n++; } }()); // Reusable object var W = []; /** * SHA-256 hash algorithm. */ var SHA256 = C_algo.SHA256 = Hasher.extend({ _doReset: function () { this._hash = new WordArray.init(H.slice(0)); }, _doProcessBlock: function (M, offset) { // Shortcut var H = this._hash.words; // Working variables var a = H[0]; var b = H[1]; var c = H[2]; var d = H[3]; var e = H[4]; var f = H[5]; var g = H[6]; var h = H[7]; // Computation for (var i = 0; i < 64; i++) { if (i < 16) { W[i] = M[offset + i] | 0; } else { var gamma0x = W[i - 15]; var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ ((gamma0x << 14) | (gamma0x >>> 18)) ^ (gamma0x >>> 3); var gamma1x = W[i - 2]; var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ ((gamma1x << 13) | (gamma1x >>> 19)) ^ (gamma1x >>> 10); W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; } var ch = (e & f) ^ (~e & g); var maj = (a & b) ^ (a & c) ^ (b & c); var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); var t1 = h + sigma1 + ch + K[i] + W[i]; var t2 = sigma0 + maj; h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0; } // Intermediate hash value H[0] = (H[0] + a) | 0; H[1] = (H[1] + b) | 0; H[2] = (H[2] + c) | 0; H[3] = (H[3] + d) | 0; H[4] = (H[4] + e) | 0; H[5] = (H[5] + f) | 0; H[6] = (H[6] + g) | 0; H[7] = (H[7] + h) | 0; }, _doFinalize: function () { // Shortcuts var data = this._data; var dataWords = data.words; var nBitsTotal = this._nDataBytes * 8; var nBitsLeft = data.sigBytes * 8; // Add padding dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; data.sigBytes = dataWords.length * 4; // Hash final blocks this._process(); // Return final computed hash return this._hash; }, clone: function () { var clone = Hasher.clone.call(this); clone._hash = this._hash.clone(); return clone; } }); /** * Shortcut function to the hasher's object interface. * * @param {WordArray|string} message The message to hash. * * @return {WordArray} The hash. * * @static * * @example * * var hash = CryptoJS.SHA256('message'); * var hash = CryptoJS.SHA256(wordArray); */ C.SHA256 = Hasher._createHelper(SHA256); /** * Shortcut function to the HMAC's object interface. * * @param {WordArray|string} message The message to hash. * @param {WordArray|string} key The secret key. * * @return {WordArray} The HMAC. * * @static * * @example * * var hmac = CryptoJS.HmacSHA256(message, key); */ C.HmacSHA256 = Hasher._createHmacHelper(SHA256); }(Math)); ================================================ FILE: client/js/total-progress.js ================================================ /* globals qq */ /** * Keeps a running tally of total upload progress for a batch of files. * * @param callback Invoked when total progress changes, passing calculated total loaded & total size values. * @param getSize Function that returns the size of a file given its ID * @constructor */ qq.TotalProgress = function(callback, getSize) { "use strict"; var perFileProgress = {}, totalLoaded = 0, totalSize = 0, lastLoadedSent = -1, lastTotalSent = -1, callbackProxy = function(loaded, total) { if (loaded !== lastLoadedSent || total !== lastTotalSent) { callback(loaded, total); } lastLoadedSent = loaded; lastTotalSent = total; }, /** * @param failed Array of file IDs that have failed * @param retryable Array of file IDs that are retryable * @returns true if none of the failed files are eligible for retry */ noRetryableFiles = function(failed, retryable) { var none = true; qq.each(failed, function(idx, failedId) { if (qq.indexOf(retryable, failedId) >= 0) { none = false; return false; } }); return none; }, onCancel = function(id) { updateTotalProgress(id, -1, -1); delete perFileProgress[id]; }, onAllComplete = function(successful, failed, retryable) { if (failed.length === 0 || noRetryableFiles(failed, retryable)) { callbackProxy(totalSize, totalSize); this.reset(); } }, onNew = function(id) { var size = getSize(id); // We might not know the size yet, such as for blob proxies if (size > 0) { updateTotalProgress(id, 0, size); perFileProgress[id] = {loaded: 0, total: size}; } }, /** * Invokes the callback with the current total progress of all files in the batch. Called whenever it may * be appropriate to re-calculate and disseminate this data. * * @param id ID of a file that has changed in some important way * @param newLoaded New loaded value for this file. -1 if this value should no longer be part of calculations * @param newTotal New total size of the file. -1 if this value should no longer be part of calculations */ updateTotalProgress = function(id, newLoaded, newTotal) { var oldLoaded = perFileProgress[id] ? perFileProgress[id].loaded : 0, oldTotal = perFileProgress[id] ? perFileProgress[id].total : 0; if (newLoaded === -1 && newTotal === -1) { totalLoaded -= oldLoaded; totalSize -= oldTotal; } else { if (newLoaded) { totalLoaded += newLoaded - oldLoaded; } if (newTotal) { totalSize += newTotal - oldTotal; } } callbackProxy(totalLoaded, totalSize); }; qq.extend(this, { // Called when a batch of files has completed uploading. onAllComplete: onAllComplete, // Called when the status of a file has changed. onStatusChange: function(id, oldStatus, newStatus) { if (newStatus === qq.status.CANCELED || newStatus === qq.status.REJECTED) { onCancel(id); } else if (newStatus === qq.status.SUBMITTING) { onNew(id); } }, // Called whenever the upload progress of an individual file has changed. onIndividualProgress: function(id, loaded, total) { updateTotalProgress(id, loaded, total); perFileProgress[id] = {loaded: loaded, total: total}; }, // Called whenever the total size of a file has changed, such as when the size of a generated blob is known. onNewSize: function(id) { onNew(id); }, reset: function() { perFileProgress = {}; totalLoaded = 0; totalSize = 0; } }); }; ================================================ FILE: client/js/traditional/all-chunks-done.ajax.requester.js ================================================ /*globals qq*/ /** * Ajax requester used to send a POST to a traditional endpoint once all chunks for a specific file have uploaded * successfully. * * @param o Options from the caller - will override the defaults. * @constructor */ qq.traditional.AllChunksDoneAjaxRequester = function(o) { "use strict"; var requester, options = { cors: { allowXdr: false, expected: false, sendCredentials: false }, endpoint: null, log: function(str, level) {}, method: "POST" }, promises = {}, endpointHandler = { get: function(id) { if (qq.isFunction(options.endpoint)) { return options.endpoint(id); } return options.endpoint; } }; qq.extend(options, o); requester = qq.extend(this, new qq.AjaxRequester({ acceptHeader: "application/json", contentType: options.jsonPayload ? "application/json" : "application/x-www-form-urlencoded", validMethods: [options.method], method: options.method, endpointStore: endpointHandler, allowXRequestedWithAndCacheControl: false, cors: options.cors, log: options.log, onComplete: function(id, xhr, isError) { var promise = promises[id]; delete promises[id]; if (isError) { promise.failure(xhr); } else { promise.success(xhr); } } })); qq.extend(this, { complete: function(id, xhr, params, headers) { var promise = new qq.Promise(); options.log("Submitting All Chunks Done request for " + id); promises[id] = promise; requester.initTransport(id) .withParams(options.params(id) || params) .withHeaders(options.headers(id) || headers) .send(xhr); return promise; } }); }; ================================================ FILE: client/js/traditional/traditional.form.upload.handler.js ================================================ /*globals qq*/ /** * Upload handler used that assumes the current user agent does not have any support for the * File API, and, therefore, makes use of iframes and forms to submit the files directly to * a generic server. * * @param options Options passed from the base handler * @param proxy Callbacks & methods used to query for or push out data/changes */ qq.traditional = qq.traditional || {}; qq.traditional.FormUploadHandler = function(options, proxy) { "use strict"; var handler = this, getName = proxy.getName, getUuid = proxy.getUuid, log = proxy.log; /** * Returns json object received by iframe from server. */ function getIframeContentJson(id, iframe) { /*jshint evil: true*/ var response, doc, innerHtml; //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases try { // iframe.contentWindow.document - for IE<7 doc = iframe.contentDocument || iframe.contentWindow.document; innerHtml = doc.body.innerHTML; log("converting iframe's innerHTML to JSON"); log("innerHTML = " + innerHtml); //plain text response may be wrapped in
 tag
            if (innerHtml && innerHtml.match(/^
 0) {
            filenameSansExt = filenameSansExt.substr(0, extIdx);
        }

        return filenameSansExt;
    }

    function getOriginalExtension(fileId) {
        var origName = spec.onGetName(fileId);
        return qq.getExtension(origName);
    }

    // Callback iff the name has been changed
    function handleNameUpdate(newFilenameInputEl, fileId) {
        var newName = newFilenameInputEl.value,
            origExtension;

        if (newName !== undefined && qq.trimStr(newName).length > 0) {
            origExtension = getOriginalExtension(fileId);

            if (origExtension !== undefined) {
                newName = newName + "." + origExtension;
            }

            spec.onSetName(fileId, newName);
        }

        spec.onEditingStatusChange(fileId, false);
    }

    // The name has been updated if the filename edit input loses focus.
    function registerInputBlurHandler(inputEl, fileId) {
        inheritedInternalApi.getDisposeSupport().attach(inputEl, "blur", function() {
            handleNameUpdate(inputEl, fileId);
        });
    }

    // The name has been updated if the user presses enter.
    function registerInputEnterKeyHandler(inputEl, fileId) {
        inheritedInternalApi.getDisposeSupport().attach(inputEl, "keyup", function(event) {

            var code = event.keyCode || event.which;

            if (code === 13) {
                handleNameUpdate(inputEl, fileId);
            }
        });
    }

    qq.extend(spec, s);

    spec.attachTo = spec.templating.getFileList();

    qq.extend(this, new qq.UiEventHandler(spec, inheritedInternalApi));

    qq.extend(inheritedInternalApi, {
        handleFilenameEdit: function(id, target, focusInput) {
            var newFilenameInputEl = spec.templating.getEditInput(id);

            spec.onEditingStatusChange(id, true);

            newFilenameInputEl.value = getFilenameSansExtension(id);

            if (focusInput) {
                newFilenameInputEl.focus();
            }

            registerInputBlurHandler(newFilenameInputEl, id);
            registerInputEnterKeyHandler(newFilenameInputEl, id);
        }
    });
};


================================================
FILE: client/js/ui.handler.events.js
================================================
/*globals qq */
// Base handler for UI (FineUploader mode) events.
// Some more specific handlers inherit from this one.
qq.UiEventHandler = function(s, protectedApi) {
    "use strict";

    var disposer = new qq.DisposeSupport(),
        spec = {
            eventType: "click",
            attachTo: null,
            onHandled: function(target, event) {}
        };

    // This makes up the "public" API methods that will be accessible
    // to instances constructing a base or child handler
    qq.extend(this, {
        addHandler: function(element) {
            addHandler(element);
        },

        dispose: function() {
            disposer.dispose();
        }
    });

    function addHandler(element) {
        disposer.attach(element, spec.eventType, function(event) {
            // Only in IE: the `event` is a property of the `window`.
            event = event || window.event;

            // On older browsers, we must check the `srcElement` instead of the `target`.
            var target = event.target || event.srcElement;

            spec.onHandled(target, event);
        });
    }

    // These make up the "protected" API methods that children of this base handler will utilize.
    qq.extend(protectedApi, {
        getFileIdFromItem: function(item) {
            return item.qqFileId;
        },

        getDisposeSupport: function() {
            return disposer;
        }
    });

    qq.extend(spec, s);

    if (spec.attachTo) {
        addHandler(spec.attachTo);
    }
};


================================================
FILE: client/js/ui.handler.focus.filenameinput.js
================================================
/*globals qq */
/**
 * Child of FilenameInputFocusInHandler.  Used to detect focus events on file edit input elements.  This child module is only
 * needed for UAs that do not support the focusin event.  Currently, only Firefox lacks this event.
 *
 * @param spec Overrides for default specifications
 */
qq.FilenameInputFocusHandler = function(spec) {
    "use strict";

    spec.eventType = "focus";
    spec.attachTo = null;

    qq.extend(this, new qq.FilenameInputFocusInHandler(spec, {}));
};


================================================
FILE: client/js/ui.handler.focusin.filenameinput.js
================================================
/*globals qq */
// Child of FilenameEditHandler.  Used to detect focusin events on file edit input elements.
qq.FilenameInputFocusInHandler = function(s, inheritedInternalApi) {
    "use strict";

    var spec = {
            templating: null,
            onGetUploadStatus: function(fileId) {},
            log: function(message, lvl) {}
        };

    if (!inheritedInternalApi) {
        inheritedInternalApi = {};
    }

    // This will be called by the parent handler when a `focusin` event is received on the list element.
    function handleInputFocus(target, event) {
        if (spec.templating.isEditInput(target)) {
            var fileId = spec.templating.getFileId(target),
                status = spec.onGetUploadStatus(fileId);

            if (status === qq.status.SUBMITTED) {
                spec.log(qq.format("Detected valid filename input focus event on file '{}', ID: {}.", spec.onGetName(fileId), fileId));
                inheritedInternalApi.handleFilenameEdit(fileId, target);
            }
        }
    }

    spec.eventType = "focusin";
    spec.onHandled = handleInputFocus;

    qq.extend(spec, s);
    qq.extend(this, new qq.FilenameEditHandler(spec, inheritedInternalApi));
};


================================================
FILE: client/js/upload-data.js
================================================
/*globals qq */
qq.UploadData = function(uploaderProxy) {
    "use strict";

    var data = [],
        byUuid = {},
        byStatus = {},
        byProxyGroupId = {},
        byBatchId = {};

    function getDataByIds(idOrIds) {
        if (qq.isArray(idOrIds)) {
            var entries = [];

            qq.each(idOrIds, function(idx, id) {
                entries.push(data[id]);
            });

            return entries;
        }

        return data[idOrIds];
    }

    function getDataByUuids(uuids) {
        if (qq.isArray(uuids)) {
            var entries = [];

            qq.each(uuids, function(idx, uuid) {
                entries.push(data[byUuid[uuid]]);
            });

            return entries;
        }

        return data[byUuid[uuids]];
    }

    function getDataByStatus(status) {
        var statusResults = [],
            statuses = [].concat(status);

        qq.each(statuses, function(index, statusEnum) {
            var statusResultIndexes = byStatus[statusEnum];

            if (statusResultIndexes !== undefined) {
                qq.each(statusResultIndexes, function(i, dataIndex) {
                    statusResults.push(data[dataIndex]);
                });
            }
        });

        return statusResults;
    }

    qq.extend(this, {
        /**
         * Adds a new file to the data cache for tracking purposes.
         *
         * @param spec Data that describes this file.  Possible properties are:
         *
         * - uuid: Initial UUID for this file.
         * - name: Initial name of this file.
         * - size: Size of this file, omit if this cannot be determined
         * - status: Initial `qq.status` for this file.  Omit for `qq.status.SUBMITTING`.
         * - batchId: ID of the batch this file belongs to
         * - proxyGroupId: ID of the proxy group associated with this file
         * - onBeforeStatusChange(fileId): callback that is executed before the status change is broadcast
         *
         * @returns {number} Internal ID for this file.
         */
        addFile: function(spec) {
            var status = spec.status || qq.status.SUBMITTING,
                id = data.push({
                    name: spec.name,
                    originalName: spec.name,
                    uuid: spec.uuid,
                    size: spec.size == null ? -1 : spec.size,
                    status: status,
                    file: spec.file
                }) - 1;

            if (spec.batchId) {
                data[id].batchId = spec.batchId;

                if (byBatchId[spec.batchId] === undefined) {
                    byBatchId[spec.batchId] = [];
                }
                byBatchId[spec.batchId].push(id);
            }

            if (spec.proxyGroupId) {
                data[id].proxyGroupId = spec.proxyGroupId;

                if (byProxyGroupId[spec.proxyGroupId] === undefined) {
                    byProxyGroupId[spec.proxyGroupId] = [];
                }
                byProxyGroupId[spec.proxyGroupId].push(id);
            }

            data[id].id = id;
            byUuid[spec.uuid] = id;

            if (byStatus[status] === undefined) {
                byStatus[status] = [];
            }
            byStatus[status].push(id);

            spec.onBeforeStatusChange && spec.onBeforeStatusChange(id);
            uploaderProxy.onStatusChange(id, null, status);

            return id;
        },

        retrieve: function(optionalFilter) {
            if (qq.isObject(optionalFilter) && data.length)  {
                if (optionalFilter.id !== undefined) {
                    return getDataByIds(optionalFilter.id);
                }

                else if (optionalFilter.uuid !== undefined) {
                    return getDataByUuids(optionalFilter.uuid);
                }

                else if (optionalFilter.status) {
                    return getDataByStatus(optionalFilter.status);
                }
            }
            else {
                return qq.extend([], data, true);
            }
        },

        removeFileRef: function(id) {
            var record = getDataByIds(id);

            if (record) {
                delete record.file;
            }
        },

        reset: function() {
            data = [];
            byUuid = {};
            byStatus = {};
            byBatchId = {};
        },

        setStatus: function(id, newStatus) {
            var oldStatus = data[id].status,
                byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id);

            byStatus[oldStatus].splice(byStatusOldStatusIndex, 1);

            data[id].status = newStatus;

            if (byStatus[newStatus] === undefined) {
                byStatus[newStatus] = [];
            }
            byStatus[newStatus].push(id);

            uploaderProxy.onStatusChange(id, oldStatus, newStatus);
        },

        uuidChanged: function(id, newUuid) {
            var oldUuid = data[id].uuid;

            data[id].uuid = newUuid;
            byUuid[newUuid] = id;
            delete byUuid[oldUuid];
        },

        updateName: function(id, newName) {
            data[id].name = newName;
        },

        updateSize: function(id, newSize) {
            data[id].size = newSize;
        },

        // Only applicable if this file has a parent that we may want to reference later.
        setParentId: function(targetId, parentId) {
            data[targetId].parentId = parentId;
        },

        getIdsInProxyGroup: function(id) {
            var proxyGroupId = data[id].proxyGroupId;

            if (proxyGroupId) {
                return byProxyGroupId[proxyGroupId];
            }
            return [];
        },

        getIdsInBatch: function(id) {
            var batchId = data[id].batchId;

            return byBatchId[batchId];
        }
    });
};

qq.status = {
    SUBMITTING: "submitting",
    SUBMITTED: "submitted",
    REJECTED: "rejected",
    QUEUED: "queued",
    CANCELED: "canceled",
    PAUSED: "paused",
    UPLOADING: "uploading",
    UPLOAD_FINALIZING: "upload finalizing",
    UPLOAD_RETRYING: "retrying upload",
    UPLOAD_SUCCESSFUL: "upload successful",
    UPLOAD_FAILED: "upload failed",
    DELETE_FAILED: "delete failed",
    DELETING: "deleting",
    DELETED: "deleted"
};


================================================
FILE: client/js/upload-handler/form.upload.handler.js
================================================
/* globals qq */
/**
 * Common APIs exposed to creators of upload via form/iframe handlers.  This is reused and possibly overridden
 * in some cases by specific form upload handlers.
 *
 * @constructor
 */
qq.FormUploadHandler = function(spec) {
    "use strict";

    var options = spec.options,
        handler = this,
        proxy = spec.proxy,
        formHandlerInstanceId = qq.getUniqueId(),
        onloadCallbacks = {},
        detachLoadEvents = {},
        postMessageCallbackTimers = {},
        isCors = options.isCors,
        inputName = options.inputName,
        getUuid = proxy.getUuid,
        log = proxy.log,
        corsMessageReceiver = new qq.WindowReceiveMessage({log: log});

    /**
     * Remove any trace of the file from the handler.
     *
     * @param id ID of the associated file
     */
    function expungeFile(id) {
        delete detachLoadEvents[id];

        // If we are dealing with CORS, we might still be waiting for a response from a loaded iframe.
        // In that case, terminate the timer waiting for a message from the loaded iframe
        // and stop listening for any more messages coming from this iframe.
        if (isCors) {
            clearTimeout(postMessageCallbackTimers[id]);
            delete postMessageCallbackTimers[id];
            corsMessageReceiver.stopReceivingMessages(id);
        }

        var iframe = document.getElementById(handler._getIframeName(id));
        if (iframe) {
            // To cancel request set src to something else.  We use src="javascript:false;"
            // because it doesn't trigger ie6 prompt on https
            /* jshint scripturl:true */
            iframe.setAttribute("src", "javascript:false;");

            qq(iframe).remove();
        }
    }

    /**
     * @param iframeName `document`-unique Name of the associated iframe
     * @returns {*} ID of the associated file
     */
    function getFileIdForIframeName(iframeName) {
        return iframeName.split("_")[0];
    }

    /**
     * Generates an iframe to be used as a target for upload-related form submits.  This also adds the iframe
     * to the current `document`.  Note that the iframe is hidden from view.
     *
     * @param name Name of the iframe.
     * @returns {HTMLIFrameElement} The created iframe
     */
    function initIframeForUpload(name) {
        var iframe = qq.toElement("