From 0869a16ed065b703345241f42b7ed21037fbac74 Mon Sep 17 00:00:00 2001 From: Mats Svensson Date: Fri, 13 Oct 2023 10:23:21 +0200 Subject: [PATCH] [FEATURE] TYPO3 version 12 (#25) * [TASK] Raise version constrain for TYPO3 to be v12 * [TASK] Require qbnk/qbank3api-phpwrapper v6.3 * [TASK] Add dependency v7.0 for qbnk/qbank3api-phpwrapper * [BUGFIX] Change ext_ files using "defined('TYPO3') or die();" * [WIP][TASK] Add compatibility with TYPO3 v12 * [TASK] Add version to be only 12.4 and up * [TASK] Add version to 12.0.0-12.4.99 * [TASK] TYPO3 Rector upgrades * [TASK] Adjust CI to be TYPO3 v12 and PHP 8.1 * [WIP] add button to TCA type 'file', possible Qbank element browser. * [WIP] remove element browser and replave with javascript * [WIP] js updates, labels, qbank service update T3v12 * [WIP] continue with BE module and templates * [WIP] be module functions * [WIP] remove unused config and script * [WIP] updates for tests, ci style fixes * [BUGFIX] fix path to lll file by ci fix * [WIP] update ci configuration * [TASK] bump version --------- Co-authored-by: Mattias Nilsson --- .github/workflows/ci.yml | 99 +- .gitignore | 14 +- .php-cs-fixer.php | 5 + .php_cs.php | 185 --- Build/FunctionalTests.xml | 13 + Build/FunctionalTestsBootstrap.php | 20 + Build/Scripts/runTests.sh | 271 ++++ Build/UnitTests.xml | 13 + Build/testing-docker/docker-compose.yml | 188 +++ .../Command/UpdateQbankFileDataCommand.php | 2 - .../Command/UpdateQbankFileStatusCommand.php | 2 - .../ExtensionConfigurationManager.php | 2 - Classes/Controller/ManagementController.php | 337 +++-- .../MediaPermanentlyDeletedException.php | 6 +- .../MissingFilePropertyException.php | 6 +- .../PersistMetaDataChangesException.php | 6 +- .../Exception/ReplaceLocalMediaException.php | 6 +- .../QbankSelectorButtonContainer.php | 230 ---- Classes/Repository/MappingRepository.php | 17 +- Classes/Repository/PropertyTypeRepository.php | 4 +- Classes/Repository/QbankFileRepository.php | 16 +- .../Repository/SysFileReferenceRepository.php | 8 +- .../PersistMetaDataChanges.php | 3 +- .../BaseMediaPropertiesCollector.php | 16 +- .../Service/Event/FileReferenceUrlEvent.php | 2 +- .../Service/Event/ResolvePageTitleEvent.php | 2 +- Classes/Service/QbankService.php | 32 +- .../InvalidTypeConverterException.php | 6 +- Classes/Utility/PropertyUtility.php | 4 +- Classes/Utility/QbankUtility.php | 2 +- .../Form/Container/FilesControlContainer.php | 143 +++ Configuration/Backend/Modules.php | 27 + Configuration/JavaScriptModules.php | 12 + .../TCA/tx_qbank_domain_model_mapping.php | 7 +- Resources/Private/Language/locallang.xlf | 4 +- ...dule_qbank.xlf => locallang_mod_qbank.xlf} | 0 Resources/Private/Layouts/Default.html | 2 +- .../Private/Templates/Management/List.html | 41 +- .../Templates/Management/Mappings.html | 21 +- .../Templates/Management/Overview.html | 14 +- Resources/Public/JavaScript/Qbank.js | 196 --- .../Public/JavaScript/qbank-connector.min.js | 1 + Resources/Public/JavaScript/qbank-media.js | 173 +++ .../TypeConverter/FloatTypeConverterTest.php | 2 +- Tests/Unit/Utility/PropertyUtilityTest.php | 35 +- Tests/Unit/Utility/QbankUtilityTest.php | 4 +- composer.json | 157 ++- ext_emconf.php | 8 +- ext_localconf.php | 151 ++- ext_tables.php | 22 +- phpcs.xml | 14 +- phpstan-baseline.neon | 111 ++ phpstan.neon | 27 +- tools/composer-normalize | Bin 868954 -> 0 bytes tools/phive | 1132 +++++++++-------- tools/php-cs-fixer | Bin 2195862 -> 0 bytes tools/phpcs | Bin 1286761 -> 0 bytes tools/phpstan | Bin 18448673 -> 0 bytes tools/typo3-typoscript-lint | Bin 2503791 -> 0 bytes tools/yaml-lint | Bin 71025 -> 0 bytes 60 files changed, 2129 insertions(+), 1692 deletions(-) create mode 100644 .php-cs-fixer.php delete mode 100644 .php_cs.php create mode 100644 Build/FunctionalTests.xml create mode 100644 Build/FunctionalTestsBootstrap.php create mode 100755 Build/Scripts/runTests.sh create mode 100644 Build/UnitTests.xml create mode 100644 Build/testing-docker/docker-compose.yml delete mode 100644 Classes/FormEngine/Container/QbankSelectorButtonContainer.php create mode 100644 Classes/Xclass/Form/Container/FilesControlContainer.php create mode 100644 Configuration/Backend/Modules.php create mode 100644 Configuration/JavaScriptModules.php rename Resources/Private/Language/{locallang_module_qbank.xlf => locallang_mod_qbank.xlf} (100%) delete mode 100644 Resources/Public/JavaScript/Qbank.js create mode 100644 Resources/Public/JavaScript/qbank-connector.min.js create mode 100644 Resources/Public/JavaScript/qbank-media.js create mode 100644 phpstan-baseline.neon delete mode 100755 tools/composer-normalize delete mode 100755 tools/php-cs-fixer delete mode 100755 tools/phpcs delete mode 100755 tools/phpstan delete mode 100755 tools/typo3-typoscript-lint delete mode 100755 tools/yaml-lint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20cda46..b083aed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,6 @@ --- name: CI -on: - push: - branches: - - master - pull_request: - schedule: - - cron: '15 3 * * 1' +on: [push, pull_request] jobs: php-lint: name: "PHP linter" @@ -26,9 +20,7 @@ jobs: fail-fast: false matrix: php-version: - - 7.2 - - 7.3 - - 7.4 + - 8.1 typoscript-lint: name: "TypoScript linter" runs-on: ubuntu-20.04 @@ -86,27 +78,8 @@ jobs: - "json:lint" - "php:cs-fixer" - "php:sniff" -# - "php:stan" php-version: - - 7.4 -# code-quality-frontend: -# name: "Code quality frontend checks" -# runs-on: ubuntu-20.04 -# strategy: -# fail-fast: false -# matrix: -# command: -# - "style" -# - "js" -# steps: -# - name: "Checkout" -# uses: actions/checkout@v2 -# - name: "Install modules" -# working-directory: ./Resources/Private -# run: yarn -# - name: "Run command" -# working-directory: ./Resources/Private -# run: "yarn lint:${{ matrix.command }}" + - 8.1 xliff-lint: name: "Xliff linter" runs-on: ubuntu-20.04 @@ -161,68 +134,6 @@ jobs: - highest - lowest php-version: - - 7.2 - - 7.3 - - 7.4 + - 8.1 typo3-version: - - ^10.4 -# functional-tests: -# name: "Functional tests" -# runs-on: ubuntu-18.04 -# needs: php-lint -# steps: -# - name: "Checkout" -# uses: actions/checkout@v2 -# - name: "Install PHP" -# uses: shivammathur/setup-php@v2 -# with: -# php-version: "${{ matrix.php-version }}" -# tools: composer:v2 -# extensions: mysqli -# coverage: none -# - name: "Show Composer version" -# run: composer --version -# - name: "Cache dependencies installed with composer" -# uses: actions/cache@v1 -# with: -# key: "php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" -# path: ~/.cache/composer -# restore-keys: "php${{ matrix.php-version }}-composer-\n" -# - env: -# TYPO3: "${{ matrix.typo3-version }}" -# name: "Install TYPO3 Core" -# run: | -# composer require --no-progress typo3/minimal:"$TYPO3" -# composer show -# - if: "matrix.composer-dependencies == 'lowest'" -# name: "Install lowest dependencies with composer" -# run: | -# composer update --no-ansi --no-interaction --no-progress --with-dependencies --prefer-lowest -# composer show -# - if: "matrix.composer-dependencies == 'highest'" -# name: "Install highest dependencies with composer" -# run: | -# composer update --no-ansi --no-interaction --no-progress --with-dependencies -# composer show -# - name: "Start MySQL" -# run: "sudo /etc/init.d/mysql start" -# - name: "Run functional tests" -# run: | -# export typo3DatabaseName="typo3"; -# export typo3DatabaseHost="127.0.0.1"; -# export typo3DatabaseUsername="root"; -# export typo3DatabasePassword="root"; -# composer ci:tests:functional -# strategy: -# fail-fast: false -# matrix: -# composer-dependencies: -# - highest -# - lowest -# php-version: -# - 7.2 -# - 7.3 -# - 7.4 -# typo3-version: -# - ^9.5 -# - ^10.4 + - ^12.4 diff --git a/.gitignore b/.gitignore index a18d28b..8a21830 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,17 @@ /*.idea /.Build/* +/.fleet +/.php-cs-fixer.cache +/.phpunit.result.cache /.php_cs.cache /Documentation-GENERATED-temp/ -/Resources/Private/node_modules/ -/Resources/Private/package-lock.json -/Resources/Private/yarn-error.log -/Resources/Private/yarn.lock /composer.lock /nbproject /var +/build +/clover.xml +/node_modules/ +/package-lock.json +/yarn-error.log +/yarn.lock +Build/testing-docker/.env diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..b39c04b --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,5 @@ +getFinder()->in('Classes')->in('Configuration')->in('Tests'); +return $config; diff --git a/.php_cs.php b/.php_cs.php deleted file mode 100644 index 12a9c4a..0000000 --- a/.php_cs.php +++ /dev/null @@ -1,185 +0,0 @@ - true, - '@PHP56Migration' => true, - '@PHP56Migration:risky' => true, - '@PHP70Migration' => true, - '@PHP71Migration' => true, - '@PHPUnit57Migration:risky' => true, - '@PHPUnit60Migration:risky' => true, - '@PHPUnit75Migration:risky' => true, - - 'align_multiline_comment' => true, - 'array_indentation' => true, - 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => true, - 'blank_line_before_statement' => true, - 'braces' => [ - 'allow_single_line_closure' => true, - ], - 'class_attributes_separation' => ['elements' => ['method']], - 'class_definition' => ['single_line' => true], - 'combine_consecutive_issets' => true, - 'combine_consecutive_unsets' => true, - 'comment_to_phpdoc' => true, - 'compact_nullable_typehint' => true, - 'concat_space' => ['spacing' => 'one'], - 'declare_equal_normalize' => true, - 'dir_constant' => true, - 'ereg_to_preg' => true, - 'error_suppression' => true, - 'escape_implicit_backslashes' => true, - 'explicit_indirect_variable' => true, - 'explicit_string_variable' => true, - 'final_internal_class' => true, - 'fopen_flag_order' => true, - 'fopen_flags' => ['b_mode' => false], - 'fully_qualified_strict_types' => true, - 'function_to_constant' => true, - 'function_typehint_space' => true, - 'hash_to_slash_comment' => true, - 'heredoc_to_nowdoc' => true, - 'implode_call' => true, - 'include' => true, - 'is_null' => true, - 'linebreak_after_opening_tag' => true, - 'list_syntax' => ['syntax' => 'short'], - 'logical_operators' => true, - 'lowercase_cast' => true, - 'lowercase_static_reference' => true, - 'magic_constant_casing' => true, - 'magic_method_casing' => true, - 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], - 'method_chaining_indentation' => true, - 'method_separation' => true, - 'modernize_types_casting' => true, - 'multiline_comment_opening_closing' => true, - 'native_function_casing' => true, - 'new_with_braces' => true, - 'no_alias_functions' => true, - 'no_alternative_syntax' => true, - 'no_binary_string' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => [ - 'tokens' => [ - 'break', - 'continue', - 'curly_brace_block', - 'extra', - 'parenthesis_brace_block', - 'return', - 'square_brace_block', - 'throw', - 'use', - ], - ], - 'no_extra_consecutive_blank_lines' => true, - 'no_homoglyph_names' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_mixed_echo_print' => ['use' => 'echo'], - 'no_multiline_whitespace_around_double_arrow' => true, - 'no_multiline_whitespace_before_semicolons' => true, - 'no_php4_constructor' => true, - 'no_short_bool_cast' => true, - 'no_short_echo_tag' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_spaces_after_function_name' => true, - 'no_spaces_around_offset' => true, - 'no_spaces_inside_parenthesis' => true, - 'no_superfluous_elseif' => true, - 'no_trailing_comma_in_list_call' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_unneeded_control_parentheses' => true, - 'no_unneeded_curly_braces' => true, - 'no_unneeded_final_method' => true, - 'no_unreachable_default_argument_value' => true, - 'no_unset_cast' => true, - 'no_unset_on_property' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'no_whitespace_before_comma_in_array' => true, - 'no_whitespace_in_blank_line' => true, - 'non_printable_character' => [ - 'use_escape_sequences_in_strings' => false, - ], - 'normalize_index_brace' => true, - 'object_operator_without_whitespace' => true, - 'ordered_imports' => true, - 'phpdoc_add_missing_param_annotation' => true, - 'phpdoc_no_package' => true, - 'phpdoc_indent' => true, - 'phpdoc_inline_tag' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_alias_tag' => true, - 'phpdoc_no_useless_inheritdoc' => true, - 'phpdoc_return_self_reference' => true, - 'phpdoc_scalar' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_summary' => true, - 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_trim_consecutive_blank_line_separation' => true, - 'phpdoc_types' => true, - 'phpdoc_types_order' => [ - 'null_adjustment' => 'always_last', - 'sort_algorithm' => 'none', - ], - 'phpdoc_var_annotation_correct_order' => true, - 'phpdoc_var_without_name' => true, - 'php_unit_construct' => true, - 'php_unit_dedicate_assert' => true, - 'php_unit_expectation' => true, - 'php_unit_fqcn_annotation' => true, - 'php_unit_method_casing' => true, - 'php_unit_mock' => true, - 'php_unit_no_expectation_annotation' => true, - 'php_unit_ordered_covers' => true, - 'php_unit_set_up_tear_down_visibility' => true, - 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], - 'protected_to_private' => true, - 'psr4' => true, - 'return_type_declaration' => true, - 'self_accessor' => true, - 'semicolon_after_instruction' => true, - 'set_type_to_cast' => true, - 'short_scalar_cast' => true, - 'single_blank_line_before_namespace' => true, - 'single_class_element_per_statement' => true, - 'single_line_comment_style' => true, - 'single_quote' => true, - 'space_after_semicolon' => [ - 'remove_in_empty_for_expressions' => true, - ], - 'standardize_not_equals' => true, - 'strict_comparison' => true, - 'strict_param' => true, - 'string_line_ending' => true, - 'standardize_increment' => true, - 'ternary_operator_spaces' => true, - 'trailing_comma_in_multiline_array' => true, - 'trim_array_spaces' => true, - 'unary_operator_spaces' => true, - 'void_return' => true, - 'whitespace_after_comma_in_array' => true, -]; - -$config = \PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setRules($rules); - -$finder = \PhpCsFixer\Finder::create() - ->in('Classes') - ->in('Tests') - ->in('Configuration'); - -return $config->setFinder($finder); diff --git a/Build/FunctionalTests.xml b/Build/FunctionalTests.xml new file mode 100644 index 0000000..5b12047 --- /dev/null +++ b/Build/FunctionalTests.xml @@ -0,0 +1,13 @@ + + + + + ../Tests/Functional/ + + + + + + + + diff --git a/Build/FunctionalTestsBootstrap.php b/Build/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..443197d --- /dev/null +++ b/Build/FunctionalTestsBootstrap.php @@ -0,0 +1,20 @@ +defineOriginalRootPath(); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); +}); diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..c6643e3 --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash + +# +# TYPO3 core test runner based on docker and docker-compose. +# + +# Function to write a .env file in Build/testing-docker/local +# This is read by docker-compose and vars defined here are +# used in Build/testing-docker/local/docker-compose.yml +setUpDockerComposeDotEnv() { + # Delete possibly existing local .env file if exists + [ -e .env ] && rm .env + # Set up a new .env file for docker-compose + echo "COMPOSE_PROJECT_NAME=local" >> .env + # To prevent access rights of files created by the testing, the docker image later + # runs with the same user that is currently executing the script. docker-compose can't + # use $UID directly itself since it is a shell variable and not an env variable, so + # we have to set it explicitly here. + echo "HOST_UID=`id -u`" >> .env + # Your local home directory for composer and npm caching + echo "HOST_HOME=${HOME}" >> .env + # Your local user + echo "ROOT_DIR"=${ROOT_DIR} >> .env + echo "HOST_USER=${USER}" >> .env + echo "TEST_FILE=${TEST_FILE}" >> .env + echo "TYPO3_VERSION=${TYPO3_VERSION}" >> .env + echo "PHP_XDEBUG_ON=${PHP_XDEBUG_ON}" >> .env + echo "PHP_XDEBUG_PORT=${PHP_XDEBUG_PORT}" >> .env + echo "PHP_VERSION=${PHP_VERSION}" >> .env + echo "DOCKER_PHP_IMAGE=${DOCKER_PHP_IMAGE}" >> .env + echo "EXTRA_TEST_OPTIONS=${EXTRA_TEST_OPTIONS}" >> .env + echo "SCRIPT_VERBOSE=${SCRIPT_VERBOSE}" >> .env +} + +# Load help text into $HELP +read -r -d '' HELP < + Specifies which test suite to run + - composerUpdate: "composer update" + - clean: clean up build and testing related files + - lint: PHP linting + - unit (default): PHP unit tests + - functional: functional tests + + -d + Only with -s functional + Specifies on which DBMS tests are performed + - mariadb (default): use mariadb + - postgres: use postgres + - sqlite: use sqlite + + -p <7.4|8.0|8.1|8.2> + Specifies the PHP minor version to be used + - 7.4: (default) use PHP 7.4 + - 8.0: use PHP 8.0 + - 8.1: use PHP 8.1 + - 8.2: use PHP 8.2 + + -t <11|12> + Only with -s composerUpdate + Specifies the TYPO3 core major version to be used + - 11: (default) use TYPO3 core v11 + - 12: use TYPO3 core v12 + + -e "" + Only with -s functional|unit + Additional options to send to phpunit tests. + For phpunit, options starting with "--" must be added after options starting with "-". + Example -e "-v --filter canRetrieveValueWithGP" to enable verbose output AND filter tests + named "canRetrieveValueWithGP" + + -x + Only with -s unit + Send information to host instance for test or system under test break points. This is especially + useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port + can be selected with -y + + -y + Send xdebug information to a different port than default 9003 if an IDE like PhpStorm + is not listening on default port. + + -u + Update existing typo3gmbh/phpXY:latest docker images. Maintenance call to docker pull latest + versions of the main php images. The images are updated once in a while and only the youngest + ones are supported by core testing. Use this if weird test errors occur. Also removes obsolete + image versions of typo3gmbh/phpXY. + + -v + Enable verbose script output. Shows variables and docker commands. + + -h + Show this help. + +EOF + +# Test if docker-compose exists, else exit out with error +if ! type "docker-compose" > /dev/null; then + echo "This script relies on docker and docker-compose. Please install" >&2 + exit 1 +fi + +# Go to the directory this script is located, so everything else is relative +# to this dir, no matter from where this script is called. +THIS_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +cd "$THIS_SCRIPT_DIR" || exit 1 + +# Go to directory that contains the local docker-compose.yml file +cd ../testing-docker || exit 1 + +# Option defaults +ROOT_DIR=`realpath ${PWD}/../../` +TEST_SUITE="unit" +DBMS="mariadb" +PHP_VERSION="8.1" +TYPO3_VERSION="12" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +EXTRA_TEST_OPTIONS="" +SCRIPT_VERBOSE=0 + +# Option parsing +# Reset in case getopts has been used previously in the shell +OPTIND=1 +# Array for invalid options +INVALID_OPTIONS=(); +# Simple option parsing based on getopts (! not getopt) +while getopts ":s:d:p:t:e:xy:huv" OPT; do + case ${OPT} in + s) + TEST_SUITE=${OPTARG} + ;; + d) + DBMS=${OPTARG} + ;; + p) + PHP_VERSION=${OPTARG} + ;; + t) + TYPO3_VERSION=${OPTARG} + ;; + e) + EXTRA_TEST_OPTIONS=${OPTARG} + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + h) + echo "${HELP}" + exit 0 + ;; + u) + TEST_SUITE=update + ;; + v) + SCRIPT_VERBOSE=1 + ;; + \?) + INVALID_OPTIONS+=(${OPTARG}) + ;; + :) + INVALID_OPTIONS+=(${OPTARG}) + ;; + esac +done + +# Exit on invalid options +if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then + echo "Invalid option(s):" >&2 + for I in "${INVALID_OPTIONS[@]}"; do + echo "-"${I} >&2 + done + echo >&2 + echo "${HELP}" >&2 + exit 1 +fi + +# Move "7.2" to "php72", the latter is the docker container name +DOCKER_PHP_IMAGE=`echo "php${PHP_VERSION}" | sed -e 's/\.//'` + +# Set $1 to first mass argument, this is the optional test file or test directory to execute +shift $((OPTIND - 1)) +if [ -n "${1}" ]; then + TEST_FILE="Web/typo3conf/ext/qbank/${1}" +fi + +if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x +fi + +# Suite execution +case ${TEST_SUITE} in + clean) + rm -rf ../../composer.lock ../../.Build/ ../../composer.json.testing + ;; + composerUpdate) + setUpDockerComposeDotEnv + cp ../../composer.json ../../composer.json.orig + if [ -f "../../composer.json.testing" ]; then + cp ../../composer.json ../../composer.json.orig + fi + docker-compose run composer_update + cp ../../composer.json ../../composer.json.testing + mv ../../composer.json.orig ../../composer.json + SUITE_EXIT_CODE=$? + docker-compose down + ;; + functional) + setUpDockerComposeDotEnv + case ${DBMS} in + mariadb) + docker-compose run functional_mariadb10 + SUITE_EXIT_CODE=$? + ;; + postgres) + docker-compose run functional_postgres10 + SUITE_EXIT_CODE=$? + ;; + sqlite) + # sqlite has a tmpfs as .Build/Web/typo3temp/var/tests/functional-sqlite-dbs/ + # Since docker is executed as root (yay!), the path to this dir is owned by + # root if docker creates it. Thank you, docker. We create the path beforehand + # to avoid permission issues. + mkdir -p ${ROOT_DIR}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/ + docker-compose run functional_sqlite + SUITE_EXIT_CODE=$? + ;; + *) + echo "Invalid -d option argument ${DBMS}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 + esac + docker-compose down + ;; + lint) + setUpDockerComposeDotEnv + docker-compose run lint + SUITE_EXIT_CODE=$? + docker-compose down + ;; + unit) + setUpDockerComposeDotEnv + docker-compose run unit + SUITE_EXIT_CODE=$? + docker-compose down + ;; + update) + # pull typo3/core-testing-*:latest versions of those ones that exist locally + docker images typo3/core-testing-*:latest --format "{{.Repository}}:latest" | xargs -I {} docker pull {} + # remove "dangling" typo3/core-testing-* images (those tagged as ) + docker images typo3/core-testing-* --filter "dangling=true" --format "{{.ID}}" | xargs -I {} docker rmi {} + ;; + *) + echo "Invalid -s option argument ${TEST_SUITE}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 +esac + +exit $SUITE_EXIT_CODE diff --git a/Build/UnitTests.xml b/Build/UnitTests.xml new file mode 100644 index 0000000..8492850 --- /dev/null +++ b/Build/UnitTests.xml @@ -0,0 +1,13 @@ + + + + + ../Tests/Unit/ + + + + + + + + diff --git a/Build/testing-docker/docker-compose.yml b/Build/testing-docker/docker-compose.yml new file mode 100644 index 0000000..cfd6e43 --- /dev/null +++ b/Build/testing-docker/docker-compose.yml @@ -0,0 +1,188 @@ +version: '2.3' +services: + mariadb10: + image: mariadb:10 + environment: + MYSQL_ROOT_PASSWORD: funcp + tmpfs: + - /var/lib/mysql/:rw,noexec,nosuid + + postgres10: + image: postgres:10-alpine + environment: + POSTGRES_PASSWORD: funcp + POSTGRES_USER: ${HOST_USER} + tmpfs: + - /var/lib/postgresql/data:rw,noexec,nosuid + + composer_update: + image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + environment: + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + if [ ${TYPO3_VERSION} -eq 11 ]; then + composer req --dev --no-update \ + typo3/cms-composer-installers:^3.0 + composer req typo3/cms-core:^11.5 --no-update + fi + if [ ${TYPO3_VERSION} -eq 12 ]; then + composer req --dev --no-update \ + "typo3/cms-composer-installers:^5.0" \ + typo3/cms-backend:^12.4 \ + typo3/cms-frontend:^12.4 \ + typo3/cms-extbase:^12.4 \ + typo3/cms-fluid:^12.4 \ + typo3/cms-install:^12.4 + composer req typo3/cms-core:^12.4 -W --no-update + fi + composer update --no-progress --no-interaction; + " + + functional_mariadb10: + image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: ${HOST_UID} + links: + - mariadb10 + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + environment: + typo3DatabaseName: func_test + typo3DatabaseUsername: root + typo3DatabasePassword: funcp + typo3DatabaseHost: mariadb10 + working_dir: ${ROOT_DIR} + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + echo Waiting for database start...; + while ! nc -z mariadb10 3306; do + sleep 1; + done; + echo Database is up; + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + else + DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ + .Build/bin/phpunit -c Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + fi + " + + functional_postgres10: + image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: ${HOST_UID} + links: + - postgres10 + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + environment: + typo3DatabaseDriver: pdo_pgsql + typo3DatabaseName: bamboo + typo3DatabaseUsername: ${HOST_USER} + typo3DatabaseHost: postgres10 + typo3DatabasePassword: funcp + working_dir: ${ROOT_DIR} + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + echo Waiting for database start...; + while ! nc -z postgres10 5432; do + sleep 1; + done; + echo Database is up; + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; + else + DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ + .Build/bin/phpunit -c Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; + fi + " + + functional_sqlite: + image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: ${HOST_UID} + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + tmpfs: + - ${ROOT_DIR}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid,uid=${HOST_UID} + environment: + typo3DatabaseDriver: pdo_sqlite + working_dir: ${ROOT_DIR} + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; + else + DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ + .Build/bin/phpunit -c Build/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; + fi + " + + lint: + image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: ${HOST_UID} + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + find . -name \\*.php ! -path "./.Build/\\*" -print0 | xargs -0 -n1 -P4 php -dxdebug.mode=off -l >/dev/null + " + + unit: + image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: ${HOST_UID} + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP' + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + else + DOCKER_HOST=`route -n | awk '/^0.0.0.0/ { print $$2 }'` + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=$${DOCKER_HOST}\" \ + .Build/bin/phpunit -c Build/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + fi + " diff --git a/Classes/Command/UpdateQbankFileDataCommand.php b/Classes/Command/UpdateQbankFileDataCommand.php index 55e27b3..de7257d 100644 --- a/Classes/Command/UpdateQbankFileDataCommand.php +++ b/Classes/Command/UpdateQbankFileDataCommand.php @@ -37,8 +37,6 @@ class UpdateQbankFileDataCommand extends Command { /** * Configure the command by defining the name, options and arguments. - * - * @return void */ public function configure(): void { diff --git a/Classes/Command/UpdateQbankFileStatusCommand.php b/Classes/Command/UpdateQbankFileStatusCommand.php index c88fd46..4c99553 100644 --- a/Classes/Command/UpdateQbankFileStatusCommand.php +++ b/Classes/Command/UpdateQbankFileStatusCommand.php @@ -40,8 +40,6 @@ class UpdateQbankFileStatusCommand extends Command { /** * Configure the command by defining the name, options and arguments. - * - * @return void */ public function configure(): void { diff --git a/Classes/Configuration/ExtensionConfigurationManager.php b/Classes/Configuration/ExtensionConfigurationManager.php index f363d6d..c2d0af6 100644 --- a/Classes/Configuration/ExtensionConfigurationManager.php +++ b/Classes/Configuration/ExtensionConfigurationManager.php @@ -295,8 +295,6 @@ public function getAutoUpdate() * Set the value of autoUpdate. * * @param int $autoUpdate - * - * @return void */ public function setAutoUpdate(int $autoUpdate): void { diff --git a/Classes/Controller/ManagementController.php b/Classes/Controller/ManagementController.php index ea20a32..4732271 100644 --- a/Classes/Controller/ManagementController.php +++ b/Classes/Controller/ManagementController.php @@ -17,54 +17,34 @@ namespace Pixelant\Qbank\Controller; +use Pixelant\Qbank\Exception\MediaPermanentlyDeletedException; use Pixelant\Qbank\Repository\MappingRepository; use Pixelant\Qbank\Repository\QbankFileRepository; use Pixelant\Qbank\Service\QbankService; use Pixelant\Qbank\Utility\PropertyUtility; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use TYPO3\CMS\Backend\Routing\UriBuilder; +use TYPO3\CMS\Backend\Module\ModuleData; +use TYPO3\CMS\Backend\Routing\UriBuilder as BackendUriBuilder; use TYPO3\CMS\Backend\Template\Components\ButtonBar; use TYPO3\CMS\Backend\Template\ModuleTemplate; +use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; -use TYPO3\CMS\Core\Http\HtmlResponse; +use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Localization\LanguageService; -use TYPO3\CMS\Core\Messaging\FlashMessage; +use TYPO3\CMS\Core\Messaging\AbstractMessage; +use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction; +use TYPO3\CMS\Core\Page\PageRenderer; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; -use TYPO3Fluid\Fluid\View\ViewInterface; +use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; -/** - * QBank Management Controller. - * - * Scope: backend - * @internal - */ -final class ManagementController +class ManagementController extends ActionController { - /** - * @var ServerRequestInterface - */ - private $request; + protected ?ModuleData $moduleData = null; - /** - * @var array - */ - private $arguments = []; - - /** - * ModuleTemplate object. - * - * @var ModuleTemplate - */ - private $moduleTemplate; - - /** - * @var ViewInterface - */ - private $view; + protected ModuleTemplate $moduleTemplate; /** * Module name for the shortcut. @@ -73,153 +53,127 @@ final class ManagementController */ private $shortcutName; - /** - * @var QbankService - */ - private $qbankService; - - /** - * @var IconFactory - */ - private $iconFactory; - - /** - * Actions to create menu for. - */ - private $actions = ['overview', 'mappings', 'list']; - /** * ManagementController constructor. */ - public function __construct() - { - $this->qbankService = GeneralUtility::makeInstance(QbankService::class); - $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class); - $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); - } + // phpcs:disable + public function __construct( + protected readonly ModuleTemplateFactory $moduleTemplateFactory, + protected readonly IconFactory $iconFactory, + protected readonly PageRenderer $pageRenderer, + protected readonly QbankService $qbankService, + protected readonly BackendUriBuilder $backendUriBuilder + ) {} + // phpcs:enable /** - * Injects the request object for the current request, and renders correct action. - * - * @param ServerRequestInterface $request the current request - * @return ResponseInterface the response with the content + * Init module state. + * This isn't done within __construct() since the controller + * object is only created once in extbase when multiple actions are called in + * one call. When those change module state, the second action would see old state. */ - public function handleRequest(ServerRequestInterface $request): ResponseInterface + public function initializeAction(): void { - $this->request = $request; - - $this->arguments = array_merge_recursive( - $request->getQueryParams(), - $request->getParsedBody() ?? [] + $this->moduleData = $this->request->getAttribute('moduleData'); + $this->moduleTemplate = $this->moduleTemplateFactory->create($this->request); + $this->moduleTemplate->setTitle( + LocalizationUtility::translate( + 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf:be.module.title', + 'qbank' + ) ); - - $action = $this->arguments['action'] ?? 'overview'; - - $this->generateDropdownMenu($request, $action); - $this->generateButtons($action); - - $this->initializeView($action); - - $actionFunction = $action . 'Action'; - if (method_exists($this, $actionFunction)) { - $this->{$actionFunction}(); - } else { - $this->overviewAction(); - } - - $this->moduleTemplate->setContent($this->view->render()); - - return new HtmlResponse($this->moduleTemplate->renderContent()); + $this->moduleTemplate->setFlashMessageQueue($this->getFlashMessageQueue()); } /** - * @param string $templateName + * Assign default variables to ModuleTemplate view */ - private function initializeView(string $templateName): void + protected function initializeView(): void { - $this->view = GeneralUtility::makeInstance(StandaloneView::class); - $this->view->setTemplate($templateName); - $this->view->setTemplateRootPaths(['EXT:qbank/Resources/Private/Templates/Management']); - $this->view->setPartialRootPaths(['EXT:qbank/Resources/Private/Partials']); - $this->view->setLayoutRootPaths(['EXT:qbank/Resources/Private/Layouts']); - $this->view->getRequest()->setControllerExtensionName('Qbank'); - $this->view->assign( - 'settings', - [ - 'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] - . ' ' - . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], - ] + $this->moduleTemplate->assignMultiple([ + 'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], + 'timeFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], + 'dateTimeFormat' => + $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], + ]); + // Load JavaScript modules + $javaScriptRenderer = $this->pageRenderer->getJavaScriptRenderer(); + $javaScriptRenderer->addJavaScriptModuleInstruction( + JavaScriptModuleInstruction::create('@typo3/filelist/file-list.js')->instance() ); - // Info window is included in this. - $this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Filelist/FileList'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-menu.js'); + $this->pageRenderer->loadJavaScriptModule('@typo3/backend/modal.js'); } /** * Generates the dropdown menu. - * - * @param ServerRequestInterface $request - * @param string $action - * @return void */ - private function generateDropdownMenu(ServerRequestInterface $request, string $action): void + private function generateDropdownMenu(string $currentAction): void { - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); - $lang = $this->getLanguageService(); - $lang->includeLLFile('EXT:qbank/Resources/Private/Language/locallang.xlf'); + $this->uriBuilder->setRequest($this->request); $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu(); $menu->setIdentifier('WebFuncJumpMenu'); - - foreach ($this->actions as $menuAction) { - $menuItem = $menu - ->makeMenuItem() - ->setActive($action === $menuAction) - ->setHref( - $uriBuilder->buildUriFromRoute('file_qbank', ['action' => $menuAction]) + $menu->addMenuItem( + $menu->makeMenuItem() + ->setTitle( + LocalizationUtility::translate( + 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf:be.menu_item.overview', + 'qbank' + ) ) - ->setTitle($lang->getLL('be.menu_item.' . $menuAction)); - - $menu->addMenuItem($menuItem); - } - - $this->shortcutName = $lang->getLL('be.menu_item.qbank_overview'); + ->setHref($this->uriBuilder->uriFor('overview')) + ->setActive($currentAction === 'overview') + ); + $menu->addMenuItem( + $menu->makeMenuItem() + ->setTitle( + LocalizationUtility::translate( + 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf:be.menu_item.list', + 'qbank' + ) + ) + ->setHref($this->uriBuilder->uriFor('list')) + ->setActive($currentAction === 'list') + ); + $menu->addMenuItem( + $menu->makeMenuItem() + ->setTitle( + LocalizationUtility::translate( + 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf:be.menu_item.mappings', + 'qbank' + ) + ) + ->setHref($this->uriBuilder->uriFor('mappings')) + ->setActive($currentAction === 'mappings') + ); $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu); } /** * Gets all buttons for the docHeader. - * @param mixed $action + * @param string $currentAction */ - private function generateButtons($action): void + private function generateButtons($currentAction): void { - $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $this->uriBuilder->setRequest($this->request); $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar(); - - if ($action === 'mappings') { - $newRecordButton = $buttonBar->makeLinkButton() - ->setHref((string)$uriBuilder->buildUriFromRoute( - 'record_edit', - [ - 'edit' => [ - 'tx_qbank_domain_model_mapping' => ['new'], - ], - 'returnUrl' => (string)$uriBuilder->buildUriFromRoute('file_qbank', ['action' => 'mappings']), - ] - )) - ->setTitle($this->getLanguageService()->getLL('be.button.add_mapping')) - ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)) - ->setShowLabelText(true); - - $buttonBar->addButton($newRecordButton, ButtonBar::BUTTON_POSITION_LEFT, 1); + if ($currentAction === 'mappings') { + $addUserButton = $buttonBar->makeLinkButton() + ->setIcon($this->iconFactory->getIcon('actions-plus', Icon::SIZE_SMALL)) + ->setTitle( + LocalizationUtility::translate( + 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf:be.button.add_mapping', + 'qbank' + ) + ) + ->setShowLabelText(true) + ->setHref((string)$this->backendUriBuilder->buildUriFromRoute('record_edit', [ + 'edit' => ['tx_qbank_domain_model_mapping' => [0 => 'new']], + 'returnUrl' => $this->request->getAttribute('normalizedParams')->getRequestUri(), + ])); + $buttonBar->addButton($addUserButton); } - $shortcutButton = $buttonBar->makeShortcutButton() - ->setModuleName('file_qbank') - ->setGetVariables(['action', 'extension']) - ->setDisplayName($this->shortcutName); - - $buttonBar->addButton($shortcutButton); - $reloadButton = $buttonBar->makeLinkButton() ->setHref($this->request->getAttribute('normalizedParams')->getRequestUri()) ->setTitle( @@ -234,40 +188,62 @@ private function generateButtons($action): void /** * Overview. */ - private function overviewAction(): void + protected function overviewAction(): ResponseInterface { + $this->generateDropdownMenu('overview'); + $this->generateButtons('overview'); $properties = $this->qbankService->fetchMediaProperties(); - $this->view->assign('properties', $properties); + $this->moduleTemplate->assign('properties', $properties); + return $this->moduleTemplate->renderResponse('Management/Overview'); } /** * Mapping. */ - private function mappingsAction(): void + protected function mappingsAction(): ResponseInterface { + $this->generateDropdownMenu('mappings'); + $this->generateButtons('mappings'); $mappingRepository = GeneralUtility::makeInstance(MappingRepository::class); $mappings = $mappingRepository->findAll(); - $this->view->assign('mappings', $mappings); - $this->view->assign('mediaProperties', $this->qbankService->fetchMediaProperties()); - $this->view->assign('fileProperties', PropertyUtility::getFileProperties()); + $this->moduleTemplate->assignMultiple([ + 'mappings' => $mappings, + 'mediaProperties' => $this->qbankService->fetchMediaProperties(), + 'fileProperties' => PropertyUtility::getFileProperties(), + ]); + return $this->moduleTemplate->renderResponse('Management/Mappings'); } /** * List. */ - private function listAction(): void + protected function listAction(): ResponseInterface { + $this->generateDropdownMenu('list'); + $this->generateButtons('list'); $qbankFileRepository = GeneralUtility::makeInstance(QbankFileRepository::class); $qbankFiles = $qbankFileRepository->findAll(); - $this->view->assign('qbankFiles', $qbankFiles); + $this->moduleTemplate->assign('qbankFiles', $qbankFiles); + return $this->moduleTemplate->renderResponse('Management/List'); } /** * Update metadata for file. */ - public function synchronizeMetadataAction(): void + public function synchronizeMetadataAction() { - $files = $this->arguments['files'] ?? [$this->arguments['file']]; + if ($this->request->hasArgument('files')) { + $files = $this->request->getArgument('files'); + } elseif ($this->request->hasArgument('file')) { + $files = [$this->request->getArgument('file')]; + } else { + $this->moduleTemplate->addFlashMessage( + 'No files could be syncronized', + 'Syncronize', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + return $this->rediectResponse('list'); + } foreach ($files as $file) { $file = (int)$file; @@ -278,30 +254,44 @@ public function synchronizeMetadataAction(): void try { $this->qbankService->synchronizeMetadata($file); - } catch (\Pixelant\Qbank\Exception\MediaPermanentlyDeletedException $th) { + } catch (MediaPermanentlyDeletedException $th) { $this->moduleTemplate->addFlashMessage( $th->getMessage(), '', - FlashMessage::ERROR + AbstractMessage::ERROR ); } } $this->moduleTemplate->addFlashMessage( - $this->getLanguageService()->getLL('be.action.updated-metadata'), + LocalizationUtility::translate( + 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf:be.action.updated-metadata', + 'qbank' + ), '', - FlashMessage::OK + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::OK ); - $this->forward('list'); + return $this->rediectResponse('list'); } /** * Replace image for file. */ - public function replaceLocalMediaAction(): void + public function replaceLocalMediaAction() { - $files = $this->arguments['files'] ?? [$this->arguments['file']]; + if ($this->request->hasArgument('files')) { + $files = $this->request->getArgument('files'); + } elseif ($this->request->hasArgument('file')) { + $files = [$this->request->getArgument('file')]; + } else { + $this->moduleTemplate->addFlashMessage( + 'No files could be syncronized', + 'Syncronize', + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::ERROR + ); + return $this->rediectResponse('list'); + } foreach ($files as $file) { $file = (int)$file; @@ -313,27 +303,26 @@ public function replaceLocalMediaAction(): void $this->qbankService->replaceLocalMedia($file); $this->moduleTemplate->addFlashMessage( - $this->getLanguageService()->getLL('be.action.updated-file'), + LocalizationUtility::translate( + 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf:be.action.updated-file', + 'qbank' + ), '', - FlashMessage::OK + \TYPO3\CMS\Core\Type\ContextualFeedbackSeverity::OK ); } - $this->forward('list'); + return $this->rediectResponse('list'); } /** - * Forward execution to $action. + * Redirect to $action to avoid parameters in URL. * * @param string $action */ - private function forward(string $action): void + private function rediectResponse(string $action): ResponseInterface { - $this->initializeView($action); - - $methodName = $action . 'Action'; - - $this->{$methodName}(); + return new RedirectResponse($this->backendUriBuilder->buildUriFromRoute('file_qbank', ['action' => $action])); } /** diff --git a/Classes/Exception/MediaPermanentlyDeletedException.php b/Classes/Exception/MediaPermanentlyDeletedException.php index c513ed2..5325528 100644 --- a/Classes/Exception/MediaPermanentlyDeletedException.php +++ b/Classes/Exception/MediaPermanentlyDeletedException.php @@ -7,6 +7,6 @@ /** * Exception thrown if a QBank media is permanently deleted. */ -class MediaPermanentlyDeletedException extends \RuntimeException -{ -} +// phpcs:disable +class MediaPermanentlyDeletedException extends \RuntimeException {} +// phpcs:enable diff --git a/Classes/Exception/MissingFilePropertyException.php b/Classes/Exception/MissingFilePropertyException.php index b06ba39..8a477da 100644 --- a/Classes/Exception/MissingFilePropertyException.php +++ b/Classes/Exception/MissingFilePropertyException.php @@ -7,6 +7,6 @@ /** * Exception thrown if a file property is missing or doesn't exist. */ -class MissingFilePropertyException extends \RuntimeException -{ -} +// phpcs:disable +class MissingFilePropertyException extends \RuntimeException {} +// phpcs:enable diff --git a/Classes/Exception/PersistMetaDataChangesException.php b/Classes/Exception/PersistMetaDataChangesException.php index a038fcc..d1d9a1e 100644 --- a/Classes/Exception/PersistMetaDataChangesException.php +++ b/Classes/Exception/PersistMetaDataChangesException.php @@ -7,6 +7,6 @@ /** * Exception thrown if errors were found in DataHandler when MetaData was updated. */ -class PersistMetaDataChangesException extends \RuntimeException -{ -} +// phpcs:disable +class PersistMetaDataChangesException extends \RuntimeException {} +// phpcs:enable diff --git a/Classes/Exception/ReplaceLocalMediaException.php b/Classes/Exception/ReplaceLocalMediaException.php index 18e5fa7..e06ccd1 100644 --- a/Classes/Exception/ReplaceLocalMediaException.php +++ b/Classes/Exception/ReplaceLocalMediaException.php @@ -7,6 +7,6 @@ /** * Exception thrown if errors were found in DataHandler when Local Media was replaced. */ -class ReplaceLocalMediaException extends \RuntimeException -{ -} +// phpcs:disable +class ReplaceLocalMediaException extends \RuntimeException {} +// phpcs:enable diff --git a/Classes/FormEngine/Container/QbankSelectorButtonContainer.php b/Classes/FormEngine/Container/QbankSelectorButtonContainer.php deleted file mode 100644 index 9169090..0000000 --- a/Classes/FormEngine/Container/QbankSelectorButtonContainer.php +++ /dev/null @@ -1,230 +0,0 @@ -renderQbankButton($inlineConfiguration); - - // Inject button before help-block - if (strpos($selector, '
') > 0) { - $selector = str_replace( - '
', - $button . '
', - $selector - ); - // Try to inject it into the form-control container - } elseif (preg_match('/<\/div><\/div>$/i', $selector)) { - $selector = preg_replace('/<\/div><\/div>$/i', $button . '
', $selector); - } else { - $selector .= $button; - } - - return $selector; - } - - /** - * @param array $inlineConfiguration - * @return string - */ - protected function renderQbankButton(array $inlineConfiguration): string - { - if (QbankUtility::getDownloadFolder() === null) { - return ''; - } - - try { - $accessToken = QbankUtility::getAccessToken(); - } catch (\Throwable $th) { - return ''; - } - - $this->addJavaScriptConfiguration($accessToken); - $this->javaScriptLocalization(); - - $appearanceConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config']['appearance']; - - $allowed = $appearanceConfiguration['qbankBrowserAllowed'] ?? $appearanceConfiguration['elementBrowserAllowed']; - - $allowedArray = GeneralUtility::trimExplode(',', $allowed, true); - if (empty($allowedArray)) { - $allowedArray = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], true); - } - $allowed = implode(',', $allowedArray); - - $currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix( - $this->data['inlineFirstPid'] - ); - $objectPrefix = $currentStructureDomObjectIdPrefix . '-' . $inlineConfiguration['foreign_table']; - - $this->requireJsModules[] = 'TYPO3/CMS/Qbank/Qbank'; - - $buttonLabel = htmlspecialchars(LocalizationUtility::translate('selector-button-control.label', 'qbank')); - $titleText = htmlspecialchars(LocalizationUtility::translate('selector-button-control.title', 'qbank')); - - $button = ' - '; - - return $button; - } - - /** - * Adds localization string for JavaScript use. - * - * @return void - */ - protected function javaScriptLocalization(): void - { - /** @var PageRenderer $pageRenderer */ - $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); - - $pageRenderer->addInlineLanguageLabelArray([ - 'qbank.modal.error-title' => $this->translate('js.modal.error-title'), - 'qbank.modal.request-failed' => $this->translate('js.modal.request-failed'), - 'qbank.modal.request-failed-error' => $this->translate('js.modal.request-failed-error'), - 'qbank.modal.illegal-extension' => $this->translate('js.modal.illegal-extension'), - ]); - } - - /** - * Populates a configuration array that will be available in JavaScript as TYPO3.settings.FormEngineInline.qbank. - * - * Properties: - * - * token - The qbank access token - * - * @param string $accessToken - * @return void - */ - protected function addJavaScriptConfiguration(string $accessToken): void - { - $extensionConfigurationManager = $this->getExtensionConfigurationManager(); - - $configuration = [ - 'token' => $accessToken, - 'host' => $extensionConfigurationManager->getHost(), - 'deploymentSites' => $extensionConfigurationManager->getDeploymentSites(), - ]; - - /** @var PageRenderer $pageRenderer */ - $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); - - $pageRenderer->addInlineSettingArray('qbank', $configuration); - } - - /** - * Returns a translated string for $key. - * - * @param string $key - * @return string|null - */ - protected function translate(string $key): ?string - { - return LocalizationUtility::translate($key, 'qbank'); - } - - /** - * Get an instance of this extension's configuration manager. - * - * @return ExtensionConfigurationManager - */ - protected function getExtensionConfigurationManager(): ExtensionConfigurationManager - { - /** @var ExtensionConfigurationManager $extensionConfigurationManager */ - $extensionConfigurationManager = GeneralUtility::makeInstance(ExtensionConfigurationManager::class); - - $languageField = $GLOBALS['TCA'][$this->data['tableName']]['ctrl']['languageField']; - - $languageId = -1; - if ($languageField) { - if (CompatibilityUtility::typo3VersionIsLessThan('11')) { - $languageId = (int)$this->data['databaseRow'][$languageField][0]; - } else { - $languageId = (int)$this->data['databaseRow'][$languageField]; - } - } - - $pageId = $this->data['defaultLanguageRow']['pid'] ?? null; - if ($this->data['tableName'] === 'pages') { - $pageId = $this->data['defaultLanguageRow']['uid'] ?? null; - } - - if ($pageId === null) { - $pageId = $this->data['databaseRow']['pid']; - if ($this->data['tableName'] === 'pages') { - $pageId = $this->data['databaseRow']['uid']; - } - } - - $extensionConfigurationManager->configureForPage( - (int)$pageId, - $languageId - ); - - return $extensionConfigurationManager; - } - - /** - * Generate inline style attribute if needed. - * Partly based on render function of - * TYPO3\CMS\Backend\Form\Container\InlineControlContainer. - * - * @return string - */ - protected function inlineStyleAttribute(): string - { - $inlineStyles = []; - $parameterArray = $this->data['parameterArray']; - - $config = $parameterArray['fieldConf']['config']; - $isReadOnly = isset($config['readOnly']) && $config['readOnly']; - - $numberOfFullyLocalizedChildren = 0; - foreach ($parameterArray['fieldConf']['children'] as $child) { - if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) { - $numberOfFullyLocalizedChildren++; - } - } - - if ($isReadOnly || $numberOfFullyLocalizedChildren >= $config['maxitems']) { - $inlineStyles[] = 'display: none'; - } - - if (count($inlineStyles) > 0) { - return 'style="' . implode(';', $inlineStyles) . '"'; - } - - return ''; - } -} diff --git a/Classes/Repository/MappingRepository.php b/Classes/Repository/MappingRepository.php index 3563f40..7f3190e 100644 --- a/Classes/Repository/MappingRepository.php +++ b/Classes/Repository/MappingRepository.php @@ -8,6 +8,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; use TYPO3\CMS\Core\Utility\GeneralUtility; class MappingRepository @@ -19,9 +20,9 @@ class MappingRepository * * @return array */ - public function findAll(): array + public function findAll(bool $includeHiddenRecords = true): array { - $resultStatement = $this->getQueryBuilder()->execute(); + $resultStatement = $this->getQueryBuilder($includeHiddenRecords)->execute(); if (!method_exists($resultStatement, 'fetchAllAssociative')) { return $resultStatement->fetchAll(FetchMode::ASSOCIATIVE); @@ -35,9 +36,9 @@ public function findAll(): array * * @return array */ - public function findAllAsKeyValuePairs(): array + public function findAllAsKeyValuePairs(bool $includeHiddenRecords = true): array { - $rows = $this->findAll(); + $rows = $this->findAll($includeHiddenRecords); $pairs = []; foreach ($rows as $row) { @@ -50,13 +51,19 @@ public function findAllAsKeyValuePairs(): array /** * @return QueryBuilder */ - protected function getQueryBuilder(): QueryBuilder + protected function getQueryBuilder(bool $includeHiddenRecords = true): QueryBuilder { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable(self::TABLE_NAME); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + + if (!$includeHiddenRecords) { + $queryBuilder->getRestrictions() + ->add(GeneralUtility::makeInstance(HiddenRestriction::class)); + } + $queryBuilder ->select('*') ->from(self::TABLE_NAME) diff --git a/Classes/Repository/PropertyTypeRepository.php b/Classes/Repository/PropertyTypeRepository.php index 9e0d358..3f999de 100644 --- a/Classes/Repository/PropertyTypeRepository.php +++ b/Classes/Repository/PropertyTypeRepository.php @@ -11,12 +11,10 @@ class PropertyTypeRepository extends AbstractRepository /** * @var array */ - protected static $propertyTypeCache = null; + protected static $propertyTypeCache; /** * Initialize PropertyTypeCache and store them with SystemName as key. - * - * @return void */ protected function initializePropertyTypeCache(): void { diff --git a/Classes/Repository/QbankFileRepository.php b/Classes/Repository/QbankFileRepository.php index 1d9937f..38b4c43 100644 --- a/Classes/Repository/QbankFileRepository.php +++ b/Classes/Repository/QbankFileRepository.php @@ -55,9 +55,7 @@ public function findAll(): array $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) ) ) - ->orderBy('sys_file.tx_qbank_file_timestamp') - ->addOrderBy('sys_file.modification_date') - ->execute(); + ->orderBy('sys_file.tx_qbank_file_timestamp')->addOrderBy('sys_file.modification_date')->executeQuery(); if (!method_exists($resultStatement, 'fetchAllAssociative')) { return $resultStatement->fetchAll(FetchMode::ASSOCIATIVE); @@ -98,9 +96,7 @@ public function fetchStatusUpdateQueue(int $limit, int $interval): array $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) ) ) - ->setMaxResults($limit) - ->orderBy('sys_file.tx_qbank_status_updated_timestamp') - ->execute(); + ->setMaxResults($limit)->orderBy('sys_file.tx_qbank_status_updated_timestamp')->executeQuery(); if (!method_exists($resultStatement, 'fetchAllAssociative')) { return $resultStatement->fetchAll(FetchMode::ASSOCIATIVE); @@ -135,12 +131,12 @@ public function fetchFilesToUpdate(int $limit): array ) ) ->andWhere( - $queryBuilder->expr()->orX( + $queryBuilder->expr()->or( $queryBuilder->expr()->gt( 'tx_qbank_remote_change_timestamp', 'tx_qbank_metadata_timestamp' ), - $queryBuilder->expr()->andX( + $queryBuilder->expr()->and( $queryBuilder->expr()->gt( 'tx_qbank_remote_replaced_by', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) @@ -152,9 +148,7 @@ public function fetchFilesToUpdate(int $limit): array ) ) ) - ->setMaxResults($limit) - ->orderBy('sys_file.tx_qbank_status_updated_timestamp') - ->execute(); + ->setMaxResults($limit)->orderBy('sys_file.tx_qbank_status_updated_timestamp')->executeQuery(); if (!method_exists($resultStatement, 'fetchAllAssociative')) { return $resultStatement->fetchAll(FetchMode::ASSOCIATIVE); diff --git a/Classes/Repository/SysFileReferenceRepository.php b/Classes/Repository/SysFileReferenceRepository.php index 5393b01..789ab3d 100644 --- a/Classes/Repository/SysFileReferenceRepository.php +++ b/Classes/Repository/SysFileReferenceRepository.php @@ -32,13 +32,7 @@ public function fetchRawSysFileReferencesByFileId($fileId): ?array $queryBuilder->createNamedParameter($fileId, \PDO::PARAM_INT) ) ) - ->andWhere( - $queryBuilder->expr()->eq( - 'table_local', - $queryBuilder->createNamedParameter('sys_file', \PDO::PARAM_STR) - ) - ) - ->execute(); + ->executeQuery(); if (!method_exists($fileReferenceData, 'fetchAllAssociative')) { return $fileReferenceData->fetchAll(FetchMode::ASSOCIATIVE); diff --git a/Classes/Service/Event/AfterFilePropertyChangesEventHandler/PersistMetaDataChanges.php b/Classes/Service/Event/AfterFilePropertyChangesEventHandler/PersistMetaDataChanges.php index b88fdbc..f47e5a0 100644 --- a/Classes/Service/Event/AfterFilePropertyChangesEventHandler/PersistMetaDataChanges.php +++ b/Classes/Service/Event/AfterFilePropertyChangesEventHandler/PersistMetaDataChanges.php @@ -6,6 +6,7 @@ use Pixelant\Qbank\Exception\PersistMetaDataChangesException; use Pixelant\Qbank\Service\Event\AfterFilePropertyChangesEvent; +use Pixelant\Qbank\Service\Event\AfterFilePropertyChangesEventHandlerInterface; use Pixelant\Qbank\Service\Event\FilePropertyChangeEventHandler\MetaDataFilePropertyChangeEventHandler; use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -13,7 +14,7 @@ /** * Persist the changes that have previously been made in the MetaDatafilePropertyChangeEventHandler. */ -class PersistMetaDataChanges implements \Pixelant\Qbank\Service\Event\AfterFilePropertyChangesEventHandlerInterface +class PersistMetaDataChanges implements AfterFilePropertyChangesEventHandlerInterface { /** * {@inheritdoc} diff --git a/Classes/Service/Event/CollectMediaPropertiesEventHandler/BaseMediaPropertiesCollector.php b/Classes/Service/Event/CollectMediaPropertiesEventHandler/BaseMediaPropertiesCollector.php index a60d103..8dd09d3 100644 --- a/Classes/Service/Event/CollectMediaPropertiesEventHandler/BaseMediaPropertiesCollector.php +++ b/Classes/Service/Event/CollectMediaPropertiesEventHandler/BaseMediaPropertiesCollector.php @@ -7,12 +7,14 @@ use Pixelant\Qbank\Domain\Model\Qbank\BaseMediaProperty; use Pixelant\Qbank\Domain\Model\Qbank\MediaProperty; use Pixelant\Qbank\Service\Event\CollectMediaPropertiesEvent; +use Pixelant\Qbank\Service\Event\CollectMediaPropertiesEventHandlerInterface; use QBNK\QBank\API\Model\PropertyType; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; /** * Adds base properties of a Qbank media object, such as "uploaded" or "name". */ -class BaseMediaPropertiesCollector implements \Pixelant\Qbank\Service\Event\CollectMediaPropertiesEventHandlerInterface +class BaseMediaPropertiesCollector implements CollectMediaPropertiesEventHandlerInterface { /** * {@inheritdoc} @@ -35,32 +37,32 @@ public static function getProperties(): array new BaseMediaProperty( PropertyType::DATATYPE_STRING, 'name', - $ll . 'name' + LocalizationUtility::translate($ll . 'name', 'qbank') ), new BaseMediaProperty( PropertyType::DATATYPE_STRING, 'filename', - $ll . 'filename' + LocalizationUtility::translate($ll . 'filename', 'qbank') ), new BaseMediaProperty( PropertyType::DATATYPE_DATETIME, 'created', - $ll . 'created' + LocalizationUtility::translate($ll . 'created', 'qbank') ), new BaseMediaProperty( PropertyType::DATATYPE_DATETIME, 'updated', - $ll . 'updated' + LocalizationUtility::translate($ll . 'updated', 'qbank') ), new BaseMediaProperty( PropertyType::DATATYPE_DATETIME, 'uploaded', - $ll . 'uploaded' + LocalizationUtility::translate($ll . 'uploaded', 'qbank') ), new BaseMediaProperty( PropertyType::DATATYPE_INTEGER, 'rating', - $ll . 'rating' + LocalizationUtility::translate($ll . 'rating', 'qbank') ), ]; } diff --git a/Classes/Service/Event/FileReferenceUrlEvent.php b/Classes/Service/Event/FileReferenceUrlEvent.php index c909df2..058bfe3 100644 --- a/Classes/Service/Event/FileReferenceUrlEvent.php +++ b/Classes/Service/Event/FileReferenceUrlEvent.php @@ -16,7 +16,7 @@ class FileReferenceUrlEvent implements StoppableEventInterface /** * @var string|null */ - protected $url = null; + protected $url; /** * @var FileReference diff --git a/Classes/Service/Event/ResolvePageTitleEvent.php b/Classes/Service/Event/ResolvePageTitleEvent.php index 2b16fc2..fa40d18 100644 --- a/Classes/Service/Event/ResolvePageTitleEvent.php +++ b/Classes/Service/Event/ResolvePageTitleEvent.php @@ -16,7 +16,7 @@ class ResolvePageTitleEvent implements StoppableEventInterface /** * @var string|null */ - protected $title = null; + protected $title; /** * @var string diff --git a/Classes/Service/QbankService.php b/Classes/Service/QbankService.php index 9bfd8ed..a2a6147 100644 --- a/Classes/Service/QbankService.php +++ b/Classes/Service/QbankService.php @@ -7,6 +7,7 @@ use Pixelant\Qbank\Configuration\ExtensionConfigurationManager; use Pixelant\Qbank\Domain\Model\Qbank\MediaProperty; use Pixelant\Qbank\Domain\Model\Qbank\MediaPropertyValue; +use Pixelant\Qbank\Exception\MediaPermanentlyDeletedException; use Pixelant\Qbank\Exception\ReplaceLocalMediaException; use Pixelant\Qbank\Repository\MappingRepository; use Pixelant\Qbank\Repository\MediaRepository; @@ -30,6 +31,7 @@ use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\EventDispatcher\EventDispatcher; use TYPO3\CMS\Core\Exception\SiteNotFoundException; +use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\ResourceFactory; @@ -89,7 +91,7 @@ public function __construct( * * @param int $id * @return File The local file representation - * @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException + * @throws FileDoesNotExistException */ public function createLocalMediaCopy(int $id): ?File { @@ -122,7 +124,7 @@ public function createLocalMediaCopy(int $id): ?File * * @param int $id * @return File|null - * @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException + * @throws FileDoesNotExistException */ protected function findLocalMediaCopy(int $id): ?File { @@ -131,12 +133,14 @@ protected function findLocalMediaCopy(int $id): ?File $fileUid = $queryBuilder ->select('uid') ->from('sys_file') - ->where($queryBuilder->expr()->eq( - 'tx_qbank_id', - $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT) - )) - ->execute() - ->fetchColumn(0); + ->where( + $queryBuilder->expr()->eq( + 'tx_qbank_id', + $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); if ($fileUid === false) { return null; @@ -226,7 +230,7 @@ public function removeMediaUsageInFileReference(int $fileReferenceId): void * Synchronize metadata for a particular file UID. * * @param int $fileId The FAL file UID - * @throws \Pixelant\Qbank\Exception\MediaPermanentlyDeletedException + * @throws MediaPermanentlyDeletedException */ public function synchronizeMetadata(int $fileId): void { @@ -243,14 +247,14 @@ public function synchronizeMetadata(int $fileId): void if (QbankUtility::qbankRequestExceptionStatesMediaIsDeleted($re)) { $this->updateFileRemoteIsDeleted($fileId); - throw new \Pixelant\Qbank\Exception\MediaPermanentlyDeletedException( + throw new MediaPermanentlyDeletedException( 'QBank Media is permanently deleted', 1625149218 ); } } - $metaDataMappings = GeneralUtility::makeInstance(MappingRepository::class)->findAllAsKeyValuePairs(); + $metaDataMappings = GeneralUtility::makeInstance(MappingRepository::class)->findAllAsKeyValuePairs(false); $qbankPropertyValues = $this ->eventDispatcher @@ -288,14 +292,14 @@ public function synchronizeMetadata(int $fileId): void * Synchronize file content for a particular file UID. * * @param int $fileId The FAL file UID - * @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException + * @throws FileDoesNotExistException * @throws ReplaceLocalMediaException */ public function replaceLocalMedia(int $fileId): void { $file = $this->resourceFactory->getFileObject($fileId); if ($file === null) { - throw new \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException( + throw new FileDoesNotExistException( 'No file found for given UID: ' . $fileId, 1623070299 ); @@ -305,7 +309,7 @@ public function replaceLocalMedia(int $fileId): void $replacedByFile = $this->createLocalMediaCopy($replacedByMediaIdentifier); if ($replacedByFile === null) { - throw new \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException( + throw new FileDoesNotExistException( 'No file found for given QBank id: ' . $replacedByMediaIdentifier, 1623306399 ); diff --git a/Classes/TypeConverter/Exception/InvalidTypeConverterException.php b/Classes/TypeConverter/Exception/InvalidTypeConverterException.php index 3bf41a5..daffbcc 100644 --- a/Classes/TypeConverter/Exception/InvalidTypeConverterException.php +++ b/Classes/TypeConverter/Exception/InvalidTypeConverterException.php @@ -9,6 +9,6 @@ /** * Exception thrown if a TypeConverter is invalid. */ -class InvalidTypeConverterException extends Exception -{ -} +// phpcs:disable +class InvalidTypeConverterException extends Exception {} +// phpcs:enable diff --git a/Classes/Utility/PropertyUtility.php b/Classes/Utility/PropertyUtility.php index 8260a19..eb4c158 100644 --- a/Classes/Utility/PropertyUtility.php +++ b/Classes/Utility/PropertyUtility.php @@ -54,7 +54,7 @@ public static function getTypeConverterForFileProperty(string $filePropertyName) return self::$filePropertyTypeConverterCache[$filePropertyName]; } - $propertyConfiguration = self::getFileProperties()[$filePropertyName]; + $propertyConfiguration = self::getFileProperties()[$filePropertyName] ?? null; if (!isset($propertyConfiguration['typeConverter'])) { throw new InvalidTypeConverterException( @@ -137,7 +137,7 @@ public static function getQbankPropertyTypeIdForSystemName(string $systemName): */ public static function getLabelForFileProperty(string $filePropertyName): string { - $label = self::getFileProperties()[$filePropertyName]['label']; + $label = self::getFileProperties()[$filePropertyName]['label'] ?? null; if ($label === null) { throw new MissingFilePropertyException( diff --git a/Classes/Utility/QbankUtility.php b/Classes/Utility/QbankUtility.php index c9a0566..4594af9 100644 --- a/Classes/Utility/QbankUtility.php +++ b/Classes/Utility/QbankUtility.php @@ -23,7 +23,7 @@ class QbankUtility /** * @var QBankApi|null */ - protected static $api = null; + protected static $api; /** * Returns the download folder object if it is writeable and accessible for the current backend user. The folder is diff --git a/Classes/Xclass/Form/Container/FilesControlContainer.php b/Classes/Xclass/Form/Container/FilesControlContainer.php new file mode 100644 index 0000000..b005e83 --- /dev/null +++ b/Classes/Xclass/Form/Container/FilesControlContainer.php @@ -0,0 +1,143 @@ +inlineStackProcessor->getCurrentStructureDomObjectIdPrefix( + $this->data['inlineFirstPid'] + ); + $objectPrefix = $currentStructureDomObjectIdPrefix . '-' . self::FILE_REFERENCE_TABLE; + + $languageService = $this->getLanguageService(); + $extensionManager = $this->getExtensionConfigurationManager(); + + $lllExtPath = 'LLL:EXT:qbank/Resources/Private/Language/locallang.xlf'; + $buttonText = $lllExtPath . ':selector-button-control.label'; + $titleText = $lllExtPath . ':selector-button-control.title'; + $modalErrorTitle = $lllExtPath . ':js.modal.error-title'; + $modalRequestFailed = $lllExtPath . ':js.modal.request-failed'; + $modalRequestFailedError = $lllExtPath . ':js.modal.request-failed-error'; + $modalIllegalExtension = $lllExtPath . ':js.modal.illegal-extension'; + + $buttonText = $languageService->sL($buttonText); + $titleText = $languageService->sL($titleText); + $modalErrorTitle = $languageService->sL($modalErrorTitle); + $modalRequestFailed = $languageService->sL($modalRequestFailed); + $modalRequestFailedError = $languageService->sL($modalRequestFailedError); + $modalIllegalExtension = $languageService->sL($modalIllegalExtension); + + $attributes = [ + 'type' => 'button', + 'class' => 'btn btn-default t3js-qbank-media-add-btn', + 'style' => !($inlineConfiguration['inline']['showCreateNewRelationButton'] ?? true) ? 'display: none;' : '', + 'title' => $titleText, + 'data-file-irre-object' => htmlspecialchars($objectPrefix), + 'data-file-allowed' => htmlspecialchars( + implode(',', $fileExtensionFilter->getAllowedFileExtensions() ?? []) + ), + 'data-qbank-host' => $extensionManager->getHost(), + 'data-qbank-deploymentsites' => implode(',', $extensionManager->getDeploymentSites() ?? []), + 'data-qbank-token' => $accessToken, + 'data-modal-error-title' => htmlspecialchars($modalErrorTitle), + 'data-modal-request-failed' => htmlspecialchars($modalRequestFailed), + 'data-modal-request-failed-error' => htmlspecialchars($modalRequestFailedError), + 'data-modal-illegal-extension' => $modalIllegalExtension, + ]; + $controls[] = ' + '; + + $this->javaScriptModules[] = JavaScriptModuleInstruction::create('@pixelant/qbank/qbank-media.js'); + + return $controls; + } + + /** + * Get an instance of this extension's configuration manager. + * + * @return ExtensionConfigurationManager + */ + protected function getExtensionConfigurationManager(): ExtensionConfigurationManager + { + /** @var ExtensionConfigurationManager $extensionConfigurationManager */ + $extensionConfigurationManager = GeneralUtility::makeInstance(ExtensionConfigurationManager::class); + + $languageField = $GLOBALS['TCA'][$this->data['tableName']]['ctrl']['languageField']; + + $languageId = -1; + if ($languageField) { + $languageId = (int)$this->data['databaseRow'][$languageField]; + } + + $pageId = $this->data['defaultLanguageRow']['pid'] ?? null; + if ($this->data['tableName'] === 'pages') { + $pageId = $this->data['defaultLanguageRow']['uid'] ?? null; + } + + if ($pageId === null) { + $pageId = $this->data['databaseRow']['pid']; + if ($this->data['tableName'] === 'pages') { + $pageId = $this->data['databaseRow']['uid']; + } + } + + $extensionConfigurationManager->configureForPage( + (int)$pageId, + $languageId + ); + + return $extensionConfigurationManager; + } +} diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php new file mode 100644 index 0000000..07b5dc6 --- /dev/null +++ b/Configuration/Backend/Modules.php @@ -0,0 +1,27 @@ + [ + 'parent' => 'file', + 'position' => ['after' => 'file_list'], + 'access' => 'group,user', + 'workspaces' => 'live', + 'path' => '/module/file/qbank', + 'icon' => 'EXT:qbank/Resources/Public/Icons/Extension.svg', + 'labels' => 'LLL:EXT:qbank/Resources/Private/Language/locallang_mod_qbank.xlf', + 'navigationComponentId' => '', + 'inheritNavigationComponentFromMainModule' => false, + 'extensionName' => 'Qbank', + 'controllerActions' => [ + ManagementController::class => [ + 'list', + 'mappings', + 'overview', + 'synchronizeMetadata', + 'replaceLocalMedia', + ], + ], + ], +]; diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..e5b4eda --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,12 @@ + ['backend'], + 'imports' => [ + // recursive definiton, all *.js files in this folder are import-mapped + // trailing slash is required per importmap-specification + '@pixelant/qbank/' => 'EXT:qbank/Resources/Public/JavaScript/', + ], +]; diff --git a/Configuration/TCA/tx_qbank_domain_model_mapping.php b/Configuration/TCA/tx_qbank_domain_model_mapping.php index caa07be..2d80bbc 100644 --- a/Configuration/TCA/tx_qbank_domain_model_mapping.php +++ b/Configuration/TCA/tx_qbank_domain_model_mapping.php @@ -7,7 +7,6 @@ 'label_alt' => 'target_property', 'label_alt_force' => true, 'crdate' => 'createdon', - 'cruser_id' => 'createdby', 'tstamp' => 'updatedon', 'versioningWS' => false, 'default_sortby' => 'source_property, target_property', @@ -51,7 +50,7 @@ 'renderType' => 'checkboxToggle', 'items' => [ [ - 0 => '', + 'label' => '', 1 => '', 'invertStateDisplay' => true, ], @@ -91,7 +90,7 @@ 'size' => 1, 'default' => '', 'items' => [ - ['', ''], + ['label' => '', 'value' => ''], ], 'itemsProcFunc' => Pixelant\Qbank\FormEngine\QbankProperyItemsProcFunc::class . '->itemsProcFunc', ], @@ -107,7 +106,7 @@ 'size' => 1, 'default' => '', 'items' => [ - ['', ''], + ['label' => '', 'value' => ''], ], 'itemsProcFunc' => Pixelant\Qbank\FormEngine\Typo3ProperyItemsProcFunc::class . '->itemsProcFunc', ], diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index 614014f..eb40e5e 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -28,7 +28,9 @@ - + + QBank + Overview diff --git a/Resources/Private/Language/locallang_module_qbank.xlf b/Resources/Private/Language/locallang_mod_qbank.xlf similarity index 100% rename from Resources/Private/Language/locallang_module_qbank.xlf rename to Resources/Private/Language/locallang_mod_qbank.xlf diff --git a/Resources/Private/Layouts/Default.html b/Resources/Private/Layouts/Default.html index 9303b72..24f730c 100644 --- a/Resources/Private/Layouts/Default.html +++ b/Resources/Private/Layouts/Default.html @@ -1 +1 @@ - + diff --git a/Resources/Private/Templates/Management/List.html b/Resources/Private/Templates/Management/List.html index 11b92f7..2823c2a 100644 --- a/Resources/Private/Templates/Management/List.html +++ b/Resources/Private/Templates/Management/List.html @@ -1,11 +1,13 @@ - + - + - +

@@ -33,7 +35,7 @@

- + @@ -44,17 +46,17 @@

- {qbankFile.tx_qbank_file_timestamp} + {qbankFile.tx_qbank_file_timestamp} - {qbankFile.tx_qbank_metadata_timestamp} + {qbankFile.tx_qbank_metadata_timestamp} - - {qbankFile.tx_qbank_remote_change_timestamp} + {qbankFile.tx_qbank_remote_change_timestamp} - @@ -71,7 +73,7 @@

- {qbankFile.tx_qbank_status_updated_timestamp} + {qbankFile.tx_qbank_status_updated_timestamp} - @@ -81,15 +83,13 @@

- - - - - + @@ -103,7 +103,6 @@

class="btn btn-success"> {f:translate(key: 'be.action.update-metadata')} - + - + - +

- @@ -21,8 +22,8 @@

- - + + @@ -34,7 +35,7 @@

\n\t\n\t\n\t\n\t\n\t\n'},useData:!0})},{"hbsfy/runtime":49}],39:[function(a,b,c){var d=a("hbsfy/runtime");b.exports=d.template({1:function(a,b,c,d){return'\t\t\t\n'},compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f,g,h="function",i=b.helperMissing,j=this.escapeExpression,k=this.lambda,l=b.blockHelperMissing,m='
  • \n\t

    '+j((f=null!=(f=b.name||(null!=a?a.name:a))?f:i,typeof f===h?f.call(a,{name:"name",hash:{},data:d}):f))+'

    \n\t
    \n';return e=this.invokePartial(c.miniloader,"\t\t","miniloader",a,void 0,b,c,d),null!=e&&(m+=e),m+='\t
    \n\t\n\t\t\n',f=null!=(f=b.isDuplicate||(null!=a?a.isDuplicate:a))?f:i,g={name:"isDuplicate",hash:{},fn:this.program(1,d),inverse:this.noop,data:d},e=typeof f===h?f.call(a,g):f,b.isDuplicate||(e=l.call(a,e,g)),null!=e&&(m+=e),m+'\t\n\t'+j((f=null!=(f=b.getImageSize||(null!=a?a.getImageSize:a))?f:i,typeof f===h?f.call(a,{name:"getImageSize",hash:{},data:d}):f))+'\n\t\n\t\t\n\t\t
    \n\t\t\t View\n\t\t
    \n\t
    \n
  • '},usePartial:!0,useData:!0})},{"hbsfy/runtime":49}],40:[function(a,b,c){var d=a("hbsfy/runtime");b.exports=d.template({1:function(a,b,c,d){var e,f=b.helperMissing;return'\t
      \n'},3:function(a,b,c,d,e){var f,g,h,i="function",j=b.helperMissing,k=this.escapeExpression,l=b.blockHelperMissing,m=this.lambda,n='\t\t
    • \n\t\t\t

      \n';return g=null!=(g=b.isDuplicate||(null!=a?a.isDuplicate:a))?g:j,h={name:"isDuplicate",hash:{},fn:this.program(4,d,e),inverse:this.noop,data:d},f=typeof g===i?g.call(a,h):g,b.isDuplicate||(f=l.call(a,f,h)),null!=f&&(n+=f),n+="\t\t\t\t"+k((g=null!=(g=b.name||(null!=a?a.name:a))?g:j,typeof g===i?g.call(a,{name:"name",hash:{},data:d}):g))+'\n\t\t\t

      \n\t\t\t
      \n',g=null!=(g=b.ifMediaHasThumb||(null!=a?a.ifMediaHasThumb:a))?g:j,h={name:"ifMediaHasThumb",hash:{},fn:this.program(6,d,e),inverse:this.program(11,d,e),data:d},f=typeof g===i?g.call(a,h):g,b.ifMediaHasThumb||(f=l.call(a,f,h)),null!=f&&(n+=f),n+='\t\t\t
      \n\t\t\t\n\t\t\t\t\n\n',f=(b.isChild||a&&a.isChild||j).call(a,null!=a?a.mediaId:a,null!=a?a.parentId:a,{name:"isChild",hash:{},fn:this.program(13,d,e),inverse:this.noop,data:d}),null!=f&&(n+=f),n+="\n",f=(b.isParent||a&&a.isParent||j).call(a,null!=a?a.mediaId:a,null!=a?a.parentId:a,{name:"isParent",hash:{},fn:this.program(13,d,e),inverse:this.noop,data:d}),null!=f&&(n+=f),n+='\t\t\t\n\t\t\t\n',f=b.if.call(a,null!=(f=null!=(f=null!=(f=null!=e[1]?e[1].picker:e[1])?f.modules:f)?f.searchResult:f)?f.showAssetDimensions:f,{name:"if",hash:{},fn:this.program(15,d,e),inverse:this.program(17,d,e),data:d}),null!=f&&(n+=f),n+='\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t
      \n\t\t\t\t\t '+k(m(null!=(f=null!=e[1]?e[1].lexicon:e[1])?f.view:f,a))+"\n",g=null!=(g=b.ifMediaIsImage||(null!=a?a.ifMediaIsImage:a))?g:j,h={name:"ifMediaIsImage",hash:{},fn:this.program(19,d,e),inverse:this.noop,data:d},f=typeof g===i?g.call(a,h):g,b.ifMediaIsImage||(f=l.call(a,f,h)),null!=f&&(n+=f),f=b.if.call(a,null!=(f=null!=(f=null!=(f=null!=e[1]?e[1].picker:e[1])?f.modules:f)?f.searchResult:f)?f.showUseButton:f,{name:"if",hash:{},fn:this.program(22,d,e),inverse:this.noop,data:d}),null!=f&&(n+=f),n+"\t\t\t\t
      \n\t\t\t
      \n\t\t
    • \n"},4:function(a,b,c,d){return'\t\t\t\t\t\n'},6:function(a,b,c,d,e){var f,g="";return f=b.if.call(a,null!=e[2]?e[2].deploymentSite:e[2],{name:"if",hash:{},fn:this.program(7,d,e),inverse:this.program(9,d,e),data:d}),null!=f&&(g+=f),g},7:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'\t\t\t\t\t\t'+h((e=null!=(e=b.name||(null!=a?a.name:a))?e:g,typeof e===f?e.call(a,{name:\n'},9:function(a,b,c,d,e){var f,g,h=this.lambda,i=this.escapeExpression,j="function",k=b.helperMissing;return'\t\t\t\t\t\t'+i((g=null!=(g=b.filename||(null!=a?a.filename:a))?g:k,typeof g===j?g.call(a,{name:\n'},11:function(a,b,c,d){var e,f=this.lambda;return'\t\t\t\t\t\n'},13:function(a,b,c,d){return'\t\t\t\t\t\n'},15:function(a,b,c,d){var e,f=b.helperMissing;return"\t\t\t\t\t"+(0,this.escapeExpression)((e=null!=(e=b.getImageSize||(null!=a?a.getImageSize:a))?e:f,"function"==typeof e?e.call(a,{name:"getImageSize",hash:{},data:d}):e))+"\n"},17:function(a,b,c,d){return"\t\t\t\t\t \n"},19:function(a,b,c,d,e){var f,g,h,i=b.helperMissing,j=b.blockHelperMissing,k="";return g=null!=(g=b.ifMediaHasPreview||(null!=a?a.ifMediaHasPreview:a))?g:i,h={name:"ifMediaHasPreview",hash:{},fn:this.program(20,d,e),inverse:this.noop,data:d},f="function"==typeof g?g.call(a,h):g,b.ifMediaHasPreview||(f=j.call(a,f,h)),null!=f&&(k+=f),k},20:function(a,b,c,d,e){var f,g,h=b.helperMissing,i=this.escapeExpression,j=this.lambda;return'\t\t\t\t\t\t\t '+i(j(null!=(f=null!=e[3]?e[3].lexicon:e[3])?f.crop:f,a))+"\n"},22:function(a,b,c,d,e){var f,g,h=b.helperMissing,i=this.escapeExpression,j=this.lambda;return'\t\t\t\t\t\t '+i(j(null!=(f=null!=e[2]?e[2].lexicon:e[2])?f.use:f,a))+"\n"},24:function(a,b,c,d){var e,f="";return e=b.unless.call(a,null!=a?a.offset:a,{name:"unless",hash:{},fn:this.program(25,d),inverse:this.noop,data:d}),null!=e&&(f+=e),f},25:function(a,b,c,d){var e,f=this.lambda;return"\t\t\t

      "+(0,this.escapeExpression)(f(null!=(e=null!=a?a.lexicon:a)?e.no_results:e,a))+"

      \n"},27:function(a,b,c,d){return"\t
    \n"},compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d,e){var f,g="";return f=b.unless.call(a,null!=a?a.offset:a,{name:"unless",hash:{},fn:this.program(1,d,e),inverse:this.noop,data:d}),null!=f&&(g+=f),f=b.each.call(a,null!=a?a.results:a,{name:"each",hash:{},fn:this.program(3,d,e),inverse:this.program(24,d,e),data:d}),null!=f&&(g+=f),f=b.unless.call(a,null!=a?a.limit:a,{name:"unless",hash:{},fn:this.program(27,d,e),inverse:this.noop,data:d}),null!=f&&(g+=f),g},useData:!0,useDepths:!0})},{"hbsfy/runtime":49}],41:[function(a,b,c){var d=a("hbsfy/runtime");b.exports=d.template({1:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'\t\t\t\t\t\n"},3:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'\t\t\t\t\t\t\t\n"},compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f=this.lambda,g=this.escapeExpression,h='
    \n\t
    \n\t\t
    \n\t\t'+g(f(null!=(e=null!=a?a.lexicon:a)?e.upload_browse:e,a))+"\n\t\t

    "+g(f(null!=(e=null!=a?a.lexicon:a)?e.upload:e,a))+'

    \n\t
    \n\n\t
    \n\t\t
    \n\t\t\t
    + - + - +

    diff --git a/Resources/Public/JavaScript/Qbank.js b/Resources/Public/JavaScript/Qbank.js deleted file mode 100644 index 035744a..0000000 --- a/Resources/Public/JavaScript/Qbank.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Module: Pixelant/QbankConnector/Qbank - * Qbank API communication - */ -// define(['jquery', 'http://connector.qbank.se/qbank-connector.min.js'], function($, QBankConnector) { -define([ - 'jquery', - 'nprogress', - 'TYPO3/CMS/Backend/Modal', - 'TYPO3/CMS/Backend/Severity', - 'TYPO3/CMS/Backend/Utility/MessageUtility', - 'TYPO3/CMS/Core/Ajax/AjaxRequest', - 'TYPO3/CMS/Core/DocumentService', - 'TYPO3/CMS/Qbank/qbank-connector' -], function( - $, - NProgress, - Modal, Severity, - MessageUtility, - AjaxRequest, - DocumentService -) { - 'use strict'; - - MessageUtility = MessageUtility.MessageUtility; - - var QbankSelectorPlugin = function(element) { - var self = this; - - self.$button = $(element); - self.irreObjectId = self.$button.data('fileIrreObject'); - self.allowedExtensions = self.$button.data('fileAllowed').split(','); - - /** - * Adds QBank media to file IRRE element. - * - * @param media - */ - self.addMedia = function (media) { - NProgress.start(); - - const request = new AjaxRequest(TYPO3.settings.ajaxUrls['qbank_download_file']); - - request.post({ - mediaId: media.mediaId - }).then( - async function(response) { - const data = await response.resolve(); - - if (response.response.status !== 200 || !data.success) { - var errorMessage = TYPO3.lang['qbank.modal.request-failed']; - - if (data.message) { - errorMessage = data.message; - } - - self.displayError(errorMessage); - - NProgress.done(); - - return; - } - - MessageUtility.send({ - actionName: 'typo3:foreignRelation:insert', - objectGroup: self.irreObjectId, - table: 'sys_file', - uid: data.fileUid, - }); - - NProgress.done(); - }, - function (error) { - self.displayError(TYPO3.lang['qbank.modal.request-failed-error'] + error.status + ' ' + error.statusText); - - NProgress.done(); - } - ); - } - - /** - * Displays an error message in a modal. - * - * @param message The error message to display - */ - self.displayError = function (message) { - const errorModal = Modal.confirm( - TYPO3.lang['qbank.modal.error-title'], - message, - Severity.error, - [{ - text: TYPO3.lang['button.ok'] || 'OK', - btnClass: 'btn-' + Severity.getCssClass(Severity.error), - name: 'ok', - active: true, - }] - ).on('confirm.button.ok', function() { - errorModal.modal('hide'); - }); - } - - /** - * Returns true if the media can be inserted. - * - * @param media result from QBank - * @returns {boolean} - */ - self.validateMedia = function (media) { - if (self.allowedExtensions.indexOf(media.extension) === -1) { - self.displayError(TYPO3.lang['qbank.modal.illegal-extension'].replace('{0}', media.extension)); - return false; - } - - return true; - } - - /** - * Opens the QBank selector inside a modal. - */ - self.openModal = function () { - self.$modal = Modal.advanced({ - type: Modal.types.default, - title: 'QBank', - content: '', - severity: Severity.default, - size: Modal.sizes.full, - callback: function(modal) { - self.$qBankIframe = window.QBCJQ('';\n_iframe = temp.firstChild;\ncontainer.appendChild(_iframe);\n\n/* _iframe.onreadystatechange = function() {\nconsole.info(_iframe.readyState);\n};*/\n\nEvents.addEvent(_iframe, 'load', function() { // _iframe.onload doesn't work in IE lte 8\nvar el;\n\ntry {\nel = _iframe.contentWindow.document || _iframe.contentDocument || window.frames[_iframe.id].document;\n\n// try to detect some standard error pages\nif (/^4(0[0-9]|1[0-7]|2[2346])\\s/.test(el.title)) { // test if title starts with 4xx HTTP error\n_status = el.title.replace(/^(\\d+).*$/, '$1');\n} else {\n_status = 200;\n// get result\n_response = Basic.trim(el.body.innerHTML);\n\n// we need to fire these at least once\ntarget.trigger({\ntype: 'progress',\nloaded: _response.length,\ntotal: _response.length\n});\n\nif (blob) { // if we were uploading a file\ntarget.trigger({\ntype: 'uploadprogress',\nloaded: blob.size || 1025,\ntotal: blob.size || 1025\n});\n}\n}\n} catch (ex) {\nif (Url.hasSameOrigin(meta.url)) {\n// if response is sent with error code, iframe in IE gets redirected to res://ieframe.dll/http_x.htm\n// which obviously results to cross domain error (wtf?)\n_status = 404;\n} else {\ncleanup.call(target, function() {\ntarget.trigger('error');\n});\nreturn;\n}\n}\n\ncleanup.call(target, function() {\ntarget.trigger('load');\n});\n}, target.uid);\n} // end createIframe\n\n// prepare data to be sent and convert if required\nif (data instanceof FormData && data.hasBlob()) {\nblob = data.getBlob();\nuid = blob.uid;\ninput = Dom.get(uid);\nform = Dom.get(uid + '_form');\nif (!form) {\nthrow new x.DOMException(x.DOMException.NOT_FOUND_ERR);\n}\n} else {\nuid = Basic.guid('uid_');\n\nform = qbankConnectorIframeDocument.createElement('form');\nform.setAttribute('id', uid + '_form');\nform.setAttribute('method', meta.method);\nform.setAttribute('enctype', 'multipart/form-data');\nform.setAttribute('encoding', 'multipart/form-data');\nform.setAttribute('target', uid + '_iframe');\n\nI.getShimContainer().appendChild(form);\n}\n\nif (data instanceof FormData) {\ndata.each(function(value, name) {\nif (value instanceof Blob) {\nif (input) {\ninput.setAttribute('name', name);\n}\n} else {\nvar hidden = qbankConnectorIframeDocument.createElement('input');\n\nBasic.extend(hidden, {\ntype : 'hidden',\nname : name,\nvalue : value\n});\n\n// make sure that input[type=\"file\"], if it's there, comes last\nif (input) {\nform.insertBefore(hidden, input);\n} else {\nform.appendChild(hidden);\n}\n}\n});\n}\n\n// set destination url\nform.setAttribute(\"action\", meta.url);\n\ncreateIframe();\nform.submit();\ntarget.trigger('loadstart');\n},\n\ngetStatus: function() {\nreturn _status;\n},\n\ngetResponse: function(responseType) {\nif ('json' === responseType) {\n// strip off
    ..
    tags that might be enclosing the response\nif (Basic.typeOf(_response) === 'string' && !!window.JSON) {\ntry {\nreturn JSON.parse(_response.replace(/^\\s*]*>/, '').replace(/<\\/pre>\\s*$/, ''));\n} catch (ex) {\nreturn null;\n}\n}\n} else if ('document' === responseType) {\n\n}\nreturn _response;\n},\n\nabort: function() {\nvar target = this;\n\nif (_iframe && _iframe.contentWindow) {\nif (_iframe.contentWindow.stop) { // FireFox/Safari/Chrome\n_iframe.contentWindow.stop();\n} else if (_iframe.contentWindow.document.execCommand) { // IE\n_iframe.contentWindow.document.execCommand('Stop');\n} else {\n_iframe.src = \"about:blank\";\n}\n}\n\ncleanup.call(this, function() {\n// target.dispatchEvent('readystatechange');\ntarget.dispatchEvent('abort');\n});\n}\n});\n}\n\nreturn (extensions.XMLHttpRequest = XMLHttpRequest);\n});\n\n// Included from: src/javascript/runtime/html4/image/Image.js\n\n/**\n* Image.js\n*\n* Copyright 2013, Moxiecode Systems AB\n* Released under GPL License.\n*\n* License: http://www.plupload.com/license\n* Contributing: http://www.plupload.com/contributing\n*/\n\n/**\n@class moxie/runtime/html4/image/Image\n@private\n*/\ndefine(\"moxie/runtime/html4/image/Image\", [\n\"moxie/runtime/html4/Runtime\",\n\"moxie/runtime/html5/image/Image\"\n], function(extensions, Image) {\nreturn (extensions.Image = Image);\n});\n\nexpose([\"moxie/core/utils/Basic\",\"moxie/core/I18n\",\"moxie/core/utils/Mime\",\"moxie/core/utils/Env\",\"moxie/core/utils/Dom\",\"moxie/core/Exceptions\",\"moxie/core/EventTarget\",\"moxie/core/utils/Encode\",\"moxie/runtime/Runtime\",\"moxie/runtime/RuntimeClient\",\"moxie/file/Blob\",\"moxie/file/File\",\"moxie/file/FileInput\",\"moxie/file/FileDrop\",\"moxie/runtime/RuntimeTarget\",\"moxie/file/FileReader\",\"moxie/core/utils/Url\",\"moxie/file/FileReaderSync\",\"moxie/xhr/FormData\",\"moxie/xhr/XMLHttpRequest\",\"moxie/runtime/Transporter\",\"moxie/image/Image\",\"moxie/core/utils/Events\"]);\n})(this);/**\n* o.js\n*\n* Copyright 2013, Moxiecode Systems AB\n* Released under GPL License.\n*\n* License: http://www.plupload.com/license\n* Contributing: http://www.plupload.com/contributing\n*/\n\n/*global moxie:true */\n\n/**\nGlobally exposed namespace with the most frequently used public classes and handy methods.\n\n@class o\n@static\n@private\n*/\n(function(exports) {\n\"use strict\";\n\nvar o = {}, inArray = exports.moxie.core.utils.Basic.inArray;\n\n// directly add some public classes\n// (we do it dynamically here, since for custom builds we cannot know beforehand what modules were included)\n(function addAlias(ns) {\nvar name, itemType;\nfor (name in ns) {\nitemType = typeof(ns[name]);\nif (itemType === 'object' && !~inArray(name, ['Exceptions', 'Env', 'Mime'])) {\naddAlias(ns[name]);\n} else if (itemType === 'function') {\no[name] = ns[name];\n}\n}\n})(exports.moxie);\n\n// add some manually\no.Env = exports.moxie.core.utils.Env;\no.Mime = exports.moxie.core.utils.Mime;\no.Exceptions = exports.moxie.core.Exceptions;\n\n// expose globally\nexports.mOxie = o;\nif (!exports.o) {\nexports.o = o;\n}\nreturn o;\n})(this);\n"},useData:!0})},{"hbsfy/runtime":49}],35:[function(a,b,c){var d=a("hbsfy/runtime");b.exports=d.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){return"\n/**\n* Plupload - multi-runtime File Uploader\n* v2.1.2\n*\n* Copyright 2013, Moxiecode Systems AB\n* Released under GPL License.\n*\n* License: http://www.plupload.com/license\n* Contributing: http://www.plupload.com/contributing\n*\n* Date: 2014-05-14\n*/\n/**\n* Plupload.js\n*\n* Copyright 2013, Moxiecode Systems AB\n* Released under GPL License.\n*\n* License: http://www.plupload.com/license\n* Contributing: http://www.plupload.com/contributing\n*/\n\n/*global mOxie:true */\n\n;(function(window, o, undef) {\n\nvar delay = window.setTimeout\n, fileFilters = {}\n;\n\n// convert plupload features to caps acceptable by mOxie\nfunction normalizeCaps(settings) {\nvar features = settings.required_features, caps = {};\n\nfunction resolve(feature, value, strict) {\n// Feature notation is deprecated, use caps (this thing here is required for backward compatibility)\nvar map = {\nchunks: 'slice_blob',\njpgresize: 'send_binary_string',\npngresize: 'send_binary_string',\nprogress: 'report_upload_progress',\nmulti_selection: 'select_multiple',\ndragdrop: 'drag_and_drop',\ndrop_element: 'drag_and_drop',\nheaders: 'send_custom_headers',\nurlstream_upload: 'send_binary_string',\ncanSendBinary: 'send_binary',\ntriggerDialog: 'summon_file_dialog'\n};\n\nif (map[feature]) {\ncaps[map[feature]] = value;\n} else if (!strict) {\ncaps[feature] = value;\n}\n}\n\nif (typeof(features) === 'string') {\nplupload.each(features.split(/\\s*,\\s*/), function(feature) {\nresolve(feature, true);\n});\n} else if (typeof(features) === 'object') {\nplupload.each(features, function(value, feature) {\nresolve(feature, value);\n});\n} else if (features === true) {\n// check settings for required features\nif (settings.chunk_size > 0) {\ncaps.slice_blob = true;\n}\n\nif (settings.resize.enabled || !settings.multipart) {\ncaps.send_binary_string = true;\n}\n\nplupload.each(settings, function(value, feature) {\nresolve(feature, !!value, true); // strict check\n});\n}\n\nreturn caps;\n}\n\n/**\n* @module plupload\n* @static\n*/\nvar plupload = {\n/**\n* Plupload version will be replaced on build.\n*\n* @property VERSION\n* @for Plupload\n* @static\n* @final\n*/\nVERSION : '2.1.2',\n\n/**\n* Inital state of the queue and also the state ones it's finished all it's uploads.\n*\n* @property STOPPED\n* @static\n* @final\n*/\nSTOPPED : 1,\n\n/**\n* Upload process is running\n*\n* @property STARTED\n* @static\n* @final\n*/\nSTARTED : 2,\n\n/**\n* File is queued for upload\n*\n* @property QUEUED\n* @static\n* @final\n*/\nQUEUED : 1,\n\n/**\n* File is being uploaded\n*\n* @property UPLOADING\n* @static\n* @final\n*/\nUPLOADING : 2,\n\n/**\n* File has failed to be uploaded\n*\n* @property FAILED\n* @static\n* @final\n*/\nFAILED : 4,\n\n/**\n* File has been uploaded successfully\n*\n* @property DONE\n* @static\n* @final\n*/\nDONE : 5,\n\n// Error constants used by the Error event\n\n/**\n* Generic error for example if an exception is thrown inside Silverlight.\n*\n* @property GENERIC_ERROR\n* @static\n* @final\n*/\nGENERIC_ERROR : -100,\n\n/**\n* HTTP transport error. For example if the server produces a HTTP status other than 200.\n*\n* @property HTTP_ERROR\n* @static\n* @final\n*/\nHTTP_ERROR : -200,\n\n/**\n* Generic I/O error. For example if it wasn't possible to open the file stream on local machine.\n*\n* @property IO_ERROR\n* @static\n* @final\n*/\nIO_ERROR : -300,\n\n/**\n* @property SECURITY_ERROR\n* @static\n* @final\n*/\nSECURITY_ERROR : -400,\n\n/**\n* Initialization error. Will be triggered if no runtime was initialized.\n*\n* @property INIT_ERROR\n* @static\n* @final\n*/\nINIT_ERROR : -500,\n\n/**\n* File size error. If the user selects a file that is too large it will be blocked and an error of this type will be triggered.\n*\n* @property FILE_SIZE_ERROR\n* @static\n* @final\n*/\nFILE_SIZE_ERROR : -600,\n\n/**\n* File extension error. If the user selects a file that isn't valid according to the filters setting.\n*\n* @property FILE_EXTENSION_ERROR\n* @static\n* @final\n*/\nFILE_EXTENSION_ERROR : -601,\n\n/**\n* Duplicate file error. If prevent_duplicates is set to true and user selects the same file again.\n*\n* @property FILE_DUPLICATE_ERROR\n* @static\n* @final\n*/\nFILE_DUPLICATE_ERROR : -602,\n\n/**\n* Runtime will try to detect if image is proper one. Otherwise will throw this error.\n*\n* @property IMAGE_FORMAT_ERROR\n* @static\n* @final\n*/\nIMAGE_FORMAT_ERROR : -700,\n\n/**\n* While working on files runtime may run out of memory and will throw this error.\n*\n* @since 2.1.2\n* @property MEMORY_ERROR\n* @static\n* @final\n*/\nMEMORY_ERROR : -701,\n\n/**\n* Each runtime has an upper limit on a dimension of the image it can handle. If bigger, will throw this error.\n*\n* @property IMAGE_DIMENSIONS_ERROR\n* @static\n* @final\n*/\nIMAGE_DIMENSIONS_ERROR : -702,\n\n/**\n* Mime type lookup table.\n*\n* @property mimeTypes\n* @type Object\n* @final\n*/\nmimeTypes : o.mimes,\n\n/**\n* In some cases sniffing is the only way around :(\n*/\nua: o.ua,\n\n/**\n* Gets the true type of the built-in object (better version of typeof).\n* @credits Angus Croll (http://javascriptweblog.wordpress.com/)\n*\n* @method typeOf\n* @static\n* @param {Object} o Object to check.\n* @return {String} Object [[Class]]\n*/\ntypeOf: o.typeOf,\n\n/**\n* Extends the specified object with another object.\n*\n* @method extend\n* @static\n* @param {Object} target Object to extend.\n* @param {Object..} obj Multiple objects to extend with.\n* @return {Object} Same as target, the extended object.\n*/\nextend : o.extend,\n\n/**\n* Generates an unique ID. This is 99.99% unique since it takes the current time and 5 random numbers.\n* The only way a user would be able to get the same ID is if the two persons at the same exact milisecond manages\n* to get 5 the same random numbers between 0-65535 it also uses a counter so each call will be guaranteed to be page unique.\n* It's more probable for the earth to be hit with an ansteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property\n* to an user unique key.\n*\n* @method guid\n* @static\n* @return {String} Virtually unique id.\n*/\nguid : o.guid,\n\n/**\n* Get array of DOM Elements by their ids.\n*\n* @method get\n* @for Utils\n* @param {String} id Identifier of the DOM Element\n* @return {Array}\n*/\nget : function get(ids) {\nvar els = [], el;\n\nif (o.typeOf(ids) !== 'array') {\nids = [ids];\n}\n\nvar i = ids.length;\nwhile (i--) {\nel = o.get(ids[i]);\nif (el) {\nels.push(el);\n}\n}\n\nreturn els.length ? els : null;\n},\n\n/**\n* Executes the callback function for each item in array/object. If you return false in the\n* callback it will break the loop.\n*\n* @method each\n* @static\n* @param {Object} obj Object to iterate.\n* @param {function} callback Callback function to execute for each item.\n*/\neach : o.each,\n\n/**\n* Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields.\n*\n* @method getPos\n* @static\n* @param {Element} node HTML element or element id to get x, y position from.\n* @param {Element} root Optional root element to stop calculations at.\n* @return {object} Absolute position of the specified element object with x, y fields.\n*/\ngetPos : o.getPos,\n\n/**\n* Returns the size of the specified node in pixels.\n*\n* @method getSize\n* @static\n* @param {Node} node Node to get the size of.\n* @return {Object} Object with a w and h property.\n*/\ngetSize : o.getSize,\n\n/**\n* Encodes the specified string.\n*\n* @method xmlEncode\n* @static\n* @param {String} s String to encode.\n* @return {String} Encoded string.\n*/\nxmlEncode : function(str) {\nvar xmlEncodeChars = {'<' : 'lt', '>' : 'gt', '&' : 'amp', '\"' : 'quot', '\\'' : '#39'}, xmlEncodeRegExp = /[<>&\\\"\\']/g;\n\nreturn str ? ('' + str).replace(xmlEncodeRegExp, function(chr) {\nreturn xmlEncodeChars[chr] ? '&' + xmlEncodeChars[chr] + ';' : chr;\n}) : str;\n},\n\n/**\n* Forces anything into an array.\n*\n* @method toArray\n* @static\n* @param {Object} obj Object with length field.\n* @return {Array} Array object containing all items.\n*/\ntoArray : o.toArray,\n\n/**\n* Find an element in array and return it's index if present, otherwise return -1.\n*\n* @method inArray\n* @static\n* @param {mixed} needle Element to find\n* @param {Array} array\n* @return {Int} Index of the element, or -1 if not found\n*/\ninArray : o.inArray,\n\n/**\n* Extends the language pack object with new items.\n*\n* @method addI18n\n* @static\n* @param {Object} pack Language pack items to add.\n* @return {Object} Extended language pack object.\n*/\naddI18n : o.addI18n,\n\n/**\n* Translates the specified string by checking for the english string in the language pack lookup.\n*\n* @method translate\n* @static\n* @param {String} str String to look for.\n* @return {String} Translated string or the input string if it wasn't found.\n*/\ntranslate : o.translate,\n\n/**\n* Checks if object is empty.\n*\n* @method isEmptyObj\n* @static\n* @param {Object} obj Object to check.\n* @return {Boolean}\n*/\nisEmptyObj : o.isEmptyObj,\n\n/**\n* Checks if specified DOM element has specified class.\n*\n* @method hasClass\n* @static\n* @param {Object} obj DOM element like object to add handler to.\n* @param {String} name Class name\n*/\nhasClass : o.hasClass,\n\n/**\n* Adds specified className to specified DOM element.\n*\n* @method addClass\n* @static\n* @param {Object} obj DOM element like object to add handler to.\n* @param {String} name Class name\n*/\naddClass : o.addClass,\n\n/**\n* Removes specified className from specified DOM element.\n*\n* @method removeClass\n* @static\n* @param {Object} obj DOM element like object to add handler to.\n* @param {String} name Class name\n*/\nremoveClass : o.removeClass,\n\n/**\n* Returns a given computed style of a DOM element.\n*\n* @method getStyle\n* @static\n* @param {Object} obj DOM element like object.\n* @param {String} name Style you want to get from the DOM element\n*/\ngetStyle : o.getStyle,\n\n/**\n* Adds an event handler to the specified object and store reference to the handler\n* in objects internal Plupload registry (@see removeEvent).\n*\n* @method addEvent\n* @static\n* @param {Object} obj DOM element like object to add handler to.\n* @param {String} name Name to add event listener to.\n* @param {Function} callback Function to call when event occurs.\n* @param {String} (optional) key that might be used to add specifity to the event record.\n*/\naddEvent : o.addEvent,\n\n/**\n* Remove event handler from the specified object. If third argument (callback)\n* is not specified remove all events with the specified name.\n*\n* @method removeEvent\n* @static\n* @param {Object} obj DOM element to remove event listener(s) from.\n* @param {String} name Name of event listener to remove.\n* @param {Function|String} (optional) might be a callback or unique key to match.\n*/\nremoveEvent: o.removeEvent,\n\n/**\n* Remove all kind of events from the specified object\n*\n* @method removeAllEvents\n* @static\n* @param {Object} obj DOM element to remove event listeners from.\n* @param {String} (optional) unique key to match, when removing events.\n*/\nremoveAllEvents: o.removeAllEvents,\n\n/**\n* Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _.\n*\n* @method cleanName\n* @static\n* @param {String} s String to clean up.\n* @return {String} Cleaned string.\n*/\ncleanName : function(name) {\nvar i, lookup;\n\n// Replace diacritics\nlookup = [\n/[\\300-\\306]/g, 'A', /[\\340-\\346]/g, 'a',\n/\\307/g, 'C', /\\347/g, 'c',\n/[\\310-\\313]/g, 'E', /[\\350-\\353]/g, 'e',\n/[\\314-\\317]/g, 'I', /[\\354-\\357]/g, 'i',\n/\\321/g, 'N', /\\361/g, 'n',\n/[\\322-\\330]/g, 'O', /[\\362-\\370]/g, 'o',\n/[\\331-\\334]/g, 'U', /[\\371-\\374]/g, 'u'\n];\n\nfor (i = 0; i < lookup.length; i += 2) {\nname = name.replace(lookup[i], lookup[i + 1]);\n}\n\n// Replace whitespace\nname = name.replace(/\\s+/g, '_');\n\n// Remove anything else\nname = name.replace(/[^a-z0-9_\\-\\.]+/gi, '');\n\nreturn name;\n},\n\n/**\n* Builds a full url out of a base URL and an object with items to append as query string items.\n*\n* @method buildUrl\n* @static\n* @param {String} url Base URL to append query string items to.\n* @param {Object} items Name/value object to serialize as a querystring.\n* @return {String} String with url + serialized query string items.\n*/\nbuildUrl : function(url, items) {\nvar query = '';\n\nplupload.each(items, function(value, name) {\nquery += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value);\n});\n\nif (query) {\nurl += (url.indexOf('?') > 0 ? '&' : '?') + query;\n}\n\nreturn url;\n},\n\n/**\n* Formats the specified number as a size string for example 1024 becomes 1 KB.\n*\n* @method formatSize\n* @static\n* @param {Number} size Size to format as string.\n* @return {String} Formatted size string.\n*/\nformatSize : function(size) {\n\nif (size === undef || /\\D/.test(size)) {\nreturn plupload.translate('N/A');\n}\n\nfunction round(num, precision) {\nreturn Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision);\n}\n\nvar boundary = Math.pow(1024, 4);\n\n// TB\nif (size > boundary) {\nreturn round(size / boundary, 1) + \" \" + plupload.translate('tb');\n}\n\n// GB\nif (size > (boundary/=1024)) {\nreturn round(size / boundary, 1) + \" \" + plupload.translate('gb');\n}\n\n// MB\nif (size > (boundary/=1024)) {\nreturn round(size / boundary, 1) + \" \" + plupload.translate('mb');\n}\n\n// KB\nif (size > 1024) {\nreturn Math.round(size / 1024) + \" \" + plupload.translate('kb');\n}\n\nreturn size + \" \" + plupload.translate('b');\n},\n\n\n/**\n* Parses the specified size string into a byte value. For example 10kb becomes 10240.\n*\n* @method parseSize\n* @static\n* @param {String|Number} size String to parse or number to just pass through.\n* @return {Number} Size in bytes.\n*/\nparseSize : o.parseSizeStr,\n\n\n/**\n* A way to predict what runtime will be choosen in the current environment with the\n* specified settings.\n*\n* @method predictRuntime\n* @static\n* @param {Object|String} config Plupload settings to check\n* @param {String} [runtimes] Comma-separated list of runtimes to check against\n* @return {String} Type of compatible runtime\n*/\npredictRuntime : function(config, runtimes) {\nvar up, runtime;\n\nup = new plupload.Uploader(config);\nruntime = o.Runtime.thatCan(up.getOption().required_features, runtimes || config.runtimes);\nup.destroy();\nreturn runtime;\n},\n\n/**\n* Registers a filter that will be executed for each file added to the queue.\n* If callback returns false, file will not be added.\n*\n* Callback receives two arguments: a value for the filter as it was specified in settings.filters\n* and a file to be filtered. Callback is executed in the context of uploader instance.\n*\n* @method addFileFilter\n* @static\n* @param {String} name Name of the filter by which it can be referenced in settings.filters\n* @param {String} cb Callback - the actual routine that every added file must pass\n*/\naddFileFilter: function(name, cb) {\nfileFilters[name] = cb;\n}\n};\n\n\nplupload.addFileFilter('mime_types', function(filters, file, cb) {\nif (filters.length && !filters.regexp.test(file.name)) {\nthis.trigger('Error', {\ncode : plupload.FILE_EXTENSION_ERROR,\nmessage : plupload.translate('File extension error.'),\nfile : file\n});\ncb(false);\n} else {\ncb(true);\n}\n});\n\n\nplupload.addFileFilter('max_file_size', function(maxSize, file, cb) {\nvar undef;\n\nmaxSize = plupload.parseSize(maxSize);\n\n// Invalid file size\nif (file.size !== undef && maxSize && file.size > maxSize) {\nthis.trigger('Error', {\ncode : plupload.FILE_SIZE_ERROR,\nmessage : plupload.translate('File size error.'),\nfile : file\n});\ncb(false);\n} else {\ncb(true);\n}\n});\n\n\nplupload.addFileFilter('prevent_duplicates', function(value, file, cb) {\nif (value) {\nvar ii = this.files.length;\nwhile (ii--) {\n// Compare by name and size (size might be 0 or undefined, but still equivalent for both)\nif (file.name === this.files[ii].name && file.size === this.files[ii].size) {\nthis.trigger('Error', {\ncode : plupload.FILE_DUPLICATE_ERROR,\nmessage : plupload.translate('Duplicate file error.'),\nfile : file\n});\ncb(false);\nreturn;\n}\n}\n}\ncb(true);\n});\n\n\n/**\n@class Uploader\n@constructor\n\n@param {Object} settings For detailed information about each option check documentation.\n@param {String|DOMElement} settings.browse_button id of the DOM element or DOM element itself to use as file dialog trigger.\n@param {String} settings.url URL of the server-side upload handler.\n@param {Number|String} [settings.chunk_size=0] Chunk size in bytes to slice the file into. Shorcuts with b, kb, mb, gb, tb suffixes also supported. `e.g. 204800 or \"204800b\" or \"200kb\"`. By default - disabled.\n@param {Boolean} [settings.send_chunk_number=true] Whether to send chunks and chunk numbers, or total and offset bytes.\n@param {String} [settings.container] id of the DOM element to use as a container for uploader structures. Defaults to document.body.\n@param {String|DOMElement} [settings.drop_element] id of the DOM element or DOM element itself to use as a drop zone for Drag-n-Drop.\n@param {String} [settings.file_data_name=\"file\"] Name for the file field in Multipart formated message.\n@param {Object} [settings.filters={}] Set of file type filters.\n@param {Array} [settings.filters.mime_types=[]] List of file types to accept, each one defined by title and list of extensions. `e.g. {title : \"Image files\", extensions : \"jpg,jpeg,gif,png\"}`. Dispatches `plupload.FILE_EXTENSION_ERROR`\n@param {String|Number} [settings.filters.max_file_size=0] Maximum file size that the user can pick, in bytes. Optionally supports b, kb, mb, gb, tb suffixes. `e.g. \"10mb\" or \"1gb\"`. By default - not set. Dispatches `plupload.FILE_SIZE_ERROR`.\n@param {Boolean} [settings.filters.prevent_duplicates=false] Do not let duplicates into the queue. Dispatches `plupload.FILE_DUPLICATE_ERROR`.\n@param {String} [settings.flash_swf_url] URL of the Flash swf.\n@param {Object} [settings.headers] Custom headers to send with the upload. Hash of name/value pairs.\n@param {Number} [settings.max_retries=0] How many times to retry the chunk or file, before triggering Error event.\n@param {Boolean} [settings.multipart=true] Whether to send file and additional parameters as Multipart formated message.\n@param {Object} [settings.multipart_params] Hash of key/value pairs to send with every file upload.\n@param {Boolean} [settings.multi_selection=true] Enable ability to select multiple files at once in file dialog.\n@param {String|Object} [settings.required_features] Either comma-separated list or hash of required features that chosen runtime should absolutely possess.\n@param {Object} [settings.resize] Enable resizng of images on client-side. Applies to `image/jpeg` and `image/png` only. `e.g. {width : 200, height : 200, quality : 90, crop: true}`\n@param {Number} [settings.resize.width] If image is bigger, it will be resized.\n@param {Number} [settings.resize.height] If image is bigger, it will be resized.\n@param {Number} [settings.resize.quality=90] Compression quality for jpegs (1-100).\n@param {Boolean} [settings.resize.crop=false] Whether to crop images to exact dimensions. By default they will be resized proportionally.\n@param {String} [settings.runtimes=\"html5,flash,silverlight,html4\"] Comma separated list of runtimes, that Plupload will try in turn, moving to the next if previous fails.\n@param {String} [settings.silverlight_xap_url] URL of the Silverlight xap.\n@param {Boolean} [settings.unique_names=false] If true will generate unique filenames for uploaded files.\n@param {Boolean} [settings.send_file_name=true] Whether to send file name as additional argument - 'name' (required for chunked uploads and some other cases where file name cannot be sent via normal ways).\n*/\nplupload.Uploader = function(options) {\n/**\n* Fires when the current RunTime has been initialized.\n*\n* @event Init\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n*/\n\n/**\n* Fires after the init event incase you need to perform actions there.\n*\n* @event PostInit\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n*/\n\n/**\n* Fires when the option is changed in via uploader.setOption().\n*\n* @event OptionChanged\n* @since 2.1\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {String} name Name of the option that was changed\n* @param {Mixed} value New value for the specified option\n* @param {Mixed} oldValue Previous value of the option\n*/\n\n/**\n* Fires when the silverlight/flash or other shim needs to move.\n*\n* @event Refresh\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n*/\n\n/**\n* Fires when the overall state is being changed for the upload queue.\n*\n* @event StateChanged\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n*/\n\n/**\n* Fires when browse_button is clicked and browse dialog shows.\n*\n* @event Browse\n* @since 2.1.2\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n*/\n\n/**\n* Fires for every filtered file before it is added to the queue.\n*\n* @event FileFiltered\n* @since 2.1\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {plupload.File} file Another file that has to be added to the queue.\n*/\n\n/**\n* Fires when the file queue is changed. In other words when files are added/removed to the files array of the uploader instance.\n*\n* @event QueueChanged\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n*/\n\n/**\n* Fires after files were filtered and added to the queue.\n*\n* @event FilesAdded\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {Array} files Array of file objects that were added to queue by the user.\n*/\n\n/**\n* Fires when file is removed from the queue.\n*\n* @event FilesRemoved\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {Array} files Array of files that got removed.\n*/\n\n/**\n* Fires when just before a file is uploaded. This event enables you to override settings\n* on the uploader instance before the file is uploaded.\n*\n* @event BeforeUpload\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {plupload.File} file File to be uploaded.\n*/\n\n/**\n* Fires when a file is to be uploaded by the runtime.\n*\n* @event UploadFile\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {plupload.File} file File to be uploaded.\n*/\n\n/**\n* Fires while a file is being uploaded. Use this event to update the current file upload progress.\n*\n* @event UploadProgress\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {plupload.File} file File that is currently being uploaded.\n*/\n\n/**\n* Fires when file chunk is uploaded.\n*\n* @event ChunkUploaded\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {plupload.File} file File that the chunk was uploaded for.\n* @param {Object} response Object with response properties.\n*/\n\n/**\n* Fires when a file is successfully uploaded.\n*\n* @event FileUploaded\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {plupload.File} file File that was uploaded.\n* @param {Object} response Object with response properties.\n*/\n\n/**\n* Fires when all files in a queue are uploaded.\n*\n* @event UploadComplete\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {Array} files Array of file objects that was added to queue/selected by the user.\n*/\n\n/**\n* Fires when a error occurs.\n*\n* @event Error\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n* @param {Object} error Contains code, message and sometimes file and other details.\n*/\n\n/**\n* Fires when destroy method is called.\n*\n* @event Destroy\n* @param {plupload.Uploader} uploader Uploader instance sending the event.\n*/\nvar uid = plupload.guid()\n, settings\n, files = []\n, preferred_caps = {}\n, fileInputs = []\n, fileDrops = []\n, startTime\n, total\n, disabled = false\n, xhr\n;\n\n\n// Private methods\nfunction uploadNext() {\nvar file, count = 0, i;\n\nif (this.state == plupload.STARTED) {\n// Find first QUEUED file\nfor (i = 0; i < files.length; i++) {\nif (!file && files[i].status == plupload.QUEUED) {\nfile = files[i];\nif (this.trigger(\"BeforeUpload\", file)) {\nfile.status = plupload.UPLOADING;\nthis.trigger(\"UploadFile\", file);\n}\n} else {\ncount++;\n}\n}\n\n// All files are DONE or FAILED\nif (count == files.length) {\nif (this.state !== plupload.STOPPED) {\nthis.state = plupload.STOPPED;\nthis.trigger(\"StateChanged\");\n}\nthis.trigger(\"UploadComplete\", files);\n}\n}\n}\n\n\nfunction calcFile(file) {\nfile.percent = file.size > 0 ? Math.ceil(file.loaded / file.size * 100) : 100;\ncalc();\n}\n\n\nfunction calc() {\nvar i, file;\n\n// Reset stats\ntotal.reset();\n\n// Check status, size, loaded etc on all files\nfor (i = 0; i < files.length; i++) {\nfile = files[i];\n\nif (file.size !== undef) {\n// We calculate totals based on original file size\ntotal.size += file.origSize;\n\n// Since we cannot predict file size after resize, we do opposite and\n// interpolate loaded amount to match magnitude of total\ntotal.loaded += file.loaded * file.origSize / file.size;\n} else {\ntotal.size = undef;\n}\n\nif (file.status == plupload.DONE) {\ntotal.uploaded++;\n} else if (file.status == plupload.FAILED) {\ntotal.failed++;\n} else {\ntotal.queued++;\n}\n}\n\n// If we couldn't calculate a total file size then use the number of files to calc percent\nif (total.size === undef) {\ntotal.percent = files.length > 0 ? Math.ceil(total.uploaded / files.length * 100) : 0;\n} else {\ntotal.bytesPerSec = Math.ceil(total.loaded / ((+new Date() - startTime || 1) / 1000.0));\ntotal.percent = total.size > 0 ? Math.ceil(total.loaded / total.size * 100) : 0;\n}\n}\n\n\nfunction getRUID() {\nvar ctrl = fileInputs[0] || fileDrops[0];\nif (ctrl) {\nreturn ctrl.getRuntime().uid;\n}\nreturn false;\n}\n\n\nfunction runtimeCan(file, cap) {\nif (file.ruid) {\nvar info = o.Runtime.getInfo(file.ruid);\nif (info) {\nreturn info.can(cap);\n}\n}\nreturn false;\n}\n\n\nfunction bindEventListeners() {\nthis.bind('FilesAdded FilesRemoved', function(up) {\nup.trigger('QueueChanged');\nup.refresh();\n});\n\nthis.bind('CancelUpload', onCancelUpload);\n\nthis.bind('BeforeUpload', onBeforeUpload);\n\nthis.bind('UploadFile', onUploadFile);\n\nthis.bind('UploadProgress', onUploadProgress);\n\nthis.bind('StateChanged', onStateChanged);\n\nthis.bind('QueueChanged', calc);\n\nthis.bind('Error', onError);\n\nthis.bind('FileUploaded', onFileUploaded);\n\nthis.bind('Destroy', onDestroy);\n}\n\n\nfunction initControls(settings, cb) {\nvar self = this, inited = 0, queue = [];\n\n// common settings\nvar options = {\nruntime_order: settings.runtimes,\nrequired_caps: settings.required_features,\npreferred_caps: preferred_caps,\nswf_url: settings.flash_swf_url,\nxap_url: settings.silverlight_xap_url\n};\n\n// add runtime specific options if any\nplupload.each(settings.runtimes.split(/\\s*,\\s*/), function(runtime) {\nif (settings[runtime]) {\noptions[runtime] = settings[runtime];\n}\n});\n\n// initialize file pickers - there can be many\nif (settings.browse_button) {\nplupload.each(settings.browse_button, function(el) {\nqueue.push(function(cb) {\nvar fileInput = new o.FileInput(plupload.extend({}, options, {\naccept: settings.filters.mime_types,\nname: settings.file_data_name,\nmultiple: settings.multi_selection,\ncontainer: settings.container,\nbrowse_button: el\n}));\n\nfileInput.onready = function() {\nvar info = o.Runtime.getInfo(this.ruid);\n\n// for backward compatibility\no.extend(self.features, {\nchunks: info.can('slice_blob'),\nmultipart: info.can('send_multipart'),\nmulti_selection: info.can('select_multiple')\n});\n\ninited++;\nfileInputs.push(this);\ncb();\n};\n\nfileInput.onchange = function() {\nself.addFile(this.files);\n};\n\nfileInput.bind('mouseenter mouseleave mousedown mouseup', function(e) {\nif (!disabled) {\nif (settings.browse_button_hover) {\nif ('mouseenter' === e.type) {\no.addClass(el, settings.browse_button_hover);\n} else if ('mouseleave' === e.type) {\no.removeClass(el, settings.browse_button_hover);\n}\n}\n\nif (settings.browse_button_active) {\nif ('mousedown' === e.type) {\no.addClass(el, settings.browse_button_active);\n} else if ('mouseup' === e.type) {\no.removeClass(el, settings.browse_button_active);\n}\n}\n}\n});\n\nfileInput.bind('mousedown', function() {\nself.trigger('Browse');\n});\n\nfileInput.bind('error runtimeerror', function() {\nfileInput = null;\ncb();\n});\n\nfileInput.init();\n});\n});\n}\n\n// initialize drop zones\nif (settings.drop_element) {\nplupload.each(settings.drop_element, function(el) {\nqueue.push(function(cb) {\nvar fileDrop = new o.FileDrop(plupload.extend({}, options, {\ndrop_zone: el\n}));\n\nfileDrop.onready = function() {\nvar info = o.Runtime.getInfo(this.ruid);\n\nself.features.dragdrop = info.can('drag_and_drop'); // for backward compatibility\n\ninited++;\nfileDrops.push(this);\ncb();\n};\n\nfileDrop.ondrop = function() {\nself.addFile(this.files);\n};\n\nfileDrop.bind('error runtimeerror', function() {\nfileDrop = null;\ncb();\n});\n\nfileDrop.init();\n});\n});\n}\n\n\no.inSeries(queue, function() {\nif (typeof(cb) === 'function') {\ncb(inited);\n}\n});\n}\n\n\nfunction resizeImage(blob, params, cb) {\nvar img = new o.Image();\n\ntry {\nimg.onload = function() {\n// no manipulation required if...\nif (params.width > this.width &&\nparams.height > this.height &&\nparams.quality === undef &&\nparams.preserve_headers &&\n!params.crop\n) {\nthis.destroy();\nreturn cb(blob);\n}\n// otherwise downsize\nimg.downsize(params.width, params.height, params.crop, params.preserve_headers);\n};\n\nimg.onresize = function() {\ncb(this.getAsBlob(blob.type, params.quality));\nthis.destroy();\n};\n\nimg.onerror = function() {\ncb(blob);\n};\n\nimg.load(blob);\n} catch(ex) {\ncb(blob);\n}\n}\n\n\nfunction setOption(option, value, init) {\nvar self = this, reinitRequired = false;\n\nfunction _setOption(option, value, init) {\nvar oldValue = settings[option];\n\nswitch (option) {\ncase 'max_file_size':\nif (option === 'max_file_size') {\nsettings.max_file_size = settings.filters.max_file_size = value;\n}\nbreak;\n\ncase 'chunk_size':\nif (value = plupload.parseSize(value)) {\nsettings[option] = value;\nsettings.send_file_name = true;\n}\nbreak;\n\ncase 'multipart':\nsettings[option] = value;\nif (!value) {\nsettings.send_file_name = true;\n}\nbreak;\n\ncase 'unique_names':\nsettings[option] = value;\nif (value) {\nsettings.send_file_name = true;\n}\nbreak;\n\ncase 'filters':\n// for sake of backward compatibility\nif (plupload.typeOf(value) === 'array') {\nvalue = {\nmime_types: value\n};\n}\n\nif (init) {\nplupload.extend(settings.filters, value);\n} else {\nsettings.filters = value;\n}\n\n// if file format filters are being updated, regenerate the matching expressions\nif (value.mime_types) {\nsettings.filters.mime_types.regexp = (function(filters) {\nvar extensionsRegExp = [];\n\nplupload.each(filters, function(filter) {\nplupload.each(filter.extensions.split(/,/), function(ext) {\nif (/^\\s*\\*\\s*$/.test(ext)) {\nextensionsRegExp.push('\\\\.*');\n} else {\nextensionsRegExp.push('\\\\.' + ext.replace(new RegExp('[' + ('/^$.*+?|()[]{}\\\\'.replace(/./g, '\\\\$&')) + ']', 'g'), '\\\\$&'));\n}\n});\n});\n\nreturn new RegExp('(' + extensionsRegExp.join('|') + ')$', 'i');\n}(settings.filters.mime_types));\n}\nbreak;\n\ncase 'resize':\nif (init) {\nplupload.extend(settings.resize, value, {\nenabled: true\n});\n} else {\nsettings.resize = value;\n}\nbreak;\n\ncase 'prevent_duplicates':\nsettings.prevent_duplicates = settings.filters.prevent_duplicates = !!value;\nbreak;\n\ncase 'browse_button':\ncase 'drop_element':\nvalue = plupload.get(value);\n\ncase 'container':\ncase 'runtimes':\ncase 'multi_selection':\ncase 'flash_swf_url':\ncase 'silverlight_xap_url':\nsettings[option] = value;\nif (!init) {\nreinitRequired = true;\n}\nbreak;\n\ndefault:\nsettings[option] = value;\n}\n\nif (!init) {\nself.trigger('OptionChanged', option, value, oldValue);\n}\n}\n\nif (typeof(option) === 'object') {\nplupload.each(option, function(value, option) {\n_setOption(option, value, init);\n});\n} else {\n_setOption(option, value, init);\n}\n\nif (init) {\n// Normalize the list of required capabilities\nsettings.required_features = normalizeCaps(plupload.extend({}, settings));\n\n// Come up with the list of capabilities that can affect default mode in a multi-mode runtimes\npreferred_caps = normalizeCaps(plupload.extend({}, settings, {\nrequired_features: true\n}));\n} else if (reinitRequired) {\nself.trigger('Destroy');\n\ninitControls.call(self, settings, function(inited) {\nif (inited) {\nself.runtime = o.Runtime.getInfo(getRUID()).type;\nself.trigger('Init', { runtime: self.runtime });\nself.trigger('PostInit');\n} else {\nself.trigger('Error', {\ncode : plupload.INIT_ERROR,\nmessage : plupload.translate('Init error.')\n});\n}\n});\n}\n}\n\n\n// Internal event handlers\nfunction onBeforeUpload(up, file) {\n// Generate unique target filenames\nif (up.settings.unique_names) {\nvar matches = file.name.match(/\\.([^.]+)$/), ext = \"part\";\nif (matches) {\next = matches[1];\n}\nfile.target_name = file.id + '.' + ext;\n}\n}\n\n\nfunction onUploadFile(up, file) {\nvar url = up.settings.url\n, chunkSize = up.settings.chunk_size\n, retries = up.settings.max_retries\n, features = up.features\n, offset = 0\n, blob\n;\n\n// make sure we start at a predictable offset\nif (file.loaded) {\noffset = file.loaded = chunkSize ? chunkSize * Math.floor(file.loaded / chunkSize) : 0;\n}\n\nfunction handleError() {\nif (retries-- > 0) {\ndelay(uploadNextChunk, 1000);\n} else {\nfile.loaded = offset; // reset all progress\n\nup.trigger('Error', {\ncode : plupload.HTTP_ERROR,\nmessage : plupload.translate('HTTP Error.'),\nfile : file,\nresponse : xhr.responseText,\nstatus : xhr.status,\nresponseHeaders: xhr.getAllResponseHeaders()\n});\n}\n}\n\nfunction uploadNextChunk() {\nvar chunkBlob, formData, args = {}, curChunkSize;\n\n// make sure that file wasn't cancelled and upload is not stopped in general\nif (file.status !== plupload.UPLOADING || up.state === plupload.STOPPED) {\nreturn;\n}\n\n// send additional 'name' parameter only if required\nif (up.settings.send_file_name) {\nargs.name = file.target_name || file.name;\n}\n\nif (chunkSize && features.chunks && blob.size > chunkSize) { // blob will be of type string if it was loaded in memory\ncurChunkSize = Math.min(chunkSize, blob.size - offset);\nchunkBlob = blob.slice(offset, offset + curChunkSize);\n} else {\ncurChunkSize = blob.size;\nchunkBlob = blob;\n}\n\n// If chunking is enabled add corresponding args, no matter if file is bigger than chunk or smaller\nif (chunkSize && features.chunks) {\n// Setup query string arguments\nif (up.settings.send_chunk_number) {\nargs.chunk = Math.ceil(offset / chunkSize);\nargs.chunks = Math.ceil(blob.size / chunkSize);\n} else { // keep support for experimental chunk format, just in case\nargs.offset = offset;\nargs.total = blob.size;\n}\n}\n\nxhr = new o.XMLHttpRequest();\n\n// Do we have upload progress support\nif (xhr.upload) {\nxhr.upload.onprogress = function(e) {\nfile.loaded = Math.min(file.size, offset + e.loaded);\nup.trigger('UploadProgress', file);\n};\n}\n\nxhr.onload = function() {\n// check if upload made itself through\nif (xhr.status >= 400) {\nhandleError();\nreturn;\n}\n\nretries = up.settings.max_retries; // reset the counter\n\n// Handle chunk response\nif (curChunkSize < blob.size) {\nchunkBlob.destroy();\n\noffset += curChunkSize;\nfile.loaded = Math.min(offset, blob.size);\n\nup.trigger('ChunkUploaded', file, {\noffset : file.loaded,\ntotal : blob.size,\nresponse : xhr.responseText,\nstatus : xhr.status,\nresponseHeaders: xhr.getAllResponseHeaders()\n});\n\n// stock Android browser doesn't fire upload progress events, but in chunking mode we can fake them\nif (o.Env.browser === 'Android Browser') {\n// doesn't harm in general, but is not required anywhere else\nup.trigger('UploadProgress', file);\n}\n} else {\nfile.loaded = file.size;\n}\n\nchunkBlob = formData = null; // Free memory\n\n// Check if file is uploaded\nif (!offset || offset >= blob.size) {\n// If file was modified, destory the copy\nif (file.size != file.origSize) {\nblob.destroy();\nblob = null;\n}\n\nup.trigger('UploadProgress', file);\n\nfile.status = plupload.DONE;\n\nup.trigger('FileUploaded', file, {\nresponse : xhr.responseText,\nstatus : xhr.status,\nresponseHeaders: xhr.getAllResponseHeaders()\n});\n} else {\n// Still chunks left\ndelay(uploadNextChunk, 1); // run detached, otherwise event handlers interfere\n}\n};\n\nxhr.onerror = function() {\nhandleError();\n};\n\nxhr.onloadend = function() {\nthis.destroy();\nxhr = null;\n};\n\n// Build multipart request\nif (up.settings.multipart && features.multipart) {\nxhr.open(\"post\", url, true);\n\n// Set custom headers\nplupload.each(up.settings.headers, function(value, name) {\nxhr.setRequestHeader(name, value);\n});\n\nformData = new o.FormData();\n\n// Add multipart params\nplupload.each(plupload.extend(args, up.settings.multipart_params), function(value, name) {\nformData.append(name, value);\n});\n\n// Add file and send it\nformData.append(up.settings.file_data_name, chunkBlob);\nxhr.send(formData, {\nruntime_order: up.settings.runtimes,\nrequired_caps: up.settings.required_features,\npreferred_caps: preferred_caps,\nswf_url: up.settings.flash_swf_url,\nxap_url: up.settings.silverlight_xap_url\n});\n} else {\n// if no multipart, send as binary stream\nurl = plupload.buildUrl(up.settings.url, plupload.extend(args, up.settings.multipart_params));\n\nxhr.open(\"post\", url, true);\n\nxhr.setRequestHeader('Content-Type', 'application/octet-stream'); // Binary stream header\n\n// Set custom headers\nplupload.each(up.settings.headers, function(value, name) {\nxhr.setRequestHeader(name, value);\n});\n\nxhr.send(chunkBlob, {\nruntime_order: up.settings.runtimes,\nrequired_caps: up.settings.required_features,\npreferred_caps: preferred_caps,\nswf_url: up.settings.flash_swf_url,\nxap_url: up.settings.silverlight_xap_url\n});\n}\n}\n\nblob = file.getSource();\n\n// Start uploading chunks\nif (up.settings.resize.enabled && runtimeCan(blob, 'send_binary_string') && !!~o.inArray(blob.type, ['image/jpeg', 'image/png'])) {\n// Resize if required\nresizeImage.call(this, blob, up.settings.resize, function(resizedBlob) {\nblob = resizedBlob;\nfile.size = resizedBlob.size;\nuploadNextChunk();\n});\n} else {\nuploadNextChunk();\n}\n}\n\n\nfunction onUploadProgress(up, file) {\ncalcFile(file);\n}\n\n\nfunction onStateChanged(up) {\nif (up.state == plupload.STARTED) {\n// Get start time to calculate bps\nstartTime = (+new Date());\n} else if (up.state == plupload.STOPPED) {\n// Reset currently uploading files\nfor (var i = up.files.length - 1; i >= 0; i--) {\nif (up.files[i].status == plupload.UPLOADING) {\nup.files[i].status = plupload.QUEUED;\ncalc();\n}\n}\n}\n}\n\n\nfunction onCancelUpload() {\nif (xhr) {\nxhr.abort();\n}\n}\n\n\nfunction onFileUploaded(up) {\ncalc();\n\n// Upload next file but detach it from the error event\n// since other custom listeners might want to stop the queue\ndelay(function() {\nuploadNext.call(up);\n}, 1);\n}\n\n\nfunction onError(up, err) {\nif (err.code === plupload.INIT_ERROR) {\nup.destroy();\n}\n// Set failed status if an error occured on a file\nelse if (err.file) {\nerr.file.status = plupload.FAILED;\ncalcFile(err.file);\n\n// Upload next file but detach it from the error event\n// since other custom listeners might want to stop the queue\nif (up.state == plupload.STARTED) { // upload in progress\nup.trigger('CancelUpload');\ndelay(function() {\nuploadNext.call(up);\n}, 1);\n}\n}\n}\n\n\nfunction onDestroy(up) {\nup.stop();\n\n// Purge the queue\nplupload.each(files, function(file) {\nfile.destroy();\n});\nfiles = [];\n\nif (fileInputs.length) {\nplupload.each(fileInputs, function(fileInput) {\nfileInput.destroy();\n});\nfileInputs = [];\n}\n\nif (fileDrops.length) {\nplupload.each(fileDrops, function(fileDrop) {\nfileDrop.destroy();\n});\nfileDrops = [];\n}\n\npreferred_caps = {};\ndisabled = false;\nstartTime = xhr = null;\ntotal.reset();\n}\n\n\n// Default settings\nsettings = {\nruntimes: o.Runtime.order,\nmax_retries: 0,\nchunk_size: 0,\nmultipart: true,\nmulti_selection: true,\nfile_data_name: 'file',\nflash_swf_url: 'js/Moxie.swf',\nsilverlight_xap_url: 'js/Moxie.xap',\nfilters: {\nmime_types: [],\nprevent_duplicates: false,\nmax_file_size: 0\n},\nresize: {\nenabled: false,\npreserve_headers: true,\ncrop: false\n},\nsend_file_name: true,\nsend_chunk_number: true\n};\n\n\nsetOption.call(this, options, null, true);\n\n// Inital total state\ntotal = new plupload.QueueProgress();\n\n// Add public methods\nplupload.extend(this, {\n\n/**\n* Unique id for the Uploader instance.\n*\n* @property id\n* @type String\n*/\nid : uid,\nuid : uid, // mOxie uses this to differentiate between event targets\n\n/**\n* Current state of the total uploading progress. This one can either be plupload.STARTED or plupload.STOPPED.\n* These states are controlled by the stop/start methods. The default value is STOPPED.\n*\n* @property state\n* @type Number\n*/\nstate : plupload.STOPPED,\n\n/**\n* Map of features that are available for the uploader runtime. Features will be filled\n* before the init event is called, these features can then be used to alter the UI for the end user.\n* Some of the current features that might be in this map is: dragdrop, chunks, jpgresize, pngresize.\n*\n* @property features\n* @type Object\n*/\nfeatures : {},\n\n/**\n* Current runtime name.\n*\n* @property runtime\n* @type String\n*/\nruntime : null,\n\n/**\n* Current upload queue, an array of File instances.\n*\n* @property files\n* @type Array\n* @see plupload.File\n*/\nfiles : files,\n\n/**\n* Object with name/value settings.\n*\n* @property settings\n* @type Object\n*/\nsettings : settings,\n\n/**\n* Total progess information. How many files has been uploaded, total percent etc.\n*\n* @property total\n* @type plupload.QueueProgress\n*/\ntotal : total,\n\n\n/**\n* Initializes the Uploader instance and adds internal event listeners.\n*\n* @method init\n*/\ninit : function() {\nvar self = this;\nif (typeof(settings.preinit) == \"function\") {\nsettings.preinit(self);\n} else {\nplupload.each(settings.preinit, function(func, name) {\nself.bind(name, func);\n});\n}\n\nbindEventListeners.call(this);\n\n// Check for required options\nif (!settings.browse_button || !settings.url) {\nthis.trigger('Error', {\ncode : plupload.INIT_ERROR,\nmessage : plupload.translate('Init error.')\n});\nreturn;\n}\n\ninitControls.call(this, settings, function(inited) {\nif (typeof(settings.init) == \"function\") {\nsettings.init(self);\n} else {\nplupload.each(settings.init, function(func, name) {\nself.bind(name, func);\n});\n}\n\nif (inited) {\nself.runtime = o.Runtime.getInfo(getRUID()).type;\n\nself.trigger('Init', { runtime: self.runtime });\nself.trigger('PostInit');\n} else {\nself.trigger('Error', {\ncode : plupload.INIT_ERROR,\nmessage : plupload.translate('Init error.')\n});\n}\n});\n},\n\n/**\n* Set the value for the specified option(s).\n*\n* @method setOption\n* @since 2.1\n* @param {String|Object} option Name of the option to change or the set of key/value pairs\n* @param {Mixed} [value] Value for the option (is ignored, if first argument is object)\n*/\nsetOption: function(option, value) {\nsetOption.call(this, option, value, !this.runtime); // until runtime not set we do not need to reinitialize\n},\n\n/**\n* Get the value for the specified option or the whole configuration, if not specified.\n*\n* @method getOption\n* @since 2.1\n* @param {String} [option] Name of the option to get\n* @return {Mixed} Value for the option or the whole set\n*/\ngetOption: function(option) {\nif (!option) {\nreturn settings;\n}\nreturn settings[option];\n},\n\n/**\n* Refreshes the upload instance by dispatching out a refresh event to all runtimes.\n* This would for example reposition flash/silverlight shims on the page.\n*\n* @method refresh\n*/\nrefresh : function() {\nif (fileInputs.length) {\nplupload.each(fileInputs, function(fileInput) {\nfileInput.trigger('Refresh');\n});\n}\nthis.trigger('Refresh');\n},\n\n/**\n* Starts uploading the queued files.\n*\n* @method start\n*/\nstart : function() {\nif (this.state != plupload.STARTED) {\nthis.state = plupload.STARTED;\nthis.trigger('StateChanged');\n\nuploadNext.call(this);\n}\n},\n\n/**\n* Stops the upload of the queued files.\n*\n* @method stop\n*/\nstop : function() {\nif (this.state != plupload.STOPPED) {\nthis.state = plupload.STOPPED;\nthis.trigger('StateChanged');\nthis.trigger('CancelUpload');\n}\n},\n\n\n/**\n* Disables/enables browse button on request.\n*\n* @method disableBrowse\n* @param {Boolean} disable Whether to disable or enable (default: true)\n*/\ndisableBrowse : function() {\ndisabled = arguments[0] !== undef ? arguments[0] : true;\n\nif (fileInputs.length) {\nplupload.each(fileInputs, function(fileInput) {\nfileInput.disable(disabled);\n});\n}\n\nthis.trigger('DisableBrowse', disabled);\n},\n\n/**\n* Returns the specified file object by id.\n*\n* @method getFile\n* @param {String} id File id to look for.\n* @return {plupload.File} File object or undefined if it wasn't found;\n*/\ngetFile : function(id) {\nvar i;\nfor (i = files.length - 1; i >= 0; i--) {\nif (files[i].id === id) {\nreturn files[i];\n}\n}\n},\n\n/**\n* Adds file to the queue programmatically. Can be native file, instance of Plupload.File,\n* instance of mOxie.File, input[type=\"file\"] element, or array of these. Fires FilesAdded,\n* if any files were added to the queue. Otherwise nothing happens.\n*\n* @method addFile\n* @since 2.0\n* @param {plupload.File|mOxie.File|File|Node|Array} file File or files to add to the queue.\n* @param {String} [fileName] If specified, will be used as a name for the file\n*/\naddFile : function(file, fileName) {\nvar self = this\n, queue = []\n, filesAdded = []\n, ruid\n;\n\nfunction filterFile(file, cb) {\nvar queue = [];\no.each(self.settings.filters, function(rule, name) {\nif (fileFilters[name]) {\nqueue.push(function(cb) {\nfileFilters[name].call(self, rule, file, function(res) {\ncb(!res);\n});\n});\n}\n});\no.inSeries(queue, cb);\n}\n\n/**\n* @method resolveFile\n* @private\n* @param {o.File|o.Blob|plupload.File|File|Blob|input[type=\"file\"]} file\n*/\nfunction resolveFile(file) {\nvar type = o.typeOf(file);\n\n// o.File\nif (file instanceof o.File) {\nif (!file.ruid && !file.isDetached()) {\nif (!ruid) { // weird case\nreturn false;\n}\nfile.ruid = ruid;\nfile.connectRuntime(ruid);\n}\nresolveFile(new plupload.File(file));\n}\n// o.Blob\nelse if (file instanceof o.Blob) {\nresolveFile(file.getSource());\nfile.destroy();\n}\n// plupload.File - final step for other branches\nelse if (file instanceof plupload.File) {\nif (fileName) {\nfile.name = fileName;\n}\n\nqueue.push(function(cb) {\n// run through the internal and user-defined filters, if any\nfilterFile(file, function(err) {\nif (!err) {\n// make files available for the filters by updating the main queue directly\nfiles.push(file);\n// collect the files that will be passed to FilesAdded event\nfilesAdded.push(file);\n\nself.trigger(\"FileFiltered\", file);\n}\ndelay(cb, 1); // do not build up recursions or eventually we might hit the limits\n});\n});\n}\n// native File or blob\nelse if (o.inArray(type, ['file', 'blob']) !== -1) {\nresolveFile(new o.File(null, file));\n}\n// input[type=\"file\"]\nelse if (type === 'node' && o.typeOf(file.files) === 'filelist') {\n// if we are dealing with input[type=\"file\"]\no.each(file.files, resolveFile);\n}\n// mixed array of any supported types (see above)\nelse if (type === 'array') {\nfileName = null; // should never happen, but unset anyway to avoid funny situations\no.each(file, resolveFile);\n}\n}\n\nruid = getRUID();\n\nresolveFile(file);\n\nif (queue.length) {\no.inSeries(queue, function() {\n// if any files left after filtration, trigger FilesAdded\nif (filesAdded.length) {\nself.trigger(\"FilesAdded\", filesAdded);\n}\n});\n}\n},\n\n/**\n* Removes a specific file.\n*\n* @method removeFile\n* @param {plupload.File|String} file File to remove from queue.\n*/\nremoveFile : function(file) {\nvar id = typeof(file) === 'string' ? file : file.id;\n\nfor (var i = files.length - 1; i >= 0; i--) {\nif (files[i].id === id) {\nreturn this.splice(i, 1)[0];\n}\n}\n},\n\n/**\n* Removes part of the queue and returns the files removed. This will also trigger the FilesRemoved and QueueChanged events.\n*\n* @method splice\n* @param {Number} start (Optional) Start index to remove from.\n* @param {Number} length (Optional) Lengh of items to remove.\n* @return {Array} Array of files that was removed.\n*/\nsplice : function(start, length) {\n// Splice and trigger events\nvar removed = files.splice(start === undef ? 0 : start, length === undef ? files.length : length);\n\n// if upload is in progress we need to stop it and restart after files are removed\nvar restartRequired = false;\nif (this.state == plupload.STARTED) { // upload in progress\nplupload.each(removed, function(file) {\nif (file.status === plupload.UPLOADING) {\nrestartRequired = true; // do not restart, unless file that is being removed is uploading\nreturn false;\n}\n});\n\nif (restartRequired) {\nthis.stop();\n}\n}\n\nthis.trigger(\"FilesRemoved\", removed);\n\n// Dispose any resources allocated by those files\nplupload.each(removed, function(file) {\nfile.destroy();\n});\n\nif (restartRequired) {\nthis.start();\n}\n\nreturn removed;\n},\n\n/**\n* Dispatches the specified event name and it's arguments to all listeners.\n*\n*\n* @method trigger\n* @param {String} name Event name to fire.\n* @param {Object..} Multiple arguments to pass along to the listener functions.\n*/\n\n/**\n* Check whether uploader has any listeners to the specified event.\n*\n* @method hasEventListener\n* @param {String} name Event name to check for.\n*/\n\n\n/**\n* Adds an event listener by name.\n*\n* @method bind\n* @param {String} name Event name to listen for.\n* @param {function} func Function to call ones the event gets fired.\n* @param {Object} scope Optional scope to execute the specified function in.\n*/\nbind : function(name, func, scope) {\nvar self = this;\n// adapt moxie EventTarget style to Plupload-like\nplupload.Uploader.prototype.bind.call(this, name, function() {\nvar args = [].slice.call(arguments);\nargs.splice(0, 1, self); // replace event object with uploader instance\nreturn func.apply(this, args);\n}, 0, scope);\n},\n\n/**\n* Removes the specified event listener.\n*\n* @method unbind\n* @param {String} name Name of event to remove.\n* @param {function} func Function to remove from listener.\n*/\n\n/**\n* Removes all event listeners.\n*\n* @method unbindAll\n*/\n\n\n/**\n* Destroys Plupload instance and cleans after itself.\n*\n* @method destroy\n*/\ndestroy : function() {\nthis.trigger('Destroy');\nsettings = total = null; // purge these exclusively\nthis.unbindAll();\n}\n});\n};\n\nplupload.Uploader.prototype = o.EventTarget.instance;\n\n/**\n* Constructs a new file instance.\n*\n* @class File\n* @constructor\n*\n* @param {Object} file Object containing file properties\n* @param {String} file.name Name of the file.\n* @param {Number} file.size File size.\n*/\nplupload.File = (function() {\nvar filepool = {};\n\nfunction PluploadFile(file) {\n\nplupload.extend(this, {\n\n/**\n* File id this is a globally unique id for the specific file.\n*\n* @property id\n* @type String\n*/\nid: plupload.guid(),\n\n/**\n* File name for example \"myfile.gif\".\n*\n* @property name\n* @type String\n*/\nname: file.name || file.fileName,\n\n/**\n* File type, `e.g image/jpeg`\n*\n* @property type\n* @type String\n*/\ntype: file.type || '',\n\n/**\n* File size in bytes (may change after client-side manupilation).\n*\n* @property size\n* @type Number\n*/\nsize: file.size || file.fileSize,\n\n/**\n* Original file size in bytes.\n*\n* @property origSize\n* @type Number\n*/\norigSize: file.size || file.fileSize,\n\n/**\n* Number of bytes uploaded of the files total size.\n*\n* @property loaded\n* @type Number\n*/\nloaded: 0,\n\n/**\n* Number of percentage uploaded of the file.\n*\n* @property percent\n* @type Number\n*/\npercent: 0,\n\n/**\n* Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE.\n*\n* @property status\n* @type Number\n* @see plupload\n*/\nstatus: plupload.QUEUED,\n\n/**\n* Date of last modification.\n*\n* @property lastModifiedDate\n* @type {String}\n*/\nlastModifiedDate: file.lastModifiedDate || (new Date()).toLocaleString(), // Thu Aug 23 2012 19:40:00 GMT+0400 (GET)\n\n/**\n* Returns native window.File object, when it's available.\n*\n* @method getNative\n* @return {window.File} or null, if plupload.File is of different origin\n*/\ngetNative: function() {\nvar file = this.getSource().getSource();\nreturn o.inArray(o.typeOf(file), ['blob', 'file']) !== -1 ? file : null;\n},\n\n/**\n* Returns mOxie.File - unified wrapper object that can be used across runtimes.\n*\n* @method getSource\n* @return {mOxie.File} or null\n*/\ngetSource: function() {\nif (!filepool[this.id]) {\nreturn null;\n}\nreturn filepool[this.id];\n},\n\n/**\n* Destroys plupload.File object.\n*\n* @method destroy\n*/\ndestroy: function() {\nvar src = this.getSource();\nif (src) {\nsrc.destroy();\ndelete filepool[this.id];\n}\n}\n});\n\nfilepool[this.id] = file;\n}\n\nreturn PluploadFile;\n}());\n\n\n/**\n* Constructs a queue progress.\n*\n* @class QueueProgress\n* @constructor\n*/\nplupload.QueueProgress = function() {\nvar self = this; // Setup alias for self to reduce code size when it's compressed\n\n/**\n* Total queue file size.\n*\n* @property size\n* @type Number\n*/\nself.size = 0;\n\n/**\n* Total bytes uploaded.\n*\n* @property loaded\n* @type Number\n*/\nself.loaded = 0;\n\n/**\n* Number of files uploaded.\n*\n* @property uploaded\n* @type Number\n*/\nself.uploaded = 0;\n\n/**\n* Number of files failed to upload.\n*\n* @property failed\n* @type Number\n*/\nself.failed = 0;\n\n/**\n* Number of files yet to be uploaded.\n*\n* @property queued\n* @type Number\n*/\nself.queued = 0;\n\n/**\n* Total percent of the uploaded bytes.\n*\n* @property percent\n* @type Number\n*/\nself.percent = 0;\n\n/**\n* Bytes uploaded per second.\n*\n* @property bytesPerSec\n* @type Number\n*/\nself.bytesPerSec = 0;\n\n/**\n* Resets the progress to it's initial values.\n*\n* @method reset\n*/\nself.reset = function() {\nself.size = self.loaded = self.uploaded = self.failed = self.queued = self.percent = self.bytesPerSec = 0;\n};\n};\n\nwindow.plupload = plupload;\n\n}(window, mOxie));\n"},useData:!0})},{"hbsfy/runtime":49}],36:[function(a,b,c){var d=a("hbsfy/runtime");b.exports=d.template({1:function(a,b,c,d,e){var f,g,h=b.helperMissing,i=this.escapeExpression,j='\t
    \n\t\t'+i((g=null!=(g=b.name||(null!=a?a.name:a))?g:h,"function"==typeof g?g.call(a,{name:"name",hash:{},data:d}):g))+"\n",f=b.each.call(a,null!=a?a.properties:a,{name:"each",hash:{},fn:this.program(4,d,e),inverse:this.noop,data:d}),null!=f&&(j+=f),j+"\t
    \n"},2:function(a,b,c,d){return"editing"},4:function(a,b,c,d,e){var f,g="";return f=b.unless.call(a,null!=(f=null!=a?a.propertyType:a)?f.duplicate:f,{name:"unless",hash:{},fn:this.program(5,d,e),inverse:this.noop,data:d}),null!=f&&(g+=f),g},5:function(a,b,c,d,e){var f,g="";return f=b.if.call(a,null!=(f=null!=(f=null!=a?a.propertyType:a)?f.definition:f)?f.hidden:f,{name:"if",hash:{},fn:this.program(6,d,e),inverse:this.program(10,d,e),data:d}),null!=f&&(g+=f),g},6:function(a,b,c,d,e){var f,g="";return f=b.if.call(a,null!=e[4]?e[4].editing:e[4],{name:"if",hash:{},fn:this.program(7,d,e),inverse:this.noop,data:d}),null!=f&&(g+=f),g},7:function(a,b,c,d){var e,f=b.helperMissing,g=this.lambda,h=this.escapeExpression,i='\t\t\t\t\t\t\n'},8:function(a,b,c,d){var e,f=b.helperMissing;return(0,this.escapeExpression)((e=null!=(e=b.value||(null!=a?a.value:a))?e:f,"function"==typeof e?e.call(a,{name:"value",hash:{},data:d}):e))},10:function(a,b,c,d,e){var f,g="";return f=b.if.call(a,null!=e[4]?e[4].editing:e[4],{name:"if",hash:{},fn:this.program(11,d,e),inverse:this.program(54,d,e),data:d}),null!=f&&(g+=f),g},11:function(a,b,c,d){var e,f="";return e=b.unless.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.mediapicker:e,{name:"unless",hash:{},fn:this.program(12,d),inverse:this.noop,data:d}),null!=e&&(f+=e),f},12:function(a,b,c,d){var e,f=this.lambda,g=this.escapeExpression,h='\t\t\t\t\t\t\t
    \n\n\t\t\t\t\t\t\t\t\n\n\t\t\t\t\t\t\t\t",e=b.if.call(a,null!=(e=null!=a?a.propertyType:a)?e.description:e,{name:"if",hash:{},fn:this.program(21,d),inverse:this.noop,data:d}),null!=e&&(h+=e),h+="\n\n",e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.minlength:e,{name:"if",hash:{},fn:this.program(23,d),inverse:this.noop,data:d}),null!=e&&(h+=e),e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.maxlength:e,{name:"if",hash:{},fn:this.program(25,d),inverse:this.noop,data:d}),null!=e&&(h+=e),h+="\n",e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.array:e,{name:"if",hash:{},fn:this.program(27,d),inverse:this.program(40,d),data:d}),null!=e&&(h+=e),h+"\t\t\t\t\t\t\t
    \n"},13:function(a,b,c,d){return"\t\t\t\t\t\t\t\t\tmandatory\n"},15:function(a,b,c,d){var e,f="";return e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.minlength:e,{name:"if",hash:{},fn:this.program(16,d),inverse:this.program(18,d),data:d}),null!=e&&(f+=e),f+"\t\t\t\t\t\t\t\t"},16:function(a,b,c,d){return"\t\t\t\t\t\t\t\t\t\tmandatory\n"},18:function(a,b,c,d){var e,f="";return e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.maxlength:e,{name:"if",hash:{},fn:this.program(19,d),inverse:this.noop,data:d}),null!=e&&(f+=e),f},19:function(a,b,c,d){return"\t\t\t\t\t\t\t\t\t\t\tmandatory\n"},21:function(a,b,c,d){var e,f=this.lambda;return''+(0,this.escapeExpression)(f(null!=(e=null!=a?a.propertyType:a)?e.description:e,a))+""},23:function(a,b,c,d){var e,f=this.lambda;return'\t\t\t\t\t\t\t\t\tMust be at least '+(0,this.escapeExpression)(f(null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.minlength:e,a))+" characters\n"},25:function(a,b,c,d){var e,f=this.lambda;return'\t\t\t\t\t\t\t\t\tCan not be more than '+(0,this.escapeExpression)(f(null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.maxlength:e,a))+" characters\n"},27:function(a,b,c,d){var e,f="";return e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.options:e,{name:"if",hash:{},fn:this.program(28,d),inverse:this.program(35,d),data:d}),null!=e&&(f+=e),f},28:function(a,b,c,d){var e,f=this.lambda,g=this.escapeExpression,h='\t\t\t\t\t\t\t\t\t\t\n"},29:function(a,b,c,d){return'required="required"'},31:function(a,b,c,d){return'multiple="multiple"'},33:function(a,b,c,d){var e=this.lambda,f=this.escapeExpression;return'\t\t\t\t\t\t\t\t\t\t\t\t\n"},35:function(a,b,c,d){var e,f="";return e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.hierarchical:e,{name:"if",hash:{},fn:this.program(36,d),inverse:this.program(38,d),data:d}),null!=e&&(f+=e),f},36:function(a,b,c,d){var e=b.helperMissing;return"\t\t\t\t\t\t\t\t\t\t\t"+(0,this.escapeExpression)((b.getHierarchicalProperty||a&&a.getHierarchicalProperty||e).call(a,null!=a?a.propertyType:a,{name:"getHierarchicalProperty",hash:{},data:d}))+"\n"},38:function(a,b,c,d){var e,f=this.lambda,g=this.escapeExpression;return'\t\t\t\t\t\t\t\t\t\t\t
      \n\t\t\t\t\t\t\t\t\t\t\t
    \n'},40:function(a,b,c,d){var e,f=b.helperMissing,g="";return e=(b.ifequal||a&&a.ifequal||f).call(a,null!=(e=null!=a?a.propertyType:a)?e.dataTypeId:e,1,{name:"ifequal",hash:{},fn:this.program(41,d),inverse:this.program(43,d),data:d}),null!=e&&(g+=e),g},41:function(a,b,c,d){var e,f=this.lambda,g=this.escapeExpression;return'\t\t\t\t\t\t\t\t\t\t\n'},43:function(a,b,c,d){var e,f="";return e=b.if.call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.multiline:e,{name:"if",hash:{},fn:this.program(44,d),inverse:this.program(49,d),data:d}),null!=e&&(f+=e),f},44:function(a,b,c,d){var e,f=this.lambda,g=this.escapeExpression,h=b.helperMissing,i='\t\t\t\t\t\t\t\t\t\t\t\n"},45:function(a,b,c,d){var e,f=this.lambda;return'minlength="'+(0,this.escapeExpression)(f(null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.minlength:e,a))+'"'},47:function(a,b,c,d){var e,f=this.lambda;return'maxlength="'+(0,this.escapeExpression)(f(null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.maxlength:e,a))+'"'},49:function(a,b,c,d){var e,f=b.helperMissing,g=this.lambda,h=this.escapeExpression,i='\t\t\t\t\t\t\t\t\t\t\t\n'},50:function(a,b,c,d){return"date"},52:function(a,b,c,d){return"text"},54:function(a,b,c,d){var e,f=this.lambda,g=this.escapeExpression,h=b.helperMissing,i='\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t\n";return e=(b.ifequal||a&&a.ifequal||h).call(a,null!=(e=null!=(e=null!=a?a.propertyType:a)?e.definition:e)?e.stringformat:e,"html",{name:"ifequal",hash:{},fn:this.program(55,d),inverse:this.program(58,d),data:d}),null!=e&&(i+=e),i+"\t\t\t\t\t\t
    \n"},55:function(a,b,c,d){var e,f,g,h=b.helperMissing,i=b.blockHelperMissing,j="\t\t\t\t\t\t\t\t";return f=null!=(f=b.getPropertyValue||(null!=a?a.getPropertyValue:a))?f:h,g={name:"getPropertyValue",hash:{},fn:this.program(56,d),inverse:this.noop,data:d},e="function"==typeof f?f.call(a,g):f,b.getPropertyValue||(e=i.call(a,e,g)),null!=e&&(j+=e),j+"\n"},56:function(a,b,c,d){var e,f,g=b.helperMissing;return f=null!=(f=b.value||(null!=a?a.value:a))?f:g,e="function"==typeof f?f.call(a,{name:"value",hash:{},data:d}):f,null!=e?e:""},58:function(a,b,c,d){var e,f,g,h=b.helperMissing,i=b.blockHelperMissing,j="\t\t\t\t\t\t\t\t";return f=null!=(f=b.getPropertyValue||(null!=a?a.getPropertyValue:a))?f:h,g={name:"getPropertyValue",hash:{},fn:this.program(8,d),inverse:this.noop,data:d},e="function"==typeof f?f.call(a,g):f,b.getPropertyValue||(e=i.call(a,e,g)),null!=e&&(j+=e),j+"\n"},60:function(a,b,c,d){return'\tNo properties available\n'},compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d,e){var f;return f=b.each.call(a,null!=a?a.propertySets:a,{name:"each",hash:{},fn:this.program(1,d,e),inverse:this.program(60,d,e),data:d}),null!=f?f:""},useData:!0,useDepths:!0})},{"hbsfy/runtime":49}],37:[function(a,b,c){var d=a("hbsfy/runtime");b.exports=d.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){return'
    \n\t
    \n\t\t
    \n\t\t
    \n\t\t
    \n\t\t
    \n\t
    \n\t
    \n\t\t
    \n\t\t
    \n\t\t
    \n\t\t
    \n\t
    \n\t
    \n\t\t
    \n\t\t
    \n\t\t
    \n\t\t
    \n\t
    \n
    '},useData:!0})},{"hbsfy/runtime":49}],38:[function(a,b,c){var d=a("hbsfy/runtime");b.exports=d.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'
    \n\t\t
    \n\t\t\t
    \n\t\t\t
    \n\t\t
    \n\t\t
    \n\t\t\t\n\t\t
    \n\t
    \n\t\t \n\t\t'+h((e=null!=(e=b.name||(null!=a?a.name:a))?e:g,typeof e===f?e.call(a,{name:"name",hash:{},data:d}):e))+' \n\t'+h((e=null!=(e=b.type||(null!=a?a.type:a))?e:g,typeof e===f?e.call(a,{name:"type",hash:{},data:d}):e))+''+h((e=null!=(e=b.sizeFormated||(null!=a?a.sizeFormated:a))?e:g,typeof e===f?e.call(a,{name:"sizeFormated",hash:{},data:d}):e))+'\n\t\t×\n\t\t\n\t\t\n\t
    \n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t
     '+g(f(null!=(e=null!=a?a.lexicon:a)?e.title:e,a))+""+g(f(null!=(e=null!=a?a.lexicon:a)?e.file_type:e,a))+""+g(f(null!=(e=null!=a?a.lexicon:a)?e.file_size:e,a))+'
    \n\t\t\n\n\t\t
    \n\t\t\t
    \n\t\t\t\t

    '+g(f(null!=(e=null!=a?a.lexicon:a)?e.category_choose:e,a))+"

    \n";return e=b.each.call(a,null!=(e=null!=a?a.metaData:a)?e.categories:e,{name:"each",hash:{},fn:this.program(1,d),inverse:this.noop,data:d}),null!=e&&(h+=e),h+='\t\t\t
    \n\t\t\t
    \n\t\t\t\t\n\t\t\t\t
    \n\t\t\t\t\t
    \n\n\t\t\t\t\t
    \n\t\t\t\t
    \n\t\t\t
    \n\t\t
    \n\t\n\n\t
    \n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t
    \n"},useData:!0})},{"hbsfy/runtime":49}],42:[function(a,b,c){"use strict";var d=a("./handlebars/base"),e=a("./handlebars/safe-string").default,f=a("./handlebars/exception").default,g=a("./handlebars/utils"),h=a("./handlebars/runtime"),i=function(){var a=new d.HandlebarsEnvironment;return g.extend(a,d),a.SafeString=e,a.Exception=f,a.Utils=g,a.escapeExpression=g.escapeExpression,a.VM=h,a.template=function(b){return h.template(b,a)},a},j=i();j.create=i,j.default=j,c.default=j},{"./handlebars/base":43,"./handlebars/exception":44,"./handlebars/runtime":45,"./handlebars/safe-string":46,"./handlebars/utils":47}],43:[function(a,b,c){"use strict";function d(a,b){this.helpers=a||{},this.partials=b||{},e(this)}function e(a){a.registerHelper("helperMissing",function(){if(1!==arguments.length)throw new g("Missing helper: '"+arguments[arguments.length-1].name+"'")}),a.registerHelper("blockHelperMissing",function(b,c){var d=c.inverse,e=c.fn;if(!0===b)return e(this);if(!1===b||null==b)return d(this);if(i(b))return b.length>0?(c.ids&&(c.ids=[c.name]),a.helpers.each(b,c)):d(this);if(c.data&&c.ids){var g=o(c.data);g.contextPath=f.appendContextPath(c.data.contextPath,c.name),c={data:g}}return e(b,c)}),a.registerHelper("each",function(a,b){if(!b)throw new g("Must pass iterator to #each");var c,d,e=b.fn,h=b.inverse,k=0,l="";if(b.data&&b.ids&&(d=f.appendContextPath(b.data.contextPath,b.ids[0])+"."),j(a)&&(a=a.call(this)),b.data&&(c=o(b.data)),a&&"object"==typeof a)if(i(a))for(var m=a.length;k= 2.0.0-beta.1"};c.REVISION_CHANGES=h;var i=f.isArray,j=f.isFunction,k=f.toString,l="[object Object]";c.HandlebarsEnvironment=d,d.prototype={constructor:d,logger:m,log:n,registerHelper:function(a,b){if(k.call(a)===l){if(b)throw new g("Arg not supported with multiple helpers");f.extend(this.helpers,a)}else this.helpers[a]=b},unregisterHelper:function(a){delete this.helpers[a]},registerPartial:function(a,b){k.call(a)===l?f.extend(this.partials,a):this.partials[a]=b},unregisterPartial:function(a){delete this.partials[a]}};var m={methodMap:{0:"debug",1:"info",2:"warn",3:"error"},DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,log:function(a,b){if(m.level<=a){var c=m.methodMap[a];"undefined"!=typeof console&&console[c]}}};c.logger=m;var n=m.log;c.log=n;var o=function(a){var b=f.extend({},a);return b._parent=a,b};c.createFrame=o},{"./exception":44,"./utils":47}],44:[function(a,b,c){"use strict";function d(a,b){var c;b&&b.firstLine&&(c=b.firstLine,a+=" - "+c+":"+b.firstColumn);for(var d=Error.prototype.constructor.call(this,a),f=0;f":">",'"':""","'":"'","`":"`"},k=/[&<>"'`]/g,l=/[&<>"'`]/;c.extend=e;var m=Object.prototype.toString;c.toString=m;var n=function(a){return"function"==typeof a};n(/x/)&&(n=function(a){return"function"==typeof a&&"[object Function]"===m.call(a)});var n;c.isFunction=n;var o=Array.isArray||function(a){return!(!a||"object"!=typeof a)&&"[object Array]"===m.call(a)};c.isArray=o,c.escapeExpression=f,c.isEmpty=g,c.appendContextPath=h},{"./safe-string":46}],48:[function(a,b,c){b.exports=a("./dist/cjs/handlebars.runtime")},{"./dist/cjs/handlebars.runtime":42}],49:[function(a,b,c){b.exports=a("handlebars/runtime").default},{"handlebars/runtime":48}],50:[function(a,b,c){var d=a("jquery");a("./core"),a("./widget"),a("./position"),a("./menu"),function(a,b){a.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var b,c,d,e=this.element[0].nodeName.toLowerCase(),f="textarea"===e,g="input"===e;this.isMultiLine=!!f||!g&&this.element.prop("isContentEditable"),this.valueMethod=this.element[f||g?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(e){if(this.element.prop("readOnly"))return b=!0,d=!0,void(c=!0);b=!1,d=!1,c=!1;var f=a.ui.keyCode;switch(e.keyCode){case f.PAGE_UP:b=!0,this._move("previousPage",e);break;case f.PAGE_DOWN:b=!0,this._move("nextPage",e);break;case f.UP:b=!0,this._keyEvent("previous",e);break;case f.DOWN:b=!0,this._keyEvent("next",e);break;case f.ENTER:case f.NUMPAD_ENTER:this.menu.active&&(b=!0,e.preventDefault(),this.menu.select(e));break;case f.TAB:this.menu.active&&this.menu.select(e);break;case f.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(e),e.preventDefault());break;default:c=!0,this._searchTimeout(e)}},keypress:function(d){if(b)return b=!1,void(this.isMultiLine&&!this.menu.element.is(":visible")||d.preventDefault());if(!c){var e=a.ui.keyCode;switch(d.keyCode){case e.PAGE_UP:this._move("previousPage",d);break;case e.PAGE_DOWN:this._move("nextPage",d);break;case e.UP:this._keyEvent("previous",d);break;case e.DOWN:this._keyEvent("next",d)}}},input:function(a){if(d)return d=!1,void a.preventDefault();this._searchTimeout(a)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(a){if(this.cancelBlur)return void delete this.cancelBlur;clearTimeout(this.searching),this.close(a),this._change(a)}}),this._initSource(),this.menu=a("