From fbf8cc2d0303bf46dc664a2ec8f9fd4cf411d4f9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 23 Nov 2017 16:01:32 +0000 Subject: [PATCH 01/31] Run acceptance tests via docker compose --- services/web/Jenkinsfile | 11 ++-- services/web/config/settings.defaults.coffee | 15 +++-- services/web/docker-compose.ci.yml | 12 ++++ services/web/docker-compose.yml | 42 ++++++++++++ services/web/makefile | 64 +++++++++++++++++++ services/web/package.json | 11 ++++ .../coffee/helpers/MockDocUpdaterApi.coffee | 2 +- .../coffee/helpers/MockDocstoreApi.coffee | 2 +- .../acceptance/coffee/helpers/redis.coffee | 1 - .../acceptance/coffee/helpers/request.coffee | 2 +- 10 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 services/web/docker-compose.ci.yml create mode 100644 services/web/docker-compose.yml create mode 100644 services/web/makefile diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 33aa807fdd..85ecafef58 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -117,19 +117,16 @@ pipeline { } } steps { + sh 'make install' + sh 'make test_unit MOCHA_ARGS="--reporter=tap"' sh 'env NODE_ENV=development ./node_modules/.bin/grunt mochaTest:unit --reporter=tap' } } stage('Acceptance Tests') { steps { - // This tagged relase of the acceptance test runner is a temporary fix - // to get the acceptance tests working before we move to a - // docker-compose workflow. See: - // https://github.com/sharelatex/web-sharelatex-internal/pull/148 - - sh 'docker pull sharelatex/sl-acceptance-test-runner:node-6.9-mongo-3.4' - sh 'docker run --rm -v $(pwd):/app --env SHARELATEX_ALLOW_PUBLIC_ACCESS=true sharelatex/sl-acceptance-test-runner:node-6.9-mongo-3.4 || (cat forever/app.log && false)' + sh 'make install' + sh "make test_acceptance MOCHA_ARGS="--reporter=tap"' } } diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 69bf0a3b7c..fccd197a83 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -35,12 +35,12 @@ module.exports = settings = # Databases # --------- mongo: - url : 'mongodb://127.0.0.1/sharelatex' + url : "mongodb://#{process.env['MONGO_HOST'] || '127.0.0.1'}/sharelatex" redis: web: - host: "localhost" - port: "6379" + host: process.env['REDIS_HOST'] || "localhost" + port: process.env['REDIS_PORT'] || "6379" password: "" # websessions: @@ -74,8 +74,8 @@ module.exports = settings = # ] api: - host: "localhost" - port: "6379" + host: process.env['REDIS_HOST'] || "localhost" + port: process.env['REDIS_PORT'] || "6379" password: "" # Service locations @@ -87,6 +87,7 @@ module.exports = settings = internal: web: port: webPort = 3000 + host: process.env['LISTEN_ADDRESS'] or 'localhost' documentupdater: port: docUpdaterPort = 3003 @@ -99,7 +100,7 @@ module.exports = settings = user: httpAuthUser pass: httpAuthPass documentupdater: - url : "http://localhost:#{docUpdaterPort}" + url : "http://#{process.env['DOCUPDATER_HOST'] or 'localhost'}:#{docUpdaterPort}" thirdPartyDataStore: url : "http://localhost:3002" emptyProjectFlushDelayMiliseconds: 5 * seconds @@ -113,7 +114,7 @@ module.exports = settings = enabled: false url : "http://localhost:3054" docstore: - url : "http://localhost:3016" + url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016" pubUrl: "http://localhost:3016" chat: url: "http://localhost:3010" diff --git a/services/web/docker-compose.ci.yml b/services/web/docker-compose.ci.yml new file mode 100644 index 0000000000..0bb7af5f74 --- /dev/null +++ b/services/web/docker-compose.ci.yml @@ -0,0 +1,12 @@ +version: "2" + +services: + test_unit: + image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + user: root + volumes: [] + + test_acceptance: + image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + user: root + volumes: [] diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml new file mode 100644 index 0000000000..fe30bb8f57 --- /dev/null +++ b/services/web/docker-compose.yml @@ -0,0 +1,42 @@ +version: "2" + +volumes: + node_modules: + +services: + npm: + image: node:6.9.5 + volumes: + - .:/app + - node_modules:/app/node_modules + working_dir: /app + + test_unit: + image: node:6.9.5 + volumes: + - .:/app + - node_modules:/app/node_modules + working_dir: /app + command: npm run test:unit + + test_acceptance: + image: node:6.9.5 + volumes: + - .:/app + - node_modules:/app/node_modules + environment: + REDIS_HOST: redis + MONGO_HOST: mongo + SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true' + LISTEN_ADDRESS: 0.0.0.0 + depends_on: + - redis + - mongo + working_dir: /app + command: npm run start + + redis: + image: redis + + mongo: + image: mongo:3.4.6 diff --git a/services/web/makefile b/services/web/makefile new file mode 100644 index 0000000000..c6e1516598 --- /dev/null +++ b/services/web/makefile @@ -0,0 +1,64 @@ +NPM := docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} run --rm npm npm +BUILD_NUMBER ?= local +BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +PROJECT_NAME = web + +all: install test + @echo "Run:" + @echo " make install to set up the project dependencies (in docker)" + @echo " make test to run all the tests for the project (in docker)" + @echo " make run to run the app (natively)" + +add: + $(NPM) install --save ${P} + +add_dev: + $(NPM) install --save-dev ${P} + +install: + $(NPM) install + +clean: + rm app.js + rm -r app/js + rm -r test/unit/js + rm -r test/acceptance/js + +test: test_unit test_acceptance + +test_unit: + docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm run test:unit -- ${MOCHA_ARGS} + +test_acceptance: test_acceptance_start_service test_acceptance_run test_acceptance_stop_service + +test_acceptance_start_service: + docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance + +test_acceptance_stop_service: + docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} stop test_acceptance + +test_acceptance_run: + docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm run test:acceptance -- ${MOCHA_ARGS} + +build: + docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . + +publish: + docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + +ci: + # When we run the tests locally we mount the local directory as a volumne + # and use a persistent node_modules folder (see docker-compose.yml). + # However, on the CI server, we want to run our tests in the image that we + # have built for deployment, which is what the docker-compose.ci.yml + # override does. + PROJECT_NAME=$(PROJECT_NAME) \ + BRANCH_NAME=$(BRANCH_NAME) \ + BUILD_NUMBER=$(BUILD_NUMBER) \ + DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" \ + $(MAKE) build test publish + +.PHONY: + add install update test test_unit test_acceptance \ + test_acceptance_start_service test_acceptance_stop_service \ + test_acceptance_run build publish ci diff --git a/services/web/package.json b/services/web/package.json index e831d929e1..f4ea19a49d 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -9,6 +9,17 @@ "directories": { "public": "./public" }, + "scripts": { + "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done", + "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000 $@ test/acceptance/js", + "test:acceptance": "npm run compile:acceptance_tests && npm run test:acceptance:wait_for_app && npm run test:acceptance:run -- $@", + "test:unit": "npm run compile:app && npm run compile:unit_tests && mocha --recursive --reporter spec $@ test/unit/js", + "compile:unit_tests": "coffee -o test/unit/js -c test/unit/coffee", + "compile:acceptance_tests": "coffee -o test/acceptance/js -c test/acceptance/coffee", + "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", + "start": "npm run compile:app && node app.js", + "echo": "echo $@" + }, "dependencies": { "archiver": "0.9.0", "async": "0.6.2", diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee index aefcd4513a..6f307b0810 100644 --- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee @@ -6,7 +6,7 @@ module.exports = MockDocUpdaterApi = app.post "/project/:project_id/flush", (req, res, next) => res.sendStatus 200 - app.listen 3003, (error) -> + app.listen 3003, '0.0.0.0', (error) -> throw error if error? .on "error", (error) -> console.error "error starting MockDocUpdaterApi:", error.message diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee index 2133d40b9f..7c8f5dbe50 100644 --- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee @@ -22,7 +22,7 @@ module.exports = MockDocStoreApi = docs = (doc for doc_id, doc of @docs[req.params.project_id]) res.send JSON.stringify docs - app.listen 3016, (error) -> + app.listen 3016, '0.0.0.0', (error) -> throw error if error? .on "error", (error) -> console.error "error starting MockDocStoreApi:", error.message diff --git a/services/web/test/acceptance/coffee/helpers/redis.coffee b/services/web/test/acceptance/coffee/helpers/redis.coffee index 9aecf6b387..7c48f97d2e 100644 --- a/services/web/test/acceptance/coffee/helpers/redis.coffee +++ b/services/web/test/acceptance/coffee/helpers/redis.coffee @@ -1,5 +1,4 @@ Settings = require('settings-sharelatex') -redis = require('redis-sharelatex') logger = require("logger-sharelatex") Async = require('async') diff --git a/services/web/test/acceptance/coffee/helpers/request.coffee b/services/web/test/acceptance/coffee/helpers/request.coffee index 879acd843a..1c7120d141 100644 --- a/services/web/test/acceptance/coffee/helpers/request.coffee +++ b/services/web/test/acceptance/coffee/helpers/request.coffee @@ -1,4 +1,4 @@ -BASE_URL = "http://localhost:3000" +BASE_URL = "http://#{process.env["HTTP_TEST_HOST"] or "localhost"}:3000" module.exports = require("request").defaults({ baseUrl: BASE_URL, followRedirect: false From d9d7c9695887c0d1ad72bb644f8602246f5771ab Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 23 Nov 2017 16:45:46 +0000 Subject: [PATCH 02/31] Get module unit tests running inside Docker as well as main tests --- services/web/Jenkinsfile | 12 +----- services/web/README.md | 63 ++++++++++++++++++++++++++++++ services/web/docker-compose.ci.yml | 12 ------ services/web/makefile | 21 +--------- services/web/package.json | 10 ++--- 5 files changed, 71 insertions(+), 47 deletions(-) delete mode 100644 services/web/docker-compose.ci.yml diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 85ecafef58..59347a266c 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -109,23 +109,15 @@ pipeline { } } - stage('Unit Test') { - agent { - docker { - image 'node:6.9.5' - reuseNode true - } - } + stage('Unit Tests') { steps { sh 'make install' - sh 'make test_unit MOCHA_ARGS="--reporter=tap"' - sh 'env NODE_ENV=development ./node_modules/.bin/grunt mochaTest:unit --reporter=tap' + sh "make test_unit MOCHA_ARGS="--reporter=tap"' } } stage('Acceptance Tests') { steps { - sh 'make install' sh "make test_acceptance MOCHA_ARGS="--reporter=tap"' } } diff --git a/services/web/README.md b/services/web/README.md index f777e7e5f5..51f73d02ec 100644 --- a/services/web/README.md +++ b/services/web/README.md @@ -17,6 +17,69 @@ web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`). +New Docker-based build process +------------------------------ + +Note that the Grunt workflow from above should still work, but we are transitioning to a +Docker based testing workflow, which is documented below: + +### Running the app + +The app runs natively using npm and Node on the local system: + +``` +$ npm install +$ npm run start +``` + +*Ideally the app would run in Docker like the tests below, but with host networking not supported in OS X, we need to run it natively until all services are Dockerised.* + +### Unit Tests + +The test suites run in Docker. + +Unit tests can be run in the `test_unit` container defined in `docker-compose.tests.yml`. + +The makefile contains a short cut to run these: + +``` +make install # Only needs running once, or when npm packages are updated +make unit_test +``` + +During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI: + +``` +make unit_test MOCHA_ARGS='--grep=AuthorizationManager' +``` + +### Acceptance Tests + +Acceptance tests are run against a live service, which runs in the `acceptance_test` container defined in `docker-compose.tests.yml`. + +To run the tests out-of-the-box, the makefile defines: + +``` +make install # Only needs running once, or when npm packages are updated +make acceptance_test +``` + +However, during development it is often useful to leave the service running for rapid iteration on the acceptance tests. This can be done with: + +``` +make acceptance_test_start_service +make acceptance_test_run # Run as many times as needed during development +make acceptance_test_stop_service +``` + +`make acceptance_test` just runs these three commands in sequence. + +During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI: + +``` +make acceptance_test_run MOCHA_ARGS='--grep=AuthorizationManager' +``` + Unit test status ---------------- diff --git a/services/web/docker-compose.ci.yml b/services/web/docker-compose.ci.yml deleted file mode 100644 index 0bb7af5f74..0000000000 --- a/services/web/docker-compose.ci.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "2" - -services: - test_unit: - image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER - user: root - volumes: [] - - test_acceptance: - image: quay.io/sharelatex/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER - user: root - volumes: [] diff --git a/services/web/makefile b/services/web/makefile index c6e1516598..8d6ebf7c2e 100644 --- a/services/web/makefile +++ b/services/web/makefile @@ -7,7 +7,6 @@ all: install test @echo "Run:" @echo " make install to set up the project dependencies (in docker)" @echo " make test to run all the tests for the project (in docker)" - @echo " make run to run the app (natively)" add: $(NPM) install --save ${P} @@ -40,25 +39,7 @@ test_acceptance_stop_service: test_acceptance_run: docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm run test:acceptance -- ${MOCHA_ARGS} -build: - docker build --pull --tag quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) . - -publish: - docker push quay.io/sharelatex/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) - -ci: - # When we run the tests locally we mount the local directory as a volumne - # and use a persistent node_modules folder (see docker-compose.yml). - # However, on the CI server, we want to run our tests in the image that we - # have built for deployment, which is what the docker-compose.ci.yml - # override does. - PROJECT_NAME=$(PROJECT_NAME) \ - BRANCH_NAME=$(BRANCH_NAME) \ - BUILD_NUMBER=$(BUILD_NUMBER) \ - DOCKER_COMPOSE_FLAGS="-f docker-compose.ci.yml" \ - $(MAKE) build test publish - .PHONY: add install update test test_unit test_acceptance \ test_acceptance_start_service test_acceptance_stop_service \ - test_acceptance_run build publish ci + test_acceptance_run diff --git a/services/web/package.json b/services/web/package.json index f4ea19a49d..a795df999e 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -11,12 +11,12 @@ }, "scripts": { "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done", - "test:acceptance:run": "mocha --recursive --reporter spec --timeout 15000 $@ test/acceptance/js", + "test:acceptance:run": "bin/acceptance_test $@", "test:acceptance": "npm run compile:acceptance_tests && npm run test:acceptance:wait_for_app && npm run test:acceptance:run -- $@", - "test:unit": "npm run compile:app && npm run compile:unit_tests && mocha --recursive --reporter spec $@ test/unit/js", - "compile:unit_tests": "coffee -o test/unit/js -c test/unit/coffee", - "compile:acceptance_tests": "coffee -o test/acceptance/js -c test/acceptance/coffee", - "compile:app": "coffee -o app/js -c app/coffee && coffee -c app.coffee", + "test:unit": "npm run compile:app && npm run compile:unit_tests && bin/unit_test $@", + "compile:unit_tests": "bin/compile_unit_tests", + "compile:acceptance_tests": "bin/compile_acceptance_tests", + "compile:app": "bin/compile_app", "start": "npm run compile:app && node app.js", "echo": "echo $@" }, From 7efef129814052733c4b2360a51627cf9e5be19c Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 23 Nov 2017 16:46:49 +0000 Subject: [PATCH 03/31] Fix Jenkinsfile syntax --- services/web/Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 59347a266c..0650eb4edb 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -112,13 +112,13 @@ pipeline { stage('Unit Tests') { steps { sh 'make install' - sh "make test_unit MOCHA_ARGS="--reporter=tap"' + sh 'make test_unit MOCHA_ARGS="--reporter=tap"' } } stage('Acceptance Tests') { steps { - sh "make test_acceptance MOCHA_ARGS="--reporter=tap"' + sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"' } } From 492b37aa6e040f5e250d908311805b471b456880 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 23 Nov 2017 16:52:43 +0000 Subject: [PATCH 04/31] Add missing bin/ files --- services/web/bin/acceptance_test | 18 ++++++++++++++++++ services/web/bin/compile_acceptance_tests | 16 ++++++++++++++++ services/web/bin/compile_app | 23 +++++++++++++++++++++++ services/web/bin/compile_unit_tests | 15 +++++++++++++++ services/web/bin/unit_test | 14 ++++++++++++++ 5 files changed, 86 insertions(+) create mode 100755 services/web/bin/acceptance_test create mode 100755 services/web/bin/compile_acceptance_tests create mode 100755 services/web/bin/compile_app create mode 100755 services/web/bin/compile_unit_tests create mode 100755 services/web/bin/unit_test diff --git a/services/web/bin/acceptance_test b/services/web/bin/acceptance_test new file mode 100755 index 0000000000..717e8542ad --- /dev/null +++ b/services/web/bin/acceptance_test @@ -0,0 +1,18 @@ +#!/bin/bash +set -e; + +MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000" + +$MOCHA "$@" test/acceptance/js + +# TODO: Module acceptance tests are hard to get working, +# because they typically require the server to be instantiated +# with a different config. + +# for dir in modules/*; +# do +# if [ -d $dir/test/acceptance/js ]; then +# $MOCHA "$@" $dir/test/acceptance/js +# fi +# done + diff --git a/services/web/bin/compile_acceptance_tests b/services/web/bin/compile_acceptance_tests new file mode 100755 index 0000000000..d60ba0cc46 --- /dev/null +++ b/services/web/bin/compile_acceptance_tests @@ -0,0 +1,16 @@ +#!/bin/bash +set -e; + +COFFEE=node_modules/.bin/coffee + +echo Compiling test/acceptance/coffee; +$COFFEE -o test/acceptance/js -c test/acceptance/coffee; + +for dir in modules/*; +do + + if [ -d $dir/test/acceptance ]; then + echo Compiling $dir/test/acceptance/coffee; + $COFFEE -o $dir/test/acceptance/js -c $dir/test/acceptance/coffee; + fi +done \ No newline at end of file diff --git a/services/web/bin/compile_app b/services/web/bin/compile_app new file mode 100755 index 0000000000..b218b01a5a --- /dev/null +++ b/services/web/bin/compile_app @@ -0,0 +1,23 @@ +#!/bin/bash +set -e; + +COFFEE=node_modules/.bin/coffee + +echo Compiling app.coffee; +$COFFEE -c app.coffee; + +echo Compiling app/coffee; +$COFFEE -o app/js -c app/coffee; + +for dir in modules/*; +do + if [ -d $dir/app/coffee ]; then + echo Compiling $dir/app/coffee; + $COFFEE -o $dir/app/js -c $dir/app/coffee; + fi + + if [ -e $dir/index.coffee ]; then + echo Compiling $dir/index.coffee; + $COFFEE -c $dir/index.coffee; + fi +done \ No newline at end of file diff --git a/services/web/bin/compile_unit_tests b/services/web/bin/compile_unit_tests new file mode 100755 index 0000000000..780a189bda --- /dev/null +++ b/services/web/bin/compile_unit_tests @@ -0,0 +1,15 @@ +#!/bin/bash +set -e; + +COFFEE=node_modules/.bin/coffee + +echo Compiling test/unit/coffee; +$COFFEE -o test/unit/js -c test/unit/coffee; + +for dir in modules/*; +do + if [ -d $dir/test/unit ]; then + echo Compiling $dir/test/unit/coffee; + $COFFEE -o $dir/test/unit/js -c $dir/test/unit/coffee; + fi +done \ No newline at end of file diff --git a/services/web/bin/unit_test b/services/web/bin/unit_test new file mode 100755 index 0000000000..da13a441fa --- /dev/null +++ b/services/web/bin/unit_test @@ -0,0 +1,14 @@ +#!/bin/bash +set -e; + +MOCHA="node_modules/.bin/mocha --recursive --reporter spec" + +$MOCHA "$@" test/unit/js + +for dir in modules/*; +do + if [ -d $dir/test/unit/js ]; then + $MOCHA "$@" $dir/test/unit/js + fi +done + From 49057a5ab7e59d2ca7cded79248ce33087817cd9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 24 Nov 2017 16:28:24 +0000 Subject: [PATCH 05/31] Only mount coffee and needed files into Docker so js isn't written back to local system --- services/web/.gitignore | 1 + services/web/{makefile => Makefile} | 36 +++++++++++++++---------- services/web/bin/generate_volumes_file | 26 ++++++++++++++++++ services/web/docker-compose.yml | 25 +++++++---------- services/web/docker-shared.template.yml | 21 +++++++++++++++ 5 files changed, 80 insertions(+), 29 deletions(-) rename services/web/{makefile => Makefile} (61%) create mode 100755 services/web/bin/generate_volumes_file create mode 100644 services/web/docker-shared.template.yml diff --git a/services/web/.gitignore b/services/web/.gitignore index 2a29f414d1..a48481690a 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -73,3 +73,4 @@ Gemfile.lock app/views/external /modules/ +docker-shared.yml diff --git a/services/web/makefile b/services/web/Makefile similarity index 61% rename from services/web/makefile rename to services/web/Makefile index 8d6ebf7c2e..25c0eb2cca 100644 --- a/services/web/makefile +++ b/services/web/Makefile @@ -8,38 +8,46 @@ all: install test @echo " make install to set up the project dependencies (in docker)" @echo " make test to run all the tests for the project (in docker)" -add: +add: docker-shared.yml $(NPM) install --save ${P} -add_dev: +add_dev: docker-shared.yml $(NPM) install --save-dev ${P} -install: +install: docker-shared.yml $(NPM) install -clean: - rm app.js - rm -r app/js - rm -r test/unit/js - rm -r test/acceptance/js +clean: docker-shared.yml + rm -f app.js + rm -rf app/js + rm -rf test/unit/js + rm -rf test/acceptance/js + # Deletes node_modules volume + docker-compose down --volumes + # Delete after docker-compose command + rm -f docker-shared.yml + +# Need regenerating if you change the web modules you have installed +docker-shared.yml: + bin/generate_volumes_file test: test_unit test_acceptance -test_unit: +test_unit: docker-shared.yml docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm run test:unit -- ${MOCHA_ARGS} test_acceptance: test_acceptance_start_service test_acceptance_run test_acceptance_stop_service -test_acceptance_start_service: +test_acceptance_start_service: docker-shared.yml docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance -test_acceptance_stop_service: - docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} stop test_acceptance +test_acceptance_stop_service: docker-shared.yml + docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo -test_acceptance_run: +test_acceptance_run: docker-shared.yml docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm run test:acceptance -- ${MOCHA_ARGS} .PHONY: - add install update test test_unit test_acceptance \ + all add install update test test_unit test_acceptance \ test_acceptance_start_service test_acceptance_stop_service \ test_acceptance_run diff --git a/services/web/bin/generate_volumes_file b/services/web/bin/generate_volumes_file new file mode 100755 index 0000000000..2117ff5342 --- /dev/null +++ b/services/web/bin/generate_volumes_file @@ -0,0 +1,26 @@ +#!/usr/bin/env python2 + +from os import listdir +from os.path import isfile, isdir, join + +volumes = [] + +for module in listdir("modules/"): + if module[0] != '.': + if isfile(join("modules", module, 'index.coffee')): + volumes.append(join("modules", module, 'index.coffee')) + for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee']: + if isdir(join("modules", module, directory)): + volumes.append(join("modules", module, directory)) + +volumes_string = map(lambda vol: "- ./" + vol + ":/app/" + vol + ":ro", volumes) +volumes_string = "\n ".join(volumes_string) + +with open("docker-shared.template.yml", "r") as f: + docker_shared_file = f.read() + +docker_shared_file = docker_shared_file.replace("MODULE_VOLUMES", volumes_string) + +with open("docker-shared.yml", "w") as f: + f.write(docker_shared_file) + diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml index fe30bb8f57..3658a87b7f 100644 --- a/services/web/docker-compose.yml +++ b/services/web/docker-compose.yml @@ -5,25 +5,21 @@ volumes: services: npm: - image: node:6.9.5 - volumes: - - .:/app - - node_modules:/app/node_modules - working_dir: /app + extends: + file: docker-shared.yml + service: app + command: npm install test_unit: - image: node:6.9.5 - volumes: - - .:/app - - node_modules:/app/node_modules - working_dir: /app + extends: + file: docker-shared.yml + service: app command: npm run test:unit test_acceptance: - image: node:6.9.5 - volumes: - - .:/app - - node_modules:/app/node_modules + extends: + file: docker-shared.yml + service: app environment: REDIS_HOST: redis MONGO_HOST: mongo @@ -32,7 +28,6 @@ services: depends_on: - redis - mongo - working_dir: /app command: npm run start redis: diff --git a/services/web/docker-shared.template.yml b/services/web/docker-shared.template.yml new file mode 100644 index 0000000000..dca6b5a224 --- /dev/null +++ b/services/web/docker-shared.template.yml @@ -0,0 +1,21 @@ +version: "2" + +services: + app: + image: node:6.9.5 + volumes: + - ./package.json:/app/package.json + - ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json + - node_modules:/app/node_modules + - ./bin:/app/bin + - ./public:/app/public + - ./app.coffee:/app/app.coffee:ro + - ./app/coffee:/app/app/coffee:ro + - ./app/templates:/app/app/templates:ro + - ./app/views:/app/app/views:ro + - ./config:/app/config + - ./test/unit/coffee:/app/test/unit/coffee:ro + - ./test/acceptance/coffee:/app/test/acceptance/coffee:ro + - ./test/smoke/coffee:/app/test/smoke/coffee:ro + MODULE_VOLUMES + working_dir: /app \ No newline at end of file From 2bc0f666bac9c48aa5ecaa7cdb19c3159d68e28e Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 24 Nov 2017 17:04:22 +0000 Subject: [PATCH 06/31] Add some documentation --- services/web/docker-shared.template.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/web/docker-shared.template.yml b/services/web/docker-shared.template.yml index dca6b5a224..48adb7a036 100644 --- a/services/web/docker-shared.template.yml +++ b/services/web/docker-shared.template.yml @@ -1,5 +1,10 @@ version: "2" +# We mount all the directories explicitly so that we are only mounting +# the coffee directories, so that the compiled js is only written inside +# the container, and not back to the local filesystem, where it would be +# root owned, and conflict with working outside of the container. + services: app: image: node:6.9.5 @@ -8,6 +13,10 @@ services: - ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json - node_modules:/app/node_modules - ./bin:/app/bin + # Copying the whole public dir is fine for now, and needed for + # some unit tests to pass, but we will want to isolate the coffee + # and vendor js files, so that the compiled js files are not written + # back to the local filesystem. - ./public:/app/public - ./app.coffee:/app/app.coffee:ro - ./app/coffee:/app/app/coffee:ro From 5e0fc24c1a014bd5852debf198fbbdf345969147 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 24 Nov 2017 17:40:24 +0000 Subject: [PATCH 07/31] Allow modules to specific their own acceptance tests --- services/web/Makefile | 21 +++++++++++++++++---- services/web/bin/acceptance_test | 16 +--------------- services/web/bin/generate_volumes_file | 2 +- services/web/package.json | 3 ++- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/services/web/Makefile b/services/web/Makefile index 25c0eb2cca..3955fe1d52 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -36,17 +36,30 @@ test: test_unit test_acceptance test_unit: docker-shared.yml docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm run test:unit -- ${MOCHA_ARGS} -test_acceptance: test_acceptance_start_service test_acceptance_run test_acceptance_stop_service +test_acceptance: test_acceptance_app test_acceptance_modules -test_acceptance_start_service: docker-shared.yml +test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service + +test_acceptance_app_start_service: docker-shared.yml docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance -test_acceptance_stop_service: docker-shared.yml +test_acceptance_app_stop_service: docker-shared.yml docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo -test_acceptance_run: docker-shared.yml +test_acceptance_app_run: docker-shared.yml docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm run test:acceptance -- ${MOCHA_ARGS} +test_acceptance_modules: docker-shared.yml + for dir in modules/*; \ + do \ + if [ -e $$dir/makefile ]; then \ + (make test_acceptance_module MODULE=$$dir) \ + fi \ + done + +test_acceptance_module: docker-shared.yml + cd $(MODULE) && make test_acceptance + .PHONY: all add install update test test_unit test_acceptance \ test_acceptance_start_service test_acceptance_stop_service \ diff --git a/services/web/bin/acceptance_test b/services/web/bin/acceptance_test index 717e8542ad..fd2e5137b5 100755 --- a/services/web/bin/acceptance_test +++ b/services/web/bin/acceptance_test @@ -1,18 +1,4 @@ #!/bin/bash set -e; - MOCHA="node_modules/.bin/mocha --recursive --reporter spec --timeout 15000" - -$MOCHA "$@" test/acceptance/js - -# TODO: Module acceptance tests are hard to get working, -# because they typically require the server to be instantiated -# with a different config. - -# for dir in modules/*; -# do -# if [ -d $dir/test/acceptance/js ]; then -# $MOCHA "$@" $dir/test/acceptance/js -# fi -# done - +$MOCHA "$@" diff --git a/services/web/bin/generate_volumes_file b/services/web/bin/generate_volumes_file index 2117ff5342..d70ac11c3d 100755 --- a/services/web/bin/generate_volumes_file +++ b/services/web/bin/generate_volumes_file @@ -9,7 +9,7 @@ for module in listdir("modules/"): if module[0] != '.': if isfile(join("modules", module, 'index.coffee')): volumes.append(join("modules", module, 'index.coffee')) - for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee']: + for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee', 'test/acceptance/config']: if isdir(join("modules", module, directory)): volumes.append(join("modules", module, directory)) diff --git a/services/web/package.json b/services/web/package.json index a795df999e..b14cdf8b20 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -12,7 +12,8 @@ "scripts": { "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done", "test:acceptance:run": "bin/acceptance_test $@", - "test:acceptance": "npm run compile:acceptance_tests && npm run test:acceptance:wait_for_app && npm run test:acceptance:run -- $@", + "test:acceptance:dir": "npm run compile:acceptance_tests && npm run test:acceptance:wait_for_app && npm run test:acceptance:run -- $@", + "test:acceptance": "npm run test:acceptance:dir test/acceptance/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && bin/unit_test $@", "compile:unit_tests": "bin/compile_unit_tests", "compile:acceptance_tests": "bin/compile_acceptance_tests", From 4c504ad8ebcbfc25aab78b6492694396ea5b65a6 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 27 Nov 2017 16:51:20 +0000 Subject: [PATCH 08/31] Remove debugging command --- services/web/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/package.json b/services/web/package.json index b14cdf8b20..3e6789920e 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -18,8 +18,7 @@ "compile:unit_tests": "bin/compile_unit_tests", "compile:acceptance_tests": "bin/compile_acceptance_tests", "compile:app": "bin/compile_app", - "start": "npm run compile:app && node app.js", - "echo": "echo $@" + "start": "npm run compile:app && node app.js" }, "dependencies": { "archiver": "0.9.0", From 054964dd8551851b8ca779073c0358101ef9bd82 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 27 Nov 2017 16:55:11 +0000 Subject: [PATCH 09/31] Clean out module js on make clean --- services/web/Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/web/Makefile b/services/web/Makefile index 3955fe1d52..88b09ce609 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -22,6 +22,13 @@ clean: docker-shared.yml rm -rf app/js rm -rf test/unit/js rm -rf test/acceptance/js + for dir in modules/*; \ + do \ + rm -f $$dir/index.js; \ + rm -rf $$dir/app/js; \ + rm -rf $$dir/test/unit/js; \ + rm -rf $$dir/test/acceptance/js; \ + done # Deletes node_modules volume docker-compose down --volumes # Delete after docker-compose command From 3e90103d9cb3523fdc34a24e2bc856196e38895a Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 27 Nov 2017 17:04:54 +0000 Subject: [PATCH 10/31] No need to bind to 0.0.0.0 when running in same container --- .../web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee | 2 +- .../web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee index 6f307b0810..aefcd4513a 100644 --- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee @@ -6,7 +6,7 @@ module.exports = MockDocUpdaterApi = app.post "/project/:project_id/flush", (req, res, next) => res.sendStatus 200 - app.listen 3003, '0.0.0.0', (error) -> + app.listen 3003, (error) -> throw error if error? .on "error", (error) -> console.error "error starting MockDocUpdaterApi:", error.message diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee index 7c8f5dbe50..2133d40b9f 100644 --- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee @@ -22,7 +22,7 @@ module.exports = MockDocStoreApi = docs = (doc for doc_id, doc of @docs[req.params.project_id]) res.send JSON.stringify docs - app.listen 3016, '0.0.0.0', (error) -> + app.listen 3016, (error) -> throw error if error? .on "error", (error) -> console.error "error starting MockDocStoreApi:", error.message From bbaacb4db4dbf1861c30408187ba8a8a086cd5fd Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 28 Nov 2017 10:52:46 +0000 Subject: [PATCH 11/31] Increase autocompile rollout to 20% --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 58893060ff..9d9b7d9a13 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -251,7 +251,7 @@ module.exports = ProjectController = # Extract data from user's ObjectId timestamp = parseInt(user_id.toString().substring(0, 8), 16) - rolloutPercentage = 10 # Percentage of users to roll out to + rolloutPercentage = 20 # Percentage of users to roll out to if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage) # Don't show if user is not part of roll out return cb(null, { enabled: false, showOnboarding: false }) From 50b30455482d0c1a56052f1cc339d256d1def206 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 29 Nov 2017 13:49:36 +0000 Subject: [PATCH 12/31] Tidy up docker-compose and makefile --- services/web/Makefile | 13 +++++++------ services/web/config/settings.defaults.coffee | 2 +- services/web/docker-compose.yml | 3 +-- services/web/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services/web/Makefile b/services/web/Makefile index 88b09ce609..cd724038e6 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -1,4 +1,5 @@ -NPM := docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} run --rm npm npm +DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml +NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm npm BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) PROJECT_NAME = web @@ -41,20 +42,20 @@ docker-shared.yml: test: test_unit test_acceptance test_unit: docker-shared.yml - docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm run test:unit -- ${MOCHA_ARGS} + docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm run test:unit -- ${MOCHA_ARGS} test_acceptance: test_acceptance_app test_acceptance_modules test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service -test_acceptance_app_start_service: docker-shared.yml - docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance +test_acceptance_app_start_service: test_acceptance_app_stop_service docker-shared.yml + docker-compose ${DOCKER_COMPOSE_FLAGS} up -d test_acceptance test_acceptance_app_stop_service: docker-shared.yml - docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo + docker-compose ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo test_acceptance_app_run: docker-shared.yml - docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm run test:acceptance -- ${MOCHA_ARGS} + docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm run test:acceptance -- ${MOCHA_ARGS} test_acceptance_modules: docker-shared.yml for dir in modules/*; \ diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index fccd197a83..98d1c9e031 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -35,7 +35,7 @@ module.exports = settings = # Databases # --------- mongo: - url : "mongodb://#{process.env['MONGO_HOST'] || '127.0.0.1'}/sharelatex" + url : process.env['MONGO_URL'] || "mongodb://127.0.0.1/sharelatex" redis: web: diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml index 3658a87b7f..aaa8666a16 100644 --- a/services/web/docker-compose.yml +++ b/services/web/docker-compose.yml @@ -22,9 +22,8 @@ services: service: app environment: REDIS_HOST: redis - MONGO_HOST: mongo + MONGO_URL: "mongodb://mongo/sharelatex" SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true' - LISTEN_ADDRESS: 0.0.0.0 depends_on: - redis - mongo diff --git a/services/web/package.json b/services/web/package.json index 3e6789920e..c221dc1052 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -13,7 +13,7 @@ "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done", "test:acceptance:run": "bin/acceptance_test $@", "test:acceptance:dir": "npm run compile:acceptance_tests && npm run test:acceptance:wait_for_app && npm run test:acceptance:run -- $@", - "test:acceptance": "npm run test:acceptance:dir test/acceptance/js", + "test:acceptance": "npm run test:acceptance:dir -- $@ test/acceptance/js", "test:unit": "npm run compile:app && npm run compile:unit_tests && bin/unit_test $@", "compile:unit_tests": "bin/compile_unit_tests", "compile:acceptance_tests": "bin/compile_acceptance_tests", From bb74f8318ab9bbc7603722b39fd094b69b72e7d6 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 29 Nov 2017 14:16:29 +0000 Subject: [PATCH 13/31] Support `make clean install` usage --- services/web/Makefile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/web/Makefile b/services/web/Makefile index cd724038e6..7ab8b89781 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -18,7 +18,7 @@ add_dev: docker-shared.yml install: docker-shared.yml $(NPM) install -clean: docker-shared.yml +clean: rm -f app.js rm -rf app/js rm -rf test/unit/js @@ -32,8 +32,9 @@ clean: docker-shared.yml done # Deletes node_modules volume docker-compose down --volumes - # Delete after docker-compose command - rm -f docker-shared.yml + # Regenerate docker-shared.yml - not stictly a 'clean', + # but lets `make clean install` work nicely + bin/generate_volumes_file # Need regenerating if you change the web modules you have installed docker-shared.yml: From c2bb8b9f8917f7f6c5c8f9fffc0bcbb9b226586b Mon Sep 17 00:00:00 2001 From: Nate Stemen Date: Wed, 29 Nov 2017 13:30:53 -0500 Subject: [PATCH 14/31] removing calls --- .../aceEditor/auto-complete/PackageManager.coffee | 2 +- .../directives/aceEditor/metadata/MetadataManager.coffee | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee index d43824b12c..08de22d846 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee @@ -29,7 +29,7 @@ define () -> packageSnippets.push { caption: "\\usepackage{}" - snippet: "\\usepackage{}" + snippet: "\\usepackage{$1}" meta: "pkg" score: 70 } diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee index 362d163196..c9308626bc 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee @@ -22,7 +22,7 @@ define [ end = change.end range = new Range(end.row, 0, end.row, end.column) lineUpToCursor = @editor.getSession().getTextRange range - if lineUpToCursor.trim() == '%' or lineUpToCursor.startsWith '\\' + if lineUpToCursor.trim() == '%' or lineUpToCursor.slice(0, 1) == '\\' range = new Range(end.row, 0, end.row, end.column + 80) lineUpToCursor = @editor.getSession().getTextRange range commandFragment = getLastCommandFragment lineUpToCursor @@ -44,9 +44,9 @@ define [ linesContainLabel or linesContainReqPackage - lastCommandFragmentIsLabel = commandFragment?.startsWith '\\label{' - lastCommandFragmentIsPackage = commandFragment?.startsWith '\\usepackage' - lastCommandFragmentIsReqPack = commandFragment?.startsWith '\\RequirePackage' + lastCommandFragmentIsLabel = commandFragment?.slice(0, 7) == '\\label{' + lastCommandFragmentIsPackage = commandFragment?.slice(0, 11) == '\\usepackage' + lastCommandFragmentIsReqPack = commandFragment?.slice(0, 15) == '\\RequirePackage' lastCommandFragmentIsMeta = lastCommandFragmentIsPackage or lastCommandFragmentIsLabel or From 870e87ebe1b6d8cbff448d3c85df9305a1ec8c7d Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 30 Nov 2017 11:00:28 +0000 Subject: [PATCH 15/31] Run npm with -q flag for less verbose test output --- services/web/Makefile | 6 +++--- services/web/package.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/web/Makefile b/services/web/Makefile index 7ab8b89781..6ce935a2cc 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -1,5 +1,5 @@ DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml -NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm npm +npm -q := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm -q npm BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) PROJECT_NAME = web @@ -43,7 +43,7 @@ docker-shared.yml: test: test_unit test_acceptance test_unit: docker-shared.yml - docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm run test:unit -- ${MOCHA_ARGS} + docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS} test_acceptance: test_acceptance_app test_acceptance_modules @@ -56,7 +56,7 @@ test_acceptance_app_stop_service: docker-shared.yml docker-compose ${DOCKER_COMPOSE_FLAGS} stop test_acceptance redis mongo test_acceptance_app_run: docker-shared.yml - docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm run test:acceptance -- ${MOCHA_ARGS} + docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS} test_acceptance_modules: docker-shared.yml for dir in modules/*; \ diff --git a/services/web/package.json b/services/web/package.json index c221dc1052..0e80d91893 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -12,13 +12,13 @@ "scripts": { "test:acceptance:wait_for_app": "echo 'Waiting for app to be accessible' && while (! curl -s -o /dev/null localhost:3000/status) do sleep 1; done", "test:acceptance:run": "bin/acceptance_test $@", - "test:acceptance:dir": "npm run compile:acceptance_tests && npm run test:acceptance:wait_for_app && npm run test:acceptance:run -- $@", - "test:acceptance": "npm run test:acceptance:dir -- $@ test/acceptance/js", - "test:unit": "npm run compile:app && npm run compile:unit_tests && bin/unit_test $@", + "test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@", + "test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js", + "test:unit": "npm -q run compile:app && npm -q run compile:unit_tests && bin/unit_test $@", "compile:unit_tests": "bin/compile_unit_tests", "compile:acceptance_tests": "bin/compile_acceptance_tests", "compile:app": "bin/compile_app", - "start": "npm run compile:app && node app.js" + "start": "npm -q run compile:app && node app.js" }, "dependencies": { "archiver": "0.9.0", From b2a3e06717dc8ac1f82a4c9b1fca2c9ec91923e8 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 30 Nov 2017 11:20:25 +0000 Subject: [PATCH 16/31] Find / replace mistake --- services/web/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/Makefile b/services/web/Makefile index 6ce935a2cc..6b1c66f211 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -1,5 +1,5 @@ DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml -npm -q := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm -q npm +NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm -q npm BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) PROJECT_NAME = web From 78709d819edb1f6a3da17fd670834c6ea1cc841d Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Thu, 30 Nov 2017 13:15:23 +0000 Subject: [PATCH 17/31] generate test acceptance files volume for modules --- services/web/bin/generate_volumes_file | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/bin/generate_volumes_file b/services/web/bin/generate_volumes_file index d70ac11c3d..49ee11c508 100755 --- a/services/web/bin/generate_volumes_file +++ b/services/web/bin/generate_volumes_file @@ -9,7 +9,7 @@ for module in listdir("modules/"): if module[0] != '.': if isfile(join("modules", module, 'index.coffee')): volumes.append(join("modules", module, 'index.coffee')) - for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee', 'test/acceptance/config']: + for directory in ['app/coffee', 'app/views', 'public/coffee', 'test/unit/coffee', 'test/acceptance/coffee', 'test/acceptance/config', 'test/acceptance/files']: if isdir(join("modules", module, directory)): volumes.append(join("modules", module, directory)) From e916fc906a51ddecbc47a4b9310fda112f398327 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Thu, 30 Nov 2017 13:15:39 +0000 Subject: [PATCH 18/31] add mkdirp dev dependency --- services/web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/package.json b/services/web/package.json index c221dc1052..f54137ea4d 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -109,6 +109,7 @@ "grunt-postcss": "^0.8.0", "grunt-sed": "^0.1.1", "grunt-shell": "^2.1.0", + "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "sandboxed-module": "0.2.0", "sinon": "^1.17.0", "timekeeper": "", From a5cdcea9c7b1d04e0cc8e96d73b2b916cf08cdc8 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 30 Nov 2017 13:32:23 +0000 Subject: [PATCH 19/31] Update modules gitignore to ignore everything except specific modules --- services/web/modules/.gitignore | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/services/web/modules/.gitignore b/services/web/modules/.gitignore index 1d30263cb3..a310279ca9 100644 --- a/services/web/modules/.gitignore +++ b/services/web/modules/.gitignore @@ -1,12 +1,6 @@ -*/app/js -*/test/unit/js -*/index.js -ldap -admin-panel -groovehq -launchpad -learn-wiki -references-search -sharelatex-saml -templates -tpr-webmodule +# Ignore all modules except for a whitelist +* +!dropbox +!github-sync +!public-registration +!.gitignore From 040546b1d33ef8f4991266ec218995f5985d72fb Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 1 Dec 2017 08:49:10 +0000 Subject: [PATCH 20/31] Move -q flag to correct place --- services/web/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/Makefile b/services/web/Makefile index 6b1c66f211..fce72a46d1 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -1,5 +1,5 @@ DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml -NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm -q npm +NPM := docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm npm npm -q BUILD_NUMBER ?= local BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) PROJECT_NAME = web From fa2a3574dbe1b776e042c2f6a261735f0da414e8 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 1 Dec 2017 09:01:36 +0000 Subject: [PATCH 21/31] Look for Makefile, not makefile --- services/web/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/Makefile b/services/web/Makefile index fce72a46d1..d106aa6b9d 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -61,7 +61,7 @@ test_acceptance_app_run: docker-shared.yml test_acceptance_modules: docker-shared.yml for dir in modules/*; \ do \ - if [ -e $$dir/makefile ]; then \ + if [ -e $$dir/Makefile ]; then \ (make test_acceptance_module MODULE=$$dir) \ fi \ done From 65e44d47706d4e9b82455723522d9cf2c69d3739 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 1 Dec 2017 09:31:22 +0000 Subject: [PATCH 22/31] Clean up old docker-shared.yml before running tests --- services/web/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 0650eb4edb..15c698655d 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -111,7 +111,7 @@ pipeline { stage('Unit Tests') { steps { - sh 'make install' + sh 'make clean install' sh 'make test_unit MOCHA_ARGS="--reporter=tap"' } } From e9733514af6e3fff2f72853b1d8a9ef9114af9d0 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 1 Dec 2017 10:03:42 +0000 Subject: [PATCH 23/31] Fail on failing module acceptance tests --- services/web/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/Makefile b/services/web/Makefile index d106aa6b9d..44017629d1 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -59,6 +59,8 @@ test_acceptance_app_run: docker-shared.yml docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS} test_acceptance_modules: docker-shared.yml + # Break and error on any module failure + set -e; \ for dir in modules/*; \ do \ if [ -e $$dir/Makefile ]; then \ From e895a495d6c344a8d3983a38886a99682b39148d Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 1 Dec 2017 10:49:00 +0000 Subject: [PATCH 24/31] Move make clean step to before compile --- services/web/Jenkinsfile | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 15c698655d..9cdebb434c 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -70,6 +70,19 @@ pipeline { sh 'ls -l node_modules/.bin' } } + + stage('Unit Tests') { + steps { + sh 'make clean install' // Removes js files, so do before compile + sh 'make test_unit MOCHA_ARGS="--reporter=tap"' + } + } + + stage('Acceptance Tests') { + steps { + sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"' + } + } stage('Compile') { agent { @@ -109,19 +122,6 @@ pipeline { } } - stage('Unit Tests') { - steps { - sh 'make clean install' - sh 'make test_unit MOCHA_ARGS="--reporter=tap"' - } - } - - stage('Acceptance Tests') { - steps { - sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"' - } - } - stage('Package') { steps { sh 'rm -rf ./node_modules/grunt*' From 2ea3caf08b7fd42e1b66e38cbc06c0642e4e17e7 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Dec 2017 11:22:41 +0000 Subject: [PATCH 25/31] Properly version the fineuploader library --- services/web/Gruntfile.coffee | 1 + services/web/app/coffee/infrastructure/ExpressLocals.coffee | 2 ++ services/web/app/coffee/infrastructure/PackageVersions.coffee | 1 + services/web/app/views/project/editor.pug | 3 ++- services/web/public/coffee/directives/fineUpload.coffee | 2 +- services/web/public/coffee/libs.coffee | 2 +- .../public/js/libs/{fineuploader.js => fineuploader-5.15.4.js} | 0 7 files changed, 8 insertions(+), 3 deletions(-) rename services/web/public/js/libs/{fineuploader.js => fineuploader-5.15.4.js} (100%) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index b93cc8dcbb..c49add4a91 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -196,6 +196,7 @@ module.exports = (grunt) -> "mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML" "pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf" "ace": "#{PackageVersions.lib('ace')}" + "fineuploader": "libs/#{PackageVersions.lib('fineuploader')}" shim: "pdfjs-dist/build/pdf": deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"] diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index fa9a223ad7..2fdf219962 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -24,6 +24,7 @@ jsPath = ace = PackageVersions.lib('ace') pdfjs = PackageVersions.lib('pdfjs') +fineuploader = PackageVersions.lib('fineuploader') getFileContent = (filePath)-> filePath = Path.join __dirname, "../../../", "public#{filePath}" @@ -37,6 +38,7 @@ getFileContent = (filePath)-> logger.log "Generating file fingerprints..." pathList = [ + ["#{jsPath}libs/#{fineuploader}.js"] ["#{jsPath}libs/require.js"] ["#{jsPath}ide.js"] ["#{jsPath}main.js"] diff --git a/services/web/app/coffee/infrastructure/PackageVersions.coffee b/services/web/app/coffee/infrastructure/PackageVersions.coffee index f8c2011c11..5acdfec666 100644 --- a/services/web/app/coffee/infrastructure/PackageVersions.coffee +++ b/services/web/app/coffee/infrastructure/PackageVersions.coffee @@ -2,6 +2,7 @@ version = { "pdfjs": "1.7.225" "moment": "2.9.0" "ace": "1.2.5" + "fineuploader": "5.15.4" } module.exports = { diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 1e1f2c982b..625bea883d 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -130,7 +130,8 @@ block requirejs "moment": "libs/#{lib('moment')}", "pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf", "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", - "ace": "#{lib('ace')}" + "ace": "#{lib('ace')}", + "fineuploader": "libs/#{lib('fineuploader')}" }, "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}", "waitSeconds": 0, diff --git a/services/web/public/coffee/directives/fineUpload.coffee b/services/web/public/coffee/directives/fineUpload.coffee index 7fffcc366a..b3d3766ba6 100644 --- a/services/web/public/coffee/directives/fineUpload.coffee +++ b/services/web/public/coffee/directives/fineUpload.coffee @@ -1,6 +1,6 @@ define [ "base" - "libs/fineuploader" + "fineuploader" ], (App, qq) -> App.directive 'fineUpload', ($timeout) -> return { diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libs.coffee index 533891548b..e99fbc13ce 100644 --- a/services/web/public/coffee/libs.coffee +++ b/services/web/public/coffee/libs.coffee @@ -6,7 +6,7 @@ define [ "libs/underscore-1.3.3" "libs/algolia-2.5.2" "libs/jquery.storage" - "libs/fineuploader" + "fineuploader" "libs/angular-sanitize-1.2.17" "libs/angular-cookie" "libs/passfield" diff --git a/services/web/public/js/libs/fineuploader.js b/services/web/public/js/libs/fineuploader-5.15.4.js similarity index 100% rename from services/web/public/js/libs/fineuploader.js rename to services/web/public/js/libs/fineuploader-5.15.4.js From 3ee6f5d4be2618f20f5d33f186c7ae5e1269a624 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 1 Dec 2017 11:27:06 +0000 Subject: [PATCH 26/31] Update fineuploader on project page --- services/web/app/views/layout.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 822923ac08..31afc17d29 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -132,7 +132,8 @@ html(itemscope, itemtype='http://schema.org/Product') // minimal requirejs configuration (can be extended/overridden) window.requirejs = { "paths" : { - "moment": "libs/#{lib('moment')}" + "moment": "libs/#{lib('moment')}", + "fineuploader": "libs/#{lib('fineuploader')}" }, "urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}", "config":{ From 381761127ea319553527a007acb9838d5cbd5eed Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 1 Dec 2017 13:26:31 +0000 Subject: [PATCH 27/31] Replace previous fineuploader path with old version --- services/web/public/js/libs/fineuploader.js | 3761 +++++++++++++++++++ 1 file changed, 3761 insertions(+) create mode 100644 services/web/public/js/libs/fineuploader.js diff --git a/services/web/public/js/libs/fineuploader.js b/services/web/public/js/libs/fineuploader.js new file mode 100644 index 0000000000..b109b8a1c1 --- /dev/null +++ b/services/web/public/js/libs/fineuploader.js @@ -0,0 +1,3761 @@ +/** + * http://github.com/Valums-File-Uploader/file-uploader + * + * Multiple file upload component with progress-bar, drag-and-drop, support for all modern browsers. + * + * Original version: 1.0 © 2010 Andrew Valums ( andrew(at)valums.com ) + * Current Maintainer (2.0+): © 2012, Ray Nicholus ( fineuploader(at)garstasio.com ) + * + * Licensed under MIT license, GNU GPL 2 or later, GNU LGPL 2 or later, see license.txt. + */ +/*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob*/ +var qq = function(element) { + "use strict"; + + return { + hide: function() { + element.style.display = 'none'; + return this; + }, + + /** Returns the function which detaches attached event */ + attach: function(type, fn) { + if (element.addEventListener){ + element.addEventListener(type, fn, false); + } else if (element.attachEvent){ + element.attachEvent('on' + type, fn); + } + return function() { + qq(element).detach(type, fn); + }; + }, + + detach: function(type, fn) { + if (element.removeEventListener){ + element.removeEventListener(type, fn, false); + } else if (element.attachEvent){ + element.detachEvent('on' + type, fn); + } + return this; + }, + + contains: function(descendant) { + // compareposition returns false in this case + if (element === descendant) { + return true; + } + + if (element.contains){ + return element.contains(descendant); + } else { + /*jslint bitwise: true*/ + return !!(descendant.compareDocumentPosition(element) & 8); + } + }, + + /** + * Insert this element before elementB. + */ + insertBefore: function(elementB) { + elementB.parentNode.insertBefore(element, elementB); + return this; + }, + + remove: function() { + element.parentNode.removeChild(element); + return this; + }, + + /** + * Sets styles for an element. + * Fixes opacity in IE6-8. + */ + css: function(styles) { + if (styles.opacity !== null){ + if (typeof element.style.opacity !== 'string' && typeof(element.filters) !== 'undefined'){ + styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')'; + } + } + qq.extend(element.style, styles); + + return this; + }, + + hasClass: function(name) { + var re = new RegExp('(^| )' + name + '( |$)'); + return re.test(element.className); + }, + + addClass: function(name) { + if (!qq(element).hasClass(name)){ + element.className += ' ' + name; + } + return this; + }, + + removeClass: function(name) { + var re = new RegExp('(^| )' + name + '( |$)'); + element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, ""); + return this; + }, + + getByClass: function(className) { + var candidates, + result = []; + + if (element.querySelectorAll){ + return element.querySelectorAll('.' + className); + } + + candidates = element.getElementsByTagName("*"); + + qq.each(candidates, function(idx, val) { + if (qq(val).hasClass(className)){ + result.push(val); + } + }); + return result; + }, + + children: function() { + var children = [], + child = element.firstChild; + + while (child){ + if (child.nodeType === 1){ + children.push(child); + } + child = child.nextSibling; + } + + return children; + }, + + setText: function(text) { + element.innerText = text; + element.textContent = text; + return this; + }, + + clearText: function() { + return qq(element).setText(""); + } + }; +}; + +qq.log = function(message, level) { + "use strict"; + + if (window.console) { + if (!level || level === 'info') { + window.console.log(message); + } + else + { + if (window.console[level]) { + window.console[level](message); + } + else { + window.console.log('<' + level + '> ' + message); + } + } + } +}; + +qq.isObject = function(variable) { + "use strict"; + return variable !== null && variable && typeof(variable) === "object" && variable.constructor === Object; +}; + +qq.isFunction = function(variable) { + "use strict"; + return typeof(variable) === "function"; +}; + +qq.trimStr = function(string) { + if (String.prototype.trim) { + return string.trim(); + } + + return string.replace(/^\s+|\s+$/g,''); +}; + +qq.isFileOrInput = function(maybeFileOrInput) { + "use strict"; + if (qq.isBlob(maybeFileOrInput) && window.File && maybeFileOrInput instanceof File) { + return true; + } + else if (window.HTMLInputElement) { + if (maybeFileOrInput instanceof HTMLInputElement) { + if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') { + return true; + } + } + } + else if (maybeFileOrInput.tagName) { + if (maybeFileOrInput.tagName.toLowerCase() === 'input') { + if (maybeFileOrInput.type && maybeFileOrInput.type.toLowerCase() === 'file') { + return true; + } + } + } + + return false; +}; + +qq.isBlob = function(maybeBlob) { + "use strict"; + return window.Blob && maybeBlob instanceof Blob; +}; + +qq.isXhrUploadSupported = function() { + "use strict"; + var input = document.createElement('input'); + input.type = 'file'; + + return ( + input.multiple !== undefined && + typeof File !== "undefined" && + typeof FormData !== "undefined" && + typeof (new XMLHttpRequest()).upload !== "undefined" ); +}; + +qq.isFolderDropSupported = function(dataTransfer) { + "use strict"; + return (dataTransfer.items && dataTransfer.items[0].webkitGetAsEntry); +}; + +qq.isFileChunkingSupported = function() { + "use strict"; + return !qq.android() && //android's impl of Blob.slice is broken + qq.isXhrUploadSupported() && + (File.prototype.slice || File.prototype.webkitSlice || File.prototype.mozSlice); +}; + +qq.extend = function (first, second, extendNested) { + "use strict"; + qq.each(second, function(prop, val) { + if (extendNested && qq.isObject(val)) { + if (first[prop] === undefined) { + first[prop] = {}; + } + qq.extend(first[prop], val, true); + } + else { + first[prop] = val; + } + }); +}; + +/** + * Searches for a given element in the array, returns -1 if it is not present. + * @param {Number} [from] The index at which to begin the search + */ +qq.indexOf = function(arr, elt, from){ + "use strict"; + + if (arr.indexOf) { + return arr.indexOf(elt, from); + } + + from = from || 0; + var len = arr.length; + + if (from < 0) { + from += len; + } + + for (; from < len; from+=1){ + if (arr.hasOwnProperty(from) && arr[from] === elt){ + return from; + } + } + return -1; +}; + +//this is a version 4 UUID +qq.getUniqueId = function(){ + "use strict"; + + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + /*jslint eqeq: true, bitwise: true*/ + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); +}; + +// +// Browsers and platforms detection + +qq.ie = function(){ + "use strict"; + return navigator.userAgent.indexOf('MSIE') !== -1; +}; +qq.ie10 = function(){ + "use strict"; + return navigator.userAgent.indexOf('MSIE 10') !== -1; +}; +qq.safari = function(){ + "use strict"; + return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1; +}; +qq.chrome = function(){ + "use strict"; + return navigator.vendor !== undefined && navigator.vendor.indexOf('Google') !== -1; +}; +qq.firefox = function(){ + "use strict"; + return (navigator.userAgent.indexOf('Mozilla') !== -1 && navigator.vendor !== undefined && navigator.vendor === ''); +}; +qq.windows = function(){ + "use strict"; + return navigator.platform === "Win32"; +}; +qq.android = function(){ + "use strict"; + return navigator.userAgent.toLowerCase().indexOf('android') !== -1; +}; + +// +// Events + +qq.preventDefault = function(e){ + "use strict"; + if (e.preventDefault){ + e.preventDefault(); + } else{ + e.returnValue = false; + } +}; + +/** + * Creates and returns element from html string + * Uses innerHTML to create an element + */ +qq.toElement = (function(){ + "use strict"; + var div = document.createElement('div'); + return function(html){ + div.innerHTML = html; + var element = div.firstChild; + div.removeChild(element); + return element; + }; +}()); + +//key and value are passed to callback for each item in the object or array +qq.each = function(obj, callback) { + "use strict"; + var key, retVal; + if (obj) { + for (key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + retVal = callback(key, obj[key]); + if (retVal === false) { + break; + } + } + } + } +}; + +/** + * obj2url() takes a json-object as argument and generates + * a querystring. pretty much like jQuery.param() + * + * how to use: + * + * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');` + * + * will result in: + * + * `http://any.url/upload?otherParam=value&a=b&c=d` + * + * @param Object JSON-Object + * @param String current querystring-part + * @return String encoded querystring + */ +qq.obj2url = function(obj, temp, prefixDone){ + "use strict"; + /*jshint laxbreak: true*/ + var i, len, + uristrings = [], + prefix = '&', + add = function(nextObj, i){ + var nextTemp = temp + ? (/\[\]$/.test(temp)) // prevent double-encoding + ? temp + : temp+'['+i+']' + : i; + if ((nextTemp !== 'undefined') && (i !== 'undefined')) { + uristrings.push( + (typeof nextObj === 'object') + ? qq.obj2url(nextObj, nextTemp, true) + : (Object.prototype.toString.call(nextObj) === '[object Function]') + ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj()) + : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj) + ); + } + }; + + if (!prefixDone && temp) { + prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?'; + uristrings.push(temp); + uristrings.push(qq.obj2url(obj)); + } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj !== 'undefined') ) { + // we wont use a for-in-loop on an array (performance) + for (i = -1, len = obj.length; i < len; i+=1){ + add(obj[i], i); + } + } else if ((typeof obj !== 'undefined') && (obj !== null) && (typeof obj === "object")){ + // for anything else but a scalar, we will use for-in-loop + for (i in obj){ + if (obj.hasOwnProperty(i)) { + add(obj[i], i); + } + } + } else { + uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj)); + } + + if (temp) { + return uristrings.join(prefix); + } else { + return uristrings.join(prefix) + .replace(/^&/, '') + .replace(/%20/g, '+'); + } +}; + +qq.obj2FormData = function(obj, formData, arrayKeyName) { + "use strict"; + if (!formData) { + formData = new FormData(); + } + + qq.each(obj, function(key, val) { + key = arrayKeyName ? arrayKeyName + '[' + key + ']' : key; + + if (qq.isObject(val)) { + qq.obj2FormData(val, formData, key); + } + else if (qq.isFunction(val)) { + formData.append(key, val()); + } + else { + formData.append(key, val); + } + }); + + return formData; +}; + +qq.obj2Inputs = function(obj, form) { + "use strict"; + var input; + + if (!form) { + form = document.createElement('form'); + } + + qq.obj2FormData(obj, { + append: function(key, val) { + input = document.createElement('input'); + input.setAttribute('name', key); + input.setAttribute('value', val); + form.appendChild(input); + } + }); + + return form; +}; + +qq.setCookie = function(name, value, days) { + var date = new Date(), + expires = ""; + + if (days) { + date.setTime(date.getTime()+(days*24*60*60*1000)); + expires = "; expires="+date.toGMTString(); + } + + document.cookie = name+"="+value+expires+"; path=/"; +}; + +qq.getCookie = function(name) { + var nameEQ = name + "=", + ca = document.cookie.split(';'), + c; + + for(var i=0;i < ca.length;i++) { + c = ca[i]; + while (c.charAt(0)==' ') { + c = c.substring(1,c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length,c.length); + } + } +}; + +qq.getCookieNames = function(regexp) { + var cookies = document.cookie.split(';'), + cookieNames = []; + + qq.each(cookies, function(idx, cookie) { + cookie = qq.trimStr(cookie); + + var equalsIdx = cookie.indexOf("="); + + if (cookie.match(regexp)) { + cookieNames.push(cookie.substr(0, equalsIdx)); + } + }); + + return cookieNames; +}; + +qq.deleteCookie = function(name) { + qq.setCookie(name, "", -1); +}; + +qq.areCookiesEnabled = function() { + var randNum = Math.random() * 100000, + name = "qqCookieTest:" + randNum; + qq.setCookie(name, 1); + + if (qq.getCookie(name)) { + qq.deleteCookie(name); + return true; + } + return false; +}; + +/** + * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not + * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js. + */ +qq.parseJson = function(json) { + /*jshint evil: true*/ + if (window.JSON && qq.isFunction(JSON.parse)) { + return JSON.parse(json); + } else { + return eval("(" + json + ")"); + } +}; + +/** + * A generic module which supports object disposing in dispose() method. + * */ +qq.DisposeSupport = function() { + "use strict"; + var disposers = []; + + return { + /** Run all registered disposers */ + dispose: function() { + var disposer; + do { + disposer = disposers.shift(); + if (disposer) { + disposer(); + } + } + while (disposer); + }, + + /** Attach event handler and register de-attacher as a disposer */ + attach: function() { + var args = arguments; + /*jslint undef:true*/ + this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1))); + }, + + /** Add disposer to the collection */ + addDisposer: function(disposeFunction) { + disposers.push(disposeFunction); + } + }; +}; +qq.UploadButton = function(o){ + this._options = { + element: null, + // if set to true adds multiple attribute to file input + multiple: false, + acceptFiles: null, + // name attribute of file input + name: 'file', + onChange: function(input){}, + hoverClass: 'qq-upload-button-hover', + focusClass: 'qq-upload-button-focus' + }; + + qq.extend(this._options, o); + this._disposeSupport = new qq.DisposeSupport(); + + this._element = this._options.element; + + // make button suitable container for input + qq(this._element).css({ + position: 'relative', + overflow: 'hidden', + // Make sure browse button is in the right side + // in Internet Explorer + direction: 'ltr' + }); + + this._input = this._createInput(); +}; + +qq.UploadButton.prototype = { + /* returns file input element */ + getInput: function(){ + return this._input; + }, + /* cleans/recreates the file input */ + reset: function(){ + if (this._input.parentNode){ + qq(this._input).remove(); + } + + qq(this._element).removeClass(this._options.focusClass); + this._input = this._createInput(); + }, + _createInput: function(){ + var input = document.createElement("input"); + + if (this._options.multiple){ + input.setAttribute("multiple", "multiple"); + } + + if (this._options.acceptFiles) input.setAttribute("accept", this._options.acceptFiles); + + input.setAttribute("type", "file"); + input.setAttribute("name", this._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', + // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118 + fontSize: '118px', + margin: 0, + padding: 0, + cursor: 'pointer', + opacity: 0 + }); + + this._element.appendChild(input); + + var self = this; + this._disposeSupport.attach(input, 'change', function(){ + self._options.onChange(input); + }); + + this._disposeSupport.attach(input, 'mouseover', function(){ + qq(self._element).addClass(self._options.hoverClass); + }); + this._disposeSupport.attach(input, 'mouseout', function(){ + qq(self._element).removeClass(self._options.hoverClass); + }); + this._disposeSupport.attach(input, 'focus', function(){ + qq(self._element).addClass(self._options.focusClass); + }); + this._disposeSupport.attach(input, 'blur', function(){ + qq(self._element).removeClass(self._options.focusClass); + }); + + // IE and Opera, unfortunately have 2 tab stops on file input + // which is unacceptable in our case, disable keyboard access + if (window.attachEvent){ + // it is IE or Opera + input.setAttribute('tabIndex', "-1"); + } + + return input; + } +}; +qq.FineUploaderBasic = function(o){ + var that = this; + this._options = { + debug: false, + button: null, + multiple: true, + maxConnections: 3, + disableCancelForFormUploads: false, + autoUpload: true, + request: { + endpoint: '/server/upload', + params: {}, + paramsInBody: true, + customHeaders: {}, + forceMultipart: true, + inputName: 'qqfile', + uuidName: 'qquuid', + totalFileSizeName: 'qqtotalfilesize' + }, + validation: { + allowedExtensions: [], + sizeLimit: 0, + minSizeLimit: 0, + stopOnFirstInvalidFile: true + }, + callbacks: { + onSubmit: function(id, name){}, + onComplete: function(id, name, responseJSON){}, + onCancel: function(id, name){}, + onUpload: function(id, name){}, + onUploadChunk: function(id, name, chunkData){}, + onResume: function(id, fileName, chunkData){}, + onProgress: function(id, name, loaded, total){}, + onError: function(id, name, reason) {}, + onAutoRetry: function(id, name, attemptNumber) {}, + onManualRetry: function(id, name) {}, + onValidateBatch: function(fileOrBlobData) {}, + onValidate: function(fileOrBlobData) {}, + onSubmitDelete: function(id) {}, + onDelete: function(id){}, + onDeleteComplete: function(id, xhr, isError){} + }, + messages: { + typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.", + sizeError: "{file} is too large, maximum file size is {sizeLimit}.", + minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", + emptyError: "{file} is empty, please select files again without it.", + noFilesError: "No files to upload.", + onLeave: "The files are being uploaded, if you leave now the upload will be cancelled." + }, + retry: { + enableAuto: false, + maxAutoAttempts: 3, + autoAttemptDelay: 5, + preventRetryResponseProperty: 'preventRetry' + }, + classes: { + buttonHover: 'qq-upload-button-hover', + buttonFocus: 'qq-upload-button-focus' + }, + chunking: { + enabled: false, + partSize: 2000000, + paramNames: { + partIndex: 'qqpartindex', + partByteOffset: 'qqpartbyteoffset', + chunkSize: 'qqchunksize', + totalFileSize: 'qqtotalfilesize', + totalParts: 'qqtotalparts', + filename: 'qqfilename' + } + }, + resume: { + enabled: false, + id: null, + cookiesExpireIn: 7, //days + paramNames: { + resuming: "qqresume" + } + }, + formatFileName: function(fileOrBlobName) { + if (fileOrBlobName.length > 33) { + fileOrBlobName = fileOrBlobName.slice(0, 19) + '...' + fileOrBlobName.slice(-14); + } + return fileOrBlobName; + }, + text: { + sizeSymbols: ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'] + }, + deleteFile : { + enabled: false, + endpoint: '/server/upload', + customHeaders: {}, + params: {} + }, + cors: { + expected: false, + sendCredentials: false + }, + blobs: { + defaultName: 'Misc data', + paramNames: { + name: 'qqblobname' + } + } + }; + + qq.extend(this._options, o, true); + this._wrapCallbacks(); + this._disposeSupport = new qq.DisposeSupport(); + + // number of files being uploaded + this._filesInProgress = []; + + this._storedIds = []; + + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + + this._paramsStore = this._createParamsStore("request"); + this._deleteFileParamsStore = this._createParamsStore("deleteFile"); + + this._endpointStore = this._createEndpointStore("request"); + this._deleteFileEndpointStore = this._createEndpointStore("deleteFile"); + + this._handler = this._createUploadHandler(); + this._deleteHandler = this._createDeleteHandler(); + + if (this._options.button){ + this._button = this._createUploadButton(this._options.button); + } + + this._preventLeaveInProgress(); +}; + +qq.FineUploaderBasic.prototype = { + log: function(str, level) { + if (this._options.debug && (!level || level === 'info')) { + qq.log('[FineUploader] ' + str); + } + else if (level && level !== 'info') { + qq.log('[FineUploader] ' + str, level); + + } + }, + setParams: function(params, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.request.params = params; + } + else { + this._paramsStore.setParams(params, id); + } + }, + setDeleteFileParams: function(params, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.deleteFile.params = params; + } + else { + this._deleteFileParamsStore.setParams(params, id); + } + }, + setEndpoint: function(endpoint, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.request.endpoint = endpoint; + } + else { + this._endpointStore.setEndpoint(endpoint, id); + } + }, + getInProgress: function(){ + return this._filesInProgress.length; + }, + uploadStoredFiles: function(){ + "use strict"; + var idToUpload; + + while(this._storedIds.length) { + idToUpload = this._storedIds.shift(); + this._filesInProgress.push(idToUpload); + this._handler.upload(idToUpload); + } + }, + clearStoredFiles: function(){ + this._storedIds = []; + }, + retry: function(id) { + if (this._onBeforeManualRetry(id)) { + this._handler.retry(id); + return true; + } + else { + return false; + } + }, + cancel: function(id) { + this._handler.cancel(id); + }, + cancelAll: function() { + var storedIdsCopy = [], + self = this; + + qq.extend(storedIdsCopy, this._storedIds); + qq.each(storedIdsCopy, function(idx, storedFileId) { + self.cancel(storedFileId); + }); + + this._handler.cancelAll(); + }, + reset: function() { + this.log("Resetting uploader..."); + this._handler.reset(); + this._filesInProgress = []; + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._button.reset(); + this._paramsStore.reset(); + this._endpointStore.reset(); + }, + addFiles: function(filesBlobDataOrInputs) { + var self = this, + verifiedFilesOrInputs = [], + index, fileOrInput; + + if (filesBlobDataOrInputs) { + if (!window.FileList || !(filesBlobDataOrInputs instanceof FileList)) { + filesBlobDataOrInputs = [].concat(filesBlobDataOrInputs); + } + + for (index = 0; index < filesBlobDataOrInputs.length; index+=1) { + fileOrInput = filesBlobDataOrInputs[index]; + + if (qq.isFileOrInput(fileOrInput)) { + verifiedFilesOrInputs.push(fileOrInput); + } + else { + self.log(fileOrInput + ' is not a File or INPUT element! Ignoring!', 'warn'); + } + } + + this.log('Processing ' + verifiedFilesOrInputs.length + ' files or inputs...'); + this._uploadFileOrBlobDataList(verifiedFilesOrInputs); + } + }, + addBlobs: function(blobDataOrArray) { + if (blobDataOrArray) { + var blobDataArray = [].concat(blobDataOrArray), + verifiedBlobDataList = [], + self = this; + + qq.each(blobDataArray, function(idx, blobData) { + if (qq.isBlob(blobData) && !qq.isFileOrInput(blobData)) { + verifiedBlobDataList.push({ + blob: blobData, + name: self._options.blobs.defaultName + }); + } + else if (qq.isObject(blobData) && blobData.blob && blobData.name) { + verifiedBlobDataList.push(blobData); + } + else { + self.log("addBlobs: entry at index " + idx + " is not a Blob or a BlobData object", "error"); + } + }); + + this._uploadFileOrBlobDataList(verifiedBlobDataList); + } + else { + this.log("undefined or non-array parameter passed into addBlobs", "error"); + } + }, + getUuid: function(id) { + return this._handler.getUuid(id); + }, + getResumableFilesData: function() { + return this._handler.getResumableFilesData(); + }, + getSize: function(id) { + return this._handler.getSize(id); + }, + getFile: function(fileOrBlobId) { + return this._handler.getFile(fileOrBlobId); + }, + deleteFile: function(id) { + this._onSubmitDelete(id); + }, + setDeleteFileEndpoint: function(endpoint, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + this._options.deleteFile.endpoint = endpoint; + } + else { + this._deleteFileEndpointStore.setEndpoint(endpoint, id); + } + }, + _createUploadButton: function(element){ + var self = this; + + var button = new qq.UploadButton({ + element: element, + multiple: this._options.multiple && qq.isXhrUploadSupported(), + acceptFiles: this._options.validation.acceptFiles, + onChange: function(input){ + self._onInputChange(input); + }, + hoverClass: this._options.classes.buttonHover, + focusClass: this._options.classes.buttonFocus + }); + + this._disposeSupport.addDisposer(function() { button.dispose(); }); + return button; + }, + _createUploadHandler: function(){ + var self = this; + + return new qq.UploadHandler({ + debug: this._options.debug, + forceMultipart: this._options.request.forceMultipart, + maxConnections: this._options.maxConnections, + customHeaders: this._options.request.customHeaders, + inputName: this._options.request.inputName, + uuidParamName: this._options.request.uuidName, + totalFileSizeParamName: this._options.request.totalFileSizeName, + cors: this._options.cors, + demoMode: this._options.demoMode, + paramsInBody: this._options.request.paramsInBody, + paramsStore: this._paramsStore, + endpointStore: this._endpointStore, + chunking: this._options.chunking, + resume: this._options.resume, + blobs: this._options.blobs, + log: function(str, level) { + self.log(str, level); + }, + onProgress: function(id, name, loaded, total){ + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + }, + onComplete: function(id, name, result, xhr){ + self._onComplete(id, name, result, xhr); + self._options.callbacks.onComplete(id, name, result); + }, + onCancel: function(id, name){ + self._onCancel(id, name); + self._options.callbacks.onCancel(id, name); + }, + onUpload: function(id, name){ + self._onUpload(id, name); + self._options.callbacks.onUpload(id, name); + }, + onUploadChunk: function(id, name, chunkData){ + self._options.callbacks.onUploadChunk(id, name, chunkData); + }, + onResume: function(id, name, chunkData) { + return self._options.callbacks.onResume(id, name, chunkData); + }, + onAutoRetry: function(id, name, responseJSON, xhr) { + self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; + + if (self._shouldAutoRetry(id, name, responseJSON)) { + self._maybeParseAndSendUploadError(id, name, responseJSON, xhr); + self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id] + 1); + self._onBeforeAutoRetry(id, name); + + self._retryTimeouts[id] = setTimeout(function() { + self._onAutoRetry(id, name, responseJSON) + }, self._options.retry.autoAttemptDelay * 1000); + + return true; + } + else { + return false; + } + } + }); + }, + _createDeleteHandler: function() { + var self = this; + + return new qq.DeleteFileAjaxRequestor({ + maxConnections: this._options.maxConnections, + customHeaders: this._options.deleteFile.customHeaders, + paramsStore: this._deleteFileParamsStore, + endpointStore: this._deleteFileEndpointStore, + demoMode: this._options.demoMode, + cors: this._options.cors, + log: function(str, level) { + self.log(str, level); + }, + onDelete: function(id) { + self._onDelete(id); + self._options.callbacks.onDelete(id); + }, + onDeleteComplete: function(id, xhr, isError) { + self._onDeleteComplete(id, xhr, isError); + self._options.callbacks.onDeleteComplete(id, xhr, isError); + } + + }); + }, + _preventLeaveInProgress: function(){ + var self = this; + + this._disposeSupport.attach(window, 'beforeunload', function(e){ + if (!self._filesInProgress.length){return;} + + var e = e || window.event; + // for ie, ff + e.returnValue = self._options.messages.onLeave; + // for webkit + return self._options.messages.onLeave; + }); + }, + _onSubmit: function(id, name){ + if (this._options.autoUpload) { + this._filesInProgress.push(id); + } + }, + _onProgress: function(id, name, loaded, total){ + }, + _onComplete: function(id, name, result, xhr){ + this._removeFromFilesInProgress(id); + this._maybeParseAndSendUploadError(id, name, result, xhr); + }, + _onCancel: function(id, name){ + this._removeFromFilesInProgress(id); + + clearTimeout(this._retryTimeouts[id]); + + var storedItemIndex = qq.indexOf(this._storedIds, id); + if (!this._options.autoUpload && storedItemIndex >= 0) { + this._storedIds.splice(storedItemIndex, 1); + } + }, + _isDeletePossible: function() { + return (this._options.deleteFile.enabled && + (!this._options.cors.expected || + (this._options.cors.expected && (qq.ie10() || !qq.ie())) + ) + ); + }, + _onSubmitDelete: function(id) { + if (this._isDeletePossible()) { + if (this._options.callbacks.onSubmitDelete(id)) { + this._deleteHandler.sendDelete(id, this.getUuid(id)); + } + } + else { + this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " + + "due to CORS on a user agent that does not support pre-flighting.", "warn"); + return false; + } + }, + _onDelete: function(fileId) {}, + _onDeleteComplete: function(id, xhr, isError) { + var name = this._handler.getName(id); + + if (isError) { + this.log("Delete request for '" + name + "' has failed.", "error"); + this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhr.status); + } + else { + this.log("Delete request for '" + name + "' has succeeded."); + } + }, + _removeFromFilesInProgress: function(id) { + var index = qq.indexOf(this._filesInProgress, id); + if (index >= 0) { + this._filesInProgress.splice(index, 1); + } + }, + _onUpload: function(id, name){}, + _onInputChange: function(input){ + if (qq.isXhrUploadSupported()){ + this.addFiles(input.files); + } else { + this.addFiles(input); + } + this._button.reset(); + }, + _onBeforeAutoRetry: function(id, name) { + this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "..."); + }, + _onAutoRetry: function(id, name, responseJSON) { + this.log("Retrying " + name + "..."); + this._autoRetries[id]++; + this._handler.retry(id); + }, + _shouldAutoRetry: function(id, name, responseJSON) { + if (!this._preventRetries[id] && this._options.retry.enableAuto) { + if (this._autoRetries[id] === undefined) { + this._autoRetries[id] = 0; + } + + return this._autoRetries[id] < this._options.retry.maxAutoAttempts + } + + return false; + }, + //return false if we should not attempt the requested retry + _onBeforeManualRetry: function(id) { + if (this._preventRetries[id]) { + this.log("Retries are forbidden for id " + id, 'warn'); + return false; + } + else if (this._handler.isValid(id)) { + var fileName = this._handler.getName(id); + + if (this._options.callbacks.onManualRetry(id, fileName) === false) { + return false; + } + + this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); + this._filesInProgress.push(id); + return true; + } + else { + this.log("'" + id + "' is not a valid file ID", 'error'); + return false; + } + }, + _maybeParseAndSendUploadError: function(id, name, response, xhr) { + //assuming no one will actually set the response code to something other than 200 and still set 'success' to true + if (!response.success){ + if (xhr && xhr.status !== 200 && !response.error) { + this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status); + } + else { + var errorReason = response.error ? response.error : "Upload failure reason unknown"; + this._options.callbacks.onError(id, name, errorReason); + } + } + }, + _uploadFileOrBlobDataList: function(fileOrBlobDataList){ + var validationDescriptors, index, batchInvalid; + + validationDescriptors = this._getValidationDescriptors(fileOrBlobDataList); + batchInvalid = this._options.callbacks.onValidateBatch(validationDescriptors) === false; + + if (!batchInvalid) { + if (fileOrBlobDataList.length > 0) { + for (index = 0; index < fileOrBlobDataList.length; index++){ + if (this._validateFileOrBlobData(fileOrBlobDataList[index])){ + this._upload(fileOrBlobDataList[index]); + } else { + if (this._options.validation.stopOnFirstInvalidFile){ + return; + } + } + } + } + else { + this._error('noFilesError', ""); + } + } + }, + _upload: function(blobOrFileContainer){ + var id = this._handler.add(blobOrFileContainer); + var name = this._handler.getName(id); + + if (this._options.callbacks.onSubmit(id, name) !== false){ + this._onSubmit(id, name); + if (this._options.autoUpload) { + this._handler.upload(id); + } + else { + this._storeForLater(id); + } + } + }, + _storeForLater: function(id) { + this._storedIds.push(id); + }, + _validateFileOrBlobData: function(fileOrBlobData){ + var validationDescriptor, name, size; + + validationDescriptor = this._getValidationDescriptor(fileOrBlobData); + name = validationDescriptor.name; + size = validationDescriptor.size; + + if (this._options.callbacks.onValidate(validationDescriptor) === false) { + return false; + } + + if (qq.isFileOrInput(fileOrBlobData) && !this._isAllowedExtension(name)){ + this._error('typeError', name); + return false; + + } + else if (size === 0){ + this._error('emptyError', name); + return false; + + } + else if (size && this._options.validation.sizeLimit && size > this._options.validation.sizeLimit){ + this._error('sizeError', name); + return false; + + } + else if (size && size < this._options.validation.minSizeLimit){ + this._error('minSizeError', name); + return false; + } + + return true; + }, + _error: function(code, name){ + var message = this._options.messages[code]; + function r(name, replacement){ message = message.replace(name, replacement); } + + var extensions = this._options.validation.allowedExtensions.join(', ').toLowerCase(); + + r('{file}', this._options.formatFileName(name)); + r('{extensions}', extensions); + r('{sizeLimit}', this._formatSize(this._options.validation.sizeLimit)); + r('{minSizeLimit}', this._formatSize(this._options.validation.minSizeLimit)); + + this._options.callbacks.onError(null, name, message); + + return message; + }, + _isAllowedExtension: function(fileName){ + var allowed = this._options.validation.allowedExtensions, + valid = false; + + if (!allowed.length) { + return true; + } + + qq.each(allowed, function(idx, allowedExt) { + /*jshint eqeqeq: true, eqnull: true*/ + var extRegex = new RegExp('\\.' + allowedExt + "$", 'i'); + + if (fileName.match(extRegex) != null) { + valid = true; + return false; + } + }); + + return valid; + }, + _formatSize: function(bytes){ + var i = -1; + do { + bytes = bytes / 1024; + i++; + } while (bytes > 99); + + return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i]; + }, + _wrapCallbacks: function() { + var self, safeCallback; + + self = this; + + safeCallback = function(name, callback, args) { + try { + return callback.apply(self, args); + } + catch (exception) { + self.log("Caught exception in '" + name + "' callback - " + exception.message, 'error'); + } + } + + for (var prop in this._options.callbacks) { + (function() { + var callbackName, callbackFunc; + callbackName = prop; + callbackFunc = self._options.callbacks[callbackName]; + self._options.callbacks[callbackName] = function() { + return safeCallback(callbackName, callbackFunc, arguments); + } + }()); + } + }, + _parseFileOrBlobDataName: function(fileOrBlobData) { + var name; + + if (qq.isFileOrInput(fileOrBlobData)) { + if (fileOrBlobData.value) { + // it is a file input + // get input value and remove path to normalize + name = fileOrBlobData.value.replace(/.*(\/|\\)/, ""); + } else { + // fix missing properties in Safari 4 and firefox 11.0a2 + name = (fileOrBlobData.fileName !== null && fileOrBlobData.fileName !== undefined) ? fileOrBlobData.fileName : fileOrBlobData.name; + } + } + else { + name = fileOrBlobData.name; + } + + return name; + }, + _parseFileOrBlobDataSize: function(fileOrBlobData) { + var size; + + if (qq.isFileOrInput(fileOrBlobData)) { + if (!fileOrBlobData.value){ + // fix missing properties in Safari 4 and firefox 11.0a2 + size = (fileOrBlobData.fileSize !== null && fileOrBlobData.fileSize !== undefined) ? fileOrBlobData.fileSize : fileOrBlobData.size; + } + } + else { + size = fileOrBlobData.blob.size; + } + + return size; + }, + _getValidationDescriptor: function(fileOrBlobData) { + var name, size, fileDescriptor; + + fileDescriptor = {}; + name = this._parseFileOrBlobDataName(fileOrBlobData); + size = this._parseFileOrBlobDataSize(fileOrBlobData); + + fileDescriptor.name = name; + if (size) { + fileDescriptor.size = size; + } + + return fileDescriptor; + }, + _getValidationDescriptors: function(files) { + var self = this, + fileDescriptors = []; + + qq.each(files, function(idx, file) { + fileDescriptors.push(self._getValidationDescriptor(file)); + }); + + return fileDescriptors; + }, + _createParamsStore: function(type) { + var paramsStore = {}, + self = this; + + return { + setParams: function(params, id) { + var paramsCopy = {}; + qq.extend(paramsCopy, params); + paramsStore[id] = paramsCopy; + }, + + getParams: function(id) { + /*jshint eqeqeq: true, eqnull: true*/ + var paramsCopy = {}; + + if (id != null && paramsStore[id]) { + qq.extend(paramsCopy, paramsStore[id]); + } + else { + qq.extend(paramsCopy, self._options[type].params); + } + + return paramsCopy; + }, + + remove: function(fileId) { + return delete paramsStore[fileId]; + }, + + reset: function() { + paramsStore = {}; + } + }; + }, + _createEndpointStore: function(type) { + var endpointStore = {}, + self = this; + + return { + setEndpoint: function(endpoint, id) { + endpointStore[id] = endpoint; + }, + + getEndpoint: function(id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id != null && endpointStore[id]) { + return endpointStore[id]; + } + + return self._options[type].endpoint; + }, + + remove: function(fileId) { + return delete endpointStore[fileId]; + }, + + reset: function() { + endpointStore = {}; + } + }; + } +}; +/*globals qq, document*/ +qq.DragAndDrop = function(o) { + "use strict"; + + var options, dz, dirPending, + droppedFiles = [], + droppedEntriesCount = 0, + droppedEntriesParsedCount = 0, + disposeSupport = new qq.DisposeSupport(); + + options = { + dropArea: null, + extraDropzones: [], + hideDropzones: true, + multiple: true, + classes: { + dropActive: null + }, + callbacks: { + dropProcessing: function(isProcessing, files) {}, + error: function(code, filename) {}, + log: function(message, level) {} + } + }; + + qq.extend(options, o); + + function maybeUploadDroppedFiles() { + if (droppedEntriesCount === droppedEntriesParsedCount && !dirPending) { + options.callbacks.log('Grabbed ' + droppedFiles.length + " files after tree traversal."); + dz.dropDisabled(false); + options.callbacks.dropProcessing(false, droppedFiles); + } + } + function addDroppedFile(file) { + droppedFiles.push(file); + droppedEntriesParsedCount+=1; + maybeUploadDroppedFiles(); + } + + function traverseFileTree(entry) { + var dirReader, i; + + droppedEntriesCount+=1; + + if (entry.isFile) { + entry.file(function(file) { + addDroppedFile(file); + }); + } + else if (entry.isDirectory) { + dirPending = true; + dirReader = entry.createReader(); + dirReader.readEntries(function(entries) { + droppedEntriesParsedCount+=1; + for (i = 0; i < entries.length; i+=1) { + traverseFileTree(entries[i]); + } + + dirPending = false; + + if (!entries.length) { + maybeUploadDroppedFiles(); + } + }); + } + } + + function handleDataTransfer(dataTransfer) { + var i, items, entry; + + options.callbacks.dropProcessing(true); + dz.dropDisabled(true); + + if (dataTransfer.files.length > 1 && !options.multiple) { + options.callbacks.dropProcessing(false); + options.callbacks.error('tooManyFilesError', ""); + dz.dropDisabled(false); + } + else { + droppedFiles = []; + droppedEntriesCount = 0; + droppedEntriesParsedCount = 0; + + if (qq.isFolderDropSupported(dataTransfer)) { + items = dataTransfer.items; + + for (i = 0; i < items.length; i+=1) { + entry = items[i].webkitGetAsEntry(); + if (entry) { + //due to a bug in Chrome's File System API impl - #149735 + if (entry.isFile) { + droppedFiles.push(items[i].getAsFile()); + if (i === items.length-1) { + maybeUploadDroppedFiles(); + } + } + + else { + traverseFileTree(entry); + } + } + } + } + else { + options.callbacks.dropProcessing(false, dataTransfer.files); + dz.dropDisabled(false); + } + } + } + + function setupDropzone(dropArea){ + dz = new qq.UploadDropZone({ + element: dropArea, + onEnter: function(e){ + qq(dropArea).addClass(options.classes.dropActive); + e.stopPropagation(); + }, + onLeaveNotDescendants: function(e){ + qq(dropArea).removeClass(options.classes.dropActive); + }, + onDrop: function(e){ + if (options.hideDropzones) { + qq(dropArea).hide(); + } + qq(dropArea).removeClass(options.classes.dropActive); + + handleDataTransfer(e.dataTransfer); + } + }); + + disposeSupport.addDisposer(function() { + dz.dispose(); + }); + + if (options.hideDropzones) { + qq(dropArea).hide(); + } + } + + function isFileDrag(dragEvent) { + var fileDrag; + + qq.each(dragEvent.dataTransfer.types, function(key, val) { + if (val === 'Files') { + fileDrag = true; + return false; + } + }); + + return fileDrag; + } + + function setupDragDrop(){ + if (options.dropArea) { + options.extraDropzones.push(options.dropArea); + } + + var i, dropzones = options.extraDropzones; + + for (i=0; i < dropzones.length; i+=1){ + setupDropzone(dropzones[i]); + } + + // IE <= 9 does not support the File API used for drag+drop uploads + if (options.dropArea && (!qq.ie() || qq.ie10())) { + disposeSupport.attach(document, 'dragenter', function(e) { + if (!dz.dropDisabled() && isFileDrag(e)) { + if (qq(options.dropArea).hasClass(options.classes.dropDisabled)) { + return; + } + + options.dropArea.style.display = 'block'; + for (i=0; i < dropzones.length; i+=1) { + dropzones[i].style.display = 'block'; + } + } + }); + } + disposeSupport.attach(document, 'dragleave', function(e){ + if (options.hideDropzones && qq.FineUploader.prototype._leaving_document_out(e)) { + for (i=0; i < dropzones.length; i+=1) { + qq(dropzones[i]).hide(); + } + } + }); + disposeSupport.attach(document, 'drop', function(e){ + if (options.hideDropzones) { + for (i=0; i < dropzones.length; i+=1) { + qq(dropzones[i]).hide(); + } + } + e.preventDefault(); + }); + } + + return { + setup: function() { + setupDragDrop(); + }, + + setupExtraDropzone: function(element) { + options.extraDropzones.push(element); + setupDropzone(element); + }, + + removeExtraDropzone: function(element) { + var i, dzs = options.extraDropzones; + for(i in dzs) { + if (dzs[i] === element) { + return dzs.splice(i, 1); + } + } + }, + + dispose: function() { + disposeSupport.dispose(); + dz.dispose(); + } + }; +}; + + +qq.UploadDropZone = function(o){ + "use strict"; + + var options, element, preventDrop, dropOutsideDisabled, disposeSupport = new qq.DisposeSupport(); + + 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 dragover_should_be_canceled(){ + 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 (dragover_should_be_canceled){ + 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.ie() && !qq.ie10()) { + 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.types.contains check is for firefox + effectTest = qq.ie10() ? true : dt.effectAllowed !== 'none'; + return dt && effectTest && (dt.files || (!isSafari && dt.types.contains && dt.types.contains('Files'))); + } + + function isOrSetDropDisabled(isDisabled) { + if (isDisabled !== undefined) { + preventDrop = isDisabled; + } + return preventDrop; + } + + function attachEvents(){ + disposeSupport.attach(element, 'dragover', function(e){ + if (!isValidFileDrag(e)) { + return; + } + + var effect = qq.ie() ? 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(); + options.onDrop(e); + } + }); + } + + disableDropOutside(); + attachEvents(); + + return { + dropDisabled: function(isDisabled) { + return isOrSetDropDisabled(isDisabled); + }, + + dispose: function() { + disposeSupport.dispose(); + } + }; +}; +/** + * Class that creates upload widget with drag-and-drop and file list + * @inherits qq.FineUploaderBasic + */ +qq.FineUploader = function(o){ + // call parent constructor + qq.FineUploaderBasic.apply(this, arguments); + + // additional options + qq.extend(this._options, { + element: null, + listElement: null, + dragAndDrop: { + extraDropzones: [], + hideDropzones: true, + disableDefaultDropzone: false + }, + text: { + uploadButton: 'Upload a file', + cancelButton: 'Cancel', + retryButton: 'Retry', + deleteButton: 'Delete', + failUpload: 'Upload failed', + dragZone: 'Drop files here to upload', + dropProcessing: 'Processing dropped files...', + formatProgress: "{percent}% of {total_size}", + waitingForResponse: "Processing..." + }, + template: '
' + + ((!this._options.dragAndDrop || !this._options.dragAndDrop.disableDefaultDropzone) ? '
{dragZoneText}
' : '') + + (!this._options.button ? '
{uploadButtonText}
' : '') + + '{dropProcessingText}' + + (!this._options.listElement ? '
    ' : '') + + '
    ', + + // template for one item in file list + fileTemplate: '
  • ' + + '
    ' + + '' + + '' + + '' + + '' + + '{cancelButtonText}' + + '{retryButtonText}' + + '{deleteButtonText}' + + '{statusText}' + + '
  • ', + classes: { + button: 'qq-upload-button', + drop: 'qq-upload-drop-area', + dropActive: 'qq-upload-drop-area-active', + dropDisabled: 'qq-upload-drop-area-disabled', + list: 'qq-upload-list', + progressBar: 'qq-progress-bar', + file: 'qq-upload-file', + spinner: 'qq-upload-spinner', + finished: 'qq-upload-finished', + retrying: 'qq-upload-retrying', + retryable: 'qq-upload-retryable', + size: 'qq-upload-size', + cancel: 'qq-upload-cancel', + deleteButton: 'qq-upload-delete', + retry: 'qq-upload-retry', + statusText: 'qq-upload-status-text', + + success: 'qq-upload-success', + fail: 'qq-upload-fail', + + successIcon: null, + failIcon: null, + + dropProcessing: 'qq-drop-processing', + dropProcessingSpinner: 'qq-drop-processing-spinner' + }, + failedUploadTextDisplay: { + mode: 'default', //default, custom, or none + maxChars: 50, + responseProperty: 'error', + enableTooltip: true + }, + messages: { + tooManyFilesError: "You may only drop one file" + }, + retry: { + showAutoRetryNote: true, + autoRetryNote: "Retrying {retryNum}/{maxAuto}...", + showButton: false + }, + deleteFile: { + forceConfirm: false, + confirmMessage: "Are you sure you want to delete {filename}?", + deletingStatusText: "Deleting...", + deletingFailedText: "Delete failed" + + }, + display: { + fileSizeOnSubmit: false + }, + showMessage: function(message){ + setTimeout(function() { + alert(message); + }, 0); + }, + showConfirm: function(message, okCallback, cancelCallback) { + setTimeout(function() { + var result = confirm(message); + if (result) { + okCallback(); + } + else if (cancelCallback) { + cancelCallback(); + } + }, 0); + } + }, true); + + // overwrite options with user supplied + qq.extend(this._options, o, true); + this._wrapCallbacks(); + + // overwrite the upload button text if any + // same for the Cancel button and Fail message text + this._options.template = this._options.template.replace(/\{dragZoneText\}/g, this._options.text.dragZone); + this._options.template = this._options.template.replace(/\{uploadButtonText\}/g, this._options.text.uploadButton); + this._options.template = this._options.template.replace(/\{dropProcessingText\}/g, this._options.text.dropProcessing); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{cancelButtonText\}/g, this._options.text.cancelButton); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{retryButtonText\}/g, this._options.text.retryButton); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{deleteButtonText\}/g, this._options.text.deleteButton); + this._options.fileTemplate = this._options.fileTemplate.replace(/\{statusText\}/g, ""); + + this._element = this._options.element; + this._element.innerHTML = this._options.template; + this._listElement = this._options.listElement || this._find(this._element, 'list'); + + this._classes = this._options.classes; + + if (!this._button) { + this._button = this._createUploadButton(this._find(this._element, 'button')); + } + + this._bindCancelAndRetryEvents(); + + this._dnd = this._setupDragAndDrop(); +}; + +// inherit from Basic Uploader +qq.extend(qq.FineUploader.prototype, qq.FineUploaderBasic.prototype); + +qq.extend(qq.FineUploader.prototype, { + clearStoredFiles: function() { + qq.FineUploaderBasic.prototype.clearStoredFiles.apply(this, arguments); + this._listElement.innerHTML = ""; + }, + addExtraDropzone: function(element){ + this._dnd.setupExtraDropzone(element); + }, + removeExtraDropzone: function(element){ + return this._dnd.removeExtraDropzone(element); + }, + getItemByFileId: function(id){ + var item = this._listElement.firstChild; + + // there can't be txt nodes in dynamically created list + // and we can use nextSibling + while (item){ + if (item.qqFileId == id) return item; + item = item.nextSibling; + } + }, + reset: function() { + qq.FineUploaderBasic.prototype.reset.apply(this, arguments); + this._element.innerHTML = this._options.template; + this._listElement = this._options.listElement || this._find(this._element, 'list'); + if (!this._options.button) { + this._button = this._createUploadButton(this._find(this._element, 'button')); + } + this._bindCancelAndRetryEvents(); + this._dnd.dispose(); + this._dnd = this._setupDragAndDrop(); + }, + _removeFileItem: function(fileId) { + var item = this.getItemByFileId(fileId); + qq(item).remove(); + }, + _setupDragAndDrop: function() { + var self = this, + dropProcessingEl = this._find(this._element, 'dropProcessing'), + dnd, preventSelectFiles, defaultDropAreaEl; + + preventSelectFiles = function(event) { + event.preventDefault(); + }; + + if (!this._options.dragAndDrop.disableDefaultDropzone) { + defaultDropAreaEl = this._find(this._options.element, 'drop'); + } + + dnd = new qq.DragAndDrop({ + dropArea: defaultDropAreaEl, + extraDropzones: this._options.dragAndDrop.extraDropzones, + hideDropzones: this._options.dragAndDrop.hideDropzones, + multiple: this._options.multiple, + classes: { + dropActive: this._options.classes.dropActive + }, + callbacks: { + dropProcessing: function(isProcessing, files) { + var input = self._button.getInput(); + + if (isProcessing) { + qq(dropProcessingEl).css({display: 'block'}); + qq(input).attach('click', preventSelectFiles); + } + else { + qq(dropProcessingEl).hide(); + qq(input).detach('click', preventSelectFiles); + } + + if (files) { + self.addFiles(files); + } + }, + error: function(code, filename) { + self._error(code, filename); + }, + log: function(message, level) { + self.log(message, level); + } + } + }); + + dnd.setup(); + + return dnd; + }, + _leaving_document_out: function(e){ + return ((qq.chrome() || (qq.safari() && qq.windows())) && e.clientX == 0 && e.clientY == 0) // null coords for Chrome and Safari Windows + || (qq.firefox() && !e.relatedTarget); // null e.relatedTarget for Firefox + }, + _storeForLater: function(id) { + qq.FineUploaderBasic.prototype._storeForLater.apply(this, arguments); + var item = this.getItemByFileId(id); + qq(this._find(item, 'spinner')).hide(); + }, + /** + * Gets one of the elements listed in this._options.classes + **/ + _find: function(parent, type){ + var element = qq(parent).getByClass(this._options.classes[type])[0]; + if (!element){ + throw new Error('element not found ' + type); + } + + return element; + }, + _onSubmit: function(id, name){ + qq.FineUploaderBasic.prototype._onSubmit.apply(this, arguments); + this._addToList(id, name); + }, + // Update the progress bar & percentage as the file is uploaded + _onProgress: function(id, name, loaded, total){ + qq.FineUploaderBasic.prototype._onProgress.apply(this, arguments); + + var item, progressBar, percent, cancelLink; + + item = this.getItemByFileId(id); + progressBar = this._find(item, 'progressBar'); + percent = Math.round(loaded / total * 100); + + if (loaded === total) { + cancelLink = this._find(item, 'cancel'); + qq(cancelLink).hide(); + + qq(progressBar).hide(); + qq(this._find(item, 'statusText')).setText(this._options.text.waitingForResponse); + + // If last byte was sent, display total file size + this._displayFileSize(id); + } + else { + // If still uploading, display percentage - total size is actually the total request(s) size + this._displayFileSize(id, loaded, total); + + qq(progressBar).css({display: 'block'}); + } + + // Update progress bar element + qq(progressBar).css({width: percent + '%'}); + }, + _onComplete: function(id, name, result, xhr){ + qq.FineUploaderBasic.prototype._onComplete.apply(this, arguments); + + var item = this.getItemByFileId(id); + + qq(this._find(item, 'statusText')).clearText(); + + qq(item).removeClass(this._classes.retrying); + qq(this._find(item, 'progressBar')).hide(); + + if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) { + qq(this._find(item, 'cancel')).hide(); + } + qq(this._find(item, 'spinner')).hide(); + + if (result.success) { + if (this._isDeletePossible()) { + this._showDeleteLink(id); + } + + qq(item).addClass(this._classes.success); + if (this._classes.successIcon) { + this._find(item, 'finished').style.display = "inline-block"; + qq(item).addClass(this._classes.successIcon); + } + } else { + qq(item).addClass(this._classes.fail); + if (this._classes.failIcon) { + this._find(item, 'finished').style.display = "inline-block"; + qq(item).addClass(this._classes.failIcon); + } + if (this._options.retry.showButton && !this._preventRetries[id]) { + qq(item).addClass(this._classes.retryable); + } + this._controlFailureTextDisplay(item, result); + } + }, + _onUpload: function(id, name){ + qq.FineUploaderBasic.prototype._onUpload.apply(this, arguments); + + this._showSpinner(id); + }, + _onCancel: function(id, name) { + qq.FineUploaderBasic.prototype._onCancel.apply(this, arguments); + this._removeFileItem(id); + }, + _onBeforeAutoRetry: function(id) { + var item, progressBar, failTextEl, retryNumForDisplay, maxAuto, retryNote; + + qq.FineUploaderBasic.prototype._onBeforeAutoRetry.apply(this, arguments); + + item = this.getItemByFileId(id); + progressBar = this._find(item, 'progressBar'); + + this._showCancelLink(item); + progressBar.style.width = 0; + qq(progressBar).hide(); + + if (this._options.retry.showAutoRetryNote) { + failTextEl = this._find(item, 'statusText'); + retryNumForDisplay = this._autoRetries[id] + 1; + maxAuto = this._options.retry.maxAutoAttempts; + + retryNote = this._options.retry.autoRetryNote.replace(/\{retryNum\}/g, retryNumForDisplay); + retryNote = retryNote.replace(/\{maxAuto\}/g, maxAuto); + + qq(failTextEl).setText(retryNote); + if (retryNumForDisplay === 1) { + qq(item).addClass(this._classes.retrying); + } + } + }, + //return false if we should not attempt the requested retry + _onBeforeManualRetry: function(id) { + if (qq.FineUploaderBasic.prototype._onBeforeManualRetry.apply(this, arguments)) { + var item = this.getItemByFileId(id); + this._find(item, 'progressBar').style.width = 0; + qq(item).removeClass(this._classes.fail); + qq(this._find(item, 'statusText')).clearText(); + this._showSpinner(id); + this._showCancelLink(item); + return true; + } + return false; + }, + _onSubmitDelete: function(id) { + if (this._isDeletePossible()) { + if (this._options.callbacks.onSubmitDelete(id) !== false) { + if (this._options.deleteFile.forceConfirm) { + this._showDeleteConfirm(id); + } + else { + this._sendDeleteRequest(id); + } + } + } + else { + this.log("Delete request ignored for file ID " + id + ", delete feature is disabled.", "warn"); + return false; + } + }, + _onDeleteComplete: function(id, xhr, isError) { + qq.FineUploaderBasic.prototype._onDeleteComplete.apply(this, arguments); + + var item = this.getItemByFileId(id), + spinnerEl = this._find(item, 'spinner'), + statusTextEl = this._find(item, 'statusText'); + + qq(spinnerEl).hide(); + + if (isError) { + qq(statusTextEl).setText(this._options.deleteFile.deletingFailedText); + this._showDeleteLink(id); + } + else { + this._removeFileItem(id); + } + }, + _sendDeleteRequest: function(id) { + var item = this.getItemByFileId(id), + deleteLink = this._find(item, 'deleteButton'), + statusTextEl = this._find(item, 'statusText'); + + qq(deleteLink).hide(); + this._showSpinner(id); + qq(statusTextEl).setText(this._options.deleteFile.deletingStatusText); + this._deleteHandler.sendDelete(id, this.getUuid(id)); + }, + _showDeleteConfirm: function(id) { + var fileName = this._handler.getName(id), + confirmMessage = this._options.deleteFile.confirmMessage.replace(/\{filename\}/g, fileName), + uuid = this.getUuid(id), + self = this; + + this._options.showConfirm(confirmMessage, function() { + self._sendDeleteRequest(id); + }); + }, + _addToList: function(id, name){ + var item = qq.toElement(this._options.fileTemplate); + if (this._options.disableCancelForFormUploads && !qq.isXhrUploadSupported()) { + var cancelLink = this._find(item, 'cancel'); + qq(cancelLink).remove(); + } + + item.qqFileId = id; + + var fileElement = this._find(item, 'file'); + qq(fileElement).setText(this._options.formatFileName(name)); + qq(this._find(item, 'size')).hide(); + if (!this._options.multiple) { + this._handler.cancelAll(); + this._clearList(); + } + + this._listElement.appendChild(item); + + if (this._options.display.fileSizeOnSubmit && qq.isXhrUploadSupported()) { + this._displayFileSize(id); + } + }, + _clearList: function(){ + this._listElement.innerHTML = ''; + this.clearStoredFiles(); + }, + _displayFileSize: function(id, loadedSize, totalSize) { + var item = this.getItemByFileId(id), + size = this.getSize(id), + sizeForDisplay = this._formatSize(size), + sizeEl = this._find(item, 'size'); + + if (loadedSize !== undefined && totalSize !== undefined) { + sizeForDisplay = this._formatProgress(loadedSize, totalSize); + } + + qq(sizeEl).css({display: 'inline'}); + qq(sizeEl).setText(sizeForDisplay); + }, + /** + * delegate click event for cancel & retry links + **/ + _bindCancelAndRetryEvents: function(){ + var self = this, + list = this._listElement; + + this._disposeSupport.attach(list, 'click', function(e){ + e = e || window.event; + var target = e.target || e.srcElement; + + if (qq(target).hasClass(self._classes.cancel) || qq(target).hasClass(self._classes.retry) || qq(target).hasClass(self._classes.deleteButton)){ + qq.preventDefault(e); + + var item = target.parentNode; + while(item.qqFileId === undefined) { + item = target = target.parentNode; + } + + if (qq(target).hasClass(self._classes.deleteButton)) { + self.deleteFile(item.qqFileId); + } + else if (qq(target).hasClass(self._classes.cancel)) { + self.cancel(item.qqFileId); + } + else { + qq(item).removeClass(self._classes.retryable); + self.retry(item.qqFileId); + } + } + }); + }, + _formatProgress: function (uploadedSize, totalSize) { + var message = this._options.text.formatProgress; + function r(name, replacement) { message = message.replace(name, replacement); } + + r('{percent}', Math.round(uploadedSize / totalSize * 100)); + r('{total_size}', this._formatSize(totalSize)); + return message; + }, + _controlFailureTextDisplay: function(item, response) { + var mode, maxChars, responseProperty, failureReason, shortFailureReason; + + mode = this._options.failedUploadTextDisplay.mode; + maxChars = this._options.failedUploadTextDisplay.maxChars; + responseProperty = this._options.failedUploadTextDisplay.responseProperty; + + if (mode === 'custom') { + failureReason = response[responseProperty]; + if (failureReason) { + if (failureReason.length > maxChars) { + shortFailureReason = failureReason.substring(0, maxChars) + '...'; + } + } + else { + failureReason = this._options.text.failUpload; + this.log("'" + responseProperty + "' is not a valid property on the server response.", 'warn'); + } + + qq(this._find(item, 'statusText')).setText(shortFailureReason || failureReason); + + if (this._options.failedUploadTextDisplay.enableTooltip) { + this._showTooltip(item, failureReason); + } + } + else if (mode === 'default') { + qq(this._find(item, 'statusText')).setText(this._options.text.failUpload); + } + else if (mode !== 'none') { + this.log("failedUploadTextDisplay.mode value of '" + mode + "' is not valid", 'warn'); + } + }, + _showTooltip: function(item, text) { + item.title = text; + }, + _showSpinner: function(id) { + var item = this.getItemByFileId(id), + spinnerEl = this._find(item, 'spinner'); + + spinnerEl.style.display = "inline-block"; + }, + _showCancelLink: function(item) { + if (!this._options.disableCancelForFormUploads || qq.isXhrUploadSupported()) { + var cancelLink = this._find(item, 'cancel'); + + qq(cancelLink).css({display: 'inline'}); + } + }, + _showDeleteLink: function(id) { + var item = this.getItemByFileId(id), + deleteLink = this._find(item, 'deleteButton'); + + qq(deleteLink).css({display: 'inline'}); + }, + _error: function(code, name){ + var message = qq.FineUploaderBasic.prototype._error.apply(this, arguments); + this._options.showMessage(message); + } +}); +/** Generic class for sending non-upload ajax requests and handling the associated responses **/ +//TODO Use XDomainRequest if expectCors = true. Not necessary now since only DELETE requests are sent and XDR doesn't support pre-flighting. +/*globals qq, XMLHttpRequest*/ +qq.AjaxRequestor = function(o) { + "use strict"; + + var log, shouldParamsBeInQueryString, + queue = [], + requestState = [], + options = { + method: 'POST', + maxConnections: 3, + customHeaders: {}, + endpointStore: {}, + paramsStore: {}, + successfulResponseCodes: [200], + demoMode: false, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onSend: function(id) {}, + onComplete: function(id, xhr, isError) {}, + onCancel: function(id) {} + }; + + qq.extend(options, o); + log = options.log; + shouldParamsBeInQueryString = getMethod() === 'GET' || getMethod() === 'DELETE'; + + + /** + * Removes element from queue, sends next request + */ + function dequeue(id) { + var i = qq.indexOf(queue, id), + max = options.maxConnections, + nextId; + + delete requestState[id]; + queue.splice(i, 1); + + if (queue.length >= max && i < max){ + nextId = queue[max-1]; + sendRequest(nextId); + } + } + + function onComplete(id) { + var xhr = requestState[id].xhr, + method = getMethod(), + isError = false; + + dequeue(id); + + if (!isResponseSuccessful(xhr.status)) { + isError = true; + log(method + " request for " + id + " has failed - response code " + xhr.status, "error"); + } + + options.onComplete(id, xhr, isError); + } + + function sendRequest(id) { + var xhr = new XMLHttpRequest(), + method = getMethod(), + params = {}, + url; + + options.onSend(id); + + if (options.paramsStore.getParams) { + params = options.paramsStore.getParams(id); + } + + url = createUrl(id, params); + + requestState[id].xhr = xhr; + xhr.onreadystatechange = getReadyStateChangeHandler(id); + xhr.open(method, url, true); + + if (options.cors.expected && options.cors.sendCredentials) { + xhr.withCredentials = true; + } + + setHeaders(id); + + log('Sending ' + method + " request for " + id); + if (!shouldParamsBeInQueryString && params) { + xhr.send(qq.obj2url(params, "")); + } + else { + xhr.send(); + } + } + + function createUrl(id, params) { + var endpoint = options.endpointStore.getEndpoint(id), + addToPath = requestState[id].addToPath; + + if (addToPath !== undefined) { + endpoint += "/" + addToPath; + } + + if (shouldParamsBeInQueryString && params) { + return qq.obj2url(params, endpoint); + } + else { + return endpoint; + } + } + + function getReadyStateChangeHandler(id) { + var xhr = requestState[id].xhr; + + return function() { + if (xhr.readyState === 4) { + onComplete(id, xhr); + } + }; + } + + function setHeaders(id) { + var xhr = requestState[id].xhr, + customHeaders = options.customHeaders; + + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + + qq.each(customHeaders, function(name, val) { + xhr.setRequestHeader(name, val); + }); + } + + function cancelRequest(id) { + var xhr = requestState[id].xhr, + method = getMethod(); + + if (xhr) { + xhr.onreadystatechange = null; + xhr.abort(); + dequeue(id); + + log('Cancelled ' + method + " for " + id); + options.onCancel(id); + + return true; + } + + return false; + } + + function isResponseSuccessful(responseCode) { + return qq.indexOf(options.successfulResponseCodes, responseCode) >= 0; + } + + function getMethod() { + if (options.demoMode) { + return "GET"; + } + + return options.method; + } + + + return { + send: function(id, addToPath) { + requestState[id] = { + addToPath: addToPath + }; + + var len = queue.push(id); + + // if too many active connections, wait... + if (len <= options.maxConnections){ + sendRequest(id); + } + }, + cancel: function(id) { + return cancelRequest(id); + } + }; +}; +/** Generic class for sending non-upload ajax requests and handling the associated responses **/ +/*globals qq, XMLHttpRequest*/ +qq.DeleteFileAjaxRequestor = function(o) { + "use strict"; + + var requestor, + options = { + endpointStore: {}, + maxConnections: 3, + customHeaders: {}, + paramsStore: {}, + demoMode: false, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onDelete: function(id) {}, + onDeleteComplete: function(id, xhr, isError) {} + }; + + qq.extend(options, o); + + requestor = new qq.AjaxRequestor({ + method: 'DELETE', + endpointStore: options.endpointStore, + paramsStore: options.paramsStore, + maxConnections: options.maxConnections, + customHeaders: options.customHeaders, + successfulResponseCodes: [200, 202, 204], + demoMode: options.demoMode, + log: options.log, + onSend: options.onDelete, + onComplete: options.onDeleteComplete + }); + + + return { + sendDelete: function(id, uuid) { + requestor.send(id, uuid); + options.log("Submitted delete file request for " + id); + } + }; +}; +qq.WindowReceiveMessage = function(o) { + var options = { + log: function(message, level) {} + }, + callbackWrapperDetachers = {}; + + qq.extend(options, o); + + return { + receiveMessage : function(id, callback) { + var onMessageCallbackWrapper = function(event) { + callback(event.data); + }; + + if (window.postMessage) { + callbackWrapperDetachers[id] = qq(window).attach("message", onMessageCallbackWrapper); + } + else { + log("iframe message passing not supported in this browser!", "error"); + } + }, + + stopReceivingMessages : function(id) { + if (window.postMessage) { + var detacher = callbackWrapperDetachers[id]; + if (detacher) { + detacher(); + } + } + } + }; +}; +/** + * Class for uploading files, uploading itself is handled by child classes + */ +/*globals qq*/ +qq.UploadHandler = function(o) { + "use strict"; + + var queue = [], + options, log, dequeue, handlerImpl; + + // Default options, can be overridden by the user + options = { + debug: false, + forceMultipart: true, + paramsInBody: false, + paramsStore: {}, + endpointStore: {}, + cors: { + expected: false, + sendCredentials: false + }, + maxConnections: 3, // maximum number of concurrent uploads + uuidParamName: 'qquuid', + totalFileSizeParamName: 'qqtotalfilesize', + chunking: { + enabled: false, + partSize: 2000000, //bytes + paramNames: { + partIndex: 'qqpartindex', + partByteOffset: 'qqpartbyteoffset', + chunkSize: 'qqchunksize', + totalParts: 'qqtotalparts', + filename: 'qqfilename' + } + }, + resume: { + enabled: false, + id: null, + cookiesExpireIn: 7, //days + paramNames: { + resuming: "qqresume" + } + }, + blobs: { + paramNames: { + name: 'qqblobname' + } + }, + log: function(str, level) {}, + onProgress: function(id, fileName, loaded, total){}, + onComplete: function(id, fileName, response, xhr){}, + onCancel: function(id, fileName){}, + onUpload: function(id, fileName){}, + onUploadChunk: function(id, fileName, chunkData){}, + onAutoRetry: function(id, fileName, response, xhr){}, + onResume: function(id, fileName, chunkData){} + + }; + qq.extend(options, o); + + log = options.log; + + /** + * Removes element from queue, starts upload of next + */ + dequeue = function(id) { + var i = qq.indexOf(queue, id), + max = options.maxConnections, + nextId; + + if (i >= 0) { + queue.splice(i, 1); + + if (queue.length >= max && i < max){ + nextId = queue[max-1]; + handlerImpl.upload(nextId); + } + } + }; + + if (qq.isXhrUploadSupported()) { + handlerImpl = new qq.UploadHandlerXhr(options, dequeue, log); + } + else { + handlerImpl = new qq.UploadHandlerForm(options, dequeue, log); + } + + + return { + /** + * Adds file or file input to the queue + * @returns id + **/ + add: function(file){ + return handlerImpl.add(file); + }, + /** + * Sends the file identified by id + */ + upload: function(id){ + var len = queue.push(id); + + // if too many active uploads, wait... + if (len <= options.maxConnections){ + return handlerImpl.upload(id); + } + }, + retry: function(id) { + var i = qq.indexOf(queue, id); + if (i >= 0) { + return handlerImpl.upload(id, true); + } + else { + return this.upload(id); + } + }, + /** + * Cancels file upload by id + */ + cancel: function(id) { + log('Cancelling ' + id); + options.paramsStore.remove(id); + handlerImpl.cancel(id); + dequeue(id); + }, + /** + * Cancels all queued or in-progress uploads + */ + cancelAll: function() { + var self = this, + queueCopy = []; + + qq.extend(queueCopy, queue); + qq.each(queueCopy, function(idx, fileId) { + self.cancel(fileId); + }); + + queue = []; + }, + /** + * Returns name of the file identified by id + */ + getName: function(id){ + return handlerImpl.getName(id); + }, + /** + * Returns size of the file identified by id + */ + getSize: function(id){ + if (handlerImpl.getSize) { + return handlerImpl.getSize(id); + } + }, + getFile: function(id) { + if (handlerImpl.getFile) { + return handlerImpl.getFile(id); + } + }, + /** + * Returns id of files being uploaded or + * waiting for their turn + */ + getQueue: function(){ + return queue; + }, + reset: function() { + log('Resetting upload handler'); + queue = []; + handlerImpl.reset(); + }, + getUuid: function(id) { + return handlerImpl.getUuid(id); + }, + /** + * Determine if the file exists. + */ + isValid: function(id) { + return handlerImpl.isValid(id); + }, + getResumableFilesData: function() { + if (handlerImpl.getResumableFilesData) { + return handlerImpl.getResumableFilesData(); + } + return []; + } + }; +}; +/*globals qq, document, setTimeout*/ +/*globals clearTimeout*/ +qq.UploadHandlerForm = function(o, uploadCompleteCallback, logCallback) { + "use strict"; + + var options = o, + inputs = [], + uuids = [], + detachLoadEvents = {}, + postMessageCallbackTimers = {}, + uploadComplete = uploadCompleteCallback, + log = logCallback, + corsMessageReceiver = new qq.WindowReceiveMessage({log: log}), + onloadCallbacks = {}, + api; + + + function detachLoadEvent(id) { + if (detachLoadEvents[id] !== undefined) { + detachLoadEvents[id](); + delete detachLoadEvents[id]; + } + } + + function registerPostMessageCallback(iframe, callback) { + var id = iframe.id; + + onloadCallbacks[uuids[id]] = callback; + + detachLoadEvents[id] = qq(iframe).attach('load', function() { + if (inputs[id]) { + log("Received iframe load event for CORS upload request (file id " + id + ")"); + + postMessageCallbackTimers[id] = setTimeout(function() { + var errorMessage = "No valid message received from loaded iframe for file id " + id; + log(errorMessage, "error"); + callback({ + error: errorMessage + }); + }, 1000); + } + }); + + corsMessageReceiver.receiveMessage(id, function(message) { + log("Received the following window message: '" + message + "'"); + var response = qq.parseJson(message), + uuid = response.uuid, + onloadCallback; + + if (uuid && onloadCallbacks[uuid]) { + clearTimeout(postMessageCallbackTimers[id]); + delete postMessageCallbackTimers[id]; + + detachLoadEvent(id); + + onloadCallback = onloadCallbacks[uuid]; + + delete onloadCallbacks[uuid]; + corsMessageReceiver.stopReceivingMessages(id); + onloadCallback(response); + } + else if (!uuid) { + log("'" + message + "' does not contain a UUID - ignoring."); + } + }); + } + + function attachLoadEvent(iframe, callback) { + /*jslint eqeq: true*/ + + if (options.cors.expected) { + registerPostMessageCallback(iframe, callback); + } + else { + detachLoadEvents[iframe.id] = qq(iframe).attach('load', function(){ + log('Received response for ' + iframe.id); + + // when we remove iframe from dom + // the request stops, but in IE load + // event fires + if (!iframe.parentNode){ + return; + } + + try { + // fixing Opera 10.53 + if (iframe.contentDocument && + iframe.contentDocument.body && + iframe.contentDocument.body.innerHTML == "false"){ + // In Opera event is fired second time + // when body.innerHTML changed from false + // to server response approx. after 1 sec + // when we upload file with iframe + return; + } + } + catch (error) { + //IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases + log('Error when attempting to access iframe during handling of upload response (' + error + ")", 'error'); + } + + callback(); + }); + } + } + + /** + * Returns json object received by iframe from server. + */ + function getIframeContentJson(iframe) { + /*jshint evil: true*/ + + var response; + + //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 + var 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(/^
    ');
    +
    +        iframe.setAttribute('id', id);
    +
    +        iframe.style.display = 'none';
    +        document.body.appendChild(iframe);
    +
    +        return iframe;
    +    }
    +
    +    /**
    +     * Creates form, that will be submitted to iframe
    +     */
    +    function createForm(id, iframe){
    +        var params = options.paramsStore.getParams(id),
    +            protocol = options.demoMode ? "GET" : "POST",
    +            form = qq.toElement('
    '), + endpoint = options.endpointStore.getEndpoint(id), + url = endpoint; + + params[options.uuidParamName] = uuids[id]; + + if (!options.paramsInBody) { + url = qq.obj2url(params, endpoint); + } + else { + qq.obj2Inputs(params, form); + } + + form.setAttribute('action', url); + form.setAttribute('target', iframe.name); + form.style.display = 'none'; + document.body.appendChild(form); + + return form; + } + + + api = { + add: function(fileInput) { + fileInput.setAttribute('name', options.inputName); + + var id = inputs.push(fileInput) - 1; + uuids[id] = qq.getUniqueId(); + + // remove file input from DOM + if (fileInput.parentNode){ + qq(fileInput).remove(); + } + + return id; + }, + getName: function(id) { + /*jslint regexp: true*/ + + // get input value and remove path to normalize + return inputs[id].value.replace(/.*(\/|\\)/, ""); + }, + isValid: function(id) { + return inputs[id] !== undefined; + }, + reset: function() { + qq.UploadHandler.prototype.reset.apply(this, arguments); + inputs = []; + uuids = []; + detachLoadEvents = {}; + }, + getUuid: function(id) { + return uuids[id]; + }, + cancel: function(id) { + options.onCancel(id, this.getName(id)); + + delete inputs[id]; + delete uuids[id]; + delete detachLoadEvents[id]; + + if (options.cors.expected) { + clearTimeout(postMessageCallbackTimers[id]); + delete postMessageCallbackTimers[id]; + corsMessageReceiver.stopReceivingMessages(id); + } + + var iframe = document.getElementById(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 + iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); //deal with "JSLint: javascript URL" warning, which apparently cannot be turned off + + qq(iframe).remove(); + } + }, + upload: function(id){ + var input = inputs[id], + fileName = api.getName(id), + iframe = createIframe(id), + form; + + if (!input){ + throw new Error('file with passed id was not added, or already uploaded or cancelled'); + } + + options.onUpload(id, this.getName(id)); + + form = createForm(id, iframe); + form.appendChild(input); + + attachLoadEvent(iframe, function(responseFromMessage){ + log('iframe loaded'); + + var response = responseFromMessage ? responseFromMessage : getIframeContentJson(iframe); + + detachLoadEvent(id); + + //we can't remove an iframe if the iframe doesn't belong to the same domain + if (!options.cors.expected) { + qq(iframe).remove(); + } + + if (!response.success) { + if (options.onAutoRetry(id, fileName, response)) { + return; + } + } + options.onComplete(id, fileName, response); + uploadComplete(id); + }); + + log('Sending upload request for ' + id); + form.submit(); + qq(form).remove(); + + return id; + } + }; + + return api; +}; +/*globals qq, File, XMLHttpRequest, FormData, Blob*/ +qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) { + "use strict"; + + var options = o, + uploadComplete = uploadCompleteCallback, + log = logCallback, + fileState = [], + cookieItemDelimiter = "|", + chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(), + resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(), + resumeId = getResumeId(), + multipart = options.forceMultipart || options.paramsInBody, + api; + + + function addChunkingSpecificParams(id, params, chunkData) { + var size = api.getSize(id), + name = api.getName(id); + + params[options.chunking.paramNames.partIndex] = chunkData.part; + params[options.chunking.paramNames.partByteOffset] = chunkData.start; + params[options.chunking.paramNames.chunkSize] = chunkData.size; + params[options.chunking.paramNames.totalParts] = chunkData.count; + params[options.totalFileSizeParamName] = size; + + /** + * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob" + * or an empty string. So, we will need to include the actual file name as a param in this case. + */ + if (multipart) { + params[options.chunking.paramNames.filename] = name; + } + } + + function addResumeSpecificParams(params) { + params[options.resume.paramNames.resuming] = true; + } + + function getChunk(fileOrBlob, startByte, endByte) { + if (fileOrBlob.slice) { + return fileOrBlob.slice(startByte, endByte); + } + else if (fileOrBlob.mozSlice) { + return fileOrBlob.mozSlice(startByte, endByte); + } + else if (fileOrBlob.webkitSlice) { + return fileOrBlob.webkitSlice(startByte, endByte); + } + } + + function getChunkData(id, chunkIndex) { + var chunkSize = options.chunking.partSize, + fileSize = api.getSize(id), + fileOrBlob = fileState[id].file || fileState[id].blobData.blob, + startBytes = chunkSize * chunkIndex, + endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize, + totalChunks = getTotalChunks(id); + + return { + part: chunkIndex, + start: startBytes, + end: endBytes, + count: totalChunks, + blob: getChunk(fileOrBlob, startBytes, endBytes), + size: endBytes - startBytes + }; + } + + function getTotalChunks(id) { + var fileSize = api.getSize(id), + chunkSize = options.chunking.partSize; + + return Math.ceil(fileSize / chunkSize); + } + + function createXhr(id) { + var xhr = new XMLHttpRequest(); + + fileState[id].xhr = xhr; + + return xhr; + } + + function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) { + var formData = new FormData(), + method = options.demoMode ? "GET" : "POST", + endpoint = options.endpointStore.getEndpoint(id), + url = endpoint, + name = api.getName(id), + size = api.getSize(id), + blobData = fileState[id].blobData; + + params[options.uuidParamName] = fileState[id].uuid; + + if (multipart) { + params[options.totalFileSizeParamName] = size; + + if (blobData) { + /** + * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob" + * or an empty string. So, we will need to include the actual file name as a param in this case. + */ + params[options.blobs.paramNames.name] = blobData.name; + } + } + + //build query string + if (!options.paramsInBody) { + if (!multipart) { + params[options.inputName] = name; + } + url = qq.obj2url(params, endpoint); + } + + xhr.open(method, url, true); + + if (options.cors.expected && options.cors.sendCredentials) { + xhr.withCredentials = true; + } + + if (multipart) { + if (options.paramsInBody) { + qq.obj2FormData(params, formData); + } + + formData.append(options.inputName, fileOrBlob); + return formData; + } + + return fileOrBlob; + } + + function setHeaders(id, xhr) { + var extraHeaders = options.customHeaders, + fileOrBlob = fileState[id].file || fileState[id].blobData.blob; + + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + + if (!multipart) { + xhr.setRequestHeader("Content-Type", "application/octet-stream"); + //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2 + xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type); + } + + qq.each(extraHeaders, function(name, val) { + xhr.setRequestHeader(name, val); + }); + } + + function handleCompletedItem(id, response, xhr) { + var name = api.getName(id), + size = api.getSize(id); + + fileState[id].attemptingResume = false; + + options.onProgress(id, name, size, size); + + options.onComplete(id, name, response, xhr); + delete fileState[id].xhr; + uploadComplete(id); + } + + function uploadNextChunk(id) { + var chunkIdx = fileState[id].remainingChunkIdxs[0], + chunkData = getChunkData(id, chunkIdx), + xhr = createXhr(id), + size = api.getSize(id), + name = api.getName(id), + toSend, params; + + if (fileState[id].loaded === undefined) { + fileState[id].loaded = 0; + } + + if (resumeEnabled && fileState[id].file) { + persistChunkData(id, chunkData); + } + + xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr); + + xhr.upload.onprogress = function(e) { + if (e.lengthComputable) { + var totalLoaded = e.loaded + fileState[id].loaded, + estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total); + + options.onProgress(id, name, totalLoaded, estTotalRequestsSize); + } + }; + + options.onUploadChunk(id, name, getChunkDataForCallback(chunkData)); + + params = options.paramsStore.getParams(id); + addChunkingSpecificParams(id, params, chunkData); + + if (fileState[id].attemptingResume) { + addResumeSpecificParams(params); + } + + toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id); + setHeaders(id, xhr); + + log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size); + xhr.send(toSend); + } + + function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) { + var chunkData = getChunkData(id, chunkIdx), + blobSize = chunkData.size, + overhead = requestSize - blobSize, + size = api.getSize(id), + chunkCount = chunkData.count, + initialRequestOverhead = fileState[id].initialRequestOverhead, + overheadDiff = overhead - initialRequestOverhead; + + fileState[id].lastRequestOverhead = overhead; + + if (chunkIdx === 0) { + fileState[id].lastChunkIdxProgress = 0; + fileState[id].initialRequestOverhead = overhead; + fileState[id].estTotalRequestsSize = size + (chunkCount * overhead); + } + else if (fileState[id].lastChunkIdxProgress !== chunkIdx) { + fileState[id].lastChunkIdxProgress = chunkIdx; + fileState[id].estTotalRequestsSize += overheadDiff; + } + + return fileState[id].estTotalRequestsSize; + } + + function getLastRequestOverhead(id) { + if (multipart) { + return fileState[id].lastRequestOverhead; + } + else { + return 0; + } + } + + function handleSuccessfullyCompletedChunk(id, response, xhr) { + var chunkIdx = fileState[id].remainingChunkIdxs.shift(), + chunkData = getChunkData(id, chunkIdx); + + fileState[id].attemptingResume = false; + fileState[id].loaded += chunkData.size + getLastRequestOverhead(id); + + if (fileState[id].remainingChunkIdxs.length > 0) { + uploadNextChunk(id); + } + else { + if (resumeEnabled) { + deletePersistedChunkData(id); + } + + handleCompletedItem(id, response, xhr); + } + } + + function isErrorResponse(xhr, response) { + return xhr.status !== 200 || !response.success || response.reset; + } + + function parseResponse(xhr) { + var response; + + try { + response = qq.parseJson(xhr.responseText); + } + catch(error) { + log('Error when attempting to parse xhr response text (' + error + ')', 'error'); + response = {}; + } + + return response; + } + + function handleResetResponse(id) { + log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error'); + + if (resumeEnabled) { + deletePersistedChunkData(id); + fileState[id].attemptingResume = false; + } + + fileState[id].remainingChunkIdxs = []; + delete fileState[id].loaded; + delete fileState[id].estTotalRequestsSize; + delete fileState[id].initialRequestOverhead; + } + + function handleResetResponseOnResumeAttempt(id) { + fileState[id].attemptingResume = false; + log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error'); + handleResetResponse(id); + api.upload(id, true); + } + + function handleNonResetErrorResponse(id, response, xhr) { + var name = api.getName(id); + + if (options.onAutoRetry(id, name, response, xhr)) { + return; + } + else { + handleCompletedItem(id, response, xhr); + } + } + + function onComplete(id, xhr) { + var response; + + // the request was aborted/cancelled + if (!fileState[id]) { + return; + } + + log("xhr - server response received for " + id); + log("responseText = " + xhr.responseText); + response = parseResponse(xhr); + + if (isErrorResponse(xhr, response)) { + if (response.reset) { + handleResetResponse(id); + } + + if (fileState[id].attemptingResume && response.reset) { + handleResetResponseOnResumeAttempt(id); + } + else { + handleNonResetErrorResponse(id, response, xhr); + } + } + else if (chunkFiles) { + handleSuccessfullyCompletedChunk(id, response, xhr); + } + else { + handleCompletedItem(id, response, xhr); + } + } + + function getChunkDataForCallback(chunkData) { + return { + partIndex: chunkData.part, + startByte: chunkData.start + 1, + endByte: chunkData.end, + totalParts: chunkData.count + }; + } + + function getReadyStateChangeHandler(id, xhr) { + return function() { + if (xhr.readyState === 4) { + onComplete(id, xhr); + } + }; + } + + function persistChunkData(id, chunkData) { + var fileUuid = api.getUuid(id), + lastByteSent = fileState[id].loaded, + initialRequestOverhead = fileState[id].initialRequestOverhead, + estTotalRequestsSize = fileState[id].estTotalRequestsSize, + cookieName = getChunkDataCookieName(id), + cookieValue = fileUuid + + cookieItemDelimiter + chunkData.part + + cookieItemDelimiter + lastByteSent + + cookieItemDelimiter + initialRequestOverhead + + cookieItemDelimiter + estTotalRequestsSize, + cookieExpDays = options.resume.cookiesExpireIn; + + qq.setCookie(cookieName, cookieValue, cookieExpDays); + } + + function deletePersistedChunkData(id) { + if (fileState[id].file) { + var cookieName = getChunkDataCookieName(id); + qq.deleteCookie(cookieName); + } + } + + function getPersistedChunkData(id) { + var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)), + filename = api.getName(id), + sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize; + + if (chunkCookieValue) { + sections = chunkCookieValue.split(cookieItemDelimiter); + + if (sections.length === 5) { + uuid = sections[0]; + partIndex = parseInt(sections[1], 10); + lastByteSent = parseInt(sections[2], 10); + initialRequestOverhead = parseInt(sections[3], 10); + estTotalRequestsSize = parseInt(sections[4], 10); + + return { + uuid: uuid, + part: partIndex, + lastByteSent: lastByteSent, + initialRequestOverhead: initialRequestOverhead, + estTotalRequestsSize: estTotalRequestsSize + }; + } + else { + log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn"); + } + } + } + + function getChunkDataCookieName(id) { + var filename = api.getName(id), + fileSize = api.getSize(id), + maxChunkSize = options.chunking.partSize, + cookieName; + + cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize; + + if (resumeId !== undefined) { + cookieName += cookieItemDelimiter + resumeId; + } + + return cookieName; + } + + function getResumeId() { + if (options.resume.id !== null && + options.resume.id !== undefined && + !qq.isFunction(options.resume.id) && + !qq.isObject(options.resume.id)) { + + return options.resume.id; + } + } + + function handleFileChunkingUpload(id, retry) { + var name = api.getName(id), + firstChunkIndex = 0, + persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex; + + if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) { + fileState[id].remainingChunkIdxs = []; + + if (resumeEnabled && !retry && fileState[id].file) { + persistedChunkInfoForResume = getPersistedChunkData(id); + if (persistedChunkInfoForResume) { + firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part); + if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) { + firstChunkIndex = persistedChunkInfoForResume.part; + fileState[id].uuid = persistedChunkInfoForResume.uuid; + fileState[id].loaded = persistedChunkInfoForResume.lastByteSent; + fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize; + fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead; + fileState[id].attemptingResume = true; + log('Resuming ' + name + " at partition index " + firstChunkIndex); + } + } + } + + for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) { + fileState[id].remainingChunkIdxs.unshift(currentChunkIndex); + } + } + + uploadNextChunk(id); + } + + function handleStandardFileUpload(id) { + var fileOrBlob = fileState[id].file || fileState[id].blobData.blob, + name = api.getName(id), + xhr, params, toSend; + + fileState[id].loaded = 0; + + xhr = createXhr(id); + + xhr.upload.onprogress = function(e){ + if (e.lengthComputable){ + fileState[id].loaded = e.loaded; + options.onProgress(id, name, e.loaded, e.total); + } + }; + + xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr); + + params = options.paramsStore.getParams(id); + toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id); + setHeaders(id, xhr); + + log('Sending upload request for ' + id); + xhr.send(toSend); + } + + + api = { + /** + * Adds File or Blob to the queue + * Returns id to use with upload, cancel + **/ + add: function(fileOrBlobData){ + var id; + + if (fileOrBlobData instanceof File) { + id = fileState.push({file: fileOrBlobData}) - 1; + } + else if (fileOrBlobData.blob instanceof Blob) { + id = fileState.push({blobData: fileOrBlobData}) - 1; + } + else { + throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)'); + } + + fileState[id].uuid = qq.getUniqueId(); + return id; + }, + getName: function(id){ + var file = fileState[id].file, + blobData = fileState[id].blobData; + + if (file) { + // fix missing name in Safari 4 + //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined + return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name; + } + else { + return blobData.name; + } + }, + getSize: function(id){ + /*jshint eqnull: true*/ + var fileOrBlob = fileState[id].file || fileState[id].blobData.blob; + + if (qq.isFileOrInput(fileOrBlob)) { + return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size; + } + else { + return fileOrBlob.size; + } + }, + getFile: function(id) { + if (fileState[id]) { + return fileState[id].file || fileState[id].blobData.blob; + } + }, + /** + * Returns uploaded bytes for file identified by id + */ + getLoaded: function(id){ + return fileState[id].loaded || 0; + }, + isValid: function(id) { + return fileState[id] !== undefined; + }, + reset: function() { + fileState = []; + }, + getUuid: function(id) { + return fileState[id].uuid; + }, + /** + * Sends the file identified by id to the server + */ + upload: function(id, retry){ + var name = this.getName(id); + + options.onUpload(id, name); + + if (chunkFiles) { + handleFileChunkingUpload(id, retry); + } + else { + handleStandardFileUpload(id); + } + }, + cancel: function(id){ + var xhr = fileState[id].xhr; + + options.onCancel(id, this.getName(id)); + + if (xhr) { + xhr.onreadystatechange = null; + xhr.abort(); + } + + if (resumeEnabled) { + deletePersistedChunkData(id); + } + + delete fileState[id]; + }, + getResumableFilesData: function() { + var matchingCookieNames = [], + resumableFilesData = []; + + if (chunkFiles && resumeEnabled) { + if (resumeId === undefined) { + matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" + + cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "=")); + } + else { + matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" + + cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" + + cookieItemDelimiter + resumeId + "=")); + } + + qq.each(matchingCookieNames, function(idx, cookieName) { + var cookiesNameParts = cookieName.split(cookieItemDelimiter); + var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter); + + resumableFilesData.push({ + name: decodeURIComponent(cookiesNameParts[1]), + size: cookiesNameParts[2], + uuid: cookieValueParts[0], + partIdx: cookieValueParts[1] + }); + }); + + return resumableFilesData; + } + return []; + } + }; + + return api; +}; From c020e4e6bd7512a555b2f9a13e59c51cad65e89e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 2 Dec 2017 12:29:06 +0000 Subject: [PATCH 28/31] moment and fineuploader are loaded seperately in editor, not packaged up --- services/web/app/views/layout.pug | 3 +-- services/web/public/coffee/libs.coffee | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 31afc17d29..822923ac08 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -132,8 +132,7 @@ html(itemscope, itemtype='http://schema.org/Product') // minimal requirejs configuration (can be extended/overridden) window.requirejs = { "paths" : { - "moment": "libs/#{lib('moment')}", - "fineuploader": "libs/#{lib('fineuploader')}" + "moment": "libs/#{lib('moment')}" }, "urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}", "config":{ diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libs.coffee index e99fbc13ce..2b0b034fb7 100644 --- a/services/web/public/coffee/libs.coffee +++ b/services/web/public/coffee/libs.coffee @@ -1,12 +1,10 @@ define [ - "moment" "libs/angular-autocomplete/angular-autocomplete" "libs/ui-bootstrap" "libs/ng-context-menu-0.1.4" "libs/underscore-1.3.3" "libs/algolia-2.5.2" "libs/jquery.storage" - "fineuploader" "libs/angular-sanitize-1.2.17" "libs/angular-cookie" "libs/passfield" From a9ca54b98ae1bf9ea26165db6d03508eaa43fcf5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Sat, 2 Dec 2017 13:02:37 +0000 Subject: [PATCH 29/31] Generate docker-compose.yml before any docker-compose command --- services/web/Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/Makefile b/services/web/Makefile index 44017629d1..98695a8c1f 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -30,11 +30,11 @@ clean: rm -rf $$dir/test/unit/js; \ rm -rf $$dir/test/acceptance/js; \ done - # Deletes node_modules volume - docker-compose down --volumes # Regenerate docker-shared.yml - not stictly a 'clean', # but lets `make clean install` work nicely bin/generate_volumes_file + # Deletes node_modules volume + docker-compose down --volumes # Need regenerating if you change the web modules you have installed docker-shared.yml: From aaa908187d7088a151c35fb0968b6c079bcfb554 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Sat, 2 Dec 2017 13:38:23 +0000 Subject: [PATCH 30/31] added layout.pug back in --- services/web/app/views/layout.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 822923ac08..31afc17d29 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -132,7 +132,8 @@ html(itemscope, itemtype='http://schema.org/Product') // minimal requirejs configuration (can be extended/overridden) window.requirejs = { "paths" : { - "moment": "libs/#{lib('moment')}" + "moment": "libs/#{lib('moment')}", + "fineuploader": "libs/#{lib('fineuploader')}" }, "urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}", "config":{ From b51ee7ea7e833955d217ee4aaaaeda5e1b2286c0 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 4 Dec 2017 09:22:47 +0000 Subject: [PATCH 31/31] Point track-changes-web-module back at master --- services/web/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile index 9cdebb434c..6421296330 100644 --- a/services/web/Jenkinsfile +++ b/services/web/Jenkinsfile @@ -42,7 +42,7 @@ pipeline { checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/tpr-webmodule'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/tpr-webmodule.git ']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/learn-wiki'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/learn-wiki-web-module.git']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/templates'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/templates-webmodule.git']]]) - checkout([$class: 'GitSCM', branches: [[name: '*/sk-unlisted-projects']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]]) + checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]]) checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-account-merge'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-account-merge.git']]]) }