diff --git a/.gitignore b/.gitignore
index 1b2d9f4c54..7bd1c8ad07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,10 +25,8 @@ test/data/sql/
.vscode
.gitignore
.DS_Store
-vendor/
docker
.docker-sync
-composer.lock
CodeSniffer.conf
test/runtime
test/runtime/*
diff --git a/lib/composer.json b/lib/composer.json
new file mode 100644
index 0000000000..0ff71b7810
--- /dev/null
+++ b/lib/composer.json
@@ -0,0 +1,28 @@
+{
+ "require": {
+ "box/spout": "2.7.3",
+ "dragonmantank/cron-expression": "2.3.1",
+ "erusev/parsedown": "1.7.4",
+ "ezyang/htmlpurifier": "4.18.0",
+ "gorhill/php-fine-diff": "0.1.0",
+ "gregwar/captcha": "1.3.0",
+ "laminas/laminas-escaper": "2.6.1",
+ "laminas/laminas-zendframework-bridge": "1.1.1",
+ "markbaker/complex": "1.5.0",
+ "markbaker/matrix": "1.2.3",
+ "masterexploder/phpthumb": "2.1",
+ "mobiledetect/mobiledetectlib": "2.8.45",
+ "mustangostang/spyc": "0.6.3",
+ "overtrue/pinyin": "4.0.5",
+ "paragonie/constant_time_encoding": "2.7.0",
+ "paragonie/random_compat": "9.99.100",
+ "pclzip/pclzip": "2.8.2",
+ "phpmailer/phpmailer": "6.10.0",
+ "phpoffice/phpspreadsheet": "1.8.2",
+ "phpoffice/phpword": "0.18.3",
+ "phpseclib/phpseclib": "3.0.46",
+ "psr/simple-cache": "1.0.1",
+ "rmccue/requests": "2.0.15",
+ "symfony/finder": "3.4.47"
+ }
+}
diff --git a/lib/composer.lock b/lib/composer.lock
new file mode 100644
index 0000000000..6fa07346c3
--- /dev/null
+++ b/lib/composer.lock
@@ -0,0 +1,1639 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "015cfb3cc9d9463368148dbf304c65c9",
+ "packages": [
+ {
+ "name": "box/spout",
+ "version": "v2.7.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/box/spout.git",
+ "reference": "3681a3421a868ab9a65da156c554f756541f452b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/box/spout/zipball/3681a3421a868ab9a65da156c554f756541f452b",
+ "reference": "3681a3421a868ab9a65da156c554f756541f452b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-xmlreader": "*",
+ "ext-zip": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.0"
+ },
+ "suggest": {
+ "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
+ "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Box\\Spout\\": "src/Spout"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Adrien Loison",
+ "email": "adrien@box.com"
+ }
+ ],
+ "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
+ "homepage": "https://www.github.com/box/spout",
+ "keywords": [
+ "OOXML",
+ "csv",
+ "excel",
+ "memory",
+ "odf",
+ "ods",
+ "office",
+ "open",
+ "php",
+ "read",
+ "scale",
+ "spreadsheet",
+ "stream",
+ "write",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/box/spout/issues",
+ "source": "https://github.com/box/spout/tree/v2.7.3"
+ },
+ "abandoned": true,
+ "time": "2017-09-25T19:44:35+00:00"
+ },
+ {
+ "name": "dragonmantank/cron-expression",
+ "version": "v2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dragonmantank/cron-expression.git",
+ "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/65b2d8ee1f10915efb3b55597da3404f096acba2",
+ "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Cron\\": "src/Cron/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Chris Tankersley",
+ "email": "chris@ctankersley.com",
+ "homepage": "https://github.com/dragonmantank"
+ }
+ ],
+ "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+ "keywords": [
+ "cron",
+ "schedule"
+ ],
+ "support": {
+ "issues": "https://github.com/dragonmantank/cron-expression/issues",
+ "source": "https://github.com/dragonmantank/cron-expression/tree/v2.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dragonmantank",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-13T00:52:37+00:00"
+ },
+ {
+ "name": "erusev/parsedown",
+ "version": "1.7.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/erusev/parsedown.git",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Parsedown": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Emanuil Rusev",
+ "email": "hello@erusev.com",
+ "homepage": "http://erusev.com"
+ }
+ ],
+ "description": "Parser for Markdown.",
+ "homepage": "http://parsedown.org",
+ "keywords": [
+ "markdown",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/erusev/parsedown/issues",
+ "source": "https://github.com/erusev/parsedown/tree/1.7.x"
+ },
+ "time": "2019-12-30T22:54:17+00:00"
+ },
+ {
+ "name": "ezyang/htmlpurifier",
+ "version": "v4.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezyang/htmlpurifier.git",
+ "reference": "cb56001e54359df7ae76dc522d08845dc741621b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b",
+ "reference": "cb56001e54359df7ae76dc522d08845dc741621b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "require-dev": {
+ "cerdic/css-tidy": "^1.7 || ^2.0",
+ "simpletest/simpletest": "dev-master"
+ },
+ "suggest": {
+ "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
+ "ext-bcmath": "Used for unit conversion and imagecrash protection",
+ "ext-iconv": "Converts text to and from non-UTF-8 encodings",
+ "ext-tidy": "Used for pretty-printing HTML"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/HTMLPurifier.composer.php"
+ ],
+ "psr-0": {
+ "HTMLPurifier": "library/"
+ },
+ "exclude-from-classmap": [
+ "/library/HTMLPurifier/Language/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Edward Z. Yang",
+ "email": "admin@htmlpurifier.org",
+ "homepage": "http://ezyang.com"
+ }
+ ],
+ "description": "Standards compliant HTML filter written in PHP",
+ "homepage": "http://htmlpurifier.org/",
+ "keywords": [
+ "html"
+ ],
+ "support": {
+ "issues": "https://github.com/ezyang/htmlpurifier/issues",
+ "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0"
+ },
+ "time": "2024-11-01T03:51:45+00:00"
+ },
+ {
+ "name": "gorhill/php-fine-diff",
+ "version": "0.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/falk-graeser/PHP-FineDiff.git",
+ "reference": "b0c9ab136958882c501d461b0ad204f908e19392"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/falk-graeser/PHP-FineDiff/zipball/b0c9ab136958882c501d461b0ad204f908e19392",
+ "reference": "b0c9ab136958882c501d461b0ad204f908e19392",
+ "shasum": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "GorHill\\FineDiff\\": "GorHill/FineDiff"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Raymond Hill",
+ "email": "r.michael@queo-group.com",
+ "homepage": "http://www.queoflow.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Falk Gräser",
+ "email": "f.graeser@queo-group.com",
+ "homepage": "http://www.queoflow.com/",
+ "role": "The-Clean-Up-Guy"
+ }
+ ],
+ "homepage": "http://www.raymondhill.net/finediff/",
+ "keywords": [
+ "finediff"
+ ],
+ "support": {
+ "source": "https://github.com/falk-graeser/PHP-FineDiff/tree/0.1.0"
+ },
+ "time": "2015-07-30T10:21:26+00:00"
+ },
+ {
+ "name": "gregwar/captcha",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Gregwar/Captcha.git",
+ "reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/4edbcd09fde4353b94ce550f43460eba73baf2cc",
+ "reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.3.0",
+ "symfony/finder": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6.4 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Gregwar\\": "src/Gregwar"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Passault",
+ "email": "g.passault@gmail.com",
+ "homepage": "http://www.gregwar.com/"
+ },
+ {
+ "name": "Jeremy Livingston",
+ "email": "jeremy.j.livingston@gmail.com"
+ }
+ ],
+ "description": "Captcha generator",
+ "homepage": "https://github.com/Gregwar/Captcha",
+ "keywords": [
+ "bot",
+ "captcha",
+ "spam"
+ ],
+ "support": {
+ "issues": "https://github.com/Gregwar/Captcha/issues",
+ "source": "https://github.com/Gregwar/Captcha/tree/v1.3.0"
+ },
+ "time": "2025-06-23T12:25:54+00:00"
+ },
+ {
+ "name": "laminas/laminas-escaper",
+ "version": "2.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-escaper.git",
+ "reference": "25f2a053eadfa92ddacb609dcbbc39362610da70"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/25f2a053eadfa92ddacb609dcbbc39362610da70",
+ "reference": "25f2a053eadfa92ddacb609dcbbc39362610da70",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-zendframework-bridge": "^1.0",
+ "php": "^5.6 || ^7.0"
+ },
+ "replace": {
+ "zendframework/zend-escaper": "self.version"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "~1.0.0",
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6.x-dev",
+ "dev-develop": "2.7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laminas\\Escaper\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "escaper",
+ "laminas"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-escaper/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-escaper/issues",
+ "rss": "https://github.com/laminas/laminas-escaper/releases.atom",
+ "source": "https://github.com/laminas/laminas-escaper"
+ },
+ "time": "2019-12-31T16:43:30+00:00"
+ },
+ {
+ "name": "laminas/laminas-zendframework-bridge",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-zendframework-bridge.git",
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642",
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "module": "Laminas\\ZendFrameworkBridge"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/autoload.php"
+ ],
+ "psr-4": {
+ "Laminas\\ZendFrameworkBridge\\": "src//"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Alias legacy ZF class names to Laminas Project equivalents.",
+ "keywords": [
+ "ZendFramework",
+ "autoloading",
+ "laminas",
+ "zf"
+ ],
+ "support": {
+ "forum": "https://discourse.laminas.dev/",
+ "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues",
+ "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom",
+ "source": "https://github.com/laminas/laminas-zendframework-bridge"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "abandoned": true,
+ "time": "2020-09-14T14:23:00+00:00"
+ },
+ {
+ "name": "markbaker/complex",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/c3131244e29c08d44fefb49e0dd35021e9e39dd2",
+ "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6.0|^7.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0|^5.0|^6.0|^7.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^4.8.35|^5.0|^6.0|^7.0",
+ "sebastian/phpcpd": "2.*",
+ "squizlabs/php_codesniffer": "^3.4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "classes/src/functions/abs.php",
+ "classes/src/functions/acos.php",
+ "classes/src/functions/acosh.php",
+ "classes/src/functions/acot.php",
+ "classes/src/functions/acoth.php",
+ "classes/src/functions/acsc.php",
+ "classes/src/functions/acsch.php",
+ "classes/src/functions/argument.php",
+ "classes/src/functions/asec.php",
+ "classes/src/functions/asech.php",
+ "classes/src/functions/asin.php",
+ "classes/src/functions/asinh.php",
+ "classes/src/functions/atan.php",
+ "classes/src/functions/atanh.php",
+ "classes/src/functions/conjugate.php",
+ "classes/src/functions/cos.php",
+ "classes/src/functions/cosh.php",
+ "classes/src/functions/cot.php",
+ "classes/src/functions/coth.php",
+ "classes/src/functions/csc.php",
+ "classes/src/functions/csch.php",
+ "classes/src/functions/exp.php",
+ "classes/src/functions/inverse.php",
+ "classes/src/functions/ln.php",
+ "classes/src/functions/log2.php",
+ "classes/src/functions/log10.php",
+ "classes/src/functions/negative.php",
+ "classes/src/functions/pow.php",
+ "classes/src/functions/rho.php",
+ "classes/src/functions/sec.php",
+ "classes/src/functions/sech.php",
+ "classes/src/functions/sin.php",
+ "classes/src/functions/sinh.php",
+ "classes/src/functions/sqrt.php",
+ "classes/src/functions/tan.php",
+ "classes/src/functions/tanh.php",
+ "classes/src/functions/theta.php",
+ "classes/src/operations/add.php",
+ "classes/src/operations/subtract.php",
+ "classes/src/operations/multiply.php",
+ "classes/src/operations/divideby.php",
+ "classes/src/operations/divideinto.php"
+ ],
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/1.5.0"
+ },
+ "time": "2020-08-26T19:47:57+00:00"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "44bb1ab01811116f01fe216ab37d921dccc6c10d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/44bb1ab01811116f01fe216ab37d921dccc6c10d",
+ "reference": "44bb1ab01811116f01fe216ab37d921dccc6c10d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6.0|^7.0.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "dev-master",
+ "phploc/phploc": "^4",
+ "phpmd/phpmd": "dev-master",
+ "phpunit/phpunit": "^5.7|^6.0|7.0",
+ "sebastian/phpcpd": "^3.0",
+ "squizlabs/php_codesniffer": "^3.0@dev"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "classes/src/Functions/adjoint.php",
+ "classes/src/Functions/antidiagonal.php",
+ "classes/src/Functions/cofactors.php",
+ "classes/src/Functions/determinant.php",
+ "classes/src/Functions/diagonal.php",
+ "classes/src/Functions/identity.php",
+ "classes/src/Functions/inverse.php",
+ "classes/src/Functions/minors.php",
+ "classes/src/Functions/trace.php",
+ "classes/src/Functions/transpose.php",
+ "classes/src/Operations/add.php",
+ "classes/src/Operations/directsum.php",
+ "classes/src/Operations/subtract.php",
+ "classes/src/Operations/multiply.php",
+ "classes/src/Operations/divideby.php",
+ "classes/src/Operations/divideinto.php"
+ ],
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/1.2.3"
+ },
+ "time": "2021-01-26T14:36:01+00:00"
+ },
+ {
+ "name": "masterexploder/phpthumb",
+ "version": "2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/masterexploder/PHPThumb.git",
+ "reference": "01f6aa65aa3d136e175f67da2b4ea3e316770d02"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/masterexploder/PHPThumb/zipball/01f6aa65aa3d136e175f67da2b4ea3e316770d02",
+ "reference": "01f6aa65aa3d136e175f67da2b4ea3e316770d02",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "PHPThumb": "src",
+ "PHPThumb\\Tests": "tests"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jarrod Nettles",
+ "email": "jarrod.nettles@icloud.com"
+ },
+ {
+ "name": "Ian Selby",
+ "email": "ian@gxdlabs.com"
+ }
+ ],
+ "description": "A library for manipulating images in PHP.",
+ "homepage": "https://github.com/masterexploder/PHPThumb",
+ "keywords": [
+ "image",
+ "resize",
+ "rotate"
+ ],
+ "support": {
+ "issues": "https://github.com/masterexploder/PHPThumb/issues",
+ "source": "https://github.com/masterexploder/PHPThumb/tree/2.1"
+ },
+ "time": "2015-09-08T22:55:44+00:00"
+ },
+ {
+ "name": "mobiledetect/mobiledetectlib",
+ "version": "2.8.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/serbanghita/Mobile-Detect.git",
+ "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96aaebcf4f50d3d2692ab81d2c5132e425bca266",
+ "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Detection": "namespaced/"
+ },
+ "classmap": [
+ "Mobile_Detect.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Serban Ghita",
+ "email": "serbanghita@gmail.com",
+ "homepage": "http://mobiledetect.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
+ "homepage": "https://github.com/serbanghita/Mobile-Detect",
+ "keywords": [
+ "detect mobile devices",
+ "mobile",
+ "mobile detect",
+ "mobile detector",
+ "php mobile detect"
+ ],
+ "support": {
+ "issues": "https://github.com/serbanghita/Mobile-Detect/issues",
+ "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.45"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/serbanghita",
+ "type": "github"
+ }
+ ],
+ "time": "2023-11-07T21:57:25+00:00"
+ },
+ {
+ "name": "mustangostang/spyc",
+ "version": "0.6.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mustangostang/spyc.git",
+ "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mustangostang/spyc/zipball/4627c838b16550b666d15aeae1e5289dd5b77da0",
+ "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.3.*@dev"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.5.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "Spyc.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "mustangostang",
+ "email": "vlad.andersen@gmail.com"
+ }
+ ],
+ "description": "A simple YAML loader/dumper class for PHP",
+ "homepage": "https://github.com/mustangostang/spyc/",
+ "keywords": [
+ "spyc",
+ "yaml",
+ "yml"
+ ],
+ "support": {
+ "issues": "https://github.com/mustangostang/spyc/issues",
+ "source": "https://github.com/mustangostang/spyc/tree/0.6.3"
+ },
+ "time": "2019-09-10T13:16:29+00:00"
+ },
+ {
+ "name": "overtrue/pinyin",
+ "version": "4.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/overtrue/pinyin.git",
+ "reference": "94fdb3ea6eca5677afd9548111d95bedcd5a4086"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/overtrue/pinyin/zipball/94fdb3ea6eca5677afd9548111d95bedcd5a4086",
+ "reference": "94fdb3ea6eca5677afd9548111d95bedcd5a4086",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/const.php"
+ ],
+ "psr-4": {
+ "Overtrue\\Pinyin\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Carlos",
+ "homepage": "http://github.com/overtrue"
+ }
+ ],
+ "description": "Chinese to pinyin translator.",
+ "homepage": "https://github.com/overtrue/pinyin",
+ "keywords": [
+ "Chinese",
+ "Pinyin",
+ "cn2pinyin"
+ ],
+ "support": {
+ "issues": "https://github.com/overtrue/pinyin/issues",
+ "source": "https://github.com/overtrue/pinyin/tree/master"
+ },
+ "time": "2019-09-20T12:43:45+00:00"
+ },
+ {
+ "name": "paragonie/constant_time_encoding",
+ "version": "v2.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/constant_time_encoding.git",
+ "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105",
+ "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6|^7|^8|^9",
+ "vimeo/psalm": "^1|^2|^3|^4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com",
+ "role": "Maintainer"
+ },
+ {
+ "name": "Steve 'Sc00bz' Thomas",
+ "email": "steve@tobtu.com",
+ "homepage": "https://www.tobtu.com",
+ "role": "Original Developer"
+ }
+ ],
+ "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+ "keywords": [
+ "base16",
+ "base32",
+ "base32_decode",
+ "base32_encode",
+ "base64",
+ "base64_decode",
+ "base64_encode",
+ "bin2hex",
+ "encoding",
+ "hex",
+ "hex2bin",
+ "rfc4648"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+ "source": "https://github.com/paragonie/constant_time_encoding"
+ },
+ "time": "2024-05-08T12:18:48+00:00"
+ },
+ {
+ "name": "paragonie/random_compat",
+ "version": "v9.99.100",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/random_compat.git",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">= 7"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*|5.*",
+ "vimeo/psalm": "^1"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ },
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "polyfill",
+ "pseudorandom",
+ "random"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/random_compat/issues",
+ "source": "https://github.com/paragonie/random_compat"
+ },
+ "time": "2020-10-15T08:29:30+00:00"
+ },
+ {
+ "name": "pclzip/pclzip",
+ "version": "2.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ivanlanin/pclzip.git",
+ "reference": "19dd1de9d3f5fc4d7d70175b4c344dee329f45fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ivanlanin/pclzip/zipball/19dd1de9d3f5fc4d7d70175b4c344dee329f45fd",
+ "reference": "19dd1de9d3f5fc4d7d70175b4c344dee329f45fd",
+ "shasum": ""
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "pclzip.lib.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1"
+ ],
+ "authors": [
+ {
+ "name": "Vincent Blavet"
+ }
+ ],
+ "description": "A PHP library that offers compression and extraction functions for Zip formatted archives",
+ "homepage": "http://www.phpconcept.net/pclzip",
+ "keywords": [
+ "php",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/ivanlanin/pclzip/issues",
+ "source": "https://github.com/ivanlanin/pclzip/tree/master"
+ },
+ "time": "2014-06-05T11:42:24+00:00"
+ },
+ {
+ "name": "phpmailer/phpmailer",
+ "version": "v6.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPMailer/PHPMailer.git",
+ "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
+ "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-hash": "*",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "doctrine/annotations": "^1.2.6 || ^1.13.3",
+ "php-parallel-lint/php-console-highlighter": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "phpcompatibility/php-compatibility": "^9.3.5",
+ "roave/security-advisories": "dev-latest",
+ "squizlabs/php_codesniffer": "^3.7.2",
+ "yoast/phpunit-polyfills": "^1.0.4"
+ },
+ "suggest": {
+ "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+ "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+ "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+ "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+ "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+ "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+ "psr/log": "For optional PSR-3 debug logging",
+ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+ "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPMailer\\PHPMailer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-only"
+ ],
+ "authors": [
+ {
+ "name": "Marcus Bointon",
+ "email": "phpmailer@synchromedia.co.uk"
+ },
+ {
+ "name": "Jim Jagielski",
+ "email": "jimjag@gmail.com"
+ },
+ {
+ "name": "Andy Prevost",
+ "email": "codeworxtech@users.sourceforge.net"
+ },
+ {
+ "name": "Brent R. Matzelle"
+ }
+ ],
+ "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+ "support": {
+ "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Synchro",
+ "type": "github"
+ }
+ ],
+ "time": "2025-04-24T15:19:31+00:00"
+ },
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "1.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "0c1346a1956347590b7db09533966307d20cb7cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/0c1346a1956347590b7db09533966307d20cb7cc",
+ "reference": "0c1346a1956347590b7db09533966307d20cb7cc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "markbaker/complex": "^1.4",
+ "markbaker/matrix": "^1.1",
+ "php": "^5.6|^7.0",
+ "psr/simple-cache": "^1.0"
+ },
+ "require-dev": {
+ "doctrine/instantiator": "^1.0.0",
+ "dompdf/dompdf": "^0.8.0",
+ "friendsofphp/php-cs-fixer": "@stable",
+ "jpgraph/jpgraph": "^4.0",
+ "mpdf/mpdf": "^7.0.0",
+ "phpcompatibility/php-compatibility": "^8.0",
+ "phpunit/phpunit": "^5.7",
+ "squizlabs/php_codesniffer": "^3.3",
+ "tecnickcom/tcpdf": "^6.2"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+ "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ },
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.8.2"
+ },
+ "time": "2019-07-08T21:21:25+00:00"
+ },
+ {
+ "name": "phpoffice/phpword",
+ "version": "0.18.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PHPWord.git",
+ "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/be0190cd5d8f95b4be08d5853b107aa4e352759a",
+ "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-xml": "*",
+ "laminas/laminas-escaper": "^2.2",
+ "php": "^5.3.3 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "dompdf/dompdf": "0.8.* || 1.0.*",
+ "ext-gd": "*",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^2.2",
+ "mpdf/mpdf": "5.7.4 || 6.* || 7.* || 8.*",
+ "php-coveralls/php-coveralls": "1.1.0 || ^2.0",
+ "phploc/phploc": "2.* || 3.* || 4.* || 5.* || 6.* || 7.*",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^4.8.36 || ^7.0",
+ "squizlabs/php_codesniffer": "^2.9 || ^3.5",
+ "tecnickcom/tcpdf": "6.*"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Allows writing PDF",
+ "ext-gd2": "Allows adding images",
+ "ext-xmlwriter": "Allows writing OOXML and ODF",
+ "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
+ "ext-zip": "Allows writing OOXML and ODF"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-develop": "0.19-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpWord\\": "src/PhpWord"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker"
+ },
+ {
+ "name": "Gabriel Bull",
+ "email": "me@gabrielbull.com",
+ "homepage": "http://gabrielbull.com/"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net/blog/"
+ },
+ {
+ "name": "Ivan Lanin",
+ "homepage": "http://ivan.lanin.org"
+ },
+ {
+ "name": "Roman Syroeshko",
+ "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
+ },
+ {
+ "name": "Antoine de Troostembergh"
+ }
+ ],
+ "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)",
+ "homepage": "http://phpoffice.github.io",
+ "keywords": [
+ "ISO IEC 29500",
+ "OOXML",
+ "Office Open XML",
+ "OpenDocument",
+ "OpenXML",
+ "PhpOffice",
+ "PhpWord",
+ "Rich Text Format",
+ "WordprocessingML",
+ "doc",
+ "docx",
+ "html",
+ "odf",
+ "odt",
+ "office",
+ "pdf",
+ "php",
+ "reader",
+ "rtf",
+ "template",
+ "template processor",
+ "word",
+ "writer"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PHPWord/issues",
+ "source": "https://github.com/PHPOffice/PHPWord/tree/0.18.3"
+ },
+ "time": "2022-02-17T15:40:03+00:00"
+ },
+ {
+ "name": "phpseclib/phpseclib",
+ "version": "3.0.46",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpseclib/phpseclib.git",
+ "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6",
+ "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6",
+ "shasum": ""
+ },
+ "require": {
+ "paragonie/constant_time_encoding": "^1|^2|^3",
+ "paragonie/random_compat": "^1.4|^2.0|^9.99.99",
+ "php": ">=5.6.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*"
+ },
+ "suggest": {
+ "ext-dom": "Install the DOM extension to load XML formatted public keys.",
+ "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
+ "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
+ "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
+ "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "phpseclib/bootstrap.php"
+ ],
+ "psr-4": {
+ "phpseclib3\\": "phpseclib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jim Wigginton",
+ "email": "terrafrost@php.net",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Patrick Monnerat",
+ "email": "pm@datasphere.ch",
+ "role": "Developer"
+ },
+ {
+ "name": "Andreas Fischer",
+ "email": "bantu@phpbb.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Hans-Jürgen Petrich",
+ "email": "petrich@tronic-media.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Graham Campbell",
+ "email": "graham@alt-three.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
+ "homepage": "http://phpseclib.sourceforge.net",
+ "keywords": [
+ "BigInteger",
+ "aes",
+ "asn.1",
+ "asn1",
+ "blowfish",
+ "crypto",
+ "cryptography",
+ "encryption",
+ "rsa",
+ "security",
+ "sftp",
+ "signature",
+ "signing",
+ "ssh",
+ "twofish",
+ "x.509",
+ "x509"
+ ],
+ "support": {
+ "issues": "https://github.com/phpseclib/phpseclib/issues",
+ "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/terrafrost",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/phpseclib",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-26T16:29:55+00:00"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
+ },
+ "time": "2017-10-23T01:57:42+00:00"
+ },
+ {
+ "name": "rmccue/requests",
+ "version": "v2.0.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/WordPress/Requests.git",
+ "reference": "877cd66169755899682f1595e057334b40d9d149"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/WordPress/Requests/zipball/877cd66169755899682f1595e057334b40d9d149",
+ "reference": "877cd66169755899682f1595e057334b40d9d149",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7",
+ "php-parallel-lint/php-console-highlighter": "^0.5.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.1",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "requests/test-server": "dev-main",
+ "roave/security-advisories": "dev-latest",
+ "squizlabs/php_codesniffer": "^3.6",
+ "wp-coding-standards/wpcs": "^2.0",
+ "yoast/phpunit-polyfills": "^1.0.0"
+ },
+ "suggest": {
+ "art4/requests-psr18-adapter": "For using Requests as a PSR-18 HTTP Client",
+ "ext-curl": "For improved performance",
+ "ext-openssl": "For secure transport support",
+ "ext-zlib": "For improved performance when decompressing encoded streams"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/Deprecated.php"
+ ],
+ "psr-4": {
+ "WpOrg\\Requests\\": "src/"
+ },
+ "classmap": [
+ "library/Requests.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Ryan McCue",
+ "homepage": "https://rmccue.io/"
+ },
+ {
+ "name": "Alain Schlesser",
+ "homepage": "https://github.com/schlessera"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/WordPress/Requests/graphs/contributors"
+ }
+ ],
+ "description": "A HTTP library written in PHP, for human beings.",
+ "homepage": "https://requests.ryanmccue.info/",
+ "keywords": [
+ "curl",
+ "fsockopen",
+ "http",
+ "idna",
+ "ipv6",
+ "iri",
+ "sockets"
+ ],
+ "support": {
+ "docs": "https://requests.ryanmccue.info/",
+ "issues": "https://github.com/WordPress/Requests/issues",
+ "source": "https://github.com/WordPress/Requests"
+ },
+ "time": "2025-01-21T10:13:31+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v3.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "b6b6ad3db3edb1b4b1c1896b1975fb684994de6e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/b6b6ad3db3edb1b4b1c1896b1975fb684994de6e",
+ "reference": "b6b6ad3db3edb1b4b1c1896b1975fb684994de6e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Finder Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v3.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-11-16T17:02:08+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.2.0"
+}
diff --git a/lib/vendor/autoload.php b/lib/vendor/autoload.php
new file mode 100644
index 0000000000..58303259d8
--- /dev/null
+++ b/lib/vendor/autoload.php
@@ -0,0 +1,7 @@
+ Performance tests status: [](https://travis-ci.org/box/spout)
+
+
+## Support
+
+You can ask questions, submit new features ideas or discuss about Spout in the chat room:
+[](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+
+## Copyright and License
+
+Copyright 2017 Box, Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/lib/vendor/box/spout/composer.json b/lib/vendor/box/spout/composer.json
new file mode 100644
index 0000000000..76485c963a
--- /dev/null
+++ b/lib/vendor/box/spout/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "box/spout",
+ "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
+ "type": "library",
+ "keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"],
+ "license": "Apache-2.0",
+ "homepage": "https://www.github.com/box/spout",
+ "authors": [
+ {
+ "name": "Adrien Loison",
+ "email": "adrien@box.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.4.0",
+ "ext-zip": "*",
+ "ext-xmlreader" : "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.0"
+ },
+ "suggest": {
+ "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
+ "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
+ },
+ "autoload": {
+ "psr-4": {
+ "Box\\Spout\\": "src/Spout"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8.x-dev"
+ }
+ }
+
+}
diff --git a/lib/vendor/box/spout/src/Spout/Autoloader/Psr4Autoloader.php b/lib/vendor/box/spout/src/Spout/Autoloader/Psr4Autoloader.php
new file mode 100644
index 0000000000..67cccc376f
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Autoloader/Psr4Autoloader.php
@@ -0,0 +1,150 @@
+prefixes[$prefix]) === false) {
+ $this->prefixes[$prefix] = array();
+ }
+
+ // retain the base directory for the namespace prefix
+ if ($prepend) {
+ array_unshift($this->prefixes[$prefix], $baseDir);
+ } else {
+ array_push($this->prefixes[$prefix], $baseDir);
+ }
+ }
+
+ /**
+ * Loads the class file for a given class name.
+ *
+ * @param string $class The fully-qualified class name.
+ * @return mixed The mapped file name on success, or boolean false on
+ * failure.
+ */
+ public function loadClass($class)
+ {
+ // the current namespace prefix
+ $prefix = $class;
+
+ // work backwards through the namespace names of the fully-qualified
+ // class name to find a mapped file name
+ while (false !== $pos = strrpos($prefix, '\\')) {
+
+ // retain the trailing namespace separator in the prefix
+ $prefix = substr($class, 0, $pos + 1);
+
+ // the rest is the relative class name
+ $relativeClass = substr($class, $pos + 1);
+
+ // try to load a mapped file for the prefix and relative class
+ $mappedFile = $this->loadMappedFile($prefix, $relativeClass);
+ if ($mappedFile !== false) {
+ return $mappedFile;
+ }
+
+ // remove the trailing namespace separator for the next iteration
+ // of strrpos()
+ $prefix = rtrim($prefix, '\\');
+ }
+
+ // never found a mapped file
+ return false;
+ }
+
+ /**
+ * Load the mapped file for a namespace prefix and relative class.
+ *
+ * @param string $prefix The namespace prefix.
+ * @param string $relativeClass The relative class name.
+ * @return mixed Boolean false if no mapped file can be loaded, or the
+ * name of the mapped file that was loaded.
+ */
+ protected function loadMappedFile($prefix, $relativeClass)
+ {
+ // are there any base directories for this namespace prefix?
+ if (isset($this->prefixes[$prefix]) === false) {
+ return false;
+ }
+
+ // look through base directories for this namespace prefix
+ foreach ($this->prefixes[$prefix] as $baseDir) {
+
+ // replace the namespace prefix with the base directory,
+ // replace namespace separators with directory separators
+ // in the relative class name, append with .php
+ $file = $baseDir
+ . str_replace('\\', '/', $relativeClass)
+ . '.php';
+
+ // if the mapped file exists, require it
+ if ($this->requireFile($file)) {
+ // yes, we're done
+ return $file;
+ }
+ }
+
+ // never found it
+ return false;
+ }
+
+ /**
+ * If a file exists, require it from the file system.
+ *
+ * @param string $file The file to require.
+ * @return bool True if the file exists, false if not.
+ */
+ protected function requireFile($file)
+ {
+ if (file_exists($file)) {
+ require $file;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Autoloader/autoload.php b/lib/vendor/box/spout/src/Spout/Autoloader/autoload.php
new file mode 100644
index 0000000000..73ee519039
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Autoloader/autoload.php
@@ -0,0 +1,15 @@
+register();
+$loader->addNamespace('Box\Spout', $srcBaseDirectory);
diff --git a/lib/vendor/box/spout/src/Spout/Common/Escaper/CSV.php b/lib/vendor/box/spout/src/Spout/Common/Escaper/CSV.php
new file mode 100644
index 0000000000..4bc2d1af61
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Escaper/CSV.php
@@ -0,0 +1,38 @@
+', '&') need to be encoded.
+ // Single and double quotes can be left as is.
+ $escapedString = htmlspecialchars($string, ENT_NOQUOTES);
+
+ // control characters values are from 0 to 1F (hex values) in the ASCII table
+ // some characters should not be escaped though: "\t", "\r" and "\n".
+ $regexPattern = '[\x00-\x08' .
+ // skipping "\t" (0x9) and "\n" (0xA)
+ '\x0B-\x0C' .
+ // skipping "\r" (0xD)
+ '\x0E-\x1F]';
+ $replacedString = preg_replace("/$regexPattern/", '�', $escapedString);
+ }
+
+ return $replacedString;
+ }
+
+ /**
+ * Unescapes the given string to make it compatible with XLSX
+ *
+ * @param string $string The string to unescape
+ * @return string The unescaped string
+ */
+ public function unescape($string)
+ {
+ // ==============
+ // = WARNING =
+ // ==============
+ // It is assumed that the given string has already had its XML entities decoded.
+ // This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
+ // Therefore there is no need to call "htmlspecialchars_decode()".
+ return $string;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Common/Escaper/XLSX.php b/lib/vendor/box/spout/src/Spout/Common/Escaper/XLSX.php
new file mode 100644
index 0000000000..7c6c61b37b
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Escaper/XLSX.php
@@ -0,0 +1,185 @@
+escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern();
+ $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap();
+ $this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap);
+ }
+
+ /**
+ * Escapes the given string to make it compatible with XLSX
+ *
+ * @param string $string The string to escape
+ * @return string The escaped string
+ */
+ public function escape($string)
+ {
+ $escapedString = $this->escapeControlCharacters($string);
+ // @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded.
+ // Single and double quotes can be left as is.
+ $escapedString = htmlspecialchars($escapedString, ENT_NOQUOTES);
+
+ return $escapedString;
+ }
+
+ /**
+ * Unescapes the given string to make it compatible with XLSX
+ *
+ * @param string $string The string to unescape
+ * @return string The unescaped string
+ */
+ public function unescape($string)
+ {
+ // ==============
+ // = WARNING =
+ // ==============
+ // It is assumed that the given string has already had its XML entities decoded.
+ // This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation).
+ // Therefore there is no need to call "htmlspecialchars_decode()".
+ $unescapedString = $this->unescapeControlCharacters($string);
+
+ return $unescapedString;
+ }
+
+ /**
+ * @return string Regex pattern containing all escapable control characters
+ */
+ protected function getEscapableControlCharactersPattern()
+ {
+ // control characters values are from 0 to 1F (hex values) in the ASCII table
+ // some characters should not be escaped though: "\t", "\r" and "\n".
+ return '[\x00-\x08' .
+ // skipping "\t" (0x9) and "\n" (0xA)
+ '\x0B-\x0C' .
+ // skipping "\r" (0xD)
+ '\x0E-\x1F]';
+ }
+
+ /**
+ * Builds the map containing control characters to be escaped
+ * mapped to their escaped values.
+ * "\t", "\r" and "\n" don't need to be escaped.
+ *
+ * NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
+ * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
+ *
+ * @return string[]
+ */
+ protected function getControlCharactersEscapingMap()
+ {
+ $controlCharactersEscapingMap = [];
+
+ // control characters values are from 0 to 1F (hex values) in the ASCII table
+ for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) {
+ $character = chr($charValue);
+ if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) {
+ $charHexValue = dechex($charValue);
+ $escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_';
+ $controlCharactersEscapingMap[$escapedChar] = $character;
+ }
+ }
+
+ return $controlCharactersEscapingMap;
+ }
+
+ /**
+ * Converts PHP control characters from the given string to OpenXML escaped control characters
+ *
+ * Excel escapes control characters with _xHHHH_ and also escapes any
+ * literal strings of that type by encoding the leading underscore.
+ * So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_.
+ *
+ * NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
+ * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
+ *
+ * @param string $string String to escape
+ * @return string
+ */
+ protected function escapeControlCharacters($string)
+ {
+ $escapedString = $this->escapeEscapeCharacter($string);
+
+ // if no control characters
+ if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) {
+ return $escapedString;
+ }
+
+ return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function($matches) {
+ return $this->controlCharactersEscapingReverseMap[$matches[0]];
+ }, $escapedString);
+ }
+
+ /**
+ * Escapes the escape character: "_x0000_" -> "_x005F_x0000_"
+ *
+ * @param string $string String to escape
+ * @return string The escaped string
+ */
+ protected function escapeEscapeCharacter($string)
+ {
+ return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string);
+ }
+
+ /**
+ * Converts OpenXML escaped control characters from the given string to PHP control characters
+ *
+ * Excel escapes control characters with _xHHHH_ and also escapes any
+ * literal strings of that type by encoding the leading underscore.
+ * So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_"
+ *
+ * NOTE: the logic has been adapted from the XlsxWriter library (BSD License)
+ * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89
+ *
+ * @param string $string String to unescape
+ * @return string
+ */
+ protected function unescapeControlCharacters($string)
+ {
+ $unescapedString = $string;
+
+ foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) {
+ // only unescape characters that don't contain the escaped escape character for now
+ $unescapedString = preg_replace("/(?unescapeEscapeCharacter($unescapedString);
+ }
+
+ /**
+ * Unecapes the escape character: "_x005F_x0000_" => "_x0000_"
+ *
+ * @param string $string String to unescape
+ * @return string The unescaped string
+ */
+ protected function unescapeEscapeCharacter($string)
+ {
+ return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Common/Exception/EncodingConversionException.php b/lib/vendor/box/spout/src/Spout/Common/Exception/EncodingConversionException.php
new file mode 100644
index 0000000000..6dcc0a8d6a
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Exception/EncodingConversionException.php
@@ -0,0 +1,13 @@
+globalFunctionsHelper = $globalFunctionsHelper;
+
+ $this->supportedEncodingsWithBom = [
+ self::ENCODING_UTF8 => self::BOM_UTF8,
+ self::ENCODING_UTF16_LE => self::BOM_UTF16_LE,
+ self::ENCODING_UTF16_BE => self::BOM_UTF16_BE,
+ self::ENCODING_UTF32_LE => self::BOM_UTF32_LE,
+ self::ENCODING_UTF32_BE => self::BOM_UTF32_BE,
+ ];
+ }
+
+ /**
+ * Returns the number of bytes to use as offset in order to skip the BOM.
+ *
+ * @param resource $filePointer Pointer to the file to check
+ * @param string $encoding Encoding of the file to check
+ * @return int Bytes offset to apply to skip the BOM (0 means no BOM)
+ */
+ public function getBytesOffsetToSkipBOM($filePointer, $encoding)
+ {
+ $byteOffsetToSkipBom = 0;
+
+ if ($this->hasBOM($filePointer, $encoding)) {
+ $bomUsed = $this->supportedEncodingsWithBom[$encoding];
+
+ // we skip the N first bytes
+ $byteOffsetToSkipBom = strlen($bomUsed);
+ }
+
+ return $byteOffsetToSkipBom;
+ }
+
+ /**
+ * Returns whether the file identified by the given pointer has a BOM.
+ *
+ * @param resource $filePointer Pointer to the file to check
+ * @param string $encoding Encoding of the file to check
+ * @return bool TRUE if the file has a BOM, FALSE otherwise
+ */
+ protected function hasBOM($filePointer, $encoding)
+ {
+ $hasBOM = false;
+
+ $this->globalFunctionsHelper->rewind($filePointer);
+
+ if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) {
+ $potentialBom = $this->supportedEncodingsWithBom[$encoding];
+ $numBytesInBom = strlen($potentialBom);
+
+ $hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom);
+ }
+
+ return $hasBOM;
+ }
+
+ /**
+ * Attempts to convert a non UTF-8 string into UTF-8.
+ *
+ * @param string $string Non UTF-8 string to be converted
+ * @param string $sourceEncoding The encoding used to encode the source string
+ * @return string The converted, UTF-8 string
+ * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
+ */
+ public function attemptConversionToUTF8($string, $sourceEncoding)
+ {
+ return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8);
+ }
+
+ /**
+ * Attempts to convert a UTF-8 string into the given encoding.
+ *
+ * @param string $string UTF-8 string to be converted
+ * @param string $targetEncoding The encoding the string should be re-encoded into
+ * @return string The converted string, encoded with the given encoding
+ * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
+ */
+ public function attemptConversionFromUTF8($string, $targetEncoding)
+ {
+ return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding);
+ }
+
+ /**
+ * Attempts to convert the given string to the given encoding.
+ * Depending on what is installed on the server, we will try to iconv or mbstring.
+ *
+ * @param string $string string to be converted
+ * @param string $sourceEncoding The encoding used to encode the source string
+ * @param string $targetEncoding The encoding the string should be re-encoded into
+ * @return string The converted string, encoded with the given encoding
+ * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed
+ */
+ protected function attemptConversion($string, $sourceEncoding, $targetEncoding)
+ {
+ // if source and target encodings are the same, it's a no-op
+ if ($sourceEncoding === $targetEncoding) {
+ return $string;
+ }
+
+ $convertedString = null;
+
+ if ($this->canUseIconv()) {
+ $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding);
+ } else if ($this->canUseMbString()) {
+ $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding);
+ } else {
+ throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\".");
+ }
+
+ if ($convertedString === false) {
+ throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed.");
+ }
+
+ return $convertedString;
+ }
+
+ /**
+ * Returns whether "iconv" can be used.
+ *
+ * @return bool TRUE if "iconv" is available and can be used, FALSE otherwise
+ */
+ protected function canUseIconv()
+ {
+ return $this->globalFunctionsHelper->function_exists('iconv');
+ }
+
+ /**
+ * Returns whether "mb_string" functions can be used.
+ * These functions come with the PHP Intl package.
+ *
+ * @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise
+ */
+ protected function canUseMbString()
+ {
+ return $this->globalFunctionsHelper->function_exists('mb_convert_encoding');
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Common/Helper/FileSystemHelper.php b/lib/vendor/box/spout/src/Spout/Common/Helper/FileSystemHelper.php
new file mode 100644
index 0000000000..3145be73b5
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Helper/FileSystemHelper.php
@@ -0,0 +1,133 @@
+baseFolderRealPath = realpath($baseFolderPath);
+ }
+
+ /**
+ * Creates an empty folder with the given name under the given parent folder.
+ *
+ * @param string $parentFolderPath The parent folder path under which the folder is going to be created
+ * @param string $folderName The name of the folder to create
+ * @return string Path of the created folder
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder
+ */
+ public function createFolder($parentFolderPath, $folderName)
+ {
+ $this->throwIfOperationNotInBaseFolder($parentFolderPath);
+
+ $folderPath = $parentFolderPath . '/' . $folderName;
+
+ $wasCreationSuccessful = mkdir($folderPath, 0777, true);
+ if (!$wasCreationSuccessful) {
+ throw new IOException("Unable to create folder: $folderPath");
+ }
+
+ return $folderPath;
+ }
+
+ /**
+ * Creates a file with the given name and content in the given folder.
+ * The parent folder must exist.
+ *
+ * @param string $parentFolderPath The parent folder path where the file is going to be created
+ * @param string $fileName The name of the file to create
+ * @param string $fileContents The contents of the file to create
+ * @return string Path of the created file
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder
+ */
+ public function createFileWithContents($parentFolderPath, $fileName, $fileContents)
+ {
+ $this->throwIfOperationNotInBaseFolder($parentFolderPath);
+
+ $filePath = $parentFolderPath . '/' . $fileName;
+
+ $wasCreationSuccessful = file_put_contents($filePath, $fileContents);
+ if ($wasCreationSuccessful === false) {
+ throw new IOException("Unable to create file: $filePath");
+ }
+
+ return $filePath;
+ }
+
+ /**
+ * Delete the file at the given path
+ *
+ * @param string $filePath Path of the file to delete
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder
+ */
+ public function deleteFile($filePath)
+ {
+ $this->throwIfOperationNotInBaseFolder($filePath);
+
+ if (file_exists($filePath) && is_file($filePath)) {
+ unlink($filePath);
+ }
+ }
+
+ /**
+ * Delete the folder at the given path as well as all its contents
+ *
+ * @param string $folderPath Path of the folder to delete
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder
+ */
+ public function deleteFolderRecursively($folderPath)
+ {
+ $this->throwIfOperationNotInBaseFolder($folderPath);
+
+ $itemIterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($itemIterator as $item) {
+ if ($item->isDir()) {
+ rmdir($item->getPathname());
+ } else {
+ unlink($item->getPathname());
+ }
+ }
+
+ rmdir($folderPath);
+ }
+
+ /**
+ * All I/O operations must occur inside the base folder, for security reasons.
+ * This function will throw an exception if the folder where the I/O operation
+ * should occur is not inside the base folder.
+ *
+ * @param string $operationFolderPath The path of the folder where the I/O operation should occur
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder
+ */
+ protected function throwIfOperationNotInBaseFolder($operationFolderPath)
+ {
+ $operationFolderRealPath = realpath($operationFolderPath);
+ $isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0);
+ if (!$isInBaseFolder) {
+ throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}");
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php b/lib/vendor/box/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php
new file mode 100644
index 0000000000..c5d6e31952
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Helper/GlobalFunctionsHelper.php
@@ -0,0 +1,318 @@
+convertToUseRealPath($filePath);
+ return file_get_contents($realFilePath);
+ }
+
+ /**
+ * Updates the given file path to use a real path.
+ * This is to avoid issues on some Windows setup.
+ *
+ * @param string $filePath File path
+ * @return string The file path using a real path
+ */
+ protected function convertToUseRealPath($filePath)
+ {
+ $realFilePath = $filePath;
+
+ if ($this->isZipStream($filePath)) {
+ if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) {
+ $documentPath = $matches[1];
+ $documentInsideZipPath = $matches[2];
+ $realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath;
+ }
+ } else {
+ $realFilePath = realpath($filePath);
+ }
+
+ return $realFilePath;
+ }
+
+ /**
+ * Returns whether the given path is a zip stream.
+ *
+ * @param string $path Path pointing to a document
+ * @return bool TRUE if path is a zip stream, FALSE otherwise
+ */
+ protected function isZipStream($path)
+ {
+ return (strpos($path, 'zip://') === 0);
+ }
+
+ /**
+ * Wrapper around global function feof()
+ * @see feof()
+ *
+ * @param resource
+ * @return bool
+ */
+ public function feof($handle)
+ {
+ return feof($handle);
+ }
+
+ /**
+ * Wrapper around global function is_readable()
+ * @see is_readable()
+ *
+ * @param string $fileName
+ * @return bool
+ */
+ public function is_readable($fileName)
+ {
+ return is_readable($fileName);
+ }
+
+ /**
+ * Wrapper around global function basename()
+ * @see basename()
+ *
+ * @param string $path
+ * @param string|void $suffix
+ * @return string
+ */
+ public function basename($path, $suffix = null)
+ {
+ return basename($path, $suffix);
+ }
+
+ /**
+ * Wrapper around global function header()
+ * @see header()
+ *
+ * @param string $string
+ * @return void
+ */
+ public function header($string)
+ {
+ header($string);
+ }
+
+ /**
+ * Wrapper around global function ob_end_clean()
+ * @see ob_end_clean()
+ *
+ * @return void
+ */
+ public function ob_end_clean()
+ {
+ if (ob_get_length() > 0) {
+ ob_end_clean();
+ }
+ }
+
+ /**
+ * Wrapper around global function iconv()
+ * @see iconv()
+ *
+ * @param string $string The string to be converted
+ * @param string $sourceEncoding The encoding of the source string
+ * @param string $targetEncoding The encoding the source string should be converted to
+ * @return string|bool the converted string or FALSE on failure.
+ */
+ public function iconv($string, $sourceEncoding, $targetEncoding)
+ {
+ return iconv($sourceEncoding, $targetEncoding, $string);
+ }
+
+ /**
+ * Wrapper around global function mb_convert_encoding()
+ * @see mb_convert_encoding()
+ *
+ * @param string $string The string to be converted
+ * @param string $sourceEncoding The encoding of the source string
+ * @param string $targetEncoding The encoding the source string should be converted to
+ * @return string|bool the converted string or FALSE on failure.
+ */
+ public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding)
+ {
+ return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
+ }
+
+ /**
+ * Wrapper around global function stream_get_wrappers()
+ * @see stream_get_wrappers()
+ *
+ * @return array
+ */
+ public function stream_get_wrappers()
+ {
+ return stream_get_wrappers();
+ }
+
+ /**
+ * Wrapper around global function function_exists()
+ * @see function_exists()
+ *
+ * @param string $functionName
+ * @return bool
+ */
+ public function function_exists($functionName)
+ {
+ return function_exists($functionName);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Common/Helper/StringHelper.php b/lib/vendor/box/spout/src/Spout/Common/Helper/StringHelper.php
new file mode 100644
index 0000000000..273104e90c
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Helper/StringHelper.php
@@ -0,0 +1,71 @@
+hasMbstringSupport = extension_loaded('mbstring');
+ }
+
+ /**
+ * Returns the length of the given string.
+ * It uses the multi-bytes function is available.
+ * @see strlen
+ * @see mb_strlen
+ *
+ * @param string $string
+ * @return int
+ */
+ public function getStringLength($string)
+ {
+ return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string);
+ }
+
+ /**
+ * Returns the position of the first occurrence of the given character/substring within the given string.
+ * It uses the multi-bytes function is available.
+ * @see strpos
+ * @see mb_strpos
+ *
+ * @param string $char Needle
+ * @param string $string Haystack
+ * @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found
+ */
+ public function getCharFirstOccurrencePosition($char, $string)
+ {
+ $position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char);
+ return ($position !== false) ? $position : -1;
+ }
+
+ /**
+ * Returns the position of the last occurrence of the given character/substring within the given string.
+ * It uses the multi-bytes function is available.
+ * @see strrpos
+ * @see mb_strrpos
+ *
+ * @param string $char Needle
+ * @param string $string Haystack
+ * @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found
+ */
+ public function getCharLastOccurrencePosition($char, $string)
+ {
+ $position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char);
+ return ($position !== false) ? $position : -1;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Common/Singleton.php b/lib/vendor/box/spout/src/Spout/Common/Singleton.php
new file mode 100644
index 0000000000..015ede8e2f
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Singleton.php
@@ -0,0 +1,41 @@
+init();
+ }
+
+ /**
+ * Initializes the singleton
+ * @return void
+ */
+ protected function init() {}
+
+ final private function __wakeup() {}
+ final private function __clone() {}
+}
diff --git a/lib/vendor/box/spout/src/Spout/Common/Type.php b/lib/vendor/box/spout/src/Spout/Common/Type.php
new file mode 100644
index 0000000000..5e9b75e614
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Common/Type.php
@@ -0,0 +1,16 @@
+globalFunctionsHelper = $globalFunctionsHelper;
+ return $this;
+ }
+
+ /**
+ * Sets whether date/time values should be returned as PHP objects or be formatted as strings.
+ *
+ * @api
+ * @param bool $shouldFormatDates
+ * @return AbstractReader
+ */
+ public function setShouldFormatDates($shouldFormatDates)
+ {
+ $this->getOptions()->setShouldFormatDates($shouldFormatDates);
+ return $this;
+ }
+
+ /**
+ * Sets whether empty rows should be returned or skipped.
+ *
+ * @api
+ * @param bool $shouldPreserveEmptyRows
+ * @return AbstractReader
+ */
+ public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
+ {
+ $this->getOptions()->setShouldPreserveEmptyRows($shouldPreserveEmptyRows);
+ return $this;
+ }
+
+ /**
+ * Prepares the reader to read the given file. It also makes sure
+ * that the file exists and is readable.
+ *
+ * @api
+ * @param string $filePath Path of the file to be read
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted
+ */
+ public function open($filePath)
+ {
+ if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) {
+ throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file.");
+ }
+
+ if (!$this->isPhpStream($filePath)) {
+ // we skip the checks if the provided file path points to a PHP stream
+ if (!$this->globalFunctionsHelper->file_exists($filePath)) {
+ throw new IOException("Could not open $filePath for reading! File does not exist.");
+ } else if (!$this->globalFunctionsHelper->is_readable($filePath)) {
+ throw new IOException("Could not open $filePath for reading! File is not readable.");
+ }
+ }
+
+ try {
+ $fileRealPath = $this->getFileRealPath($filePath);
+ $this->openReader($fileRealPath);
+ $this->isStreamOpened = true;
+ } catch (\Exception $exception) {
+ throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})");
+ }
+ }
+
+ /**
+ * Returns the real path of the given path.
+ * If the given path is a valid stream wrapper, returns the path unchanged.
+ *
+ * @param string $filePath
+ * @return string
+ */
+ protected function getFileRealPath($filePath)
+ {
+ if ($this->isSupportedStreamWrapper($filePath)) {
+ return $filePath;
+ }
+
+ // Need to use realpath to fix "Can't open file" on some Windows setup
+ return realpath($filePath);
+ }
+
+ /**
+ * Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used.
+ * For example, php://temp => php, s3://path/to/file => s3...
+ *
+ * @param string $filePath Path of the file to be read
+ * @return string|null The stream wrapper scheme or NULL if not a stream wrapper
+ */
+ protected function getStreamWrapperScheme($filePath)
+ {
+ $streamScheme = null;
+ if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
+ $streamScheme = $matches[1];
+ }
+ return $streamScheme;
+ }
+
+ /**
+ * Checks if the given path is an unsupported stream wrapper
+ * (like local path, php://temp, mystream://foo/bar...).
+ *
+ * @param string $filePath Path of the file to be read
+ * @return bool Whether the given path is an unsupported stream wrapper
+ */
+ protected function isStreamWrapper($filePath)
+ {
+ return ($this->getStreamWrapperScheme($filePath) !== null);
+ }
+
+ /**
+ * Checks if the given path is an supported stream wrapper
+ * (like php://temp, mystream://foo/bar...).
+ * If the given path is a local path, returns true.
+ *
+ * @param string $filePath Path of the file to be read
+ * @return bool Whether the given path is an supported stream wrapper
+ */
+ protected function isSupportedStreamWrapper($filePath)
+ {
+ $streamScheme = $this->getStreamWrapperScheme($filePath);
+ return ($streamScheme !== null) ?
+ in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) :
+ true;
+ }
+
+ /**
+ * Checks if a path is a PHP stream (like php://output, php://memory, ...)
+ *
+ * @param string $filePath Path of the file to be read
+ * @return bool Whether the given path maps to a PHP stream
+ */
+ protected function isPhpStream($filePath)
+ {
+ $streamScheme = $this->getStreamWrapperScheme($filePath);
+ return ($streamScheme === 'php');
+ }
+
+ /**
+ * Returns an iterator to iterate over sheets.
+ *
+ * @api
+ * @return \Iterator To iterate over sheets
+ * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader
+ */
+ public function getSheetIterator()
+ {
+ if (!$this->isStreamOpened) {
+ throw new ReaderNotOpenedException('Reader should be opened first.');
+ }
+
+ return $this->getConcreteSheetIterator();
+ }
+
+ /**
+ * Closes the reader, preventing any additional reading
+ *
+ * @api
+ * @return void
+ */
+ public function close()
+ {
+ if ($this->isStreamOpened) {
+ $this->closeReader();
+
+ $sheetIterator = $this->getConcreteSheetIterator();
+ if ($sheetIterator) {
+ $sheetIterator->end();
+ }
+
+ $this->isStreamOpened = false;
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/CSV/Reader.php b/lib/vendor/box/spout/src/Spout/Reader/CSV/Reader.php
new file mode 100644
index 0000000000..648a12d36a
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/CSV/Reader.php
@@ -0,0 +1,149 @@
+options)) {
+ $this->options = new ReaderOptions();
+ }
+ return $this->options;
+ }
+
+ /**
+ * Sets the field delimiter for the CSV.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $fieldDelimiter Character that delimits fields
+ * @return Reader
+ */
+ public function setFieldDelimiter($fieldDelimiter)
+ {
+ $this->getOptions()->setFieldDelimiter($fieldDelimiter);
+ return $this;
+ }
+
+ /**
+ * Sets the field enclosure for the CSV.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $fieldEnclosure Character that enclose fields
+ * @return Reader
+ */
+ public function setFieldEnclosure($fieldEnclosure)
+ {
+ $this->getOptions()->setFieldEnclosure($fieldEnclosure);
+ return $this;
+ }
+
+ /**
+ * Sets the encoding of the CSV file to be read.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $encoding Encoding of the CSV file to be read
+ * @return Reader
+ */
+ public function setEncoding($encoding)
+ {
+ $this->getOptions()->setEncoding($encoding);
+ return $this;
+ }
+
+ /**
+ * Sets the EOL for the CSV.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $endOfLineCharacter used to properly get lines from the CSV file.
+ * @return Reader
+ */
+ public function setEndOfLineCharacter($endOfLineCharacter)
+ {
+ $this->getOptions()->setEndOfLineCharacter($endOfLineCharacter);
+ return $this;
+ }
+
+ /**
+ * Returns whether stream wrappers are supported
+ *
+ * @return bool
+ */
+ protected function doesSupportStreamWrapper()
+ {
+ return true;
+ }
+
+ /**
+ * Opens the file at the given path to make it ready to be read.
+ * If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
+ *
+ * @param string $filePath Path of the CSV file to be read
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException
+ */
+ protected function openReader($filePath)
+ {
+ $this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings');
+ ini_set('auto_detect_line_endings', '1');
+
+ $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r');
+ if (!$this->filePointer) {
+ throw new IOException("Could not open file $filePath for reading.");
+ }
+
+ $this->sheetIterator = new SheetIterator(
+ $this->filePointer,
+ $this->getOptions(),
+ $this->globalFunctionsHelper
+ );
+ }
+
+ /**
+ * Returns an iterator to iterate over sheets.
+ *
+ * @return SheetIterator To iterate over sheets
+ */
+ protected function getConcreteSheetIterator()
+ {
+ return $this->sheetIterator;
+ }
+
+
+ /**
+ * Closes the reader. To be used after reading the file.
+ *
+ * @return void
+ */
+ protected function closeReader()
+ {
+ if ($this->filePointer) {
+ $this->globalFunctionsHelper->fclose($this->filePointer);
+ }
+
+ ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/CSV/ReaderOptions.php b/lib/vendor/box/spout/src/Spout/Reader/CSV/ReaderOptions.php
new file mode 100644
index 0000000000..9a1adb89d9
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/CSV/ReaderOptions.php
@@ -0,0 +1,110 @@
+fieldDelimiter;
+ }
+
+ /**
+ * Sets the field delimiter for the CSV.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $fieldDelimiter Character that delimits fields
+ * @return ReaderOptions
+ */
+ public function setFieldDelimiter($fieldDelimiter)
+ {
+ $this->fieldDelimiter = $fieldDelimiter;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFieldEnclosure()
+ {
+ return $this->fieldEnclosure;
+ }
+
+ /**
+ * Sets the field enclosure for the CSV.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $fieldEnclosure Character that enclose fields
+ * @return ReaderOptions
+ */
+ public function setFieldEnclosure($fieldEnclosure)
+ {
+ $this->fieldEnclosure = $fieldEnclosure;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getEncoding()
+ {
+ return $this->encoding;
+ }
+
+ /**
+ * Sets the encoding of the CSV file to be read.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $encoding Encoding of the CSV file to be read
+ * @return ReaderOptions
+ */
+ public function setEncoding($encoding)
+ {
+ $this->encoding = $encoding;
+ return $this;
+ }
+
+ /**
+ * @return string EOL for the CSV
+ */
+ public function getEndOfLineCharacter()
+ {
+ return $this->endOfLineCharacter;
+ }
+
+ /**
+ * Sets the EOL for the CSV.
+ * Needs to be called before opening the reader.
+ *
+ * @param string $endOfLineCharacter used to properly get lines from the CSV file.
+ * @return ReaderOptions
+ */
+ public function setEndOfLineCharacter($endOfLineCharacter)
+ {
+ $this->endOfLineCharacter = $endOfLineCharacter;
+ return $this;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/CSV/RowIterator.php b/lib/vendor/box/spout/src/Spout/Reader/CSV/RowIterator.php
new file mode 100644
index 0000000000..a2a6672c77
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/CSV/RowIterator.php
@@ -0,0 +1,260 @@
+filePointer = $filePointer;
+ $this->fieldDelimiter = $options->getFieldDelimiter();
+ $this->fieldEnclosure = $options->getFieldEnclosure();
+ $this->encoding = $options->getEncoding();
+ $this->inputEOLDelimiter = $options->getEndOfLineCharacter();
+ $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
+ $this->globalFunctionsHelper = $globalFunctionsHelper;
+
+ $this->encodingHelper = new EncodingHelper($globalFunctionsHelper);
+ }
+
+ /**
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ *
+ * @return void
+ */
+ public function rewind()
+ {
+ $this->rewindAndSkipBom();
+
+ $this->numReadRows = 0;
+ $this->rowDataBuffer = null;
+
+ $this->next();
+ }
+
+ /**
+ * This rewinds and skips the BOM if inserted at the beginning of the file
+ * by moving the file pointer after it, so that it is not read.
+ *
+ * @return void
+ */
+ protected function rewindAndSkipBom()
+ {
+ $byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding);
+
+ // sets the cursor after the BOM (0 means no BOM, so rewind it)
+ $this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom);
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ return ($this->filePointer && !$this->hasReachedEndOfFile);
+ }
+
+ /**
+ * Move forward to next element. Reads data for the next unprocessed row.
+ * @link http://php.net/manual/en/iterator.next.php
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
+ */
+ public function next()
+ {
+ $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
+
+ if (!$this->hasReachedEndOfFile) {
+ $this->readDataForNextRow();
+ }
+ }
+
+ /**
+ * @return void
+ * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
+ */
+ protected function readDataForNextRow()
+ {
+ do {
+ $rowData = $this->getNextUTF8EncodedRow();
+ } while ($this->shouldReadNextRow($rowData));
+
+ if ($rowData !== false) {
+ // str_replace will replace NULL values by empty strings
+ $this->rowDataBuffer = str_replace(null, null, $rowData);
+ $this->numReadRows++;
+ } else {
+ // If we reach this point, it means end of file was reached.
+ // This happens when the last lines are empty lines.
+ $this->hasReachedEndOfFile = true;
+ }
+ }
+
+ /**
+ * @param array|bool $currentRowData
+ * @return bool Whether the data for the current row can be returned or if we need to keep reading
+ */
+ protected function shouldReadNextRow($currentRowData)
+ {
+ $hasSuccessfullyFetchedRowData = ($currentRowData !== false);
+ $hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer);
+ $isEmptyLine = $this->isEmptyLine($currentRowData);
+
+ return (
+ (!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) ||
+ (!$this->shouldPreserveEmptyRows && $isEmptyLine)
+ );
+ }
+
+ /**
+ * Returns the next row, converted if necessary to UTF-8.
+ * As fgetcsv() does not manage correctly encoding for non UTF-8 data,
+ * we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes)
+ *
+ * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read
+ * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8
+ */
+ protected function getNextUTF8EncodedRow()
+ {
+ $encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure);
+ if ($encodedRowData === false) {
+ return false;
+ }
+
+ foreach ($encodedRowData as $cellIndex => $cellValue) {
+ switch($this->encoding) {
+ case EncodingHelper::ENCODING_UTF16_LE:
+ case EncodingHelper::ENCODING_UTF32_LE:
+ // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
+ $cellValue = ltrim($cellValue);
+ break;
+
+ case EncodingHelper::ENCODING_UTF16_BE:
+ case EncodingHelper::ENCODING_UTF32_BE:
+ // remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data
+ $cellValue = rtrim($cellValue);
+ break;
+ }
+
+ $encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding);
+ }
+
+ return $encodedRowData;
+ }
+
+ /**
+ * Returns the end of line delimiter, encoded using the same encoding as the CSV.
+ * The return value is cached.
+ *
+ * @return string
+ */
+ protected function getEncodedEOLDelimiter()
+ {
+ if (!isset($this->encodedEOLDelimiter)) {
+ $this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding);
+ }
+
+ return $this->encodedEOLDelimiter;
+ }
+
+ /**
+ * @param array|bool $lineData Array containing the cells value for the line
+ * @return bool Whether the given line is empty
+ */
+ protected function isEmptyLine($lineData)
+ {
+ return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null);
+ }
+
+ /**
+ * Return the current element from the buffer
+ * @link http://php.net/manual/en/iterator.current.php
+ *
+ * @return array|null
+ */
+ public function current()
+ {
+ return $this->rowDataBuffer;
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ *
+ * @return int
+ */
+ public function key()
+ {
+ return $this->numReadRows;
+ }
+
+ /**
+ * Cleans up what was created to iterate over the object.
+ *
+ * @return void
+ */
+ public function end()
+ {
+ // do nothing
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/CSV/Sheet.php b/lib/vendor/box/spout/src/Spout/Reader/CSV/Sheet.php
new file mode 100644
index 0000000000..9a688db0da
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/CSV/Sheet.php
@@ -0,0 +1,62 @@
+rowIterator = new RowIterator($filePointer, $options, $globalFunctionsHelper);
+ }
+
+ /**
+ * @api
+ * @return \Box\Spout\Reader\CSV\RowIterator
+ */
+ public function getRowIterator()
+ {
+ return $this->rowIterator;
+ }
+
+ /**
+ * @api
+ * @return int Index of the sheet
+ */
+ public function getIndex()
+ {
+ return 0;
+ }
+
+ /**
+ * @api
+ * @return string Name of the sheet - empty string since CSV does not support that
+ */
+ public function getName()
+ {
+ return '';
+ }
+
+ /**
+ * @api
+ * @return bool Always TRUE as there is only one sheet
+ */
+ public function isActive()
+ {
+ return true;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/CSV/SheetIterator.php b/lib/vendor/box/spout/src/Spout/Reader/CSV/SheetIterator.php
new file mode 100644
index 0000000000..58a9480c4b
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/CSV/SheetIterator.php
@@ -0,0 +1,95 @@
+sheet = new Sheet($filePointer, $options, $globalFunctionsHelper);
+ }
+
+ /**
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ *
+ * @return void
+ */
+ public function rewind()
+ {
+ $this->hasReadUniqueSheet = false;
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ return (!$this->hasReadUniqueSheet);
+ }
+
+ /**
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ *
+ * @return void
+ */
+ public function next()
+ {
+ $this->hasReadUniqueSheet = true;
+ }
+
+ /**
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ *
+ * @return \Box\Spout\Reader\CSV\Sheet
+ */
+ public function current()
+ {
+ return $this->sheet;
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ *
+ * @return int
+ */
+ public function key()
+ {
+ return 1;
+ }
+
+ /**
+ * Cleans up what was created to iterate over the object.
+ *
+ * @return void
+ */
+ public function end()
+ {
+ // do nothing
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/Common/ReaderOptions.php b/lib/vendor/box/spout/src/Spout/Reader/Common/ReaderOptions.php
new file mode 100644
index 0000000000..4ab7a07f48
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/Common/ReaderOptions.php
@@ -0,0 +1,58 @@
+shouldFormatDates;
+ }
+
+ /**
+ * Sets whether date/time values should be returned as PHP objects or be formatted as strings.
+ *
+ * @param bool $shouldFormatDates
+ * @return ReaderOptions
+ */
+ public function setShouldFormatDates($shouldFormatDates)
+ {
+ $this->shouldFormatDates = $shouldFormatDates;
+ return $this;
+ }
+
+ /**
+ * @return bool Whether empty rows should be returned or skipped.
+ */
+ public function shouldPreserveEmptyRows()
+ {
+ return $this->shouldPreserveEmptyRows;
+ }
+
+ /**
+ * Sets whether empty rows should be returned or skipped.
+ *
+ * @param bool $shouldPreserveEmptyRows
+ * @return ReaderOptions
+ */
+ public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows)
+ {
+ $this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows;
+ return $this;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/Common/XMLProcessor.php b/lib/vendor/box/spout/src/Spout/Reader/Common/XMLProcessor.php
new file mode 100644
index 0000000000..d8a1da8d27
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/Common/XMLProcessor.php
@@ -0,0 +1,152 @@
+xmlReader = $xmlReader;
+ }
+
+ /**
+ * @param string $nodeName A callback may be triggered when a node with this name is read
+ * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
+ * @param callable $callback Callback to execute when the read node has the given name and type
+ * @return XMLProcessor
+ */
+ public function registerCallback($nodeName, $nodeType, $callback)
+ {
+ $callbackKey = $this->getCallbackKey($nodeName, $nodeType);
+ $this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback);
+
+ return $this;
+ }
+
+ /**
+ * @param string $nodeName Name of the node
+ * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
+ * @return string Key used to store the associated callback
+ */
+ private function getCallbackKey($nodeName, $nodeType)
+ {
+ return "$nodeName$nodeType";
+ }
+
+ /**
+ * Because the callback can be a "protected" function, we don't want to use call_user_func() directly
+ * but instead invoke the callback using Reflection. This allows the invocation of "protected" functions.
+ * Since some functions can be called a lot, we pre-process the callback to only return the elements that
+ * will be needed to invoke the callback later.
+ *
+ * @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME]
+ * @return array Associative array containing the elements needed to invoke the callback using Reflection
+ */
+ private function getInvokableCallbackData($callback)
+ {
+ $callbackObject = $callback[0];
+ $callbackMethodName = $callback[1];
+ $reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName);
+ $reflectionMethod->setAccessible(true);
+
+ return [
+ self::CALLBACK_REFLECTION_METHOD => $reflectionMethod,
+ self::CALLBACK_REFLECTION_OBJECT => $callbackObject,
+ ];
+ }
+
+ /**
+ * Resumes the reading of the XML file where it was left off.
+ * Stops whenever a callback indicates that reading should stop or at the end of the file.
+ *
+ * @return void
+ * @throws \Box\Spout\Reader\Exception\XMLProcessingException
+ */
+ public function readUntilStopped()
+ {
+ while ($this->xmlReader->read()) {
+ $nodeType = $this->xmlReader->nodeType;
+ $nodeNamePossiblyWithPrefix = $this->xmlReader->name;
+ $nodeNameWithoutPrefix = $this->xmlReader->localName;
+
+ $callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType);
+
+ if ($callbackData !== null) {
+ $callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]);
+
+ if ($callbackResponse === self::PROCESSING_STOP) {
+ // stop reading
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * @param string $nodeNamePossiblyWithPrefix Name of the node, possibly prefixed
+ * @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed
+ * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END]
+ * @return array|null Callback data to be used for execution when a node of the given name/type is read or NULL if none found
+ */
+ private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType)
+ {
+ // With prefixed nodes, we should match if (by order of preference):
+ // 1. the callback was registered with the prefixed node name (e.g. "x:worksheet")
+ // 2. the callback was registered with the un-prefixed node name (e.g. "worksheet")
+ $callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType);
+ $callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType);
+ $hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix);
+
+ $callbackKeyToUse = $callbackKeyForUnPrefixedName;
+ if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) {
+ $callbackKeyToUse = $callbackKeyForPossiblyPrefixedName;
+ }
+
+ // Using isset here because it is way faster than array_key_exists...
+ return isset($this->callbacks[$callbackKeyToUse]) ? $this->callbacks[$callbackKeyToUse] : null;
+ }
+
+ /**
+ * @param array $callbackData Associative array containing data to invoke the callback using Reflection
+ * @param array $args Arguments to pass to the callback
+ * @return int Callback response
+ */
+ private function invokeCallback($callbackData, $args)
+ {
+ $reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD];
+ $callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT];
+
+ return $reflectionMethod->invokeArgs($callbackObject, $args);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php b/lib/vendor/box/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php
new file mode 100644
index 0000000000..a030c121e8
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/Exception/IteratorNotRewindableException.php
@@ -0,0 +1,13 @@
+shouldFormatDates = $shouldFormatDates;
+
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance();
+ }
+
+ /**
+ * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
+ * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13
+ *
+ * @param \DOMNode $node
+ * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error
+ */
+ public function extractAndFormatNodeValue($node)
+ {
+ $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE);
+
+ switch ($cellType) {
+ case self::CELL_TYPE_STRING:
+ return $this->formatStringCellValue($node);
+ case self::CELL_TYPE_FLOAT:
+ return $this->formatFloatCellValue($node);
+ case self::CELL_TYPE_BOOLEAN:
+ return $this->formatBooleanCellValue($node);
+ case self::CELL_TYPE_DATE:
+ return $this->formatDateCellValue($node);
+ case self::CELL_TYPE_TIME:
+ return $this->formatTimeCellValue($node);
+ case self::CELL_TYPE_CURRENCY:
+ return $this->formatCurrencyCellValue($node);
+ case self::CELL_TYPE_PERCENTAGE:
+ return $this->formatPercentageCellValue($node);
+ case self::CELL_TYPE_VOID:
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Returns the cell String value.
+ *
+ * @param \DOMNode $node
+ * @return string The value associated with the cell
+ */
+ protected function formatStringCellValue($node)
+ {
+ $pNodeValues = [];
+ $pNodes = $node->getElementsByTagName(self::XML_NODE_P);
+
+ foreach ($pNodes as $pNode) {
+ $currentPValue = '';
+
+ foreach ($pNode->childNodes as $childNode) {
+ if ($childNode instanceof \DOMText) {
+ $currentPValue .= $childNode->nodeValue;
+ } else if ($childNode->nodeName === self::XML_NODE_S) {
+ $spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C);
+ $numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1;
+ $currentPValue .= str_repeat(' ', $numSpaces);
+ } else if ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) {
+ $currentPValue .= $childNode->nodeValue;
+ }
+ }
+
+ $pNodeValues[] = $currentPValue;
+ }
+
+ $escapedCellValue = implode("\n", $pNodeValues);
+ $cellValue = $this->escaper->unescape($escapedCellValue);
+ return $cellValue;
+ }
+
+ /**
+ * Returns the cell Numeric value from the given node.
+ *
+ * @param \DOMNode $node
+ * @return int|float The value associated with the cell
+ */
+ protected function formatFloatCellValue($node)
+ {
+ $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
+ $nodeIntValue = intval($nodeValue);
+ // The "==" is intentionally not a "===" because only the value matters, not the type
+ $cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
+ return $cellValue;
+ }
+
+ /**
+ * Returns the cell Boolean value from the given node.
+ *
+ * @param \DOMNode $node
+ * @return bool The value associated with the cell
+ */
+ protected function formatBooleanCellValue($node)
+ {
+ $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE);
+ // !! is similar to boolval()
+ $cellValue = !!$nodeValue;
+ return $cellValue;
+ }
+
+ /**
+ * Returns the cell Date value from the given node.
+ *
+ * @param \DOMNode $node
+ * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
+ */
+ protected function formatDateCellValue($node)
+ {
+ // The XML node looks like this:
+ //
+ // 05/19/16 04:39 PM
+ //
+
+ if ($this->shouldFormatDates) {
+ // The date is already formatted in the "p" tag
+ $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
+ return $nodeWithValueAlreadyFormatted->nodeValue;
+ } else {
+ // otherwise, get it from the "date-value" attribute
+ try {
+ $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE);
+ return new \DateTime($nodeValue);
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the cell Time value from the given node.
+ *
+ * @param \DOMNode $node
+ * @return \DateInterval|string|null The value associated with the cell or NULL if invalid time value
+ */
+ protected function formatTimeCellValue($node)
+ {
+ // The XML node looks like this:
+ //
+ // 01:24:00 PM
+ //
+
+ if ($this->shouldFormatDates) {
+ // The date is already formatted in the "p" tag
+ $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0);
+ return $nodeWithValueAlreadyFormatted->nodeValue;
+ } else {
+ // otherwise, get it from the "time-value" attribute
+ try {
+ $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE);
+ return new \DateInterval($nodeValue);
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns the cell Currency value from the given node.
+ *
+ * @param \DOMNode $node
+ * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR")
+ */
+ protected function formatCurrencyCellValue($node)
+ {
+ $value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE);
+ $currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY);
+
+ return "$value $currency";
+ }
+
+ /**
+ * Returns the cell Percentage value from the given node.
+ *
+ * @param \DOMNode $node
+ * @return int|float The value associated with the cell
+ */
+ protected function formatPercentageCellValue($node)
+ {
+ // percentages are formatted like floats
+ return $this->formatFloatCellValue($node);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/ODS/Helper/SettingsHelper.php b/lib/vendor/box/spout/src/Spout/Reader/ODS/Helper/SettingsHelper.php
new file mode 100644
index 0000000000..a5388ef4b6
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/ODS/Helper/SettingsHelper.php
@@ -0,0 +1,51 @@
+openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) {
+ return null;
+ }
+
+ $activeSheetName = null;
+
+ try {
+ while ($xmlReader->readUntilNodeFound(self::XML_NODE_CONFIG_ITEM)) {
+ if ($xmlReader->getAttribute(self::XML_ATTRIBUTE_CONFIG_NAME) === self::XML_ATTRIBUTE_VALUE_ACTIVE_TABLE) {
+ $activeSheetName = $xmlReader->readString();
+ break;
+ }
+ }
+ } catch (XMLProcessingException $exception) {
+ // do nothing
+ }
+
+ $xmlReader->close();
+
+ return $activeSheetName;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/ODS/Reader.php b/lib/vendor/box/spout/src/Spout/Reader/ODS/Reader.php
new file mode 100644
index 0000000000..dbdc47b8db
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/ODS/Reader.php
@@ -0,0 +1,85 @@
+options)) {
+ $this->options = new ReaderOptions();
+ }
+ return $this->options;
+ }
+
+ /**
+ * Returns whether stream wrappers are supported
+ *
+ * @return bool
+ */
+ protected function doesSupportStreamWrapper()
+ {
+ return false;
+ }
+
+ /**
+ * Opens the file at the given file path to make it ready to be read.
+ *
+ * @param string $filePath Path of the file to be read
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
+ * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
+ */
+ protected function openReader($filePath)
+ {
+ $this->zip = new \ZipArchive();
+
+ if ($this->zip->open($filePath) === true) {
+ $this->sheetIterator = new SheetIterator($filePath, $this->getOptions());
+ } else {
+ throw new IOException("Could not open $filePath for reading.");
+ }
+ }
+
+ /**
+ * Returns an iterator to iterate over sheets.
+ *
+ * @return SheetIterator To iterate over sheets
+ */
+ protected function getConcreteSheetIterator()
+ {
+ return $this->sheetIterator;
+ }
+
+ /**
+ * Closes the reader. To be used after reading the file.
+ *
+ * @return void
+ */
+ protected function closeReader()
+ {
+ if ($this->zip) {
+ $this->zip->close();
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/ODS/ReaderOptions.php b/lib/vendor/box/spout/src/Spout/Reader/ODS/ReaderOptions.php
new file mode 100644
index 0000000000..2d29640f3e
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/ODS/ReaderOptions.php
@@ -0,0 +1,14 @@
+" element
+ * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
+ */
+ public function __construct($xmlReader, $options)
+ {
+ $this->xmlReader = $xmlReader;
+ $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
+ $this->cellValueFormatter = new CellValueFormatter($options->shouldFormatDates());
+
+ // Register all callbacks to process different nodes when reading the XML file
+ $this->xmlProcessor = new XMLProcessor($this->xmlReader);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']);
+ }
+
+ /**
+ * Rewind the Iterator to the first element.
+ * NOTE: It can only be done once, as it is not possible to read an XML file backwards.
+ * @link http://php.net/manual/en/iterator.rewind.php
+ *
+ * @return void
+ * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once
+ */
+ public function rewind()
+ {
+ // Because sheet and row data is located in the file, we can't rewind both the
+ // sheet iterator and the row iterator, as XML file cannot be read backwards.
+ // Therefore, rewinding the row iterator has been disabled.
+ if ($this->hasAlreadyBeenRewound) {
+ throw new IteratorNotRewindableException();
+ }
+
+ $this->hasAlreadyBeenRewound = true;
+ $this->lastRowIndexProcessed = 0;
+ $this->nextRowIndexToBeProcessed = 1;
+ $this->rowDataBuffer = null;
+ $this->hasReachedEndOfFile = false;
+
+ $this->next();
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ return (!$this->hasReachedEndOfFile);
+ }
+
+ /**
+ * Move forward to next element. Empty rows will be skipped.
+ * @link http://php.net/manual/en/iterator.next.php
+ *
+ * @return void
+ * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
+ * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
+ */
+ public function next()
+ {
+ if ($this->doesNeedDataForNextRowToBeProcessed()) {
+ $this->readDataForNextRow();
+ }
+
+ $this->lastRowIndexProcessed++;
+ }
+
+ /**
+ * Returns whether we need data for the next row to be processed.
+ * We DO need to read data if:
+ * - we have not read any rows yet
+ * OR
+ * - the next row to be processed immediately follows the last read row
+ *
+ * @return bool Whether we need data for the next row to be processed.
+ */
+ protected function doesNeedDataForNextRowToBeProcessed()
+ {
+ $hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
+
+ return (
+ !$hasReadAtLeastOneRow ||
+ $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1
+ );
+ }
+
+ /**
+ * @return void
+ * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
+ * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
+ */
+ protected function readDataForNextRow()
+ {
+ $this->currentlyProcessedRowData = [];
+
+ try {
+ $this->xmlProcessor->readUntilStopped();
+ } catch (XMLProcessingException $exception) {
+ throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]");
+ }
+
+ $this->rowDataBuffer = $this->currentlyProcessedRowData;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processRowStartingNode($xmlReader)
+ {
+ // Reset data from current row
+ $this->hasAlreadyReadOneCellInCurrentRow = false;
+ $this->lastProcessedCellValue = null;
+ $this->numColumnsRepeated = 1;
+ $this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader);
+
+ return XMLProcessor::PROCESSING_CONTINUE;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processCellStartingNode($xmlReader)
+ {
+ $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader);
+
+ // NOTE: expand() will automatically decode all XML entities of the child nodes
+ $node = $xmlReader->expand();
+ $currentCellValue = $this->getCellValue($node);
+
+ // process cell N only after having read cell N+1 (see below why)
+ if ($this->hasAlreadyReadOneCellInCurrentRow) {
+ for ($i = 0; $i < $this->numColumnsRepeated; $i++) {
+ $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
+ }
+ }
+
+ $this->hasAlreadyReadOneCellInCurrentRow = true;
+ $this->lastProcessedCellValue = $currentCellValue;
+ $this->numColumnsRepeated = $currentNumColumnsRepeated;
+
+ return XMLProcessor::PROCESSING_CONTINUE;
+ }
+
+ /**
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processRowEndingNode()
+ {
+ $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRowData, $this->lastProcessedCellValue);
+
+ // if the fetched row is empty and we don't want to preserve it...
+ if (!$this->shouldPreserveEmptyRows && $isEmptyRow) {
+ // ... skip it
+ return XMLProcessor::PROCESSING_CONTINUE;
+ }
+
+ // if the row is empty, we don't want to return more than one cell
+ $actualNumColumnsRepeated = (!$isEmptyRow) ? $this->numColumnsRepeated : 1;
+
+ // Only add the value if the last read cell is not a trailing empty cell repeater in Excel.
+ // The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData".
+ // This is to avoid creating a lot of empty cells, as Excel adds a last empty ""
+ // with a number-columns-repeated value equals to the number of (supported columns - used columns).
+ // In Excel, the number of supported columns is 16384, but we don't want to returns rows with
+ // always 16384 cells.
+ if ((count($this->currentlyProcessedRowData) + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) {
+ for ($i = 0; $i < $actualNumColumnsRepeated; $i++) {
+ $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue;
+ }
+ }
+
+ // If we are processing row N and the row is repeated M times,
+ // then the next row to be processed will be row (N+M).
+ $this->nextRowIndexToBeProcessed += $this->numRowsRepeated;
+
+ // at this point, we have all the data we need for the row
+ // so that we can populate the buffer
+ return XMLProcessor::PROCESSING_STOP;
+ }
+
+ /**
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processTableEndingNode()
+ {
+ // The closing "" marks the end of the file
+ $this->hasReachedEndOfFile = true;
+
+ return XMLProcessor::PROCESSING_STOP;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
+ * @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing
+ */
+ protected function getNumRowsRepeatedForCurrentNode($xmlReader)
+ {
+ $numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED);
+ return ($numRowsRepeated !== null) ? intval($numRowsRepeated) : 1;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
+ * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing
+ */
+ protected function getNumColumnsRepeatedForCurrentNode($xmlReader)
+ {
+ $numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED);
+ return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1;
+ }
+
+ /**
+ * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
+ *
+ * @param \DOMNode $node
+ * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error
+ */
+ protected function getCellValue($node)
+ {
+ return $this->cellValueFormatter->extractAndFormatNodeValue($node);
+ }
+
+ /**
+ * After finishing processing each cell, a row is considered empty if it contains
+ * no cells or if the value of the last read cell is an empty string.
+ * After finishing processing each cell, the last read cell is not part of the
+ * row data yet (as we still need to apply the "num-columns-repeated" attribute).
+ *
+ * @param array $rowData
+ * @param string|int|float|bool|\DateTime|\DateInterval|null The value of the last read cell
+ * @return bool Whether the row is empty
+ */
+ protected function isEmptyRow($rowData, $lastReadCellValue)
+ {
+ return (
+ count($rowData) === 0 &&
+ (!isset($lastReadCellValue) || trim($lastReadCellValue) === '')
+ );
+ }
+
+ /**
+ * Return the current element, from the buffer.
+ * @link http://php.net/manual/en/iterator.current.php
+ *
+ * @return array|null
+ */
+ public function current()
+ {
+ return $this->rowDataBuffer;
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ *
+ * @return int
+ */
+ public function key()
+ {
+ return $this->lastRowIndexProcessed;
+ }
+
+
+ /**
+ * Cleans up what was created to iterate over the object.
+ *
+ * @return void
+ */
+ public function end()
+ {
+ $this->xmlReader->close();
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/ODS/Sheet.php b/lib/vendor/box/spout/src/Spout/Reader/ODS/Sheet.php
new file mode 100644
index 0000000000..794ad3a3ae
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/ODS/Sheet.php
@@ -0,0 +1,81 @@
+" element
+ * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
+ * @param string $sheetName Name of the sheet
+ * @param bool $isSheetActive Whether the sheet was defined as active
+ * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options
+ */
+ public function __construct($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $options)
+ {
+ $this->rowIterator = new RowIterator($xmlReader, $options);
+ $this->index = $sheetIndex;
+ $this->name = $sheetName;
+ $this->isActive = $isSheetActive;
+ }
+
+ /**
+ * @api
+ * @return \Box\Spout\Reader\ODS\RowIterator
+ */
+ public function getRowIterator()
+ {
+ return $this->rowIterator;
+ }
+
+ /**
+ * @api
+ * @return int Index of the sheet, based on order in the workbook (zero-based)
+ */
+ public function getIndex()
+ {
+ return $this->index;
+ }
+
+ /**
+ * @api
+ * @return string Name of the sheet
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @api
+ * @return bool Whether the sheet was defined as active
+ */
+ public function isActive()
+ {
+ return $this->isActive;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/ODS/SheetIterator.php b/lib/vendor/box/spout/src/Spout/Reader/ODS/SheetIterator.php
new file mode 100644
index 0000000000..995c13601a
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/ODS/SheetIterator.php
@@ -0,0 +1,168 @@
+filePath = $filePath;
+ $this->options = $options;
+ $this->xmlReader = new XMLReader();
+
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance();
+
+ $settingsHelper = new SettingsHelper();
+ $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath);
+ }
+
+ /**
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data
+ */
+ public function rewind()
+ {
+ $this->xmlReader->close();
+
+ if ($this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH) === false) {
+ $contentXmlFilePath = $this->filePath . '#' . self::CONTENT_XML_FILE_PATH;
+ throw new IOException("Could not open \"{$contentXmlFilePath}\".");
+ }
+
+ try {
+ $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
+ } catch (XMLProcessingException $exception) {
+ throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
+ }
+
+ $this->currentSheetIndex = 0;
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ return $this->hasFoundSheet;
+ }
+
+ /**
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ *
+ * @return void
+ */
+ public function next()
+ {
+ $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE);
+
+ if ($this->hasFoundSheet) {
+ $this->currentSheetIndex++;
+ }
+ }
+
+ /**
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ *
+ * @return \Box\Spout\Reader\ODS\Sheet
+ */
+ public function current()
+ {
+ $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME);
+ $sheetName = $this->escaper->unescape($escapedSheetName);
+ $isActiveSheet = $this->isActiveSheet($sheetName, $this->currentSheetIndex, $this->activeSheetName);
+
+ return new Sheet($this->xmlReader, $this->currentSheetIndex, $sheetName, $isActiveSheet, $this->options);
+ }
+
+ /**
+ * Returns whether the current sheet was defined as the active one
+ *
+ * @param string $sheetName Name of the current sheet
+ * @param int $sheetIndex Index of the current sheet
+ * @param string|null Name of the sheet that was defined as active or NULL if none defined
+ * @return bool Whether the current sheet was defined as the active one
+ */
+ private function isActiveSheet($sheetName, $sheetIndex, $activeSheetName)
+ {
+ // The given sheet is active if its name matches the defined active sheet's name
+ // or if no information about the active sheet was found, it defaults to the first sheet.
+ return (
+ ($activeSheetName === null && $sheetIndex === 0) ||
+ ($activeSheetName === $sheetName)
+ );
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ *
+ * @return int
+ */
+ public function key()
+ {
+ return $this->currentSheetIndex + 1;
+ }
+
+ /**
+ * Cleans up what was created to iterate over the object.
+ *
+ * @return void
+ */
+ public function end()
+ {
+ $this->xmlReader->close();
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/ReaderFactory.php b/lib/vendor/box/spout/src/Spout/Reader/ReaderFactory.php
new file mode 100644
index 0000000000..93a52cb867
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/ReaderFactory.php
@@ -0,0 +1,48 @@
+setGlobalFunctionsHelper(new GlobalFunctionsHelper());
+
+ return $reader;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/ReaderInterface.php b/lib/vendor/box/spout/src/Spout/Reader/ReaderInterface.php
new file mode 100644
index 0000000000..8ecde30802
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/ReaderInterface.php
@@ -0,0 +1,36 @@
+initialUseInternalErrorsValue = libxml_use_internal_errors(true);
+ }
+
+ /**
+ * Throws an XMLProcessingException if an error occured.
+ * It also always resets the "libxml_use_internal_errors" setting back to its initial value.
+ *
+ * @return void
+ * @throws \Box\Spout\Reader\Exception\XMLProcessingException
+ */
+ protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured()
+ {
+ if ($this->hasXMLErrorOccured()) {
+ $this->resetXMLInternalErrorsSetting();
+ throw new XMLProcessingException($this->getLastXMLErrorMessage());
+ }
+
+ $this->resetXMLInternalErrorsSetting();
+ }
+
+ /**
+ * Returns whether the a XML error has occured since the last time errors were cleared.
+ *
+ * @return bool TRUE if an error occured, FALSE otherwise
+ */
+ private function hasXMLErrorOccured()
+ {
+ return (libxml_get_last_error() !== false);
+ }
+
+ /**
+ * Returns the error message for the last XML error that occured.
+ * @see libxml_get_last_error
+ *
+ * @return String|null Last XML error message or null if no error
+ */
+ private function getLastXMLErrorMessage()
+ {
+ $errorMessage = null;
+ $error = libxml_get_last_error();
+
+ if ($error !== false) {
+ $errorMessage = trim($error->message);
+ }
+
+ return $errorMessage;
+ }
+
+ /**
+ * @return void
+ */
+ protected function resetXMLInternalErrorsSetting()
+ {
+ libxml_use_internal_errors($this->initialUseInternalErrorsValue);
+ }
+
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/Wrapper/XMLReader.php b/lib/vendor/box/spout/src/Spout/Reader/Wrapper/XMLReader.php
new file mode 100644
index 0000000000..08e99fc656
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/Wrapper/XMLReader.php
@@ -0,0 +1,175 @@
+getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath);
+
+ // We need to check first that the file we are trying to read really exist because:
+ // - PHP emits a warning when trying to open a file that does not exist.
+ // - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779)
+ if ($this->fileExistsWithinZip($realPathURI)) {
+ $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET);
+ }
+
+ return $wasOpenSuccessful;
+ }
+
+ /**
+ * Returns the real path for the given path components.
+ * This is useful to avoid issues on some Windows setup.
+ *
+ * @param string $zipFilePath Path to the ZIP file
+ * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip
+ * @return string The real path URI
+ */
+ public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath)
+ {
+ return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPath);
+ }
+
+ /**
+ * Returns whether the file at the given location exists
+ *
+ * @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml"
+ * @return bool TRUE if the file exists, FALSE otherwise
+ */
+ protected function fileExistsWithinZip($zipStreamURI)
+ {
+ $doesFileExists = false;
+
+ $pattern = '/zip:\/\/([^#]+)#(.*)/';
+ if (preg_match($pattern, $zipStreamURI, $matches)) {
+ $zipFilePath = $matches[1];
+ $innerFilePath = $matches[2];
+
+ $zip = new \ZipArchive();
+ if ($zip->open($zipFilePath) === true) {
+ $doesFileExists = ($zip->locateName($innerFilePath) !== false);
+ $zip->close();
+ }
+ }
+
+ return $doesFileExists;
+ }
+
+ /**
+ * Move to next node in document
+ * @see \XMLReader::read
+ *
+ * @return bool TRUE on success or FALSE on failure
+ * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
+ */
+ public function read()
+ {
+ $this->useXMLInternalErrors();
+
+ $wasReadSuccessful = parent::read();
+
+ $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
+
+ return $wasReadSuccessful;
+ }
+
+ /**
+ * Read until the element with the given name is found, or the end of the file.
+ *
+ * @param string $nodeName Name of the node to find
+ * @return bool TRUE on success or FALSE on failure
+ * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
+ */
+ public function readUntilNodeFound($nodeName)
+ {
+ do {
+ $wasReadSuccessful = $this->read();
+ $isNotPositionedOnStartingNode = !$this->isPositionedOnStartingNode($nodeName);
+ } while ($wasReadSuccessful && $isNotPositionedOnStartingNode);
+
+ return $wasReadSuccessful;
+ }
+
+ /**
+ * Move cursor to next node skipping all subtrees
+ * @see \XMLReader::next
+ *
+ * @param string|void $localName The name of the next node to move to
+ * @return bool TRUE on success or FALSE on failure
+ * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred
+ */
+ public function next($localName = null)
+ {
+ $this->useXMLInternalErrors();
+
+ $wasNextSuccessful = parent::next($localName);
+
+ $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured();
+
+ return $wasNextSuccessful;
+ }
+
+ /**
+ * @param string $nodeName
+ * @return bool Whether the XML Reader is currently positioned on the starting node with given name
+ */
+ public function isPositionedOnStartingNode($nodeName)
+ {
+ return $this->isPositionedOnNode($nodeName, XMLReader::ELEMENT);
+ }
+
+ /**
+ * @param string $nodeName
+ * @return bool Whether the XML Reader is currently positioned on the ending node with given name
+ */
+ public function isPositionedOnEndingNode($nodeName)
+ {
+ return $this->isPositionedOnNode($nodeName, XMLReader::END_ELEMENT);
+ }
+
+ /**
+ * @param string $nodeName
+ * @param int $nodeType
+ * @return bool Whether the XML Reader is currently positioned on the node with given name and type
+ */
+ private function isPositionedOnNode($nodeName, $nodeType)
+ {
+ // In some cases, the node has a prefix (for instance, "" can also be "").
+ // So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName").
+ // @see https://github.com/box/spout/issues/233
+ $hasPrefix = (strpos($nodeName, ':') !== false);
+ $currentNodeName = ($hasPrefix) ? $this->name : $this->localName;
+
+ return ($this->nodeType === $nodeType && $currentNodeName === $nodeName);
+ }
+
+ /**
+ * @return string The name of the current node, un-prefixed
+ */
+ public function getCurrentNodeName()
+ {
+ return $this->localName;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php
new file mode 100644
index 0000000000..6077839419
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/CellHelper.php
@@ -0,0 +1,106 @@
+ 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6,
+ 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13,
+ 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20,
+ 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25,
+ ];
+
+ /**
+ * Fills the missing indexes of an array with a given value.
+ * For instance, $dataArray = []; $a[1] = 1; $a[3] = 3;
+ * Calling fillMissingArrayIndexes($dataArray, 'FILL') will return this array: ['FILL', 1, 'FILL', 3]
+ *
+ * @param array $dataArray The array to fill
+ * @param string|void $fillValue optional
+ * @return array
+ */
+ public static function fillMissingArrayIndexes($dataArray, $fillValue = '')
+ {
+ if (empty($dataArray)) {
+ return [];
+ }
+ $existingIndexes = array_keys($dataArray);
+
+ $newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue);
+ $dataArray += $newIndexes;
+
+ ksort($dataArray);
+
+ return $dataArray;
+ }
+
+ /**
+ * Returns the base 10 column index associated to the cell index (base 26).
+ * Excel uses A to Z letters for column indexing, where A is the 1st column,
+ * Z is the 26th and AA is the 27th.
+ * The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26.
+ *
+ * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
+ * @return int
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
+ */
+ public static function getColumnIndexFromCellIndex($cellIndex)
+ {
+ if (!self::isValidCellIndex($cellIndex)) {
+ throw new InvalidArgumentException('Cannot get column index from an invalid cell index.');
+ }
+
+ $columnIndex = 0;
+
+ // Remove row information
+ $columnLetters = preg_replace('/\d/', '', $cellIndex);
+
+ // strlen() is super slow too... Using isset() is way faster and not too unreadable,
+ // since we checked before that there are between 1 and 3 letters.
+ $columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1;
+
+ // Looping over the different letters of the column is slower than this method.
+ // Also, not using the pow() function because it's slooooow...
+ switch ($columnLength) {
+ case 1:
+ $columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]);
+ break;
+ case 2:
+ $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26;
+ $secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]];
+ $columnIndex = $firstLetterIndex + $secondLetterIndex;
+ break;
+ case 3:
+ $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676;
+ $secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26;
+ $thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]];
+ $columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex;
+ break;
+ }
+
+ return $columnIndex;
+ }
+
+ /**
+ * Returns whether a cell index is valid, in an Excel world.
+ * To be valid, the cell index should start with capital letters and be followed by numbers.
+ * There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'.
+ *
+ * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...)
+ * @return bool
+ */
+ protected static function isValidCellIndex($cellIndex)
+ {
+ return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
new file mode 100644
index 0000000000..b4c625689c
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php
@@ -0,0 +1,300 @@
+sharedStringsHelper = $sharedStringsHelper;
+ $this->styleHelper = $styleHelper;
+ $this->shouldFormatDates = $shouldFormatDates;
+
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $this->escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
+ }
+
+ /**
+ * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
+ *
+ * @param \DOMNode $node
+ * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
+ */
+ public function extractAndFormatNodeValue($node)
+ {
+ // Default cell type is "n"
+ $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC;
+ $cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID));
+ $vNodeValue = $this->getVNodeValue($node);
+
+ if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) {
+ return $vNodeValue;
+ }
+
+ switch ($cellType) {
+ case self::CELL_TYPE_INLINE_STRING:
+ return $this->formatInlineStringCellValue($node);
+ case self::CELL_TYPE_SHARED_STRING:
+ return $this->formatSharedStringCellValue($vNodeValue);
+ case self::CELL_TYPE_STR:
+ return $this->formatStrCellValue($vNodeValue);
+ case self::CELL_TYPE_BOOLEAN:
+ return $this->formatBooleanCellValue($vNodeValue);
+ case self::CELL_TYPE_NUMERIC:
+ return $this->formatNumericCellValue($vNodeValue, $cellStyleId);
+ case self::CELL_TYPE_DATE:
+ return $this->formatDateCellValue($vNodeValue);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Returns the cell's string value from a node's nested value node
+ *
+ * @param \DOMNode $node
+ * @return string The value associated with the cell
+ */
+ protected function getVNodeValue($node)
+ {
+ // for cell types having a "v" tag containing the value.
+ // if not, the returned value should be empty string.
+ $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0);
+ return ($vNode !== null) ? $vNode->nodeValue : '';
+ }
+
+ /**
+ * Returns the cell String value where string is inline.
+ *
+ * @param \DOMNode $node
+ * @return string The value associated with the cell (null when the cell has an error)
+ */
+ protected function formatInlineStringCellValue($node)
+ {
+ // inline strings are formatted this way:
+ // [INLINE_STRING]
+ $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0);
+ $cellValue = $this->escaper->unescape($tNode->nodeValue);
+ return $cellValue;
+ }
+
+ /**
+ * Returns the cell String value from shared-strings file using nodeValue index.
+ *
+ * @param string $nodeValue
+ * @return string The value associated with the cell (null when the cell has an error)
+ */
+ protected function formatSharedStringCellValue($nodeValue)
+ {
+ // shared strings are formatted this way:
+ // [SHARED_STRING_INDEX]
+ $sharedStringIndex = intval($nodeValue);
+ $escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex);
+ $cellValue = $this->escaper->unescape($escapedCellValue);
+ return $cellValue;
+ }
+
+ /**
+ * Returns the cell String value, where string is stored in value node.
+ *
+ * @param string $nodeValue
+ * @return string The value associated with the cell (null when the cell has an error)
+ */
+ protected function formatStrCellValue($nodeValue)
+ {
+ $escapedCellValue = trim($nodeValue);
+ $cellValue = $this->escaper->unescape($escapedCellValue);
+ return $cellValue;
+ }
+
+ /**
+ * Returns the cell Numeric value from string of nodeValue.
+ * The value can also represent a timestamp and a DateTime will be returned.
+ *
+ * @param string $nodeValue
+ * @param int $cellStyleId 0 being the default style
+ * @return int|float|\DateTime|null The value associated with the cell
+ */
+ protected function formatNumericCellValue($nodeValue, $cellStyleId)
+ {
+ // Numeric values can represent numbers as well as timestamps.
+ // We need to look at the style of the cell to determine whether it is one or the other.
+ $shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId);
+
+ if ($shouldFormatAsDate) {
+ return $this->formatExcelTimestampValue(floatval($nodeValue), $cellStyleId);
+ } else {
+ $nodeIntValue = intval($nodeValue);
+ return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue);
+ }
+ }
+
+ /**
+ * Returns a cell's PHP Date value, associated to the given timestamp.
+ * NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
+ * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1.
+ *
+ * @param float $nodeValue
+ * @param int $cellStyleId 0 being the default style
+ * @return \DateTime|null The value associated with the cell or NULL if invalid date value
+ */
+ protected function formatExcelTimestampValue($nodeValue, $cellStyleId)
+ {
+ // Fix for the erroneous leap year in Excel
+ if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) {
+ --$nodeValue;
+ }
+
+ if ($nodeValue >= 1) {
+ // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01.
+ return $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId);
+ } else if ($nodeValue >= 0) {
+ // Values between 0 and 1 represent "times".
+ return $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId);
+ } else {
+ // invalid date
+ return null;
+ }
+ }
+
+ /**
+ * Returns a cell's PHP DateTime value, associated to the given timestamp.
+ * Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date).
+ *
+ * @param float $nodeValue
+ * @param int $cellStyleId 0 being the default style
+ * @return \DateTime|string The value associated with the cell
+ */
+ protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId)
+ {
+ $time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY);
+ $hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR);
+ $minutes = floor($time / self::NUM_SECONDS_IN_ONE_MINUTE) - ($hours * self::NUM_SECONDS_IN_ONE_MINUTE);
+ $seconds = $time - ($hours * self::NUM_SECONDS_IN_ONE_HOUR) - ($minutes * self::NUM_SECONDS_IN_ONE_MINUTE);
+
+ // using the base Excel date (Jan 1st, 1900) - not relevant here
+ $dateObj = new \DateTime('1900-01-01');
+ $dateObj->setTime($hours, $minutes, $seconds);
+
+ if ($this->shouldFormatDates) {
+ $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
+ $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
+ return $dateObj->format($phpDateFormat);
+ } else {
+ return $dateObj;
+ }
+ }
+
+ /**
+ * Returns a cell's PHP Date value, associated to the given timestamp.
+ * NOTE: The timestamp is a float representing the number of days since January 1st, 1900.
+ *
+ * @param float $nodeValue
+ * @param int $cellStyleId 0 being the default style
+ * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
+ */
+ protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId)
+ {
+ // Do not use any unix timestamps for calculation to prevent
+ // issues with numbers exceeding 2^31.
+ $secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY;
+ $secondsRemainder = round($secondsRemainder, 0);
+
+ try {
+ $dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31');
+ $dateObj->modify('+' . intval($nodeValue) . 'days');
+ $dateObj->modify('+' . $secondsRemainder . 'seconds');
+
+ if ($this->shouldFormatDates) {
+ $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId);
+ $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode);
+ return $dateObj->format($phpDateFormat);
+ } else {
+ return $dateObj;
+ }
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the cell Boolean value from a specific node's Value.
+ *
+ * @param string $nodeValue
+ * @return bool The value associated with the cell
+ */
+ protected function formatBooleanCellValue($nodeValue)
+ {
+ // !! is similar to boolval()
+ $cellValue = !!$nodeValue;
+ return $cellValue;
+ }
+
+ /**
+ * Returns a cell's PHP Date value, associated to the given stored nodeValue.
+ * @see ECMA-376 Part 1 - §18.17.4
+ *
+ * @param string $nodeValue ISO 8601 Date string
+ * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value
+ */
+ protected function formatDateCellValue($nodeValue)
+ {
+ // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php)
+ try {
+ return ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue);
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
new file mode 100644
index 0000000000..9dba4c685a
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php
@@ -0,0 +1,124 @@
+ [
+ // Time
+ 'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem
+ ':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month)
+ 'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month)
+ 'ss' => 's', // Seconds, with leading zeros
+ '.s' => '', // Ignore (fractional seconds format does not exist in PHP)
+
+ // Date
+ 'e' => 'Y', // Full numeric representation of a year, 4 digits
+ 'yyyy' => 'Y', // Full numeric representation of a year, 4 digits
+ 'yy' => 'y', // Two digit representation of a year
+ 'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...)
+ 'mmmm' => 'F', // Full textual representation of a month
+ 'mmm' => 'M', // Short textual representation of a month, three letters
+ 'mm' => 'm', // Numeric representation of a month, with leading zeros
+ 'm' => 'n', // Numeric representation of a month, without leading zeros
+ 'dddd' => 'l', // Full textual representation of the day of the week
+ 'ddd' => 'D', // Textual representation of a day, three letters
+ 'dd' => 'd', // Day of the month, 2 digits with leading zeros
+ 'd' => 'j', // Day of the month without leading zeros
+ ],
+ self::KEY_HOUR_12 => [
+ 'hh' => 'h', // 12-hour format of an hour without leading zeros
+ 'h' => 'g', // 12-hour format of an hour without leading zeros
+ ],
+ self::KEY_HOUR_24 => [
+ 'hh' => 'H', // 24-hour hours with leading zero
+ 'h' => 'G', // 24-hour format of an hour without leading zeros
+ ],
+ ];
+
+ /**
+ * Converts the given Excel date format to a format understandable by the PHP date function.
+ *
+ * @param string $excelDateFormat Excel date format
+ * @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php)
+ */
+ public static function toPHPDateFormat($excelDateFormat)
+ {
+ // Remove brackets potentially present at the beginning of the format string
+ // and text portion of the format at the end of it (starting with ";")
+ // See §18.8.31 of ECMA-376 for more detail.
+ $dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat);
+
+ // Double quotes are used to escape characters that must not be interpreted.
+ // For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y"
+ // By exploding the format string using double quote as a delimiter, we can get all parts
+ // that must be transformed (even indexes) and all parts that must not be (odd indexes).
+ $dateFormatParts = explode('"', $dateFormat);
+
+ foreach ($dateFormatParts as $partIndex => $dateFormatPart) {
+ // do not look at odd indexes
+ if ($partIndex % 2 === 1) {
+ continue;
+ }
+
+ // Make sure all characters are lowercase, as the mapping table is using lowercase characters
+ $transformedPart = strtolower($dateFormatPart);
+
+ // Remove escapes related to non-format characters
+ $transformedPart = str_replace('\\', '', $transformedPart);
+
+ // Apply general transformation first...
+ $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]);
+
+ // ... then apply hour transformation, for 12-hour or 24-hour format
+ if (self::has12HourFormatMarker($dateFormatPart)) {
+ $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]);
+ } else {
+ $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]);
+ }
+
+ // overwrite the parts array with the new transformed part
+ $dateFormatParts[$partIndex] = $transformedPart;
+ }
+
+ // Merge all transformed parts back together
+ $phpDateFormat = implode('"', $dateFormatParts);
+
+ // Finally, to have the date format compatible with the DateTime::format() function, we need to escape
+ // all characters that are inside double quotes (and double quotes must be removed).
+ // For instance, ["Day " dd] should become [\D\a\y\ dd]
+ $phpDateFormat = preg_replace_callback('/"(.+?)"/', function($matches) {
+ $stringToEscape = $matches[1];
+ $letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY);
+ return '\\' . implode('\\', $letters);
+ }, $phpDateFormat);
+
+ return $phpDateFormat;
+ }
+
+ /**
+ * @param string $excelDateFormat Date format as defined by Excel
+ * @return bool Whether the given date format has the 12-hour format marker
+ */
+ private static function has12HourFormatMarker($excelDateFormat)
+ {
+ return (stripos($excelDateFormat, 'am/pm') !== false);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php
new file mode 100644
index 0000000000..36e0bfef0b
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php
@@ -0,0 +1,159 @@
+ 20 * 600 ≈ 12KB
+ */
+ const AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB = 12;
+
+ /**
+ * To avoid running out of memory when extracting a huge number of shared strings, they can be saved to temporary files
+ * instead of in memory. Then, when accessing a string, the corresponding file contents will be loaded in memory
+ * and the string will be quickly retrieved.
+ * The performance bottleneck is not when creating these temporary files, but rather when loading their content.
+ * Because the contents of the last loaded file stays in memory until another file needs to be loaded, it works
+ * best when the indexes of the shared strings are sorted in the sheet data.
+ * 10,000 was chosen because it creates small files that are fast to be loaded in memory.
+ */
+ const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000;
+
+ /** @var CachingStrategyFactory|null Singleton instance */
+ protected static $instance = null;
+
+ /**
+ * Private constructor for singleton
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Returns the singleton instance of the factory
+ *
+ * @return CachingStrategyFactory
+ */
+ public static function getInstance()
+ {
+ if (self::$instance === null) {
+ self::$instance = new CachingStrategyFactory();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Returns the best caching strategy, given the number of unique shared strings
+ * and the amount of memory available.
+ *
+ * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
+ * @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored
+ * @return CachingStrategyInterface The best caching strategy
+ */
+ public function getBestCachingStrategy($sharedStringsUniqueCount, $tempFolder = null)
+ {
+ if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) {
+ return new InMemoryStrategy($sharedStringsUniqueCount);
+ } else {
+ return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE);
+ }
+ }
+
+ /**
+ * Returns whether it is safe to use in-memory caching, given the number of unique shared strings
+ * and the amount of memory available.
+ *
+ * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
+ * @return bool
+ */
+ protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)
+ {
+ // if the number of shared strings in unknown, do not use "in memory" strategy
+ if ($sharedStringsUniqueCount === null) {
+ return false;
+ }
+
+ $memoryAvailable = $this->getMemoryLimitInKB();
+
+ if ($memoryAvailable === -1) {
+ // if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe
+ return ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE);
+ } else {
+ $memoryNeeded = $sharedStringsUniqueCount * self::AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB;
+ return ($memoryAvailable > $memoryNeeded);
+ }
+ }
+
+ /**
+ * Returns the PHP "memory_limit" in Kilobytes
+ *
+ * @return float
+ */
+ protected function getMemoryLimitInKB()
+ {
+ $memoryLimitFormatted = $this->getMemoryLimitFromIni();
+ $memoryLimitFormatted = strtolower(trim($memoryLimitFormatted));
+
+ // No memory limit
+ if ($memoryLimitFormatted === '-1') {
+ return -1;
+ }
+
+ if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) {
+ $amount = intval($matches[1]);
+ $unit = $matches[2];
+
+ switch ($unit) {
+ case 'b': return ($amount / 1024);
+ case 'k': return $amount;
+ case 'm': return ($amount * 1024);
+ case 'g': return ($amount * 1024 * 1024);
+ case 't': return ($amount * 1024 * 1024 * 1024);
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the formatted "memory_limit" value
+ *
+ * @return string
+ */
+ protected function getMemoryLimitFromIni()
+ {
+ return ini_get('memory_limit');
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php
new file mode 100644
index 0000000000..631222a321
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php
@@ -0,0 +1,44 @@
+fileSystemHelper = new FileSystemHelper($rootTempFolder);
+ $this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings'));
+
+ $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile;
+
+ $this->globalFunctionsHelper = new GlobalFunctionsHelper();
+ $this->tempFilePointer = null;
+ }
+
+ /**
+ * Adds the given string to the cache.
+ *
+ * @param string $sharedString The string to be added to the cache
+ * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
+ * @return void
+ */
+ public function addStringForIndex($sharedString, $sharedStringIndex)
+ {
+ $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
+
+ if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
+ if ($this->tempFilePointer) {
+ $this->globalFunctionsHelper->fclose($this->tempFilePointer);
+ }
+ $this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w');
+ }
+
+ // The shared string retrieval logic expects each cell data to be on one line only
+ // Encoding the line feed character allows to preserve this assumption
+ $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString);
+
+ $this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL);
+ }
+
+ /**
+ * Returns the path for the temp file that should contain the string for the given index
+ *
+ * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
+ * @return string The temp file path for the given index
+ */
+ protected function getSharedStringTempFilePath($sharedStringIndex)
+ {
+ $numTempFile = intval($sharedStringIndex / $this->maxNumStringsPerTempFile);
+ return $this->tempFolder . '/sharedstrings' . $numTempFile;
+ }
+
+ /**
+ * Closes the cache after the last shared string was added.
+ * This prevents any additional string from being added to the cache.
+ *
+ * @return void
+ */
+ public function closeCache()
+ {
+ // close pointer to the last temp file that was written
+ if ($this->tempFilePointer) {
+ $this->globalFunctionsHelper->fclose($this->tempFilePointer);
+ }
+ }
+
+
+ /**
+ * Returns the string located at the given index from the cache.
+ *
+ * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
+ * @return string The shared string at the given index
+ * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
+ */
+ public function getStringAtIndex($sharedStringIndex)
+ {
+ $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex);
+ $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile;
+
+ if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) {
+ throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex");
+ }
+
+ if ($this->inMemoryTempFilePath !== $tempFilePath) {
+ // free memory
+ unset($this->inMemoryTempFileContents);
+
+ $this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath));
+ $this->inMemoryTempFilePath = $tempFilePath;
+ }
+
+ $sharedString = null;
+
+ // Using isset here because it is way faster than array_key_exists...
+ if (isset($this->inMemoryTempFileContents[$indexInFile])) {
+ $escapedSharedString = $this->inMemoryTempFileContents[$indexInFile];
+ $sharedString = $this->unescapeLineFeed($escapedSharedString);
+ }
+
+ if ($sharedString === null) {
+ throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
+ }
+
+ return rtrim($sharedString, PHP_EOL);
+ }
+
+ /**
+ * Escapes the line feed characters (\n)
+ *
+ * @param string $unescapedString
+ * @return string
+ */
+ private function escapeLineFeed($unescapedString)
+ {
+ return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString);
+ }
+
+ /**
+ * Unescapes the line feed characters (\n)
+ *
+ * @param string $escapedString
+ * @return string
+ */
+ private function unescapeLineFeed($escapedString)
+ {
+ return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString);
+ }
+
+ /**
+ * Destroys the cache, freeing memory and removing any created artifacts
+ *
+ * @return void
+ */
+ public function clearCache()
+ {
+ if ($this->tempFolder) {
+ $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder);
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php
new file mode 100644
index 0000000000..c6a5321a55
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php
@@ -0,0 +1,83 @@
+inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount);
+ $this->isCacheClosed = false;
+ }
+
+ /**
+ * Adds the given string to the cache.
+ *
+ * @param string $sharedString The string to be added to the cache
+ * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
+ * @return void
+ */
+ public function addStringForIndex($sharedString, $sharedStringIndex)
+ {
+ if (!$this->isCacheClosed) {
+ $this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString);
+ }
+ }
+
+ /**
+ * Closes the cache after the last shared string was added.
+ * This prevents any additional string from being added to the cache.
+ *
+ * @return void
+ */
+ public function closeCache()
+ {
+ $this->isCacheClosed = true;
+ }
+
+ /**
+ * Returns the string located at the given index from the cache.
+ *
+ * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
+ * @return string The shared string at the given index
+ * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
+ */
+ public function getStringAtIndex($sharedStringIndex)
+ {
+ try {
+ return $this->inMemoryCache->offsetGet($sharedStringIndex);
+ } catch (\RuntimeException $e) {
+ throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex");
+ }
+ }
+
+ /**
+ * Destroys the cache, freeing memory and removing any created artifacts
+ *
+ * @return void
+ */
+ public function clearCache()
+ {
+ unset($this->inMemoryCache);
+ $this->isCacheClosed = false;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php
new file mode 100644
index 0000000000..fc04c794dc
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php
@@ -0,0 +1,234 @@
+filePath = $filePath;
+ $this->tempFolder = $tempFolder;
+ }
+
+ /**
+ * Returns whether the XLSX file contains a shared strings XML file
+ *
+ * @return bool
+ */
+ public function hasSharedStrings()
+ {
+ $hasSharedStrings = false;
+ $zip = new \ZipArchive();
+
+ if ($zip->open($this->filePath) === true) {
+ $hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false);
+ $zip->close();
+ }
+
+ return $hasSharedStrings;
+ }
+
+ /**
+ * Builds an in-memory array containing all the shared strings of the sheet.
+ * All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'.
+ * It is then accessed by the sheet data, via the string index in the built table.
+ *
+ * More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx
+ *
+ * The XML file can be really big with sheets containing a lot of data. That is why
+ * we need to use a XML reader that provides streaming like the XMLReader library.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read
+ */
+ public function extractSharedStrings()
+ {
+ $xmlReader = new XMLReader();
+ $sharedStringIndex = 0;
+
+ if ($xmlReader->openFileInZip($this->filePath, self::SHARED_STRINGS_XML_FILE_PATH) === false) {
+ throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".');
+ }
+
+ try {
+ $sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader);
+ $this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount);
+
+ $xmlReader->readUntilNodeFound(self::XML_NODE_SI);
+
+ while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SI) {
+ $this->processSharedStringsItem($xmlReader, $sharedStringIndex);
+ $sharedStringIndex++;
+
+ // jump to the next '' tag
+ $xmlReader->next(self::XML_NODE_SI);
+ }
+
+ $this->cachingStrategy->closeCache();
+
+ } catch (XMLProcessingException $exception) {
+ throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]");
+ }
+
+ $xmlReader->close();
+ }
+
+ /**
+ * Returns the shared strings unique count, as specified in tag.
+ *
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance
+ * @return int|null Number of unique shared strings in the sharedStrings.xml file
+ * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read
+ */
+ protected function getSharedStringsUniqueCount($xmlReader)
+ {
+ $xmlReader->next(self::XML_NODE_SST);
+
+ // Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE)
+ while ($xmlReader->getCurrentNodeName() === self::XML_NODE_SST && $xmlReader->nodeType !== XMLReader::ELEMENT) {
+ $xmlReader->read();
+ }
+
+ $uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_UNIQUE_COUNT);
+
+ // some software do not add the "uniqueCount" attribute but only use the "count" one
+ // @see https://github.com/box/spout/issues/254
+ if ($uniqueCount === null) {
+ $uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_COUNT);
+ }
+
+ return ($uniqueCount !== null) ? intval($uniqueCount) : null;
+ }
+
+ /**
+ * Returns the best shared strings caching strategy.
+ *
+ * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown)
+ * @return CachingStrategyInterface
+ */
+ protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount)
+ {
+ return CachingStrategyFactory::getInstance()
+ ->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder);
+ }
+
+ /**
+ * Processes the shared strings item XML node which the given XML reader is positioned on.
+ *
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on a "" node
+ * @param int $sharedStringIndex Index of the processed shared strings item
+ * @return void
+ */
+ protected function processSharedStringsItem($xmlReader, $sharedStringIndex)
+ {
+ $sharedStringValue = '';
+
+ // NOTE: expand() will automatically decode all XML entities of the child nodes
+ $siNode = $xmlReader->expand();
+ $textNodes = $siNode->getElementsByTagName(self::XML_NODE_T);
+
+ foreach ($textNodes as $textNode) {
+ if ($this->shouldExtractTextNodeValue($textNode)) {
+ $textNodeValue = $textNode->nodeValue;
+ $shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode);
+
+ $sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : trim($textNodeValue);
+ }
+ }
+
+ $this->cachingStrategy->addStringForIndex($sharedStringValue, $sharedStringIndex);
+ }
+
+ /**
+ * Not all text nodes' values must be extracted.
+ * Some text nodes are part of a node describing the pronunciation for instance.
+ * We'll only consider the nodes whose parents are "" or "".
+ *
+ * @param \DOMElement $textNode Text node to check
+ * @return bool Whether the given text node's value must be extracted
+ */
+ protected function shouldExtractTextNodeValue($textNode)
+ {
+ $parentTagName = $textNode->parentNode->localName;
+ return ($parentTagName === self::XML_NODE_SI || $parentTagName === self::XML_NODE_R);
+ }
+
+ /**
+ * If the text node has the attribute 'xml:space="preserve"', then preserve whitespace.
+ *
+ * @param \DOMElement $textNode The text node element () whose whitespace may be preserved
+ * @return bool Whether whitespace should be preserved
+ */
+ protected function shouldPreserveWhitespace($textNode)
+ {
+ $spaceValue = $textNode->getAttribute(self::XML_ATTRIBUTE_XML_SPACE);
+ return ($spaceValue === self::XML_ATTRIBUTE_VALUE_PRESERVE);
+ }
+
+ /**
+ * Returns the shared string at the given index, using the previously chosen caching strategy.
+ *
+ * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file
+ * @return string The shared string at the given index
+ * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index
+ */
+ public function getStringAtIndex($sharedStringIndex)
+ {
+ return $this->cachingStrategy->getStringAtIndex($sharedStringIndex);
+ }
+
+ /**
+ * Destroys the cache, freeing memory and removing any created artifacts
+ *
+ * @return void
+ */
+ public function cleanup()
+ {
+ if ($this->cachingStrategy) {
+ $this->cachingStrategy->clearCache();
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php
new file mode 100644
index 0000000000..b74ba01bc3
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/SheetHelper.php
@@ -0,0 +1,156 @@
+filePath = $filePath;
+ $this->options = $options;
+ $this->sharedStringsHelper = $sharedStringsHelper;
+ $this->globalFunctionsHelper = $globalFunctionsHelper;
+ }
+
+ /**
+ * Returns the sheets metadata of the file located at the previously given file path.
+ * The paths to the sheets' data are read from the [Content_Types].xml file.
+ *
+ * @return Sheet[] Sheets within the XLSX file
+ */
+ public function getSheets()
+ {
+ $sheets = [];
+ $sheetIndex = 0;
+ $activeSheetIndex = 0; // By default, the first sheet is active
+
+ $xmlReader = new XMLReader();
+ if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) {
+ while ($xmlReader->read()) {
+ if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) {
+ // The "workbookView" node is located before "sheet" nodes, ensuring that
+ // the active sheet is known before parsing sheets data.
+ $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB);
+ } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) {
+ $isSheetActive = ($sheetIndex === $activeSheetIndex);
+ $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive);
+ $sheetIndex++;
+ } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) {
+ // stop reading once all sheets have been read
+ break;
+ }
+ }
+
+ $xmlReader->close();
+ }
+
+ return $sheets;
+ }
+
+ /**
+ * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml".
+ * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID
+ * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res").
+ *
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml"
+ * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based)
+ * @param bool $isSheetActive Whether this sheet was defined as active
+ * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance
+ */
+ protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive)
+ {
+ $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID);
+ $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME);
+
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
+ $sheetName = $escaper->unescape($escapedSheetName);
+
+ $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId);
+
+ return new Sheet(
+ $this->filePath, $sheetDataXMLFilePath,
+ $sheetIndexZeroBased, $sheetName, $isSheetActive,
+ $this->options, $this->sharedStringsHelper
+ );
+ }
+
+ /**
+ * @param string $sheetId The sheet ID, as defined in "workbook.xml"
+ * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID
+ */
+ protected function getSheetDataXMLFilePathForSheetId($sheetId)
+ {
+ $sheetDataXMLFilePath = '';
+
+ // find the file path of the sheet, by looking at the "workbook.xml.res" file
+ $xmlReader = new XMLReader();
+ if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) {
+ while ($xmlReader->read()) {
+ if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) {
+ $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID);
+
+ if ($relationshipSheetId === $sheetId) {
+ // In workbook.xml.rels, it is only "worksheets/sheet1.xml"
+ // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml"
+ $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET);
+
+ // sometimes, the sheet data file path already contains "/xl/"...
+ if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) {
+ $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath;
+ break;
+ }
+ }
+ }
+ }
+
+ $xmlReader->close();
+ }
+
+ return $sheetDataXMLFilePath;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php
new file mode 100644
index 0000000000..000adab887
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Helper/StyleHelper.php
@@ -0,0 +1,330 @@
+ 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy'
+ 15 => 'd-mmm-yy',
+ 16 => 'd-mmm',
+ 17 => 'mmm-yy',
+ 18 => 'h:mm AM/PM',
+ 19 => 'h:mm:ss AM/PM',
+ 20 => 'h:mm',
+ 21 => 'h:mm:ss',
+ 22 => 'm/d/yyyy h:mm', // @NOTE: ECMA spec is 'm/d/yy h:mm',
+ 45 => 'mm:ss',
+ 46 => '[h]:mm:ss',
+ 47 => 'mm:ss.0', // @NOTE: ECMA spec is 'mmss.0',
+ ];
+
+ /** @var string Path of the XLSX file being read */
+ protected $filePath;
+
+ /** @var array Array containing the IDs of built-in number formats indicating a date */
+ protected $builtinNumFmtIdIndicatingDates;
+
+ /** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */
+ protected $customNumberFormats;
+
+ /** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */
+ protected $stylesAttributes;
+
+ /** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */
+ protected $numFmtIdToIsDateFormatCache = [];
+
+ /**
+ * @param string $filePath Path of the XLSX file being read
+ */
+ public function __construct($filePath)
+ {
+ $this->filePath = $filePath;
+ $this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping);
+ }
+
+ /**
+ * Returns whether the style with the given ID should consider
+ * numeric values as timestamps and format the cell as a date.
+ *
+ * @param int $styleId Zero-based style ID
+ * @return bool Whether the cell with the given cell should display a date instead of a numeric value
+ */
+ public function shouldFormatNumericValueAsDate($styleId)
+ {
+ $stylesAttributes = $this->getStylesAttributes();
+
+ // Default style (0) does not format numeric values as timestamps. Only custom styles do.
+ // Also if the style ID does not exist in the styles.xml file, format as numeric value.
+ // Using isset here because it is way faster than array_key_exists...
+ if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) {
+ return false;
+ }
+
+ $styleAttributes = $stylesAttributes[$styleId];
+
+ return $this->doesStyleIndicateDate($styleAttributes);
+ }
+
+ /**
+ * Reads the styles.xml file and extract the relevant information from the file.
+ *
+ * @return void
+ */
+ protected function extractRelevantInfo()
+ {
+ $this->customNumberFormats = [];
+ $this->stylesAttributes = [];
+
+ $xmlReader = new XMLReader();
+
+ if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) {
+ while ($xmlReader->read()) {
+ if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) {
+ $this->extractNumberFormats($xmlReader);
+
+ } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) {
+ $this->extractStyleAttributes($xmlReader);
+ }
+ }
+
+ $xmlReader->close();
+ }
+ }
+
+ /**
+ * Extracts number formats from the "numFmt" nodes.
+ * For simplicity, the styles attributes are kept in memory. This is possible thanks
+ * to the reuse of formats. So 1 million cells should not use 1 million formats.
+ *
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node
+ * @return void
+ */
+ protected function extractNumberFormats($xmlReader)
+ {
+ while ($xmlReader->read()) {
+ if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) {
+ $numFmtId = intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID));
+ $formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE);
+ $this->customNumberFormats[$numFmtId] = $formatCode;
+ } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) {
+ // Once done reading "numFmts" node's children
+ break;
+ }
+ }
+ }
+
+ /**
+ * Extracts style attributes from the "xf" nodes, inside the "cellXfs" section.
+ * For simplicity, the styles attributes are kept in memory. This is possible thanks
+ * to the reuse of styles. So 1 million cells should not use 1 million styles.
+ *
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node
+ * @return void
+ */
+ protected function extractStyleAttributes($xmlReader)
+ {
+ while ($xmlReader->read()) {
+ if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) {
+ $numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID);
+ $normalizedNumFmtId = ($numFmtId !== null) ? intval($numFmtId) : null;
+
+ $applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT);
+ $normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? !!$applyNumberFormat : null;
+
+ $this->stylesAttributes[] = [
+ self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId,
+ self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat,
+ ];
+ } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) {
+ // Once done reading "cellXfs" node's children
+ break;
+ }
+ }
+ }
+
+ /**
+ * @return array The custom number formats
+ */
+ protected function getCustomNumberFormats()
+ {
+ if (!isset($this->customNumberFormats)) {
+ $this->extractRelevantInfo();
+ }
+
+ return $this->customNumberFormats;
+ }
+
+ /**
+ * @return array The styles attributes
+ */
+ protected function getStylesAttributes()
+ {
+ if (!isset($this->stylesAttributes)) {
+ $this->extractRelevantInfo();
+ }
+
+ return $this->stylesAttributes;
+ }
+
+ /**
+ * @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId")
+ * @return bool Whether the style with the given attributes indicates that the number is a date
+ */
+ protected function doesStyleIndicateDate($styleAttributes)
+ {
+ $applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT];
+ $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
+
+ // A style may apply a date format if it has:
+ // - "applyNumberFormat" attribute not set to "false"
+ // - "numFmtId" attribute set
+ // This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format,
+ // but this is not necessarily a date.
+ if ($applyNumberFormat === false || $numFmtId === null) {
+ return false;
+ }
+
+ return $this->doesNumFmtIdIndicateDate($numFmtId);
+ }
+
+ /**
+ * Returns whether the number format ID indicates that the number is a date.
+ * The result is cached to avoid recomputing the same thing over and over, as
+ * "numFmtId" attributes can be shared between multiple styles.
+ *
+ * @param int $numFmtId
+ * @return bool Whether the number format ID indicates that the number is a date
+ */
+ protected function doesNumFmtIdIndicateDate($numFmtId)
+ {
+ if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) {
+ $formatCode = $this->getFormatCodeForNumFmtId($numFmtId);
+
+ $this->numFmtIdToIsDateFormatCache[$numFmtId] = (
+ $this->isNumFmtIdBuiltInDateFormat($numFmtId) ||
+ $this->isFormatCodeCustomDateFormat($formatCode)
+ );
+ }
+
+ return $this->numFmtIdToIsDateFormatCache[$numFmtId];
+ }
+
+ /**
+ * @param int $numFmtId
+ * @return string|null The custom number format or NULL if none defined for the given numFmtId
+ */
+ protected function getFormatCodeForNumFmtId($numFmtId)
+ {
+ $customNumberFormats = $this->getCustomNumberFormats();
+
+ // Using isset here because it is way faster than array_key_exists...
+ return (isset($customNumberFormats[$numFmtId])) ? $customNumberFormats[$numFmtId] : null;
+ }
+
+ /**
+ * @param int $numFmtId
+ * @return bool Whether the number format ID indicates that the number is a date
+ */
+ protected function isNumFmtIdBuiltInDateFormat($numFmtId)
+ {
+ return in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates);
+ }
+
+ /**
+ * @param string|null $formatCode
+ * @return bool Whether the given format code indicates that the number is a date
+ */
+ protected function isFormatCodeCustomDateFormat($formatCode)
+ {
+ // if no associated format code or if using the default "General" format
+ if ($formatCode === null || strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) {
+ return false;
+ }
+
+ return $this->isFormatCodeMatchingDateFormatPattern($formatCode);
+ }
+
+ /**
+ * @param string $formatCode
+ * @return bool Whether the given format code matches a date format pattern
+ */
+ protected function isFormatCodeMatchingDateFormatPattern($formatCode)
+ {
+ // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\")
+ $pattern = '((?getStylesAttributes();
+ $styleAttributes = $stylesAttributes[$styleId];
+ $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID];
+
+ if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) {
+ $numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId];
+ } else {
+ $customNumberFormats = $this->getCustomNumberFormats();
+ $numberFormatCode = $customNumberFormats[$numFmtId];
+ }
+
+ return $numberFormatCode;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Reader.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Reader.php
new file mode 100644
index 0000000000..76e8e325cf
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Reader.php
@@ -0,0 +1,113 @@
+options)) {
+ $this->options = new ReaderOptions();
+ }
+ return $this->options;
+ }
+
+ /**
+ * @param string $tempFolder Temporary folder where the temporary files will be created
+ * @return Reader
+ */
+ public function setTempFolder($tempFolder)
+ {
+ $this->getOptions()->setTempFolder($tempFolder);
+ return $this;
+ }
+
+ /**
+ * Returns whether stream wrappers are supported
+ *
+ * @return bool
+ */
+ protected function doesSupportStreamWrapper()
+ {
+ return false;
+ }
+
+ /**
+ * Opens the file at the given file path to make it ready to be read.
+ * It also parses the sharedStrings.xml file to get all the shared strings available in memory
+ * and fetches all the available sheets.
+ *
+ * @param string $filePath Path of the file to be read
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read
+ * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file
+ */
+ protected function openReader($filePath)
+ {
+ $this->zip = new \ZipArchive();
+
+ if ($this->zip->open($filePath) === true) {
+ $this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->getOptions()->getTempFolder());
+
+ if ($this->sharedStringsHelper->hasSharedStrings()) {
+ // Extracts all the strings from the sheets for easy access in the future
+ $this->sharedStringsHelper->extractSharedStrings();
+ }
+
+ $this->sheetIterator = new SheetIterator($filePath, $this->getOptions(), $this->sharedStringsHelper, $this->globalFunctionsHelper);
+ } else {
+ throw new IOException("Could not open $filePath for reading.");
+ }
+ }
+
+ /**
+ * Returns an iterator to iterate over sheets.
+ *
+ * @return SheetIterator To iterate over sheets
+ */
+ protected function getConcreteSheetIterator()
+ {
+ return $this->sheetIterator;
+ }
+
+ /**
+ * Closes the reader. To be used after reading the file.
+ *
+ * @return void
+ */
+ protected function closeReader()
+ {
+ if ($this->zip) {
+ $this->zip->close();
+ }
+
+ if ($this->sharedStringsHelper) {
+ $this->sharedStringsHelper->cleanup();
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/ReaderOptions.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/ReaderOptions.php
new file mode 100644
index 0000000000..5f78c5d280
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/ReaderOptions.php
@@ -0,0 +1,33 @@
+tempFolder;
+ }
+
+ /**
+ * @param string|null $tempFolder Temporary folder where the temporary files will be created
+ * @return ReaderOptions
+ */
+ public function setTempFolder($tempFolder)
+ {
+ $this->tempFolder = $tempFolder;
+ return $this;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/RowIterator.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/RowIterator.php
new file mode 100644
index 0000000000..e70c617433
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/RowIterator.php
@@ -0,0 +1,405 @@
+filePath = $filePath;
+ $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath);
+
+ $this->xmlReader = new XMLReader();
+
+ $this->styleHelper = new StyleHelper($filePath);
+ $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $options->shouldFormatDates());
+
+ $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows();
+
+ // Register all callbacks to process different nodes when reading the XML file
+ $this->xmlProcessor = new XMLProcessor($this->xmlReader);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_DIMENSION, XMLProcessor::NODE_TYPE_START, [$this, 'processDimensionStartingNode']);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']);
+ $this->xmlProcessor->registerCallback(self::XML_NODE_WORKSHEET, XMLProcessor::NODE_TYPE_END, [$this, 'processWorksheetEndingNode']);
+ }
+
+ /**
+ * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml
+ * @return string Path of the XML file containing the sheet data,
+ * without the leading slash.
+ */
+ protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath)
+ {
+ return ltrim($sheetDataXMLFilePath, '/');
+ }
+
+ /**
+ * Rewind the Iterator to the first element.
+ * Initializes the XMLReader object that reads the associated sheet data.
+ * The XMLReader is configured to be safe from billion laughs attack.
+ * @link http://php.net/manual/en/iterator.rewind.php
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read
+ */
+ public function rewind()
+ {
+ $this->xmlReader->close();
+
+ if ($this->xmlReader->openFileInZip($this->filePath, $this->sheetDataXMLFilePath) === false) {
+ throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\".");
+ }
+
+ $this->numReadRows = 0;
+ $this->lastRowIndexProcessed = 0;
+ $this->nextRowIndexToBeProcessed = 0;
+ $this->rowDataBuffer = null;
+ $this->hasReachedEndOfFile = false;
+ $this->numColumns = 0;
+
+ $this->next();
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ return (!$this->hasReachedEndOfFile);
+ }
+
+ /**
+ * Move forward to next element. Reads data describing the next unprocessed row.
+ * @link http://php.net/manual/en/iterator.next.php
+ *
+ * @return void
+ * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
+ * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
+ */
+ public function next()
+ {
+ $this->nextRowIndexToBeProcessed++;
+
+ if ($this->doesNeedDataForNextRowToBeProcessed()) {
+ $this->readDataForNextRow();
+ }
+ }
+
+ /**
+ * Returns whether we need data for the next row to be processed.
+ * We don't need to read data if:
+ * we have already read at least one row
+ * AND
+ * we need to preserve empty rows
+ * AND
+ * the last row that was read is not the row that need to be processed
+ * (i.e. if we need to return empty rows)
+ *
+ * @return bool Whether we need data for the next row to be processed.
+ */
+ protected function doesNeedDataForNextRowToBeProcessed()
+ {
+ $hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0);
+
+ return (
+ !$hasReadAtLeastOneRow ||
+ !$this->shouldPreserveEmptyRows ||
+ $this->lastRowIndexProcessed < $this->nextRowIndexToBeProcessed
+ );
+ }
+
+ /**
+ * @return void
+ * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found
+ * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML
+ */
+ protected function readDataForNextRow()
+ {
+ $this->currentlyProcessedRowData = [];
+
+ try {
+ $this->xmlProcessor->readUntilStopped();
+ } catch (XMLProcessingException $exception) {
+ throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]");
+ }
+
+ $this->rowDataBuffer = $this->currentlyProcessedRowData;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processDimensionStartingNode($xmlReader)
+ {
+ // Read dimensions of the sheet
+ $dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet)
+ if (preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) {
+ $this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1;
+ }
+
+ return XMLProcessor::PROCESSING_CONTINUE;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processRowStartingNode($xmlReader)
+ {
+ // Reset index of the last processed column
+ $this->lastColumnIndexProcessed = -1;
+
+ // Mark the last processed row as the one currently being read
+ $this->lastRowIndexProcessed = $this->getRowIndex($xmlReader);
+
+ // Read spans info if present
+ $numberOfColumnsForRow = $this->numColumns;
+ $spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance
+ if ($spans) {
+ list(, $numberOfColumnsForRow) = explode(':', $spans);
+ $numberOfColumnsForRow = intval($numberOfColumnsForRow);
+ }
+
+ $this->currentlyProcessedRowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : [];
+
+ return XMLProcessor::PROCESSING_CONTINUE;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processCellStartingNode($xmlReader)
+ {
+ $currentColumnIndex = $this->getColumnIndex($xmlReader);
+
+ // NOTE: expand() will automatically decode all XML entities of the child nodes
+ $node = $xmlReader->expand();
+ $this->currentlyProcessedRowData[$currentColumnIndex] = $this->getCellValue($node);
+ $this->lastColumnIndexProcessed = $currentColumnIndex;
+
+ return XMLProcessor::PROCESSING_CONTINUE;
+ }
+
+ /**
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processRowEndingNode()
+ {
+ // if the fetched row is empty and we don't want to preserve it..,
+ if (!$this->shouldPreserveEmptyRows && $this->isEmptyRow($this->currentlyProcessedRowData)) {
+ // ... skip it
+ return XMLProcessor::PROCESSING_CONTINUE;
+ }
+
+ $this->numReadRows++;
+
+ // If needed, we fill the empty cells
+ if ($this->numColumns === 0) {
+ $this->currentlyProcessedRowData = CellHelper::fillMissingArrayIndexes($this->currentlyProcessedRowData);
+ }
+
+ // at this point, we have all the data we need for the row
+ // so that we can populate the buffer
+ return XMLProcessor::PROCESSING_STOP;
+ }
+
+ /**
+ * @return int A return code that indicates what action should the processor take next
+ */
+ protected function processWorksheetEndingNode()
+ {
+ // The closing "" marks the end of the file
+ $this->hasReachedEndOfFile = true;
+
+ return XMLProcessor::PROCESSING_STOP;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node
+ * @return int Row index
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
+ */
+ protected function getRowIndex($xmlReader)
+ {
+ // Get "r" attribute if present (from something like
+ $currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX);
+
+ return ($currentRowIndex !== null) ?
+ intval($currentRowIndex) :
+ $this->lastRowIndexProcessed + 1;
+ }
+
+ /**
+ * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node
+ * @return int Column index
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid
+ */
+ protected function getColumnIndex($xmlReader)
+ {
+ // Get "r" attribute if present (from something like
+ $currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX);
+
+ return ($currentCellIndex !== null) ?
+ CellHelper::getColumnIndexFromCellIndex($currentCellIndex) :
+ $this->lastColumnIndexProcessed + 1;
+ }
+
+ /**
+ * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node.
+ *
+ * @param \DOMNode $node
+ * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error)
+ */
+ protected function getCellValue($node)
+ {
+ return $this->cellValueFormatter->extractAndFormatNodeValue($node);
+ }
+
+ /**
+ * @param array $rowData
+ * @return bool Whether the given row is empty
+ */
+ protected function isEmptyRow($rowData)
+ {
+ return (count($rowData) === 1 && key($rowData) === '');
+ }
+
+ /**
+ * Return the current element, either an empty row or from the buffer.
+ * @link http://php.net/manual/en/iterator.current.php
+ *
+ * @return array|null
+ */
+ public function current()
+ {
+ $rowDataForRowToBeProcessed = $this->rowDataBuffer;
+
+ if ($this->shouldPreserveEmptyRows) {
+ // when we need to preserve empty rows, we will either return
+ // an empty row or the last row read. This depends whether the
+ // index of last row that was read matches the index of the last
+ // row whose value should be returned.
+ if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) {
+ // return empty row if mismatch between last processed row
+ // and the row that needs to be returned
+ $rowDataForRowToBeProcessed = [''];
+ }
+ }
+
+ return $rowDataForRowToBeProcessed;
+ }
+
+ /**
+ * Return the key of the current element. Here, the row index.
+ * @link http://php.net/manual/en/iterator.key.php
+ *
+ * @return int
+ */
+ public function key()
+ {
+ // TODO: This should return $this->nextRowIndexToBeProcessed
+ // but to avoid a breaking change, the return value for
+ // this function has been kept as the number of rows read.
+ return $this->shouldPreserveEmptyRows ?
+ $this->nextRowIndexToBeProcessed :
+ $this->numReadRows;
+ }
+
+
+ /**
+ * Cleans up what was created to iterate over the object.
+ *
+ * @return void
+ */
+ public function end()
+ {
+ $this->xmlReader->close();
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/Sheet.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Sheet.php
new file mode 100644
index 0000000000..9baaef247f
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/Sheet.php
@@ -0,0 +1,79 @@
+rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper);
+ $this->index = $sheetIndex;
+ $this->name = $sheetName;
+ $this->isActive = $isSheetActive;
+ }
+
+ /**
+ * @api
+ * @return \Box\Spout\Reader\XLSX\RowIterator
+ */
+ public function getRowIterator()
+ {
+ return $this->rowIterator;
+ }
+
+ /**
+ * @api
+ * @return int Index of the sheet, based on order in the workbook (zero-based)
+ */
+ public function getIndex()
+ {
+ return $this->index;
+ }
+
+ /**
+ * @api
+ * @return string Name of the sheet
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @api
+ * @return bool Whether the sheet was defined as active
+ */
+ public function isActive()
+ {
+ return $this->isActive;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Reader/XLSX/SheetIterator.php b/lib/vendor/box/spout/src/Spout/Reader/XLSX/SheetIterator.php
new file mode 100644
index 0000000000..7ba07d3428
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Reader/XLSX/SheetIterator.php
@@ -0,0 +1,114 @@
+sheets = $sheetHelper->getSheets();
+
+ if (count($this->sheets) === 0) {
+ throw new NoSheetsFoundException('The file must contain at least one sheet.');
+ }
+ }
+
+ /**
+ * Rewind the Iterator to the first element
+ * @link http://php.net/manual/en/iterator.rewind.php
+ *
+ * @return void
+ */
+ public function rewind()
+ {
+ $this->currentSheetIndex = 0;
+ }
+
+ /**
+ * Checks if current position is valid
+ * @link http://php.net/manual/en/iterator.valid.php
+ *
+ * @return bool
+ */
+ public function valid()
+ {
+ return ($this->currentSheetIndex < count($this->sheets));
+ }
+
+ /**
+ * Move forward to next element
+ * @link http://php.net/manual/en/iterator.next.php
+ *
+ * @return void
+ */
+ public function next()
+ {
+ // Using isset here because it is way faster than array_key_exists...
+ if (isset($this->sheets[$this->currentSheetIndex])) {
+ $currentSheet = $this->sheets[$this->currentSheetIndex];
+ $currentSheet->getRowIterator()->end();
+
+ $this->currentSheetIndex++;
+ }
+ }
+
+ /**
+ * Return the current element
+ * @link http://php.net/manual/en/iterator.current.php
+ *
+ * @return \Box\Spout\Reader\XLSX\Sheet
+ */
+ public function current()
+ {
+ return $this->sheets[$this->currentSheetIndex];
+ }
+
+ /**
+ * Return the key of the current element
+ * @link http://php.net/manual/en/iterator.key.php
+ *
+ * @return int
+ */
+ public function key()
+ {
+ return $this->currentSheetIndex + 1;
+ }
+
+ /**
+ * Cleans up what was created to iterate over the object.
+ *
+ * @return void
+ */
+ public function end()
+ {
+ // make sure we are not leaking memory in case the iteration stopped before the end
+ foreach ($this->sheets as $sheet) {
+ $sheet->getRowIterator()->end();
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php b/lib/vendor/box/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php
new file mode 100644
index 0000000000..d11966055a
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/AbstractMultiSheetsWriter.php
@@ -0,0 +1,119 @@
+throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
+
+ $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
+ return $this;
+ }
+
+ /**
+ * Returns all the workbook's sheets
+ *
+ * @api
+ * @return Common\Sheet[] All the workbook's sheets
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
+ */
+ public function getSheets()
+ {
+ $this->throwIfBookIsNotAvailable();
+
+ $externalSheets = [];
+ $worksheets = $this->getWorkbook()->getWorksheets();
+
+ /** @var Common\Internal\WorksheetInterface $worksheet */
+ foreach ($worksheets as $worksheet) {
+ $externalSheets[] = $worksheet->getExternalSheet();
+ }
+
+ return $externalSheets;
+ }
+
+ /**
+ * Creates a new sheet and make it the current sheet. The data will now be written to this sheet.
+ *
+ * @api
+ * @return Common\Sheet The created sheet
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
+ */
+ public function addNewSheetAndMakeItCurrent()
+ {
+ $this->throwIfBookIsNotAvailable();
+ $worksheet = $this->getWorkbook()->addNewSheetAndMakeItCurrent();
+
+ return $worksheet->getExternalSheet();
+ }
+
+ /**
+ * Returns the current sheet
+ *
+ * @api
+ * @return Common\Sheet The current sheet
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
+ */
+ public function getCurrentSheet()
+ {
+ $this->throwIfBookIsNotAvailable();
+ return $this->getWorkbook()->getCurrentWorksheet()->getExternalSheet();
+ }
+
+ /**
+ * Sets the given sheet as the current one. New data will be written to this sheet.
+ * The writing will resume where it stopped (i.e. data won't be truncated).
+ *
+ * @api
+ * @param Common\Sheet $sheet The sheet to set as current
+ * @return void
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet
+ * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
+ */
+ public function setCurrentSheet($sheet)
+ {
+ $this->throwIfBookIsNotAvailable();
+ $this->getWorkbook()->setCurrentSheet($sheet);
+ }
+
+ /**
+ * Checks if the book has been created. Throws an exception if not created yet.
+ *
+ * @return void
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet
+ */
+ protected function throwIfBookIsNotAvailable()
+ {
+ if (!$this->getWorkbook()) {
+ throw new WriterNotOpenedException('The writer must be opened before performing this action.');
+ }
+ }
+}
+
diff --git a/lib/vendor/box/spout/src/Spout/Writer/AbstractWriter.php b/lib/vendor/box/spout/src/Spout/Writer/AbstractWriter.php
new file mode 100644
index 0000000000..83e190f5f3
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/AbstractWriter.php
@@ -0,0 +1,384 @@
+defaultRowStyle = $this->getDefaultRowStyle();
+ $this->resetRowStyleToDefault();
+ }
+
+ /**
+ * Sets the default styles for all rows added with "addRow".
+ * Overriding the default style instead of using "addRowWithStyle" improves performance by 20%.
+ * @see https://github.com/box/spout/issues/272
+ *
+ * @param Style\Style $defaultStyle
+ * @return AbstractWriter
+ */
+ public function setDefaultRowStyle($defaultStyle)
+ {
+ $this->defaultRowStyle = $defaultStyle;
+ $this->resetRowStyleToDefault();
+ return $this;
+ }
+
+ /**
+ * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper
+ * @return AbstractWriter
+ */
+ public function setGlobalFunctionsHelper($globalFunctionsHelper)
+ {
+ $this->globalFunctionsHelper = $globalFunctionsHelper;
+ return $this;
+ }
+
+ /**
+ * Inits the writer and opens it to accept data.
+ * By using this method, the data will be written to a file.
+ *
+ * @api
+ * @param string $outputFilePath Path of the output file that will contain the data
+ * @return AbstractWriter
+ * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable
+ */
+ public function openToFile($outputFilePath)
+ {
+ $this->outputFilePath = $outputFilePath;
+
+ $this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+');
+ $this->throwIfFilePointerIsNotAvailable();
+
+ $this->openWriter();
+ $this->isWriterOpened = true;
+
+ return $this;
+ }
+
+ /**
+ * Inits the writer and opens it to accept data.
+ * By using this method, the data will be outputted directly to the browser.
+ *
+ * @codeCoverageIgnore
+ *
+ * @api
+ * @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept
+ * @return AbstractWriter
+ * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened
+ */
+ public function openToBrowser($outputFileName)
+ {
+ $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName);
+
+ $this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w');
+ $this->throwIfFilePointerIsNotAvailable();
+
+ // Clear any previous output (otherwise the generated file will be corrupted)
+ // @see https://github.com/box/spout/issues/241
+ $this->globalFunctionsHelper->ob_end_clean();
+
+ // Set headers
+ $this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType);
+ $this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"');
+
+ /*
+ * When forcing the download of a file over SSL,IE8 and lower browsers fail
+ * if the Cache-Control and Pragma headers are not set.
+ *
+ * @see http://support.microsoft.com/KB/323308
+ * @see https://github.com/liuggio/ExcelBundle/issues/45
+ */
+ $this->globalFunctionsHelper->header('Cache-Control: max-age=0');
+ $this->globalFunctionsHelper->header('Pragma: public');
+
+ $this->openWriter();
+ $this->isWriterOpened = true;
+
+ return $this;
+ }
+
+ /**
+ * Checks if the pointer to the file/stream to write to is available.
+ * Will throw an exception if not available.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the pointer is not available
+ */
+ protected function throwIfFilePointerIsNotAvailable()
+ {
+ if (!$this->filePointer) {
+ throw new IOException('File pointer has not be opened');
+ }
+ }
+
+ /**
+ * Checks if the writer has already been opened, since some actions must be done before it gets opened.
+ * Throws an exception if already opened.
+ *
+ * @param string $message Error message
+ * @return void
+ * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be.
+ */
+ protected function throwIfWriterAlreadyOpened($message)
+ {
+ if ($this->isWriterOpened) {
+ throw new WriterAlreadyOpenedException($message);
+ }
+ }
+
+ /**
+ * Write given data to the output. New data will be appended to end of stream.
+ *
+ * @param array $dataRow Array containing data to be streamed.
+ * If empty, no data is added (i.e. not even as a blank row)
+ * Example: $dataRow = ['data1', 1234, null, '', 'data5', false];
+ * @api
+ * @return AbstractWriter
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
+ * @throws \Box\Spout\Common\Exception\IOException If unable to write data
+ * @throws \Box\Spout\Common\Exception\SpoutException If anything else goes wrong while writing data
+ */
+ public function addRow(array $dataRow)
+ {
+ if ($this->isWriterOpened) {
+ // empty $dataRow should not add an empty line
+ if (!empty($dataRow)) {
+ try {
+ $this->addRowToWriter($dataRow, $this->rowStyle);
+ } catch (SpoutException $e) {
+ // if an exception occurs while writing data,
+ // close the writer and remove all files created so far.
+ $this->closeAndAttemptToCleanupAllFiles();
+
+ // re-throw the exception to alert developers of the error
+ throw $e;
+ }
+ }
+ } else {
+ throw new WriterNotOpenedException('The writer needs to be opened before adding row.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Write given data to the output and apply the given style.
+ * @see addRow
+ *
+ * @api
+ * @param array $dataRow Array of array containing data to be streamed.
+ * @param Style\Style $style Style to be applied to the row.
+ * @return AbstractWriter
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
+ * @throws \Box\Spout\Common\Exception\IOException If unable to write data
+ */
+ public function addRowWithStyle(array $dataRow, $style)
+ {
+ if (!$style instanceof Style\Style) {
+ throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
+ }
+
+ $this->setRowStyle($style);
+ $this->addRow($dataRow);
+ $this->resetRowStyleToDefault();
+
+ return $this;
+ }
+
+ /**
+ * Write given data to the output. New data will be appended to end of stream.
+ *
+ * @api
+ * @param array $dataRows Array of array containing data to be streamed.
+ * If a row is empty, it won't be added (i.e. not even as a blank row)
+ * Example: $dataRows = [
+ * ['data11', 12, , '', 'data13'],
+ * ['data21', 'data22', null, false],
+ * ];
+ * @return AbstractWriter
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
+ * @throws \Box\Spout\Common\Exception\IOException If unable to write data
+ */
+ public function addRows(array $dataRows)
+ {
+ if (!empty($dataRows)) {
+ $firstRow = reset($dataRows);
+ if (!is_array($firstRow)) {
+ throw new InvalidArgumentException('The input should be an array of arrays');
+ }
+
+ foreach ($dataRows as $dataRow) {
+ $this->addRow($dataRow);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Write given data to the output and apply the given style.
+ * @see addRows
+ *
+ * @api
+ * @param array $dataRows Array of array containing data to be streamed.
+ * @param Style\Style $style Style to be applied to the rows.
+ * @return AbstractWriter
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer
+ * @throws \Box\Spout\Common\Exception\IOException If unable to write data
+ */
+ public function addRowsWithStyle(array $dataRows, $style)
+ {
+ if (!$style instanceof Style\Style) {
+ throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.');
+ }
+
+ $this->setRowStyle($style);
+ $this->addRows($dataRows);
+ $this->resetRowStyleToDefault();
+
+ return $this;
+ }
+
+ /**
+ * Returns the default style to be applied to rows.
+ * Can be overriden by children to have a custom style.
+ *
+ * @return Style\Style
+ */
+ protected function getDefaultRowStyle()
+ {
+ return (new StyleBuilder())->build();
+ }
+
+ /**
+ * Sets the style to be applied to the next written rows
+ * until it is changed or reset.
+ *
+ * @param Style\Style $style
+ * @return void
+ */
+ private function setRowStyle($style)
+ {
+ // Merge given style with the default one to inherit custom properties
+ $this->rowStyle = $style->mergeWith($this->defaultRowStyle);
+ }
+
+ /**
+ * Resets the style to be applied to the next written rows.
+ *
+ * @return void
+ */
+ private function resetRowStyleToDefault()
+ {
+ $this->rowStyle = $this->defaultRowStyle;
+ }
+
+ /**
+ * Closes the writer. This will close the streamer as well, preventing new data
+ * to be written to the file.
+ *
+ * @api
+ * @return void
+ */
+ public function close()
+ {
+ if (!$this->isWriterOpened) {
+ return;
+ }
+
+ $this->closeWriter();
+
+ if (is_resource($this->filePointer)) {
+ $this->globalFunctionsHelper->fclose($this->filePointer);
+ }
+
+ $this->isWriterOpened = false;
+ }
+
+ /**
+ * Closes the writer and attempts to cleanup all files that were
+ * created during the writing process (temp files & final file).
+ *
+ * @return void
+ */
+ private function closeAndAttemptToCleanupAllFiles()
+ {
+ // close the writer, which should remove all temp files
+ $this->close();
+
+ // remove output file if it was created
+ if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) {
+ $outputFolderPath = dirname($this->outputFilePath);
+ $fileSystemHelper = new FileSystemHelper($outputFolderPath);
+ $fileSystemHelper->deleteFile($this->outputFilePath);
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/CSV/Writer.php b/lib/vendor/box/spout/src/Spout/Writer/CSV/Writer.php
new file mode 100644
index 0000000000..f7f1fda541
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/CSV/Writer.php
@@ -0,0 +1,118 @@
+fieldDelimiter = $fieldDelimiter;
+ return $this;
+ }
+
+ /**
+ * Sets the field enclosure for the CSV
+ *
+ * @api
+ * @param string $fieldEnclosure Character that enclose fields
+ * @return Writer
+ */
+ public function setFieldEnclosure($fieldEnclosure)
+ {
+ $this->fieldEnclosure = $fieldEnclosure;
+ return $this;
+ }
+
+ /**
+ * Set if a BOM has to be added to the file
+ *
+ * @param bool $shouldAddBOM
+ * @return Writer
+ */
+ public function setShouldAddBOM($shouldAddBOM)
+ {
+ $this->shouldAddBOM = (bool) $shouldAddBOM;
+ return $this;
+ }
+
+ /**
+ * Opens the CSV streamer and makes it ready to accept data.
+ *
+ * @return void
+ */
+ protected function openWriter()
+ {
+ if ($this->shouldAddBOM) {
+ // Adds UTF-8 BOM for Unicode compatibility
+ $this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8);
+ }
+ }
+
+ /**
+ * Adds data to the currently opened writer.
+ *
+ * @param array $dataRow Array containing data to be written.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @param \Box\Spout\Writer\Style\Style $style Ignored here since CSV does not support styling.
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If unable to write data
+ */
+ protected function addRowToWriter(array $dataRow, $style)
+ {
+ $wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure);
+ if ($wasWriteSuccessful === false) {
+ throw new IOException('Unable to write data');
+ }
+
+ $this->lastWrittenRowIndex++;
+ if ($this->lastWrittenRowIndex % self::FLUSH_THRESHOLD === 0) {
+ $this->globalFunctionsHelper->fflush($this->filePointer);
+ }
+ }
+
+ /**
+ * Closes the CSV streamer, preventing any additional writing.
+ * If set, sets the headers and redirects output to the browser.
+ *
+ * @return void
+ */
+ protected function closeWriter()
+ {
+ $this->lastWrittenRowIndex = 0;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php b/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php
new file mode 100644
index 0000000000..fa5e26745e
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php
@@ -0,0 +1,138 @@
+ [STYLE_ID] mapping table, keeping track of the registered styles */
+ protected $serializedStyleToStyleIdMappingTable = [];
+
+ /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */
+ protected $styleIdToStyleMappingTable = [];
+
+ /**
+ * @param \Box\Spout\Writer\Style\Style $defaultStyle
+ */
+ public function __construct($defaultStyle)
+ {
+ // This ensures that the default style is the first one to be registered
+ $this->registerStyle($defaultStyle);
+ }
+
+ /**
+ * Registers the given style as a used style.
+ * Duplicate styles won't be registered more than once.
+ *
+ * @param \Box\Spout\Writer\Style\Style $style The style to be registered
+ * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID.
+ */
+ public function registerStyle($style)
+ {
+ $serializedStyle = $style->serialize();
+
+ if (!$this->hasStyleAlreadyBeenRegistered($style)) {
+ $nextStyleId = count($this->serializedStyleToStyleIdMappingTable);
+ $style->setId($nextStyleId);
+
+ $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId;
+ $this->styleIdToStyleMappingTable[$nextStyleId] = $style;
+ }
+
+ return $this->getStyleFromSerializedStyle($serializedStyle);
+ }
+
+ /**
+ * Returns whether the given style has already been registered.
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return bool
+ */
+ protected function hasStyleAlreadyBeenRegistered($style)
+ {
+ $serializedStyle = $style->serialize();
+
+ // Using isset here because it is way faster than array_key_exists...
+ return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]);
+ }
+
+ /**
+ * Returns the registered style associated to the given serialization.
+ *
+ * @param string $serializedStyle The serialized style from which the actual style should be fetched from
+ * @return \Box\Spout\Writer\Style\Style
+ */
+ protected function getStyleFromSerializedStyle($serializedStyle)
+ {
+ $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle];
+ return $this->styleIdToStyleMappingTable[$styleId];
+ }
+
+ /**
+ * @return \Box\Spout\Writer\Style\Style[] List of registered styles
+ */
+ protected function getRegisteredStyles()
+ {
+ return array_values($this->styleIdToStyleMappingTable);
+ }
+
+ /**
+ * Returns the default style
+ *
+ * @return \Box\Spout\Writer\Style\Style Default style
+ */
+ protected function getDefaultStyle()
+ {
+ // By construction, the default style has ID 0
+ return $this->styleIdToStyleMappingTable[0];
+ }
+
+ /**
+ * Apply additional styles if the given row needs it.
+ * Typically, set "wrap text" if a cell contains a new line.
+ *
+ * @param \Box\Spout\Writer\Style\Style $style The original style
+ * @param array $dataRow The row the style will be applied to
+ * @return \Box\Spout\Writer\Style\Style The updated style
+ */
+ public function applyExtraStylesIfNeeded($style, $dataRow)
+ {
+ $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow);
+ return $updatedStyle;
+ }
+
+ /**
+ * Set the "wrap text" option if a cell of the given row contains a new line.
+ *
+ * @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines
+ * are ignored even when the "wrap text" option is set. This only occurs with
+ * inline strings (shared strings do work fine).
+ * A workaround would be to encode "\n" as "_x000D_" but it does not work
+ * on the Windows version of Excel...
+ *
+ * @param \Box\Spout\Writer\Style\Style $style The original style
+ * @param array $dataRow The row the style will be applied to
+ * @return \Box\Spout\Writer\Style\Style The eventually updated style
+ */
+ protected function applyWrapTextIfCellContainsNewLine($style, $dataRow)
+ {
+ // if the "wrap text" option is already set, no-op
+ if ($style->hasSetWrapText()) {
+ return $style;
+ }
+
+ foreach ($dataRow as $cell) {
+ if (is_string($cell) && strpos($cell, "\n") !== false) {
+ $style->setShouldWrapText();
+ break;
+ }
+ }
+
+ return $style;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/CellHelper.php b/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/CellHelper.php
new file mode 100644
index 0000000000..50ead93be4
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/CellHelper.php
@@ -0,0 +1,91 @@
+ cell index */
+ private static $columnIndexToCellIndexCache = [];
+
+ /**
+ * Returns the cell index (base 26) associated to the base 10 column index.
+ * Excel uses A to Z letters for column indexing, where A is the 1st column,
+ * Z is the 26th and AA is the 27th.
+ * The mapping is zero based, so that 0 maps to A, B maps to 1, Z to 25 and AA to 26.
+ *
+ * @param int $columnIndex The Excel column index (0, 42, ...)
+ * @return string The associated cell index ('A', 'BC', ...)
+ */
+ public static function getCellIndexFromColumnIndex($columnIndex)
+ {
+ $originalColumnIndex = $columnIndex;
+
+ // Using isset here because it is way faster than array_key_exists...
+ if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) {
+ $cellIndex = '';
+ $capitalAAsciiValue = ord('A');
+
+ do {
+ $modulus = $columnIndex % 26;
+ $cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex;
+
+ // substracting 1 because it's zero-based
+ $columnIndex = intval($columnIndex / 26) - 1;
+
+ } while ($columnIndex >= 0);
+
+ self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex;
+ }
+
+ return self::$columnIndexToCellIndexCache[$originalColumnIndex];
+ }
+
+ /**
+ * @param $value
+ * @return bool Whether the given value is considered "empty"
+ */
+ public static function isEmpty($value)
+ {
+ return ($value === null || $value === '');
+ }
+
+ /**
+ * @param $value
+ * @return bool Whether the given value is a non empty string
+ */
+ public static function isNonEmptyString($value)
+ {
+ return (gettype($value) === 'string' && $value !== '');
+ }
+
+ /**
+ * Returns whether the given value is numeric.
+ * A numeric value is from type "integer" or "double" ("float" is not returned by gettype).
+ *
+ * @param $value
+ * @return bool Whether the given value is numeric
+ */
+ public static function isNumeric($value)
+ {
+ $valueType = gettype($value);
+ return ($valueType === 'integer' || $valueType === 'double');
+ }
+
+ /**
+ * Returns whether the given value is boolean.
+ * "true"/"false" and 0/1 are not booleans.
+ *
+ * @param $value
+ * @return bool Whether the given value is boolean
+ */
+ public static function isBoolean($value)
+ {
+ return gettype($value) === 'boolean';
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/ZipHelper.php b/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/ZipHelper.php
new file mode 100644
index 0000000000..1670d179bf
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Common/Helper/ZipHelper.php
@@ -0,0 +1,217 @@
+tmpFolderPath = $tmpFolderPath;
+ }
+
+ /**
+ * Returns the already created ZipArchive instance or
+ * creates one if none exists.
+ *
+ * @return \ZipArchive
+ */
+ protected function createOrGetZip()
+ {
+ if (!isset($this->zip)) {
+ $this->zip = new \ZipArchive();
+ $zipFilePath = $this->getZipFilePath();
+
+ $this->zip->open($zipFilePath, \ZipArchive::CREATE|\ZipArchive::OVERWRITE);
+ }
+
+ return $this->zip;
+ }
+
+ /**
+ * @return string Path where the zip file of the given folder will be created
+ */
+ public function getZipFilePath()
+ {
+ return $this->tmpFolderPath . self::ZIP_EXTENSION;
+ }
+
+ /**
+ * Adds the given file, located under the given root folder to the archive.
+ * The file will be compressed.
+ *
+ * Example of use:
+ * addFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
+ * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
+ *
+ * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
+ * @param string $localFilePath Path of the file to be added, under the root folder
+ * @param string|void $existingFileMode Controls what to do when trying to add an existing file
+ * @return void
+ */
+ public function addFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
+ {
+ $this->addFileToArchiveWithCompressionMethod(
+ $rootFolderPath,
+ $localFilePath,
+ $existingFileMode,
+ \ZipArchive::CM_DEFAULT
+ );
+ }
+
+ /**
+ * Adds the given file, located under the given root folder to the archive.
+ * The file will NOT be compressed.
+ *
+ * Example of use:
+ * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
+ * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
+ *
+ * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
+ * @param string $localFilePath Path of the file to be added, under the root folder
+ * @param string|void $existingFileMode Controls what to do when trying to add an existing file
+ * @return void
+ */
+ public function addUncompressedFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
+ {
+ $this->addFileToArchiveWithCompressionMethod(
+ $rootFolderPath,
+ $localFilePath,
+ $existingFileMode,
+ \ZipArchive::CM_STORE
+ );
+ }
+
+ /**
+ * Adds the given file, located under the given root folder to the archive.
+ * The file will NOT be compressed.
+ *
+ * Example of use:
+ * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml');
+ * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml'
+ *
+ * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree.
+ * @param string $localFilePath Path of the file to be added, under the root folder
+ * @param string $existingFileMode Controls what to do when trying to add an existing file
+ * @param int $compressionMethod The compression method
+ * @return void
+ */
+ protected function addFileToArchiveWithCompressionMethod($rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod)
+ {
+ $zip = $this->createOrGetZip();
+
+ if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) {
+ $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath);
+ $zip->addFile($normalizedFullFilePath, $localFilePath);
+
+ if (self::canChooseCompressionMethod()) {
+ $zip->setCompressionName($localFilePath, $compressionMethod);
+ }
+ }
+ }
+
+ /**
+ * @return bool Whether it is possible to choose the desired compression method to be used
+ */
+ public static function canChooseCompressionMethod()
+ {
+ // setCompressionName() is a PHP7+ method...
+ return (method_exists(new \ZipArchive(), 'setCompressionName'));
+ }
+
+ /**
+ * @param string $folderPath Path to the folder to be zipped
+ * @param string|void $existingFileMode Controls what to do when trying to add an existing file
+ * @return void
+ */
+ public function addFolderToArchive($folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE)
+ {
+ $zip = $this->createOrGetZip();
+
+ $folderRealPath = $this->getNormalizedRealPath($folderPath) . '/';
+ $itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST);
+
+ foreach ($itemIterator as $itemInfo) {
+ $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname());
+ $itemLocalPath = str_replace($folderRealPath, '', $itemRealPath);
+
+ if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) {
+ $zip->addFile($itemRealPath, $itemLocalPath);
+ }
+ }
+ }
+
+ /**
+ * @param \ZipArchive $zip
+ * @param string $itemLocalPath
+ * @param string $existingFileMode
+ * @return bool Whether the file should be added to the archive or skipped
+ */
+ protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode)
+ {
+ // Skip files if:
+ // - EXISTING_FILES_SKIP mode chosen
+ // - File already exists in the archive
+ return ($existingFileMode === self::EXISTING_FILES_SKIP && $zip->locateName($itemLocalPath) !== false);
+ }
+
+ /**
+ * Returns canonicalized absolute pathname, containing only forward slashes.
+ *
+ * @param string $path Path to normalize
+ * @return string Normalized and canonicalized path
+ */
+ protected function getNormalizedRealPath($path)
+ {
+ $realPath = realpath($path);
+ return str_replace(DIRECTORY_SEPARATOR, '/', $realPath);
+ }
+
+ /**
+ * Closes the archive and copies it into the given stream
+ *
+ * @param resource $streamPointer Pointer to the stream to copy the zip
+ * @return void
+ */
+ public function closeArchiveAndCopyToStream($streamPointer)
+ {
+ $zip = $this->createOrGetZip();
+ $zip->close();
+ unset($this->zip);
+
+ $this->copyZipToStream($streamPointer);
+ }
+
+ /**
+ * Streams the contents of the zip file into the given stream
+ *
+ * @param resource $pointer Pointer to the stream to copy the zip
+ * @return void
+ */
+ protected function copyZipToStream($pointer)
+ {
+ $zipFilePointer = fopen($this->getZipFilePath(), 'r');
+ stream_copy_to_stream($zipFilePointer, $pointer);
+ fclose($zipFilePointer);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php b/lib/vendor/box/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php
new file mode 100644
index 0000000000..e852e1a84d
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Common/Internal/AbstractWorkbook.php
@@ -0,0 +1,192 @@
+shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically;
+ $this->internalId = uniqid();
+ }
+
+ /**
+ * @return \Box\Spout\Writer\Common\Helper\AbstractStyleHelper The specific style helper
+ */
+ abstract protected function getStyleHelper();
+
+ /**
+ * @return int Maximum number of rows/columns a sheet can contain
+ */
+ abstract protected function getMaxRowsPerWorksheet();
+
+ /**
+ * Creates a new sheet in the workbook. The current sheet remains unchanged.
+ *
+ * @return WorksheetInterface The created sheet
+ * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
+ */
+ abstract public function addNewSheet();
+
+ /**
+ * Creates a new sheet in the workbook and make it the current sheet.
+ * The writing will resume where it stopped (i.e. data won't be truncated).
+ *
+ * @return WorksheetInterface The created sheet
+ * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
+ */
+ public function addNewSheetAndMakeItCurrent()
+ {
+ $worksheet = $this->addNewSheet();
+ $this->setCurrentWorksheet($worksheet);
+
+ return $worksheet;
+ }
+
+ /**
+ * @return WorksheetInterface[] All the workbook's sheets
+ */
+ public function getWorksheets()
+ {
+ return $this->worksheets;
+ }
+
+ /**
+ * Returns the current sheet
+ *
+ * @return WorksheetInterface The current sheet
+ */
+ public function getCurrentWorksheet()
+ {
+ return $this->currentWorksheet;
+ }
+
+ /**
+ * Sets the given sheet as the current one. New data will be written to this sheet.
+ * The writing will resume where it stopped (i.e. data won't be truncated).
+ *
+ * @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current
+ * @return void
+ * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook
+ */
+ public function setCurrentSheet($sheet)
+ {
+ $worksheet = $this->getWorksheetFromExternalSheet($sheet);
+ if ($worksheet !== null) {
+ $this->currentWorksheet = $worksheet;
+ } else {
+ throw new SheetNotFoundException('The given sheet does not exist in the workbook.');
+ }
+ }
+
+ /**
+ * @param WorksheetInterface $worksheet
+ * @return void
+ */
+ protected function setCurrentWorksheet($worksheet)
+ {
+ $this->currentWorksheet = $worksheet;
+ }
+
+ /**
+ * Returns the worksheet associated to the given external sheet.
+ *
+ * @param \Box\Spout\Writer\Common\Sheet $sheet
+ * @return WorksheetInterface|null The worksheet associated to the given external sheet or null if not found.
+ */
+ protected function getWorksheetFromExternalSheet($sheet)
+ {
+ $worksheetFound = null;
+
+ foreach ($this->worksheets as $worksheet) {
+ if ($worksheet->getExternalSheet() === $sheet) {
+ $worksheetFound = $worksheet;
+ break;
+ }
+ }
+
+ return $worksheetFound;
+ }
+
+ /**
+ * Adds data to the current sheet.
+ * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
+ * with the creation of new worksheets if one worksheet has reached its maximum capicity.
+ *
+ * @param array $dataRow Array containing data to be written. Cannot be empty.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing
+ * @throws \Box\Spout\Writer\Exception\WriterException If unable to write data
+ */
+ public function addRowToCurrentWorksheet($dataRow, $style)
+ {
+ $currentWorksheet = $this->getCurrentWorksheet();
+ $hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows();
+ $styleHelper = $this->getStyleHelper();
+
+ // if we reached the maximum number of rows for the current sheet...
+ if ($hasReachedMaxRows) {
+ // ... continue writing in a new sheet if option set
+ if ($this->shouldCreateNewSheetsAutomatically) {
+ $currentWorksheet = $this->addNewSheetAndMakeItCurrent();
+
+ $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
+ $registeredStyle = $styleHelper->registerStyle($updatedStyle);
+ $currentWorksheet->addRow($dataRow, $registeredStyle);
+ } else {
+ // otherwise, do nothing as the data won't be read anyways
+ }
+ } else {
+ $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow);
+ $registeredStyle = $styleHelper->registerStyle($updatedStyle);
+ $currentWorksheet->addRow($dataRow, $registeredStyle);
+ }
+ }
+
+ /**
+ * @return bool Whether the current worksheet has reached the maximum number of rows per sheet.
+ */
+ protected function hasCurrentWorkseetReachedMaxRows()
+ {
+ $currentWorksheet = $this->getCurrentWorksheet();
+ return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet());
+ }
+
+ /**
+ * Closes the workbook and all its associated sheets.
+ * All the necessary files are written to disk and zipped together to create the ODS file.
+ * All the temporary files are then deleted.
+ *
+ * @param resource $finalFilePointer Pointer to the ODS that will be created
+ * @return void
+ */
+ abstract public function close($finalFilePointer);
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php b/lib/vendor/box/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php
new file mode 100644
index 0000000000..fda0793486
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Common/Internal/WorkbookInterface.php
@@ -0,0 +1,74 @@
+ [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */
+ protected static $SHEETS_NAME_USED = [];
+
+ /** @var int Index of the sheet, based on order in the workbook (zero-based) */
+ protected $index;
+
+ /** @var string ID of the sheet's associated workbook. Used to restrict sheet name uniqueness enforcement to a single workbook */
+ protected $associatedWorkbookId;
+
+ /** @var string Name of the sheet */
+ protected $name;
+
+ /** @var \Box\Spout\Common\Helper\StringHelper */
+ protected $stringHelper;
+
+ /**
+ * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based)
+ * @param string $associatedWorkbookId ID of the sheet's associated workbook
+ */
+ public function __construct($sheetIndex, $associatedWorkbookId)
+ {
+ $this->index = $sheetIndex;
+ $this->associatedWorkbookId = $associatedWorkbookId;
+ if (!isset(self::$SHEETS_NAME_USED[$associatedWorkbookId])) {
+ self::$SHEETS_NAME_USED[$associatedWorkbookId] = [];
+ }
+
+ $this->stringHelper = new StringHelper();
+ $this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1));
+ }
+
+ /**
+ * @api
+ * @return int Index of the sheet, based on order in the workbook (zero-based)
+ */
+ public function getIndex()
+ {
+ return $this->index;
+ }
+
+ /**
+ * @api
+ * @return string Name of the sheet
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Sets the name of the sheet. Note that Excel has some restrictions on the name:
+ * - it should not be blank
+ * - it should not exceed 31 characters
+ * - it should not contain these characters: \ / ? * : [ or ]
+ * - it should be unique
+ *
+ * @api
+ * @param string $name Name of the sheet
+ * @return Sheet
+ * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
+ */
+ public function setName($name)
+ {
+ $this->throwIfNameIsInvalid($name);
+
+ $this->name = $name;
+ self::$SHEETS_NAME_USED[$this->associatedWorkbookId][$this->index] = $name;
+
+ return $this;
+ }
+
+ /**
+ * Throws an exception if the given sheet's name is not valid.
+ * @see Sheet::setName for validity rules.
+ *
+ * @param string $name
+ * @return void
+ * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid.
+ */
+ protected function throwIfNameIsInvalid($name)
+ {
+ if (!is_string($name)) {
+ $actualType = gettype($name);
+ $errorMessage = "The sheet's name is invalid. It must be a string ($actualType given).";
+ throw new InvalidSheetNameException($errorMessage);
+ }
+
+ $failedRequirements = [];
+ $nameLength = $this->stringHelper->getStringLength($name);
+
+ if (!$this->isNameUnique($name)) {
+ $failedRequirements[] = 'It should be unique';
+ } else {
+ if ($nameLength === 0) {
+ $failedRequirements[] = 'It should not be blank';
+ } else {
+ if ($nameLength > self::MAX_LENGTH_SHEET_NAME) {
+ $failedRequirements[] = 'It should not exceed 31 characters';
+ }
+
+ if ($this->doesContainInvalidCharacters($name)) {
+ $failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]';
+ }
+
+ if ($this->doesStartOrEndWithSingleQuote($name)) {
+ $failedRequirements[] = 'It should not start or end with a single quote';
+ }
+ }
+ }
+
+ if (count($failedRequirements) !== 0) {
+ $errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - ";
+ $errorMessage .= implode("\n - ", $failedRequirements);
+ throw new InvalidSheetNameException($errorMessage);
+ }
+ }
+
+ /**
+ * Returns whether the given name contains at least one invalid character.
+ * @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list.
+ *
+ * @param string $name
+ * @return bool TRUE if the name contains invalid characters, FALSE otherwise.
+ */
+ protected function doesContainInvalidCharacters($name)
+ {
+ return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name);
+ }
+
+ /**
+ * Returns whether the given name starts or ends with a single quote
+ *
+ * @param string $name
+ * @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise.
+ */
+ protected function doesStartOrEndWithSingleQuote($name)
+ {
+ $startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0);
+ $endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1));
+
+ return ($startsWithSingleQuote || $endsWithSingleQuote);
+ }
+
+ /**
+ * Returns whether the given name is unique.
+ *
+ * @param string $name
+ * @return bool TRUE if the name is unique, FALSE otherwise.
+ */
+ protected function isNameUnique($name)
+ {
+ foreach (self::$SHEETS_NAME_USED[$this->associatedWorkbookId] as $sheetIndex => $sheetName) {
+ if ($sheetIndex !== $this->index && $sheetName === $name) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Exception/Border/InvalidNameException.php b/lib/vendor/box/spout/src/Spout/Writer/Exception/Border/InvalidNameException.php
new file mode 100644
index 0000000000..13ac06c2dc
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Exception/Border/InvalidNameException.php
@@ -0,0 +1,16 @@
+
+ */
+class BorderHelper
+{
+ /**
+ * Width mappings
+ *
+ * @var array
+ */
+ protected static $widthMap = [
+ Border::WIDTH_THIN => '0.75pt',
+ Border::WIDTH_MEDIUM => '1.75pt',
+ Border::WIDTH_THICK => '2.5pt',
+ ];
+
+ /**
+ * Style mapping
+ *
+ * @var array
+ */
+ protected static $styleMap = [
+ Border::STYLE_SOLID => 'solid',
+ Border::STYLE_DASHED => 'dashed',
+ Border::STYLE_DOTTED => 'dotted',
+ Border::STYLE_DOUBLE => 'double',
+ ];
+
+ /**
+ * @param BorderPart $borderPart
+ * @return string
+ */
+ public static function serializeBorderPart(BorderPart $borderPart)
+ {
+ $definition = 'fo:border-%s="%s"';
+
+ if ($borderPart->getStyle() === Border::STYLE_NONE) {
+ $borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none');
+ } else {
+ $attributes = [
+ self::$widthMap[$borderPart->getWidth()],
+ self::$styleMap[$borderPart->getStyle()],
+ '#' . $borderPart->getColor(),
+ ];
+ $borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes));
+ }
+
+ return $borderPartDefinition;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php b/lib/vendor/box/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php
new file mode 100644
index 0000000000..34ace0ad13
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/ODS/Helper/FileSystemHelper.php
@@ -0,0 +1,279 @@
+rootFolder;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSheetsContentTempFolder()
+ {
+ return $this->sheetsContentTempFolder;
+ }
+
+ /**
+ * Creates all the folders needed to create a ODS file, as well as the files that won't change.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
+ */
+ public function createBaseFilesAndFolders()
+ {
+ $this
+ ->createRootFolder()
+ ->createMetaInfoFolderAndFile()
+ ->createSheetsContentTempFolder()
+ ->createMetaFile()
+ ->createMimetypeFile();
+ }
+
+ /**
+ * Creates the folder that will be used as root
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
+ */
+ protected function createRootFolder()
+ {
+ $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));
+ return $this;
+ }
+
+ /**
+ * Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file
+ */
+ protected function createMetaInfoFolderAndFile()
+ {
+ $this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);
+
+ $this->createManifestFile();
+
+ return $this;
+ }
+
+ /**
+ * Creates the "manifest.xml" file under the "META-INF" folder (under root)
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
+ */
+ protected function createManifestFile()
+ {
+ $manifestXmlFileContents = <<
+
+
+
+
+
+
+EOD;
+
+ $this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the temp folder where specific sheets content will be written to.
+ * This folder is not part of the final ODS file and is only used to be able to jump between sheets.
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
+ */
+ protected function createSheetsContentTempFolder()
+ {
+ $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME);
+ return $this;
+ }
+
+ /**
+ * Creates the "meta.xml" file under the root folder
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
+ */
+ protected function createMetaFile()
+ {
+ $appName = self::APP_NAME;
+ $createdDate = (new \DateTime())->format(\DateTime::W3C);
+
+ $metaXmlFileContents = <<
+
+
+ $appName
+ $createdDate
+ $createdDate
+
+
+EOD;
+
+ $this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the "mimetype" file under the root folder
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
+ */
+ protected function createMimetypeFile()
+ {
+ $this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);
+ return $this;
+ }
+
+ /**
+ * Creates the "content.xml" file under the root folder
+ *
+ * @param Worksheet[] $worksheets
+ * @param StyleHelper $styleHelper
+ * @return FileSystemHelper
+ */
+ public function createContentFile($worksheets, $styleHelper)
+ {
+ $contentXmlFileContents = <<
+
+EOD;
+
+ $contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent();
+ $contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets));
+
+ $contentXmlFileContents .= '';
+
+ $this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents);
+
+ // Append sheets content to "content.xml"
+ $contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME;
+ $contentXmlHandle = fopen($contentXmlFilePath, 'a');
+
+ foreach ($worksheets as $worksheet) {
+ // write the "" node, with the final sheet's name
+ fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString());
+
+ $worksheetFilePath = $worksheet->getWorksheetFilePath();
+ $this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);
+
+ fwrite($contentXmlHandle, '');
+ }
+
+ $contentXmlFileContents = '';
+
+ fwrite($contentXmlHandle, $contentXmlFileContents);
+ fclose($contentXmlHandle);
+
+ return $this;
+ }
+
+ /**
+ * Streams the content of the file at the given path into the target resource.
+ * Depending on which mode the target resource was created with, it will truncate then copy
+ * or append the content to the target file.
+ *
+ * @param string $sourceFilePath Path of the file whose content will be copied
+ * @param resource $targetResource Target resource that will receive the content
+ * @return void
+ */
+ protected function copyFileContentsToTarget($sourceFilePath, $targetResource)
+ {
+ $sourceHandle = fopen($sourceFilePath, 'r');
+ stream_copy_to_stream($sourceHandle, $targetResource);
+ fclose($sourceHandle);
+ }
+
+ /**
+ * Deletes the temporary folder where sheets content was stored.
+ *
+ * @return FileSystemHelper
+ */
+ public function deleteWorksheetTempFolder()
+ {
+ $this->deleteFolderRecursively($this->sheetsContentTempFolder);
+ return $this;
+ }
+
+
+ /**
+ * Creates the "styles.xml" file under the root folder
+ *
+ * @param StyleHelper $styleHelper
+ * @param int $numWorksheets Number of created worksheets
+ * @return FileSystemHelper
+ */
+ public function createStylesFile($styleHelper, $numWorksheets)
+ {
+ $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets);
+ $this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Zips the root folder and streams the contents of the zip into the given stream
+ *
+ * @param resource $streamPointer Pointer to the stream to copy the zip
+ * @return void
+ */
+ public function zipRootFolderAndCopyToStream($streamPointer)
+ {
+ $zipHelper = new ZipHelper($this->rootFolder);
+
+ // In order to have the file's mime type detected properly, files need to be added
+ // to the zip file in a particular order.
+ // @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
+ $zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME);
+
+ $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
+ $zipHelper->closeArchiveAndCopyToStream($streamPointer);
+
+ // once the zip is copied, remove it
+ $this->deleteFile($zipHelper->getZipFilePath());
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php b/lib/vendor/box/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php
new file mode 100644
index 0000000000..f5ad3bc38e
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/ODS/Helper/StyleHelper.php
@@ -0,0 +1,356 @@
+ [] Map whose keys contain all the fonts used */
+ protected $usedFontsSet = [];
+
+ /**
+ * Registers the given style as a used style.
+ * Duplicate styles won't be registered more than once.
+ *
+ * @param \Box\Spout\Writer\Style\Style $style The style to be registered
+ * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID.
+ */
+ public function registerStyle($style)
+ {
+ $this->usedFontsSet[$style->getFontName()] = true;
+ return parent::registerStyle($style);
+ }
+
+ /**
+ * @return string[] List of used fonts name
+ */
+ protected function getUsedFonts()
+ {
+ return array_keys($this->usedFontsSet);
+ }
+
+ /**
+ * Returns the content of the "styles.xml" file, given a list of styles.
+ *
+ * @param int $numWorksheets Number of worksheets created
+ * @return string
+ */
+ public function getStylesXMLFileContent($numWorksheets)
+ {
+ $content = <<
+
+EOD;
+
+ $content .= $this->getFontFaceSectionContent();
+ $content .= $this->getStylesSectionContent();
+ $content .= $this->getAutomaticStylesSectionContent($numWorksheets);
+ $content .= $this->getMasterStylesSectionContent($numWorksheets);
+
+ $content .= <<
+EOD;
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section, inside "styles.xml" file.
+ *
+ * @return string
+ */
+ protected function getFontFaceSectionContent()
+ {
+ $content = '';
+ foreach ($this->getUsedFonts() as $fontName) {
+ $content .= '';
+ }
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section, inside "styles.xml" file.
+ *
+ * @return string
+ */
+ protected function getStylesSectionContent()
+ {
+ $defaultStyle = $this->getDefaultStyle();
+
+ return <<
+
+
+
+
+
+
+
+
+EOD;
+ }
+
+ /**
+ * Returns the content of the "" section, inside "styles.xml" file.
+ *
+ * @param int $numWorksheets Number of worksheets created
+ * @return string
+ */
+ protected function getAutomaticStylesSectionContent($numWorksheets)
+ {
+ $content = '';
+
+ for ($i = 1; $i <= $numWorksheets; $i++) {
+ $content .= <<
+
+
+
+
+EOD;
+ }
+
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section, inside "styles.xml" file.
+ *
+ * @param int $numWorksheets Number of worksheets created
+ * @return string
+ */
+ protected function getMasterStylesSectionContent($numWorksheets)
+ {
+ $content = '';
+
+ for ($i = 1; $i <= $numWorksheets; $i++) {
+ $content .= <<
+
+
+
+
+
+EOD;
+ }
+
+ $content .= '';
+
+ return $content;
+ }
+
+
+ /**
+ * Returns the contents of the "" section, inside "content.xml" file.
+ *
+ * @return string
+ */
+ public function getContentXmlFontFaceSectionContent()
+ {
+ $content = '';
+ foreach ($this->getUsedFonts() as $fontName) {
+ $content .= '';
+ }
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the contents of the "" section, inside "content.xml" file.
+ *
+ * @param int $numWorksheets Number of worksheets created
+ * @return string
+ */
+ public function getContentXmlAutomaticStylesSectionContent($numWorksheets)
+ {
+ $content = '';
+
+ foreach ($this->getRegisteredStyles() as $style) {
+ $content .= $this->getStyleSectionContent($style);
+ }
+
+ $content .= <<
+
+
+
+
+
+EOD;
+
+ for ($i = 1; $i <= $numWorksheets; $i++) {
+ $content .= <<
+
+
+EOD;
+ }
+
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the contents of the "" section, inside "" section
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return string
+ */
+ protected function getStyleSectionContent($style)
+ {
+ $styleIndex = $style->getId() + 1; // 1-based
+
+ $content = '';
+
+ $content .= $this->getTextPropertiesSectionContent($style);
+ $content .= $this->getTableCellPropertiesSectionContent($style);
+
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the contents of the "" section, inside "" section
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return string
+ */
+ private function getTextPropertiesSectionContent($style)
+ {
+ $content = '';
+
+ if ($style->shouldApplyFont()) {
+ $content .= $this->getFontSectionContent($style);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Returns the contents of the "" section, inside "" section
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return string
+ */
+ private function getFontSectionContent($style)
+ {
+ $defaultStyle = $this->getDefaultStyle();
+
+ $content = 'getFontColor();
+ if ($fontColor !== $defaultStyle->getFontColor()) {
+ $content .= ' fo:color="#' . $fontColor . '"';
+ }
+
+ $fontName = $style->getFontName();
+ if ($fontName !== $defaultStyle->getFontName()) {
+ $content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"';
+ }
+
+ $fontSize = $style->getFontSize();
+ if ($fontSize !== $defaultStyle->getFontSize()) {
+ $content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"';
+ }
+
+ if ($style->isFontBold()) {
+ $content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"';
+ }
+ if ($style->isFontItalic()) {
+ $content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"';
+ }
+ if ($style->isFontUnderline()) {
+ $content .= ' style:text-underline-style="solid" style:text-underline-type="single"';
+ }
+ if ($style->isFontStrikethrough()) {
+ $content .= ' style:text-line-through-style="solid"';
+ }
+
+ $content .= '/>';
+
+ return $content;
+ }
+
+ /**
+ * Returns the contents of the "" section, inside "" section
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return string
+ */
+ private function getTableCellPropertiesSectionContent($style)
+ {
+ $content = '';
+
+ if ($style->shouldWrapText()) {
+ $content .= $this->getWrapTextXMLContent();
+ }
+
+ if ($style->shouldApplyBorder()) {
+ $content .= $this->getBorderXMLContent($style);
+ }
+
+ if ($style->shouldApplyBackgroundColor()) {
+ $content .= $this->getBackgroundColorXMLContent($style);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Returns the contents of the wrap text definition for the "" section
+ *
+ * @return string
+ */
+ private function getWrapTextXMLContent()
+ {
+ return '';
+ }
+
+ /**
+ * Returns the contents of the borders definition for the "" section
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return string
+ */
+ private function getBorderXMLContent($style)
+ {
+ $borderProperty = '';
+
+ $borders = array_map(function (BorderPart $borderPart) {
+ return BorderHelper::serializeBorderPart($borderPart);
+ }, $style->getBorder()->getParts());
+
+ return sprintf($borderProperty, implode(' ', $borders));
+ }
+
+ /**
+ * Returns the contents of the background color definition for the "" section
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return string
+ */
+ private function getBackgroundColorXMLContent($style)
+ {
+ return sprintf(
+ '',
+ $style->getBackgroundColor()
+ );
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/ODS/Internal/Workbook.php b/lib/vendor/box/spout/src/Spout/Writer/ODS/Internal/Workbook.php
new file mode 100644
index 0000000000..fc64ada824
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/ODS/Internal/Workbook.php
@@ -0,0 +1,119 @@
+fileSystemHelper = new FileSystemHelper($tempFolder);
+ $this->fileSystemHelper->createBaseFilesAndFolders();
+
+ $this->styleHelper = new StyleHelper($defaultRowStyle);
+ }
+
+ /**
+ * @return \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles to ODS files
+ */
+ protected function getStyleHelper()
+ {
+ return $this->styleHelper;
+ }
+
+ /**
+ * @return int Maximum number of rows/columns a sheet can contain
+ */
+ protected function getMaxRowsPerWorksheet()
+ {
+ return self::$maxRowsPerWorksheet;
+ }
+
+ /**
+ * Creates a new sheet in the workbook. The current sheet remains unchanged.
+ *
+ * @return Worksheet The created sheet
+ * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
+ */
+ public function addNewSheet()
+ {
+ $newSheetIndex = count($this->worksheets);
+ $sheet = new Sheet($newSheetIndex, $this->internalId);
+
+ $sheetsContentTempFolder = $this->fileSystemHelper->getSheetsContentTempFolder();
+ $worksheet = new Worksheet($sheet, $sheetsContentTempFolder);
+ $this->worksheets[] = $worksheet;
+
+ return $worksheet;
+ }
+
+ /**
+ * Closes the workbook and all its associated sheets.
+ * All the necessary files are written to disk and zipped together to create the ODS file.
+ * All the temporary files are then deleted.
+ *
+ * @param resource $finalFilePointer Pointer to the ODS that will be created
+ * @return void
+ */
+ public function close($finalFilePointer)
+ {
+ /** @var Worksheet[] $worksheets */
+ $worksheets = $this->worksheets;
+ $numWorksheets = count($worksheets);
+
+ foreach ($worksheets as $worksheet) {
+ $worksheet->close();
+ }
+
+ // Finish creating all the necessary files before zipping everything together
+ $this->fileSystemHelper
+ ->createContentFile($worksheets, $this->styleHelper)
+ ->deleteWorksheetTempFolder()
+ ->createStylesFile($this->styleHelper, $numWorksheets)
+ ->zipRootFolderAndCopyToStream($finalFilePointer);
+
+ $this->cleanupTempFolder();
+ }
+
+ /**
+ * Deletes the root folder created in the temp folder and all its contents.
+ *
+ * @return void
+ */
+ protected function cleanupTempFolder()
+ {
+ $xlsxRootFolder = $this->fileSystemHelper->getRootFolder();
+ $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/ODS/Internal/Worksheet.php b/lib/vendor/box/spout/src/Spout/Writer/ODS/Internal/Worksheet.php
new file mode 100644
index 0000000000..0920b6dbaa
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/ODS/Internal/Worksheet.php
@@ -0,0 +1,233 @@
+externalSheet = $externalSheet;
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $this->stringsEscaper = \Box\Spout\Common\Escaper\ODS::getInstance();
+ $this->worksheetFilePath = $worksheetFilesFolder . '/sheet' . $externalSheet->getIndex() . '.xml';
+
+ $this->stringHelper = new StringHelper();
+
+ $this->startSheet();
+ }
+
+ /**
+ * Prepares the worksheet to accept data
+ * The XML file does not contain the "" node as it contains the sheet's name
+ * which may change during the execution of the program. It will be added at the end.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
+ */
+ protected function startSheet()
+ {
+ $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w');
+ $this->throwIfSheetFilePointerIsNotAvailable();
+ }
+
+ /**
+ * Checks if the book has been created. Throws an exception if not created yet.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
+ */
+ protected function throwIfSheetFilePointerIsNotAvailable()
+ {
+ if (!$this->sheetFilePointer) {
+ throw new IOException('Unable to open sheet for writing.');
+ }
+ }
+
+ /**
+ * @return string Path to the temporary sheet content XML file
+ */
+ public function getWorksheetFilePath()
+ {
+ return $this->worksheetFilePath;
+ }
+
+ /**
+ * Returns the table XML root node as string.
+ *
+ * @return string
node as string
+ */
+ public function getTableElementStartAsString()
+ {
+ $escapedSheetName = $this->stringsEscaper->escape($this->externalSheet->getName());
+ $tableStyleName = 'ta' . ($this->externalSheet->getIndex() + 1);
+
+ $tableElement = '';
+ $tableElement .= '';
+
+ return $tableElement;
+ }
+
+ /**
+ * @return \Box\Spout\Writer\Common\Sheet The "external" sheet
+ */
+ public function getExternalSheet()
+ {
+ return $this->externalSheet;
+ }
+
+ /**
+ * @return int The index of the last written row
+ */
+ public function getLastWrittenRowIndex()
+ {
+ return $this->lastWrittenRowIndex;
+ }
+
+ /**
+ * Adds data to the worksheet.
+ *
+ * @param array $dataRow Array containing data to be written. Cannot be empty.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
+ */
+ public function addRow($dataRow, $style)
+ {
+ // $dataRow can be an associative array. We need to transform
+ // it into a regular array, as we'll use the numeric indexes.
+ $dataRowWithNumericIndexes = array_values($dataRow);
+
+ $styleIndex = ($style->getId() + 1); // 1-based
+ $cellsCount = count($dataRow);
+ $this->maxNumColumns = max($this->maxNumColumns, $cellsCount);
+
+ $data = '';
+
+ $currentCellIndex = 0;
+ $nextCellIndex = 1;
+
+ for ($i = 0; $i < $cellsCount; $i++) {
+ $currentCellValue = $dataRowWithNumericIndexes[$currentCellIndex];
+
+ // Using isset here because it is way faster than array_key_exists...
+ if (!isset($dataRowWithNumericIndexes[$nextCellIndex]) ||
+ $currentCellValue !== $dataRowWithNumericIndexes[$nextCellIndex]) {
+
+ $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);
+ $data .= $this->getCellXML($currentCellValue, $styleIndex, $numTimesValueRepeated);
+
+ $currentCellIndex = $nextCellIndex;
+ }
+
+ $nextCellIndex++;
+ }
+
+ $data .= '';
+
+ $wasWriteSuccessful = fwrite($this->sheetFilePointer, $data);
+ if ($wasWriteSuccessful === false) {
+ throw new IOException("Unable to write data in {$this->worksheetFilePath}");
+ }
+
+ // only update the count if the write worked
+ $this->lastWrittenRowIndex++;
+ }
+
+ /**
+ * Returns the cell XML content, given its value.
+ *
+ * @param mixed $cellValue The value to be written
+ * @param int $styleIndex Index of the used style
+ * @param int $numTimesValueRepeated Number of times the value is consecutively repeated
+ * @return string The cell XML content
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
+ */
+ protected function getCellXML($cellValue, $styleIndex, $numTimesValueRepeated)
+ {
+ $data = 'stringsEscaper->escape($cellValueLine) . '';
+ }
+
+ $data .= '';
+ } else if (CellHelper::isBoolean($cellValue)) {
+ $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cellValue . '">';
+ $data .= '' . $cellValue . '';
+ $data .= '';
+ } else if (CellHelper::isNumeric($cellValue)) {
+ $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">';
+ $data .= '' . $cellValue . '';
+ $data .= '';
+ } else if (empty($cellValue)) {
+ $data .= '/>';
+ } else {
+ throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
+ }
+
+ return $data;
+ }
+
+ /**
+ * Closes the worksheet
+ *
+ * @return void
+ */
+ public function close()
+ {
+ if (!is_resource($this->sheetFilePointer)) {
+ return;
+ }
+
+ fclose($this->sheetFilePointer);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/ODS/Writer.php b/lib/vendor/box/spout/src/Spout/Writer/ODS/Writer.php
new file mode 100644
index 0000000000..6571d0071d
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/ODS/Writer.php
@@ -0,0 +1,93 @@
+throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
+
+ $this->tempFolder = $tempFolder;
+ return $this;
+ }
+
+ /**
+ * Configures the write and sets the current sheet pointer to a new sheet.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing
+ */
+ protected function openWriter()
+ {
+ $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir();
+ $this->book = new Workbook($tempFolder, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle);
+ $this->book->addNewSheetAndMakeItCurrent();
+ }
+
+ /**
+ * @return Internal\Workbook The workbook representing the file to be written
+ */
+ protected function getWorkbook()
+ {
+ return $this->book;
+ }
+
+ /**
+ * Adds data to the currently opened writer.
+ * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
+ * with the creation of new worksheets if one worksheet has reached its maximum capicity.
+ *
+ * @param array $dataRow Array containing data to be written.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
+ * @return void
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet
+ * @throws \Box\Spout\Common\Exception\IOException If unable to write data
+ */
+ protected function addRowToWriter(array $dataRow, $style)
+ {
+ $this->throwIfBookIsNotAvailable();
+ $this->book->addRowToCurrentWorksheet($dataRow, $style);
+ }
+
+ /**
+ * Closes the writer, preventing any additional writing.
+ *
+ * @return void
+ */
+ protected function closeWriter()
+ {
+ if ($this->book) {
+ $this->book->close($this->filePointer);
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Style/Border.php b/lib/vendor/box/spout/src/Spout/Writer/Style/Border.php
new file mode 100644
index 0000000000..75f6a497bd
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Style/Border.php
@@ -0,0 +1,85 @@
+setParts($borderParts);
+ }
+
+ /**
+ * @param $name The name of the border part
+ * @return null|BorderPart
+ */
+ public function getPart($name)
+ {
+ return $this->hasPart($name) ? $this->parts[$name] : null;
+ }
+
+ /**
+ * @param $name The name of the border part
+ * @return bool
+ */
+ public function hasPart($name)
+ {
+ return isset($this->parts[$name]);
+ }
+
+ /**
+ * @return array
+ */
+ public function getParts()
+ {
+ return $this->parts;
+ }
+
+ /**
+ * Set BorderParts
+ * @param array $parts
+ */
+ public function setParts($parts)
+ {
+ unset($this->parts);
+ foreach ($parts as $part) {
+ $this->addPart($part);
+ }
+ }
+
+ /**
+ * @param BorderPart $borderPart
+ * @return self
+ */
+ public function addPart(BorderPart $borderPart)
+ {
+ $this->parts[$borderPart->getName()] = $borderPart;
+ return $this;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Style/BorderBuilder.php b/lib/vendor/box/spout/src/Spout/Writer/Style/BorderBuilder.php
new file mode 100644
index 0000000000..c0b8aea980
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Style/BorderBuilder.php
@@ -0,0 +1,75 @@
+border = new Border();
+ }
+
+ /**
+ * @param string|void $color Border A RGB color code
+ * @param string|void $width Border width @see BorderPart::allowedWidths
+ * @param string|void $style Border style @see BorderPart::allowedStyles
+ * @return BorderBuilder
+ */
+ public function setBorderTop($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
+ {
+ $this->border->addPart(new BorderPart(Border::TOP, $color, $width, $style));
+ return $this;
+ }
+
+ /**
+ * @param string|void $color Border A RGB color code
+ * @param string|void $width Border width @see BorderPart::allowedWidths
+ * @param string|void $style Border style @see BorderPart::allowedStyles
+ * @return BorderBuilder
+ */
+ public function setBorderRight($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
+ {
+ $this->border->addPart(new BorderPart(Border::RIGHT, $color, $width, $style));
+ return $this;
+ }
+
+ /**
+ * @param string|void $color Border A RGB color code
+ * @param string|void $width Border width @see BorderPart::allowedWidths
+ * @param string|void $style Border style @see BorderPart::allowedStyles
+ * @return BorderBuilder
+ */
+ public function setBorderBottom($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
+ {
+ $this->border->addPart(new BorderPart(Border::BOTTOM, $color, $width, $style));
+ return $this;
+ }
+
+ /**
+ * @param string|void $color Border A RGB color code
+ * @param string|void $width Border width @see BorderPart::allowedWidths
+ * @param string|void $style Border style @see BorderPart::allowedStyles
+ * @return BorderBuilder
+ */
+ public function setBorderLeft($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID)
+ {
+ $this->border->addPart(new BorderPart(Border::LEFT, $color, $width, $style));
+ return $this;
+ }
+
+ /**
+ * @return Border
+ */
+ public function build()
+ {
+ return $this->border;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Style/BorderPart.php b/lib/vendor/box/spout/src/Spout/Writer/Style/BorderPart.php
new file mode 100644
index 0000000000..9ade797842
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Style/BorderPart.php
@@ -0,0 +1,184 @@
+setName($name);
+ $this->setColor($color);
+ $this->setWidth($width);
+ $this->setStyle($style);
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name The name of the border part @see BorderPart::$allowedNames
+ * @throws InvalidNameException
+ * @return void
+ */
+ public function setName($name)
+ {
+ if (!in_array($name, self::$allowedNames)) {
+ throw new InvalidNameException($name);
+ }
+ $this->name = $name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStyle()
+ {
+ return $this->style;
+ }
+
+ /**
+ * @param string $style The style of the border part @see BorderPart::$allowedStyles
+ * @throws InvalidStyleException
+ * @return void
+ */
+ public function setStyle($style)
+ {
+ if (!in_array($style, self::$allowedStyles)) {
+ throw new InvalidStyleException($style);
+ }
+ $this->style = $style;
+ }
+
+ /**
+ * @return string
+ */
+ public function getColor()
+ {
+ return $this->color;
+ }
+
+ /**
+ * @param string $color The color of the border part @see Color::rgb()
+ * @return void
+ */
+ public function setColor($color)
+ {
+ $this->color = $color;
+ }
+
+ /**
+ * @return string
+ */
+ public function getWidth()
+ {
+ return $this->width;
+ }
+
+ /**
+ * @param string $width The width of the border part @see BorderPart::$allowedWidths
+ * @throws InvalidWidthException
+ * @return void
+ */
+ public function setWidth($width)
+ {
+ if (!in_array($width, self::$allowedWidths)) {
+ throw new InvalidWidthException($width);
+ }
+ $this->width = $width;
+ }
+
+ /**
+ * @return array
+ */
+ public static function getAllowedStyles()
+ {
+ return self::$allowedStyles;
+ }
+
+ /**
+ * @return array
+ */
+ public static function getAllowedNames()
+ {
+ return self::$allowedNames;
+ }
+
+ /**
+ * @return array
+ */
+ public static function getAllowedWidths()
+ {
+ return self::$allowedWidths;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Style/Color.php b/lib/vendor/box/spout/src/Spout/Writer/Style/Color.php
new file mode 100644
index 0000000000..3c86eb706b
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Style/Color.php
@@ -0,0 +1,87 @@
+ 255) {
+ throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent");
+ }
+ }
+
+ /**
+ * Converts the color component to its corresponding hexadecimal value
+ *
+ * @param int $colorComponent Color component, 0 - 255
+ * @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d"
+ */
+ protected static function convertColorComponentToHex($colorComponent)
+ {
+ return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT);
+ }
+
+ /**
+ * Returns the ARGB color of the given RGB color,
+ * assuming that alpha value is always 1.
+ *
+ * @param string $rgbColor RGB color like "FF08B2"
+ * @return string ARGB color
+ */
+ public static function toARGB($rgbColor)
+ {
+ return 'FF' . $rgbColor;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Style/Style.php b/lib/vendor/box/spout/src/Spout/Writer/Style/Style.php
new file mode 100644
index 0000000000..b408ad3ca1
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Style/Style.php
@@ -0,0 +1,426 @@
+id;
+ }
+
+ /**
+ * @param int $id
+ * @return Style
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+ return $this;
+ }
+
+ /**
+ * @return Border
+ */
+ public function getBorder()
+ {
+ return $this->border;
+ }
+
+ /**
+ * @param Border $border
+ * @return Style
+ */
+ public function setBorder(Border $border)
+ {
+ $this->shouldApplyBorder = true;
+ $this->border = $border;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function shouldApplyBorder()
+ {
+ return $this->shouldApplyBorder;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isFontBold()
+ {
+ return $this->fontBold;
+ }
+
+ /**
+ * @return Style
+ */
+ public function setFontBold()
+ {
+ $this->fontBold = true;
+ $this->hasSetFontBold = true;
+ $this->shouldApplyFont = true;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isFontItalic()
+ {
+ return $this->fontItalic;
+ }
+
+ /**
+ * @return Style
+ */
+ public function setFontItalic()
+ {
+ $this->fontItalic = true;
+ $this->hasSetFontItalic = true;
+ $this->shouldApplyFont = true;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isFontUnderline()
+ {
+ return $this->fontUnderline;
+ }
+
+ /**
+ * @return Style
+ */
+ public function setFontUnderline()
+ {
+ $this->fontUnderline = true;
+ $this->hasSetFontUnderline = true;
+ $this->shouldApplyFont = true;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isFontStrikethrough()
+ {
+ return $this->fontStrikethrough;
+ }
+
+ /**
+ * @return Style
+ */
+ public function setFontStrikethrough()
+ {
+ $this->fontStrikethrough = true;
+ $this->hasSetFontStrikethrough = true;
+ $this->shouldApplyFont = true;
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getFontSize()
+ {
+ return $this->fontSize;
+ }
+
+ /**
+ * @param int $fontSize Font size, in pixels
+ * @return Style
+ */
+ public function setFontSize($fontSize)
+ {
+ $this->fontSize = $fontSize;
+ $this->hasSetFontSize = true;
+ $this->shouldApplyFont = true;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFontColor()
+ {
+ return $this->fontColor;
+ }
+
+ /**
+ * Sets the font color.
+ *
+ * @param string $fontColor ARGB color (@see Color)
+ * @return Style
+ */
+ public function setFontColor($fontColor)
+ {
+ $this->fontColor = $fontColor;
+ $this->hasSetFontColor = true;
+ $this->shouldApplyFont = true;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFontName()
+ {
+ return $this->fontName;
+ }
+
+ /**
+ * @param string $fontName Name of the font to use
+ * @return Style
+ */
+ public function setFontName($fontName)
+ {
+ $this->fontName = $fontName;
+ $this->hasSetFontName = true;
+ $this->shouldApplyFont = true;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function shouldWrapText()
+ {
+ return $this->shouldWrapText;
+ }
+
+ /**
+ * @param bool|void $shouldWrap Should the text be wrapped
+ * @return Style
+ */
+ public function setShouldWrapText($shouldWrap = true)
+ {
+ $this->shouldWrapText = $shouldWrap;
+ $this->hasSetWrapText = true;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasSetWrapText()
+ {
+ return $this->hasSetWrapText;
+ }
+
+ /**
+ * @return bool Whether specific font properties should be applied
+ */
+ public function shouldApplyFont()
+ {
+ return $this->shouldApplyFont;
+ }
+
+ /**
+ * Sets the background color
+ * @param string $color ARGB color (@see Color)
+ * @return Style
+ */
+ public function setBackgroundColor($color)
+ {
+ $this->hasSetBackgroundColor = true;
+ $this->backgroundColor = $color;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getBackgroundColor()
+ {
+ return $this->backgroundColor;
+ }
+
+ /**
+ *
+ * @return bool Whether the background color should be applied
+ */
+ public function shouldApplyBackgroundColor()
+ {
+ return $this->hasSetBackgroundColor;
+ }
+
+ /**
+ * Serializes the style for future comparison with other styles.
+ * The ID is excluded from the comparison, as we only care about
+ * actual style properties.
+ *
+ * @return string The serialized style
+ */
+ public function serialize()
+ {
+ // In order to be able to properly compare style, set static ID value
+ $currentId = $this->id;
+ $this->setId(0);
+
+ $serializedStyle = serialize($this);
+
+ $this->setId($currentId);
+
+ return $serializedStyle;
+ }
+
+ /**
+ * Merges the current style with the given style, using the given style as a base. This means that:
+ * - if current style and base style both have property A set, use current style property's value
+ * - if current style has property A set but base style does not, use current style property's value
+ * - if base style has property A set but current style does not, use base style property's value
+ *
+ * @NOTE: This function returns a new style.
+ *
+ * @param Style $baseStyle
+ * @return Style New style corresponding to the merge of the 2 styles
+ */
+ public function mergeWith($baseStyle)
+ {
+ $mergedStyle = clone $this;
+
+ $this->mergeFontStyles($mergedStyle, $baseStyle);
+ $this->mergeOtherFontProperties($mergedStyle, $baseStyle);
+ $this->mergeCellProperties($mergedStyle, $baseStyle);
+
+ return $mergedStyle;
+ }
+
+ /**
+ * @param Style $styleToUpdate (passed as reference)
+ * @param Style $baseStyle
+ * @return void
+ */
+ private function mergeFontStyles($styleToUpdate, $baseStyle)
+ {
+ if (!$this->hasSetFontBold && $baseStyle->isFontBold()) {
+ $styleToUpdate->setFontBold();
+ }
+ if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) {
+ $styleToUpdate->setFontItalic();
+ }
+ if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) {
+ $styleToUpdate->setFontUnderline();
+ }
+ if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) {
+ $styleToUpdate->setFontStrikethrough();
+ }
+ }
+
+ /**
+ * @param Style $styleToUpdate Style to update (passed as reference)
+ * @param Style $baseStyle
+ * @return void
+ */
+ private function mergeOtherFontProperties($styleToUpdate, $baseStyle)
+ {
+ if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) {
+ $styleToUpdate->setFontSize($baseStyle->getFontSize());
+ }
+ if (!$this->hasSetFontColor && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) {
+ $styleToUpdate->setFontColor($baseStyle->getFontColor());
+ }
+ if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) {
+ $styleToUpdate->setFontName($baseStyle->getFontName());
+ }
+ }
+
+ /**
+ * @param Style $styleToUpdate Style to update (passed as reference)
+ * @param Style $baseStyle
+ * @return void
+ */
+ private function mergeCellProperties($styleToUpdate, $baseStyle)
+ {
+ if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) {
+ $styleToUpdate->setShouldWrapText();
+ }
+ if (!$this->getBorder() && $baseStyle->shouldApplyBorder()) {
+ $styleToUpdate->setBorder($baseStyle->getBorder());
+ }
+ if (!$this->hasSetBackgroundColor && $baseStyle->shouldApplyBackgroundColor()) {
+ $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor());
+ }
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/Style/StyleBuilder.php b/lib/vendor/box/spout/src/Spout/Writer/Style/StyleBuilder.php
new file mode 100644
index 0000000000..2676cbe6fc
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/Style/StyleBuilder.php
@@ -0,0 +1,159 @@
+style = new Style();
+ }
+
+ /**
+ * Makes the font bold.
+ *
+ * @api
+ * @return StyleBuilder
+ */
+ public function setFontBold()
+ {
+ $this->style->setFontBold();
+ return $this;
+ }
+
+ /**
+ * Makes the font italic.
+ *
+ * @api
+ * @return StyleBuilder
+ */
+ public function setFontItalic()
+ {
+ $this->style->setFontItalic();
+ return $this;
+ }
+
+ /**
+ * Makes the font underlined.
+ *
+ * @api
+ * @return StyleBuilder
+ */
+ public function setFontUnderline()
+ {
+ $this->style->setFontUnderline();
+ return $this;
+ }
+
+ /**
+ * Makes the font struck through.
+ *
+ * @api
+ * @return StyleBuilder
+ */
+ public function setFontStrikethrough()
+ {
+ $this->style->setFontStrikethrough();
+ return $this;
+ }
+
+ /**
+ * Sets the font size.
+ *
+ * @api
+ * @param int $fontSize Font size, in pixels
+ * @return StyleBuilder
+ */
+ public function setFontSize($fontSize)
+ {
+ $this->style->setFontSize($fontSize);
+ return $this;
+ }
+
+ /**
+ * Sets the font color.
+ *
+ * @api
+ * @param string $fontColor ARGB color (@see Color)
+ * @return StyleBuilder
+ */
+ public function setFontColor($fontColor)
+ {
+ $this->style->setFontColor($fontColor);
+ return $this;
+ }
+
+ /**
+ * Sets the font name.
+ *
+ * @api
+ * @param string $fontName Name of the font to use
+ * @return StyleBuilder
+ */
+ public function setFontName($fontName)
+ {
+ $this->style->setFontName($fontName);
+ return $this;
+ }
+
+ /**
+ * Makes the text wrap in the cell if requested
+ *
+ * @api
+ * @param bool $shouldWrap Should the text be wrapped
+ * @return StyleBuilder
+ */
+ public function setShouldWrapText($shouldWrap = true)
+ {
+ $this->style->setShouldWrapText($shouldWrap);
+ return $this;
+ }
+
+ /**
+ * Set a border
+ *
+ * @param Border $border
+ * @return $this
+ */
+ public function setBorder(Border $border)
+ {
+ $this->style->setBorder($border);
+ return $this;
+ }
+
+ /**
+ * Sets a background color
+ *
+ * @api
+ * @param string $color ARGB color (@see Color)
+ * @return StyleBuilder
+ */
+ public function setBackgroundColor($color)
+ {
+ $this->style->setBackgroundColor($color);
+ return $this;
+ }
+
+ /**
+ * Returns the configured style. The style is cached and can be reused.
+ *
+ * @api
+ * @return Style
+ */
+ public function build()
+ {
+ return $this->style;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/WriterFactory.php b/lib/vendor/box/spout/src/Spout/Writer/WriterFactory.php
new file mode 100644
index 0000000000..7588940c7c
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/WriterFactory.php
@@ -0,0 +1,48 @@
+setGlobalFunctionsHelper(new GlobalFunctionsHelper());
+
+ return $writer;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/WriterInterface.php b/lib/vendor/box/spout/src/Spout/Writer/WriterInterface.php
new file mode 100644
index 0000000000..93a6ee24b6
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/WriterInterface.php
@@ -0,0 +1,91 @@
+ [
+ Border::WIDTH_THIN => 'thin',
+ Border::WIDTH_MEDIUM => 'medium',
+ Border::WIDTH_THICK => 'thick'
+ ],
+ Border::STYLE_DOTTED => [
+ Border::WIDTH_THIN => 'dotted',
+ Border::WIDTH_MEDIUM => 'dotted',
+ Border::WIDTH_THICK => 'dotted',
+ ],
+ Border::STYLE_DASHED => [
+ Border::WIDTH_THIN => 'dashed',
+ Border::WIDTH_MEDIUM => 'mediumDashed',
+ Border::WIDTH_THICK => 'mediumDashed',
+ ],
+ Border::STYLE_DOUBLE => [
+ Border::WIDTH_THIN => 'double',
+ Border::WIDTH_MEDIUM => 'double',
+ Border::WIDTH_THICK => 'double',
+ ],
+ Border::STYLE_NONE => [
+ Border::WIDTH_THIN => 'none',
+ Border::WIDTH_MEDIUM => 'none',
+ Border::WIDTH_THICK => 'none',
+ ],
+ ];
+
+ /**
+ * @param BorderPart $borderPart
+ * @return string
+ */
+ public static function serializeBorderPart(BorderPart $borderPart)
+ {
+ $borderStyle = self::getBorderStyle($borderPart);
+
+ $colorEl = $borderPart->getColor() ? sprintf('', $borderPart->getColor()) : '';
+ $partEl = sprintf(
+ '<%s style="%s">%s%s>',
+ $borderPart->getName(),
+ $borderStyle,
+ $colorEl,
+ $borderPart->getName()
+ );
+
+ return $partEl . PHP_EOL;
+ }
+
+ /**
+ * Get the style definition from the style map
+ *
+ * @param BorderPart $borderPart
+ * @return string
+ */
+ protected static function getBorderStyle(BorderPart $borderPart)
+ {
+ return self::$xlsxStyleMap[$borderPart->getStyle()][$borderPart->getWidth()];
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php
new file mode 100644
index 0000000000..86515f30c5
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php
@@ -0,0 +1,371 @@
+rootFolder;
+ }
+
+ /**
+ * @return string
+ */
+ public function getXlFolder()
+ {
+ return $this->xlFolder;
+ }
+
+ /**
+ * @return string
+ */
+ public function getXlWorksheetsFolder()
+ {
+ return $this->xlWorksheetsFolder;
+ }
+
+ /**
+ * Creates all the folders needed to create a XLSX file, as well as the files that won't change.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
+ */
+ public function createBaseFilesAndFolders()
+ {
+ $this
+ ->createRootFolder()
+ ->createRelsFolderAndFile()
+ ->createDocPropsFolderAndFiles()
+ ->createXlFolderAndSubFolders();
+ }
+
+ /**
+ * Creates the folder that will be used as root
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
+ */
+ protected function createRootFolder()
+ {
+ $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('xlsx', true));
+ return $this;
+ }
+
+ /**
+ * Creates the "_rels" folder under the root folder as well as the ".rels" file in it
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the ".rels" file
+ */
+ protected function createRelsFolderAndFile()
+ {
+ $this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME);
+
+ $this->createRelsFile();
+
+ return $this;
+ }
+
+ /**
+ * Creates the ".rels" file under the "_rels" folder (under root)
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
+ */
+ protected function createRelsFile()
+ {
+ $relsFileContents = <<
+
+
+
+
+
+EOD;
+
+ $this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or one of the files
+ */
+ protected function createDocPropsFolderAndFiles()
+ {
+ $this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME);
+
+ $this->createAppXmlFile();
+ $this->createCoreXmlFile();
+
+ return $this;
+ }
+
+ /**
+ * Creates the "app.xml" file under the "docProps" folder
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
+ */
+ protected function createAppXmlFile()
+ {
+ $appName = self::APP_NAME;
+ $appXmlFileContents = <<
+
+ $appName
+ 0
+
+EOD;
+
+ $this->createFileWithContents($this->docPropsFolder, self::APP_XML_FILE_NAME, $appXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the "core.xml" file under the "docProps" folder
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the file
+ */
+ protected function createCoreXmlFile()
+ {
+ $createdDate = (new \DateTime())->format(\DateTime::W3C);
+ $coreXmlFileContents = <<
+
+ $createdDate
+ $createdDate
+ 0
+
+EOD;
+
+ $this->createFileWithContents($this->docPropsFolder, self::CORE_XML_FILE_NAME, $coreXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the "xl" folder under the root folder as well as its subfolders
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the folders
+ */
+ protected function createXlFolderAndSubFolders()
+ {
+ $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME);
+ $this->createXlRelsFolder();
+ $this->createXlWorksheetsFolder();
+
+ return $this;
+ }
+
+ /**
+ * Creates the "_rels" folder under the "xl" folder
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
+ */
+ protected function createXlRelsFolder()
+ {
+ $this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME);
+ return $this;
+ }
+
+ /**
+ * Creates the "worksheets" folder under the "xl" folder
+ *
+ * @return FileSystemHelper
+ * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
+ */
+ protected function createXlWorksheetsFolder()
+ {
+ $this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME);
+ return $this;
+ }
+
+ /**
+ * Creates the "[Content_Types].xml" file under the root folder
+ *
+ * @param Worksheet[] $worksheets
+ * @return FileSystemHelper
+ */
+ public function createContentTypesFile($worksheets)
+ {
+ $contentTypesXmlFileContents = <<
+
+
+
+
+EOD;
+
+ /** @var Worksheet $worksheet */
+ foreach ($worksheets as $worksheet) {
+ $contentTypesXmlFileContents .= '';
+ }
+
+ $contentTypesXmlFileContents .= <<
+
+
+
+
+EOD;
+
+ $this->createFileWithContents($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME, $contentTypesXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the "workbook.xml" file under the "xl" folder
+ *
+ * @param Worksheet[] $worksheets
+ * @return FileSystemHelper
+ */
+ public function createWorkbookFile($worksheets)
+ {
+ $workbookXmlFileContents = <<
+
+
+EOD;
+
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
+
+ /** @var Worksheet $worksheet */
+ foreach ($worksheets as $worksheet) {
+ $worksheetName = $worksheet->getExternalSheet()->getName();
+ $worksheetId = $worksheet->getId();
+ $workbookXmlFileContents .= '';
+ }
+
+ $workbookXmlFileContents .= <<
+
+EOD;
+
+ $this->createFileWithContents($this->xlFolder, self::WORKBOOK_XML_FILE_NAME, $workbookXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the "workbook.xml.res" file under the "xl/_res" folder
+ *
+ * @param Worksheet[] $worksheets
+ * @return FileSystemHelper
+ */
+ public function createWorkbookRelsFile($worksheets)
+ {
+ $workbookRelsXmlFileContents = <<
+
+
+
+EOD;
+
+ /** @var Worksheet $worksheet */
+ foreach ($worksheets as $worksheet) {
+ $worksheetId = $worksheet->getId();
+ $workbookRelsXmlFileContents .= '';
+ }
+
+ $workbookRelsXmlFileContents .= '';
+
+ $this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Creates the "styles.xml" file under the "xl" folder
+ *
+ * @param StyleHelper $styleHelper
+ * @return FileSystemHelper
+ */
+ public function createStylesFile($styleHelper)
+ {
+ $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent();
+ $this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
+
+ return $this;
+ }
+
+ /**
+ * Zips the root folder and streams the contents of the zip into the given stream
+ *
+ * @param resource $streamPointer Pointer to the stream to copy the zip
+ * @return void
+ */
+ public function zipRootFolderAndCopyToStream($streamPointer)
+ {
+ $zipHelper = new ZipHelper($this->rootFolder);
+
+ // In order to have the file's mime type detected properly, files need to be added
+ // to the zip file in a particular order.
+ // "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first.
+ $zipHelper->addFileToArchive($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME);
+ $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::WORKBOOK_XML_FILE_NAME);
+ $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::STYLES_XML_FILE_NAME);
+
+ $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
+ $zipHelper->closeArchiveAndCopyToStream($streamPointer);
+
+ // once the zip is copied, remove it
+ $this->deleteFile($zipHelper->getZipFilePath());
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php
new file mode 100644
index 0000000000..292b663ce8
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php
@@ -0,0 +1,107 @@
+
+sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w');
+
+ $this->throwIfSharedStringsFilePointerIsNotAvailable();
+
+ // the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later
+ $header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>';
+ fwrite($this->sharedStringsFilePointer, $header);
+
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
+ }
+
+ /**
+ * Checks if the book has been created. Throws an exception if not created yet.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
+ */
+ protected function throwIfSharedStringsFilePointerIsNotAvailable()
+ {
+ if (!$this->sharedStringsFilePointer) {
+ throw new IOException('Unable to open shared strings file for writing.');
+ }
+ }
+
+ /**
+ * Writes the given string into the sharedStrings.xml file.
+ * Starting and ending whitespaces are preserved.
+ *
+ * @param string $string
+ * @return int ID of the written shared string
+ */
+ public function writeString($string)
+ {
+ fwrite($this->sharedStringsFilePointer, '' . $this->stringsEscaper->escape($string) . '');
+ $this->numSharedStrings++;
+
+ // Shared string ID is zero-based
+ return ($this->numSharedStrings - 1);
+ }
+
+ /**
+ * Finishes writing the data in the sharedStrings.xml file and closes the file.
+ *
+ * @return void
+ */
+ public function close()
+ {
+ if (!is_resource($this->sharedStringsFilePointer)) {
+ return;
+ }
+
+ fwrite($this->sharedStringsFilePointer, '');
+
+ // Replace the default strings count with the actual number of shared strings in the file header
+ $firstPartHeaderLength = strlen(self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER);
+ $defaultStringsCountPartLength = strlen(self::DEFAULT_STRINGS_COUNT_PART);
+
+ // Adding 1 to take into account the space between the last xml attribute and "count"
+ fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1);
+ fwrite($this->sharedStringsFilePointer, sprintf("%-{$defaultStringsCountPartLength}s", 'count="' . $this->numSharedStrings . '" uniqueCount="' . $this->numSharedStrings . '"'));
+
+ fclose($this->sharedStringsFilePointer);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php
new file mode 100644
index 0000000000..4a13c9562f
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Helper/StyleHelper.php
@@ -0,0 +1,344 @@
+ [FILL_ID] maps a style to a fill declaration
+ */
+ protected $styleIdToFillMappingTable = [];
+
+ /**
+ * Excel preserves two default fills with index 0 and 1
+ * Since Excel is the dominant vendor - we play along here
+ *
+ * @var int The fill index counter for custom fills.
+ */
+ protected $fillIndex = 2;
+
+ /**
+ * @var array
+ */
+ protected $registeredBorders = [];
+
+ /**
+ * @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration
+ */
+ protected $styleIdToBorderMappingTable = [];
+
+ /**
+ * XLSX specific operations on the registered styles
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ * @return \Box\Spout\Writer\Style\Style
+ */
+ public function registerStyle($style)
+ {
+ $registeredStyle = parent::registerStyle($style);
+ $this->registerFill($registeredStyle);
+ $this->registerBorder($registeredStyle);
+ return $registeredStyle;
+ }
+
+ /**
+ * Register a fill definition
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ */
+ protected function registerFill($style)
+ {
+ $styleId = $style->getId();
+
+ // Currently - only solid backgrounds are supported
+ // so $backgroundColor is a scalar value (RGB Color)
+ $backgroundColor = $style->getBackgroundColor();
+
+ if ($backgroundColor) {
+ $isBackgroundColorRegistered = isset($this->registeredFills[$backgroundColor]);
+
+ // We need to track the already registered background definitions
+ if ($isBackgroundColorRegistered) {
+ $registeredStyleId = $this->registeredFills[$backgroundColor];
+ $registeredFillId = $this->styleIdToFillMappingTable[$registeredStyleId];
+ $this->styleIdToFillMappingTable[$styleId] = $registeredFillId;
+ } else {
+ $this->registeredFills[$backgroundColor] = $styleId;
+ $this->styleIdToFillMappingTable[$styleId] = $this->fillIndex++;
+ }
+
+ } else {
+ // The fillId maps a style to a fill declaration
+ // When there is no background color definition - we default to 0
+ $this->styleIdToFillMappingTable[$styleId] = 0;
+ }
+ }
+
+ /**
+ * Register a border definition
+ *
+ * @param \Box\Spout\Writer\Style\Style $style
+ */
+ protected function registerBorder($style)
+ {
+ $styleId = $style->getId();
+
+ if ($style->shouldApplyBorder()) {
+ $border = $style->getBorder();
+ $serializedBorder = serialize($border);
+
+ $isBorderAlreadyRegistered = isset($this->registeredBorders[$serializedBorder]);
+
+ if ($isBorderAlreadyRegistered) {
+ $registeredStyleId = $this->registeredBorders[$serializedBorder];
+ $registeredBorderId = $this->styleIdToBorderMappingTable[$registeredStyleId];
+ $this->styleIdToBorderMappingTable[$styleId] = $registeredBorderId;
+ } else {
+ $this->registeredBorders[$serializedBorder] = $styleId;
+ $this->styleIdToBorderMappingTable[$styleId] = count($this->registeredBorders);
+ }
+
+ } else {
+ // If no border should be applied - the mapping is the default border: 0
+ $this->styleIdToBorderMappingTable[$styleId] = 0;
+ }
+ }
+
+
+ /**
+ * For empty cells, we can specify a style or not. If no style are specified,
+ * then the software default will be applied. But sometimes, it may be useful
+ * to override this default style, for instance if the cell should have a
+ * background color different than the default one or some borders
+ * (fonts property don't really matter here).
+ *
+ * @param int $styleId
+ * @return bool Whether the cell should define a custom style
+ */
+ public function shouldApplyStyleOnEmptyCell($styleId)
+ {
+ $hasStyleCustomFill = (isset($this->styleIdToFillMappingTable[$styleId]) && $this->styleIdToFillMappingTable[$styleId] !== 0);
+ $hasStyleCustomBorders = (isset($this->styleIdToBorderMappingTable[$styleId]) && $this->styleIdToBorderMappingTable[$styleId] !== 0);
+
+ return ($hasStyleCustomFill || $hasStyleCustomBorders);
+ }
+
+
+ /**
+ * Returns the content of the "styles.xml" file, given a list of styles.
+ *
+ * @return string
+ */
+ public function getStylesXMLFileContent()
+ {
+ $content = <<
+
+EOD;
+
+ $content .= $this->getFontsSectionContent();
+ $content .= $this->getFillsSectionContent();
+ $content .= $this->getBordersSectionContent();
+ $content .= $this->getCellStyleXfsSectionContent();
+ $content .= $this->getCellXfsSectionContent();
+ $content .= $this->getCellStylesSectionContent();
+
+ $content .= <<
+EOD;
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section.
+ *
+ * @return string
+ */
+ protected function getFontsSectionContent()
+ {
+ $content = '';
+
+ /** @var \Box\Spout\Writer\Style\Style $style */
+ foreach ($this->getRegisteredStyles() as $style) {
+ $content .= '';
+
+ $content .= '';
+ $content .= '';
+ $content .= '';
+
+ if ($style->isFontBold()) {
+ $content .= '';
+ }
+ if ($style->isFontItalic()) {
+ $content .= '';
+ }
+ if ($style->isFontUnderline()) {
+ $content .= '';
+ }
+ if ($style->isFontStrikethrough()) {
+ $content .= '';
+ }
+
+ $content .= '';
+ }
+
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section.
+ *
+ * @return string
+ */
+ protected function getFillsSectionContent()
+ {
+ // Excel reserves two default fills
+ $fillsCount = count($this->registeredFills) + 2;
+ $content = sprintf('', $fillsCount);
+
+ $content .= '';
+ $content .= '';
+
+ // The other fills are actually registered by setting a background color
+ foreach ($this->registeredFills as $styleId) {
+ /** @var Style $style */
+ $style = $this->styleIdToStyleMappingTable[$styleId];
+
+ $backgroundColor = $style->getBackgroundColor();
+ $content .= sprintf(
+ '',
+ $backgroundColor
+ );
+ }
+
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section.
+ *
+ * @return string
+ */
+ protected function getBordersSectionContent()
+ {
+
+ // There is one default border with index 0
+ $borderCount = count($this->registeredBorders) + 1;
+
+ $content = '';
+
+ // Default border starting at index 0
+ $content .= '';
+
+ foreach ($this->registeredBorders as $styleId) {
+ /** @var \Box\Spout\Writer\Style\Style $style */
+ $style = $this->styleIdToStyleMappingTable[$styleId];
+ $border = $style->getBorder();
+ $content .= '';
+
+ // @link https://github.com/box/spout/issues/271
+ $sortOrder = ['left', 'right', 'top', 'bottom'];
+
+ foreach ($sortOrder as $partName) {
+ if ($border->hasPart($partName)) {
+ /** @var $part \Box\Spout\Writer\Style\BorderPart */
+ $part = $border->getPart($partName);
+ $content .= BorderHelper::serializeBorderPart($part);
+ }
+
+ }
+
+ $content .= '';
+ }
+
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section.
+ *
+ * @return string
+ */
+ protected function getCellStyleXfsSectionContent()
+ {
+ return <<
+
+
+EOD;
+ }
+
+ /**
+ * Returns the content of the "" section.
+ *
+ * @return string
+ */
+ protected function getCellXfsSectionContent()
+ {
+ $registeredStyles = $this->getRegisteredStyles();
+
+ $content = '';
+
+ foreach ($registeredStyles as $style) {
+ $styleId = $style->getId();
+ $fillId = $this->styleIdToFillMappingTable[$styleId];
+ $borderId = $this->styleIdToBorderMappingTable[$styleId];
+
+ $content .= 'shouldApplyFont()) {
+ $content .= ' applyFont="1"';
+ }
+
+ $content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0);
+
+ if ($style->shouldWrapText()) {
+ $content .= ' applyAlignment="1">';
+ $content .= '';
+ $content .= '';
+ } else {
+ $content .= '/>';
+ }
+ }
+
+ $content .= '';
+
+ return $content;
+ }
+
+ /**
+ * Returns the content of the "" section.
+ *
+ * @return string
+ */
+ protected function getCellStylesSectionContent()
+ {
+ return <<
+
+
+EOD;
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/XLSX/Internal/Workbook.php b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Internal/Workbook.php
new file mode 100644
index 0000000000..bcdce7f725
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Internal/Workbook.php
@@ -0,0 +1,135 @@
+shouldUseInlineStrings = $shouldUseInlineStrings;
+
+ $this->fileSystemHelper = new FileSystemHelper($tempFolder);
+ $this->fileSystemHelper->createBaseFilesAndFolders();
+
+ $this->styleHelper = new StyleHelper($defaultRowStyle);
+
+ // This helper will be shared by all sheets
+ $xlFolder = $this->fileSystemHelper->getXlFolder();
+ $this->sharedStringsHelper = new SharedStringsHelper($xlFolder);
+ }
+
+ /**
+ * @return \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles to XLSX files
+ */
+ protected function getStyleHelper()
+ {
+ return $this->styleHelper;
+ }
+
+ /**
+ * @return int Maximum number of rows/columns a sheet can contain
+ */
+ protected function getMaxRowsPerWorksheet()
+ {
+ return self::$maxRowsPerWorksheet;
+ }
+
+ /**
+ * Creates a new sheet in the workbook. The current sheet remains unchanged.
+ *
+ * @return Worksheet The created sheet
+ * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing
+ */
+ public function addNewSheet()
+ {
+ $newSheetIndex = count($this->worksheets);
+ $sheet = new Sheet($newSheetIndex, $this->internalId);
+
+ $worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder();
+ $worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->styleHelper, $this->shouldUseInlineStrings);
+ $this->worksheets[] = $worksheet;
+
+ return $worksheet;
+ }
+
+ /**
+ * Closes the workbook and all its associated sheets.
+ * All the necessary files are written to disk and zipped together to create the XLSX file.
+ * All the temporary files are then deleted.
+ *
+ * @param resource $finalFilePointer Pointer to the XLSX that will be created
+ * @return void
+ */
+ public function close($finalFilePointer)
+ {
+ /** @var Worksheet[] $worksheets */
+ $worksheets = $this->worksheets;
+
+ foreach ($worksheets as $worksheet) {
+ $worksheet->close();
+ }
+
+ $this->sharedStringsHelper->close();
+
+ // Finish creating all the necessary files before zipping everything together
+ $this->fileSystemHelper
+ ->createContentTypesFile($worksheets)
+ ->createWorkbookFile($worksheets)
+ ->createWorkbookRelsFile($worksheets)
+ ->createStylesFile($this->styleHelper)
+ ->zipRootFolderAndCopyToStream($finalFilePointer);
+
+ $this->cleanupTempFolder();
+ }
+
+ /**
+ * Deletes the root folder created in the temp folder and all its contents.
+ *
+ * @return void
+ */
+ protected function cleanupTempFolder()
+ {
+ $xlsxRootFolder = $this->fileSystemHelper->getRootFolder();
+ $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php
new file mode 100644
index 0000000000..0bd909d184
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Internal/Worksheet.php
@@ -0,0 +1,275 @@
+
+
+EOD;
+
+ /** @var \Box\Spout\Writer\Common\Sheet The "external" sheet */
+ protected $externalSheet;
+
+ /** @var string Path to the XML file that will contain the sheet data */
+ protected $worksheetFilePath;
+
+ /** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */
+ protected $sharedStringsHelper;
+
+ /** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles */
+ protected $styleHelper;
+
+ /** @var bool Whether inline or shared strings should be used */
+ protected $shouldUseInlineStrings;
+
+ /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */
+ protected $stringsEscaper;
+
+ /** @var \Box\Spout\Common\Helper\StringHelper String helper */
+ protected $stringHelper;
+
+ /** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */
+ protected $sheetFilePointer;
+
+ /** @var int Index of the last written row */
+ protected $lastWrittenRowIndex = 0;
+
+ /**
+ * @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet
+ * @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored
+ * @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings
+ * @param \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles
+ * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
+ * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
+ */
+ public function __construct($externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $styleHelper, $shouldUseInlineStrings)
+ {
+ $this->externalSheet = $externalSheet;
+ $this->sharedStringsHelper = $sharedStringsHelper;
+ $this->styleHelper = $styleHelper;
+ $this->shouldUseInlineStrings = $shouldUseInlineStrings;
+
+ /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */
+ $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance();
+ $this->stringHelper = new StringHelper();
+
+ $this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml';
+ $this->startSheet();
+ }
+
+ /**
+ * Prepares the worksheet to accept data
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
+ */
+ protected function startSheet()
+ {
+ $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w');
+ $this->throwIfSheetFilePointerIsNotAvailable();
+
+ fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER);
+ fwrite($this->sheetFilePointer, '');
+ }
+
+ /**
+ * Checks if the book has been created. Throws an exception if not created yet.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing
+ */
+ protected function throwIfSheetFilePointerIsNotAvailable()
+ {
+ if (!$this->sheetFilePointer) {
+ throw new IOException('Unable to open sheet for writing.');
+ }
+ }
+
+ /**
+ * @return \Box\Spout\Writer\Common\Sheet The "external" sheet
+ */
+ public function getExternalSheet()
+ {
+ return $this->externalSheet;
+ }
+
+ /**
+ * @return int The index of the last written row
+ */
+ public function getLastWrittenRowIndex()
+ {
+ return $this->lastWrittenRowIndex;
+ }
+
+ /**
+ * @return int The ID of the worksheet
+ */
+ public function getId()
+ {
+ // sheet index is zero-based, while ID is 1-based
+ return $this->externalSheet->getIndex() + 1;
+ }
+
+ /**
+ * Adds data to the worksheet.
+ *
+ * @param array $dataRow Array containing data to be written. Cannot be empty.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
+ */
+ public function addRow($dataRow, $style)
+ {
+ if (!$this->isEmptyRow($dataRow)) {
+ $this->addNonEmptyRow($dataRow, $style);
+ }
+
+ $this->lastWrittenRowIndex++;
+ }
+
+ /**
+ * Returns whether the given row is empty
+ *
+ * @param array $dataRow Array containing data to be written. Cannot be empty.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @return bool Whether the given row is empty
+ */
+ protected function isEmptyRow($dataRow)
+ {
+ $numCells = count($dataRow);
+ // using "reset()" instead of "$dataRow[0]" because $dataRow can be an associative array
+ return ($numCells === 1 && CellHelper::isEmpty(reset($dataRow)));
+ }
+
+ /**
+ * Adds non empty row to the worksheet.
+ *
+ * @param array $dataRow Array containing data to be written. Cannot be empty.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style.
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written
+ * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported
+ */
+ protected function addNonEmptyRow($dataRow, $style)
+ {
+ $cellNumber = 0;
+ $rowIndex = $this->lastWrittenRowIndex + 1;
+ $numCells = count($dataRow);
+
+ $rowXML = '';
+
+ foreach($dataRow as $cellValue) {
+ $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style->getId());
+ $cellNumber++;
+ }
+
+ $rowXML .= '';
+
+ $wasWriteSuccessful = fwrite($this->sheetFilePointer, $rowXML);
+ if ($wasWriteSuccessful === false) {
+ throw new IOException("Unable to write data in {$this->worksheetFilePath}");
+ }
+ }
+
+ /**
+ * Build and return xml for a single cell.
+ *
+ * @param int $rowIndex
+ * @param int $cellNumber
+ * @param mixed $cellValue
+ * @param int $styleId
+ * @return string
+ * @throws InvalidArgumentException If the given value cannot be processed
+ */
+ protected function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId)
+ {
+ $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber);
+ $cellXML = 'getCellXMLFragmentForNonEmptyString($cellValue);
+ } else if (CellHelper::isBoolean($cellValue)) {
+ $cellXML .= ' t="b">' . intval($cellValue) . '';
+ } else if (CellHelper::isNumeric($cellValue)) {
+ $cellXML .= '>' . $cellValue . '';
+ } else if (empty($cellValue)) {
+ if ($this->styleHelper->shouldApplyStyleOnEmptyCell($styleId)) {
+ $cellXML .= '/>';
+ } else {
+ // don't write empty cells that do no need styling
+ // NOTE: not appending to $cellXML is the right behavior!!
+ $cellXML = '';
+ }
+ } else {
+ throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue));
+ }
+
+ return $cellXML;
+ }
+
+ /**
+ * Returns the XML fragment for a cell containing a non empty string
+ *
+ * @param string $cellValue The cell value
+ * @return string The XML fragment representing the cell
+ * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell
+ */
+ protected function getCellXMLFragmentForNonEmptyString($cellValue)
+ {
+ if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) {
+ throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)');
+ }
+
+ if ($this->shouldUseInlineStrings) {
+ $cellXMLFragment = ' t="inlineStr">' . $this->stringsEscaper->escape($cellValue) . '';
+ } else {
+ $sharedStringId = $this->sharedStringsHelper->writeString($cellValue);
+ $cellXMLFragment = ' t="s">' . $sharedStringId . '';
+ }
+
+ return $cellXMLFragment;
+ }
+
+ /**
+ * Closes the worksheet
+ *
+ * @return void
+ */
+ public function close()
+ {
+ if (!is_resource($this->sheetFilePointer)) {
+ return;
+ }
+
+ fwrite($this->sheetFilePointer, '');
+ fwrite($this->sheetFilePointer, '');
+ fclose($this->sheetFilePointer);
+ }
+}
diff --git a/lib/vendor/box/spout/src/Spout/Writer/XLSX/Writer.php b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Writer.php
new file mode 100644
index 0000000000..965955a384
--- /dev/null
+++ b/lib/vendor/box/spout/src/Spout/Writer/XLSX/Writer.php
@@ -0,0 +1,132 @@
+throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
+
+ $this->tempFolder = $tempFolder;
+ return $this;
+ }
+
+ /**
+ * Use inline string to be more memory efficient. If set to false, it will use shared strings.
+ * This must be set before opening the writer.
+ *
+ * @api
+ * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used
+ * @return Writer
+ * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened
+ */
+ public function setShouldUseInlineStrings($shouldUseInlineStrings)
+ {
+ $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.');
+
+ $this->shouldUseInlineStrings = $shouldUseInlineStrings;
+ return $this;
+ }
+
+ /**
+ * Configures the write and sets the current sheet pointer to a new sheet.
+ *
+ * @return void
+ * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing
+ */
+ protected function openWriter()
+ {
+ if (!$this->book) {
+ $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir();
+ $this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle);
+ $this->book->addNewSheetAndMakeItCurrent();
+ }
+ }
+
+ /**
+ * @return Internal\Workbook The workbook representing the file to be written
+ */
+ protected function getWorkbook()
+ {
+ return $this->book;
+ }
+
+ /**
+ * Adds data to the currently opened writer.
+ * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination
+ * with the creation of new worksheets if one worksheet has reached its maximum capicity.
+ *
+ * @param array $dataRow Array containing data to be written.
+ * Example $dataRow = ['data1', 1234, null, '', 'data5'];
+ * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row.
+ * @return void
+ * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet
+ * @throws \Box\Spout\Common\Exception\IOException If unable to write data
+ */
+ protected function addRowToWriter(array $dataRow, $style)
+ {
+ $this->throwIfBookIsNotAvailable();
+ $this->book->addRowToCurrentWorksheet($dataRow, $style);
+ }
+
+ /**
+ * Returns the default style to be applied to rows.
+ *
+ * @return \Box\Spout\Writer\Style\Style
+ */
+ protected function getDefaultRowStyle()
+ {
+ return (new StyleBuilder())
+ ->setFontSize(self::DEFAULT_FONT_SIZE)
+ ->setFontName(self::DEFAULT_FONT_NAME)
+ ->build();
+ }
+
+ /**
+ * Closes the writer, preventing any additional writing.
+ *
+ * @return void
+ */
+ protected function closeWriter()
+ {
+ if ($this->book) {
+ $this->book->close($this->filePointer);
+ }
+ }
+}
diff --git a/lib/vendor/composer/ClassLoader.php b/lib/vendor/composer/ClassLoader.php
new file mode 100644
index 0000000000..afef3fa2ad
--- /dev/null
+++ b/lib/vendor/composer/ClassLoader.php
@@ -0,0 +1,572 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see https://www.php-fig.org/psr/psr-0/
+ * @see https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ /** @var ?string */
+ private $vendorDir;
+
+ // PSR-4
+ /**
+ * @var array[]
+ * @psalm-var array>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array[]
+ * @psalm-var array>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var array[]
+ * @psalm-var array
+ */
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ /**
+ * @var array[]
+ * @psalm-var array>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var array[]
+ * @psalm-var array
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var string[]
+ * @psalm-var array
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var bool[]
+ * @psalm-var array
+ */
+ private $missingClasses = array();
+
+ /** @var ?string */
+ private $apcuPrefix;
+
+ /**
+ * @var self[]
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param ?string $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return array>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return array
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return array
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return string[] Array of classname => path
+ * @psalm-return array
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param string[] $classMap Class to filename map
+ * @psalm-param array $classMap
+ *
+ * @return void
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param string[]|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @return void
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param string[]|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ (array) $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param string[]|string $paths The PSR-0 base directories
+ *
+ * @return void
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param string[]|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ *
+ * @return void
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ *
+ * @return void
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ *
+ * @return void
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ *
+ * @return void
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+ if (null === $this->vendorDir) {
+ return;
+ }
+
+ if ($prepend) {
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+ } else {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ self::$registeredLoaders[$this->vendorDir] = $this;
+ }
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ *
+ * @return void
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+
+ if (null !== $this->vendorDir) {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ }
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return true|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ includeFile($file);
+
+ return true;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the currently registered loaders indexed by their corresponding vendor directories.
+ *
+ * @return self[]
+ */
+ public static function getRegisteredLoaders()
+ {
+ return self::$registeredLoaders;
+ }
+
+ /**
+ * @param string $class
+ * @param string $ext
+ * @return string|false
+ */
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ * @private
+ */
+function includeFile($file)
+{
+ include $file;
+}
diff --git a/lib/vendor/composer/InstalledVersions.php b/lib/vendor/composer/InstalledVersions.php
new file mode 100644
index 0000000000..948bb4098b
--- /dev/null
+++ b/lib/vendor/composer/InstalledVersions.php
@@ -0,0 +1,360 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ */
+class InstalledVersions
+{
+ /**
+ * @var mixed[]|null
+ * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array}|array{}|null
+ */
+ private static $installed;
+
+ /**
+ * @var bool|null
+ */
+ private static $canGetVendors;
+
+ /**
+ * @var array[]
+ * @psalm-var array}>
+ */
+ private static $installedByVendor = array();
+
+ /**
+ * Returns a list of all package names which are present, either by being installed, replaced or provided
+ *
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackages()
+ {
+ $packages = array();
+ foreach (self::getInstalled() as $installed) {
+ $packages[] = array_keys($installed['versions']);
+ }
+
+ if (1 === \count($packages)) {
+ return $packages[0];
+ }
+
+ return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+ }
+
+ /**
+ * Returns a list of all package names with a specific type e.g. 'library'
+ *
+ * @param string $type
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackagesByType($type)
+ {
+ $packagesByType = array();
+
+ foreach (self::getInstalled() as $installed) {
+ foreach ($installed['versions'] as $name => $package) {
+ if (isset($package['type']) && $package['type'] === $type) {
+ $packagesByType[] = $name;
+ }
+ }
+ }
+
+ return $packagesByType;
+ }
+
+ /**
+ * Checks whether the given package is installed
+ *
+ * This also returns true if the package name is provided or replaced by another package
+ *
+ * @param string $packageName
+ * @param bool $includeDevRequirements
+ * @return bool
+ */
+ public static function isInstalled($packageName, $includeDevRequirements = true)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (isset($installed['versions'][$packageName])) {
+ return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given package satisfies a version constraint
+ *
+ * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+ *
+ * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+ *
+ * @param VersionParser $parser Install composer/semver to have access to this class and functionality
+ * @param string $packageName
+ * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+ * @return bool
+ */
+ public static function satisfies(VersionParser $parser, $packageName, $constraint)
+ {
+ $constraint = $parser->parseConstraints($constraint);
+ $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+ return $provided->matches($constraint);
+ }
+
+ /**
+ * Returns a version constraint representing all the range(s) which are installed for a given package
+ *
+ * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+ * whether a given version of a package is installed, and not just whether it exists
+ *
+ * @param string $packageName
+ * @return string Version constraint usable with composer/semver
+ */
+ public static function getVersionRanges($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ $ranges = array();
+ if (isset($installed['versions'][$packageName]['pretty_version'])) {
+ $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+ }
+ if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+ }
+ if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+ }
+ if (array_key_exists('provided', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+ }
+
+ return implode(' || ', $ranges);
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getPrettyVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['pretty_version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+ */
+ public static function getReference($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['reference'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['reference'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+ */
+ public static function getInstallPath($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @return array
+ * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
+ */
+ public static function getRootPackage()
+ {
+ $installed = self::getInstalled();
+
+ return $installed[0]['root'];
+ }
+
+ /**
+ * Returns the raw installed.php data for custom implementations
+ *
+ * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+ * @return array[]
+ * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array}
+ */
+ public static function getRawData()
+ {
+ @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ self::$installed = include __DIR__ . '/installed.php';
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ return self::$installed;
+ }
+
+ /**
+ * Returns the raw data of all installed.php which are currently loaded for custom implementations
+ *
+ * @return array[]
+ * @psalm-return list}>
+ */
+ public static function getAllRawData()
+ {
+ return self::getInstalled();
+ }
+
+ /**
+ * Lets you reload the static array from another file
+ *
+ * This is only useful for complex integrations in which a project needs to use
+ * this class but then also needs to execute another project's autoloader in process,
+ * and wants to ensure both projects have access to their version of installed.php.
+ *
+ * A typical case would be PHPUnit, where it would need to make sure it reads all
+ * the data it needs from this class, then call reload() with
+ * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+ * the project in which it runs can then also use this class safely, without
+ * interference between PHPUnit's dependencies and the project's dependencies.
+ *
+ * @param array[] $data A vendor/composer/installed.php data set
+ * @return void
+ *
+ * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} $data
+ */
+ public static function reload($data)
+ {
+ self::$installed = $data;
+ self::$installedByVendor = array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list}>
+ */
+ private static function getInstalled()
+ {
+ if (null === self::$canGetVendors) {
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+ }
+
+ $installed = array();
+ $copiedLocalDir = false;
+
+ if (self::$canGetVendors) {
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ if (isset(self::$installedByVendor[$vendorDir])) {
+ $installed[] = self::$installedByVendor[$vendorDir];
+ } elseif (is_file($vendorDir.'/composer/installed.php')) {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require $vendorDir.'/composer/installed.php';
+ self::$installedByVendor[$vendorDir] = $required;
+ $installed[] = $required;
+ if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+ self::$installed = $required;
+ $copiedLocalDir = true;
+ }
+ }
+ }
+ }
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require __DIR__ . '/installed.php';
+ self::$installed = $required;
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ if (self::$installed !== array() && !$copiedLocalDir) {
+ $installed[] = self::$installed;
+ }
+
+ return $installed;
+ }
+}
diff --git a/lib/vendor/composer/LICENSE b/lib/vendor/composer/LICENSE
new file mode 100644
index 0000000000..f27399a042
--- /dev/null
+++ b/lib/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/lib/vendor/composer/autoload_classmap.php b/lib/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000000..9bfd96643c
--- /dev/null
+++ b/lib/vendor/composer/autoload_classmap.php
@@ -0,0 +1,13 @@
+ $vendorDir . '/composer/InstalledVersions.php',
+ 'Mobile_Detect' => $vendorDir . '/mobiledetect/mobiledetectlib/Mobile_Detect.php',
+ 'PclZip' => $vendorDir . '/pclzip/pclzip/pclzip.lib.php',
+ 'Requests' => $vendorDir . '/rmccue/requests/library/Requests.php',
+);
diff --git a/lib/vendor/composer/autoload_files.php b/lib/vendor/composer/autoload_files.php
new file mode 100644
index 0000000000..e4c5145101
--- /dev/null
+++ b/lib/vendor/composer/autoload_files.php
@@ -0,0 +1,73 @@
+ $vendorDir . '/laminas/laminas-zendframework-bridge/src/autoload.php',
+ 'abede361264e2ae69ec1eee813a101af' => $vendorDir . '/markbaker/complex/classes/src/functions/abs.php',
+ '21a5860fbef5be28db5ddfbc3cca67c4' => $vendorDir . '/markbaker/complex/classes/src/functions/acos.php',
+ '1546e3f9d127f2a9bb2d1b6c31c26ef1' => $vendorDir . '/markbaker/complex/classes/src/functions/acosh.php',
+ 'd2516f7f4fba5ea5905f494b4a8262e0' => $vendorDir . '/markbaker/complex/classes/src/functions/acot.php',
+ '4511163d560956219b96882c0980b65e' => $vendorDir . '/markbaker/complex/classes/src/functions/acoth.php',
+ 'c361f5616dc2a8da4fa3e137077cd4ea' => $vendorDir . '/markbaker/complex/classes/src/functions/acsc.php',
+ '02d68920fc98da71991ce569c91df0f6' => $vendorDir . '/markbaker/complex/classes/src/functions/acsch.php',
+ '88e19525eae308b4a6aa3419364875d3' => $vendorDir . '/markbaker/complex/classes/src/functions/argument.php',
+ '60e8e2d0827b58bfc904f13957e51849' => $vendorDir . '/markbaker/complex/classes/src/functions/asec.php',
+ '13d2f040713999eab66c359b4d79871d' => $vendorDir . '/markbaker/complex/classes/src/functions/asech.php',
+ '838ab38beb32c68a79d3cd2c007d5a04' => $vendorDir . '/markbaker/complex/classes/src/functions/asin.php',
+ 'bb28eccd0f8f008333a1b3c163d604ac' => $vendorDir . '/markbaker/complex/classes/src/functions/asinh.php',
+ '9e483de83558c98f7d3feaa402c78cb3' => $vendorDir . '/markbaker/complex/classes/src/functions/atan.php',
+ '36b74b5b765ded91ee58c8ee3c0e85e3' => $vendorDir . '/markbaker/complex/classes/src/functions/atanh.php',
+ '05c15ee9510da7fd6bf6136f436500c0' => $vendorDir . '/markbaker/complex/classes/src/functions/conjugate.php',
+ 'd3208dfbce2505e370788f9f22f6785f' => $vendorDir . '/markbaker/complex/classes/src/functions/cos.php',
+ '141cf1fb3a3046f8b64534b0ebab33ca' => $vendorDir . '/markbaker/complex/classes/src/functions/cosh.php',
+ 'be660df75fd0dbe7fa7c03b7434b3294' => $vendorDir . '/markbaker/complex/classes/src/functions/cot.php',
+ '01e31ea298a51bc9e91517e3ce6b9e76' => $vendorDir . '/markbaker/complex/classes/src/functions/coth.php',
+ '803ddd97f7b1da68982a7b087c3476f6' => $vendorDir . '/markbaker/complex/classes/src/functions/csc.php',
+ '3001cdfd101ec3c32da34ee43c2e149b' => $vendorDir . '/markbaker/complex/classes/src/functions/csch.php',
+ '77b2d7629ef2a93fabb8c56754a91051' => $vendorDir . '/markbaker/complex/classes/src/functions/exp.php',
+ '4a4471296dec796c21d4f4b6552396a9' => $vendorDir . '/markbaker/complex/classes/src/functions/inverse.php',
+ 'c3e9897e1744b88deb56fcdc39d34d85' => $vendorDir . '/markbaker/complex/classes/src/functions/ln.php',
+ 'a83cacf2de942cff288de15a83afd26d' => $vendorDir . '/markbaker/complex/classes/src/functions/log2.php',
+ '6a861dacc9ee2f3061241d4c7772fa21' => $vendorDir . '/markbaker/complex/classes/src/functions/log10.php',
+ '4d2522d968c8ba78d6c13548a1b4200e' => $vendorDir . '/markbaker/complex/classes/src/functions/negative.php',
+ 'fd587ca933fc0447fa5ab4843bdd97f7' => $vendorDir . '/markbaker/complex/classes/src/functions/pow.php',
+ '383ef01c62028fc78cd4388082fce3c2' => $vendorDir . '/markbaker/complex/classes/src/functions/rho.php',
+ '150fbd1b95029dc47292da97ecab9375' => $vendorDir . '/markbaker/complex/classes/src/functions/sec.php',
+ '549abd9bae174286d660bdaa07407c68' => $vendorDir . '/markbaker/complex/classes/src/functions/sech.php',
+ '6bfbf5eaea6b17a0ed85cb21ba80370c' => $vendorDir . '/markbaker/complex/classes/src/functions/sin.php',
+ '22efe13f1a497b8e199540ae2d9dc59c' => $vendorDir . '/markbaker/complex/classes/src/functions/sinh.php',
+ 'e90135ab8e787795a509ed7147de207d' => $vendorDir . '/markbaker/complex/classes/src/functions/sqrt.php',
+ 'bb0a7923ffc6a90919cd64ec54ff06bc' => $vendorDir . '/markbaker/complex/classes/src/functions/tan.php',
+ '2d302f32ce0fd4e433dd91c5bb404a28' => $vendorDir . '/markbaker/complex/classes/src/functions/tanh.php',
+ '24dd4658a952171a4ee79218c4f9fd06' => $vendorDir . '/markbaker/complex/classes/src/functions/theta.php',
+ 'e49b7876281d6f5bc39536dde96d1f4a' => $vendorDir . '/markbaker/complex/classes/src/operations/add.php',
+ '47596e02b43cd6da7700134fd08f88cf' => $vendorDir . '/markbaker/complex/classes/src/operations/subtract.php',
+ '883af48563631547925fa4c3b48ead07' => $vendorDir . '/markbaker/complex/classes/src/operations/multiply.php',
+ 'f190e3308e6ca23234a2875edc985c03' => $vendorDir . '/markbaker/complex/classes/src/operations/divideby.php',
+ 'ac9e33ce6841aa5bf5d16d465a2f03a7' => $vendorDir . '/markbaker/complex/classes/src/operations/divideinto.php',
+ '3af723442581d6c310bf44543f9f5c60' => $vendorDir . '/markbaker/matrix/classes/src/Functions/adjoint.php',
+ 'd803221834c8b57fec95debb5406a797' => $vendorDir . '/markbaker/matrix/classes/src/Functions/antidiagonal.php',
+ '4714cafbd3be4c72c274a25eae9396bb' => $vendorDir . '/markbaker/matrix/classes/src/Functions/cofactors.php',
+ '89719dc7c77436609d1c1c31f0797b8f' => $vendorDir . '/markbaker/matrix/classes/src/Functions/determinant.php',
+ 'c28af79ec7730859d83f2d4310b8dd0b' => $vendorDir . '/markbaker/matrix/classes/src/Functions/diagonal.php',
+ 'c5d82bf1ac485e445f911e55789ab4e6' => $vendorDir . '/markbaker/matrix/classes/src/Functions/identity.php',
+ '0d2d594de24a247f7a33499e933aa21e' => $vendorDir . '/markbaker/matrix/classes/src/Functions/inverse.php',
+ 'f37c25880804a014ef40c8bffbab1b10' => $vendorDir . '/markbaker/matrix/classes/src/Functions/minors.php',
+ 'd6e4e42171df0dbea253b3067fefda38' => $vendorDir . '/markbaker/matrix/classes/src/Functions/trace.php',
+ '2c9b19fa954fd3e6fcc7e7a1383caddd' => $vendorDir . '/markbaker/matrix/classes/src/Functions/transpose.php',
+ '0a538fc9b897450ec362480ebbebe94f' => $vendorDir . '/markbaker/matrix/classes/src/Operations/add.php',
+ 'f0843f7f4089ec2343c7445544356385' => $vendorDir . '/markbaker/matrix/classes/src/Operations/directsum.php',
+ 'ad3e8c29aa16d134661a414265677b61' => $vendorDir . '/markbaker/matrix/classes/src/Operations/subtract.php',
+ '8d37dad4703fab45bfec9dd0bbf3278e' => $vendorDir . '/markbaker/matrix/classes/src/Operations/multiply.php',
+ '4888a6f58c08148ebe17682f9ce9b2a8' => $vendorDir . '/markbaker/matrix/classes/src/Operations/divideby.php',
+ 'eef6fa3879d3efa347cd24d5eb348f85' => $vendorDir . '/markbaker/matrix/classes/src/Operations/divideinto.php',
+ '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
+ '04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php',
+ 'dc1275c308c5b416beb314b6317daca2' => $vendorDir . '/overtrue/pinyin/src/const.php',
+ 'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
+ '941748b3c8cae4466c827dfb5ca9602a' => $vendorDir . '/rmccue/requests/library/Deprecated.php',
+);
diff --git a/lib/vendor/composer/autoload_namespaces.php b/lib/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000000..58307f00f5
--- /dev/null
+++ b/lib/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,14 @@
+ array($vendorDir . '/erusev/parsedown'),
+ 'PHPThumb\\Tests' => array($vendorDir . '/masterexploder/phpthumb/tests'),
+ 'PHPThumb' => array($vendorDir . '/masterexploder/phpthumb/src'),
+ 'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'),
+ 'Detection' => array($vendorDir . '/mobiledetect/mobiledetectlib/namespaced'),
+);
diff --git a/lib/vendor/composer/autoload_psr4.php b/lib/vendor/composer/autoload_psr4.php
new file mode 100644
index 0000000000..1f75ac97b2
--- /dev/null
+++ b/lib/vendor/composer/autoload_psr4.php
@@ -0,0 +1,26 @@
+ array($vendorDir . '/phpseclib/phpseclib/phpseclib'),
+ 'WpOrg\\Requests\\' => array($vendorDir . '/rmccue/requests/src'),
+ 'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
+ 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
+ 'PhpOffice\\PhpWord\\' => array($vendorDir . '/phpoffice/phpword/src/PhpWord'),
+ 'PhpOffice\\PhpSpreadsheet\\' => array($vendorDir . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet'),
+ 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
+ 'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
+ 'Overtrue\\Pinyin\\' => array($vendorDir . '/overtrue/pinyin/src'),
+ 'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
+ 'Laminas\\ZendFrameworkBridge\\' => array($vendorDir . '/laminas/laminas-zendframework-bridge/src'),
+ 'Laminas\\Escaper\\' => array($vendorDir . '/laminas/laminas-escaper/src'),
+ 'Gregwar\\' => array($vendorDir . '/gregwar/captcha/src/Gregwar'),
+ 'GorHill\\FineDiff\\' => array($vendorDir . '/gorhill/php-fine-diff/GorHill/FineDiff'),
+ 'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
+ 'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
+ 'Box\\Spout\\' => array($vendorDir . '/box/spout/src/Spout'),
+);
diff --git a/lib/vendor/composer/autoload_real.php b/lib/vendor/composer/autoload_real.php
new file mode 100644
index 0000000000..362686ce20
--- /dev/null
+++ b/lib/vendor/composer/autoload_real.php
@@ -0,0 +1,80 @@
+= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+ if ($useStaticLoader) {
+ require __DIR__ . '/autoload_static.php';
+
+ call_user_func(\Composer\Autoload\ComposerStaticInitafe523bf1918581efcc4a020da6c4d4e::getInitializer($loader));
+ } else {
+ $map = require __DIR__ . '/autoload_namespaces.php';
+ foreach ($map as $namespace => $path) {
+ $loader->set($namespace, $path);
+ }
+
+ $map = require __DIR__ . '/autoload_psr4.php';
+ foreach ($map as $namespace => $path) {
+ $loader->setPsr4($namespace, $path);
+ }
+
+ $classMap = require __DIR__ . '/autoload_classmap.php';
+ if ($classMap) {
+ $loader->addClassMap($classMap);
+ }
+ }
+
+ $loader->register(true);
+
+ if ($useStaticLoader) {
+ $includeFiles = Composer\Autoload\ComposerStaticInitafe523bf1918581efcc4a020da6c4d4e::$files;
+ } else {
+ $includeFiles = require __DIR__ . '/autoload_files.php';
+ }
+ foreach ($includeFiles as $fileIdentifier => $file) {
+ composerRequireafe523bf1918581efcc4a020da6c4d4e($fileIdentifier, $file);
+ }
+
+ return $loader;
+ }
+}
+
+/**
+ * @param string $fileIdentifier
+ * @param string $file
+ * @return void
+ */
+function composerRequireafe523bf1918581efcc4a020da6c4d4e($fileIdentifier, $file)
+{
+ if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+ $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+ require $file;
+ }
+}
diff --git a/lib/vendor/composer/autoload_static.php b/lib/vendor/composer/autoload_static.php
new file mode 100644
index 0000000000..d67255451c
--- /dev/null
+++ b/lib/vendor/composer/autoload_static.php
@@ -0,0 +1,246 @@
+ __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src/autoload.php',
+ 'abede361264e2ae69ec1eee813a101af' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/abs.php',
+ '21a5860fbef5be28db5ddfbc3cca67c4' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/acos.php',
+ '1546e3f9d127f2a9bb2d1b6c31c26ef1' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/acosh.php',
+ 'd2516f7f4fba5ea5905f494b4a8262e0' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/acot.php',
+ '4511163d560956219b96882c0980b65e' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/acoth.php',
+ 'c361f5616dc2a8da4fa3e137077cd4ea' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/acsc.php',
+ '02d68920fc98da71991ce569c91df0f6' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/acsch.php',
+ '88e19525eae308b4a6aa3419364875d3' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/argument.php',
+ '60e8e2d0827b58bfc904f13957e51849' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/asec.php',
+ '13d2f040713999eab66c359b4d79871d' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/asech.php',
+ '838ab38beb32c68a79d3cd2c007d5a04' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/asin.php',
+ 'bb28eccd0f8f008333a1b3c163d604ac' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/asinh.php',
+ '9e483de83558c98f7d3feaa402c78cb3' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/atan.php',
+ '36b74b5b765ded91ee58c8ee3c0e85e3' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/atanh.php',
+ '05c15ee9510da7fd6bf6136f436500c0' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/conjugate.php',
+ 'd3208dfbce2505e370788f9f22f6785f' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/cos.php',
+ '141cf1fb3a3046f8b64534b0ebab33ca' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/cosh.php',
+ 'be660df75fd0dbe7fa7c03b7434b3294' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/cot.php',
+ '01e31ea298a51bc9e91517e3ce6b9e76' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/coth.php',
+ '803ddd97f7b1da68982a7b087c3476f6' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/csc.php',
+ '3001cdfd101ec3c32da34ee43c2e149b' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/csch.php',
+ '77b2d7629ef2a93fabb8c56754a91051' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/exp.php',
+ '4a4471296dec796c21d4f4b6552396a9' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/inverse.php',
+ 'c3e9897e1744b88deb56fcdc39d34d85' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/ln.php',
+ 'a83cacf2de942cff288de15a83afd26d' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/log2.php',
+ '6a861dacc9ee2f3061241d4c7772fa21' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/log10.php',
+ '4d2522d968c8ba78d6c13548a1b4200e' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/negative.php',
+ 'fd587ca933fc0447fa5ab4843bdd97f7' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/pow.php',
+ '383ef01c62028fc78cd4388082fce3c2' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/rho.php',
+ '150fbd1b95029dc47292da97ecab9375' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/sec.php',
+ '549abd9bae174286d660bdaa07407c68' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/sech.php',
+ '6bfbf5eaea6b17a0ed85cb21ba80370c' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/sin.php',
+ '22efe13f1a497b8e199540ae2d9dc59c' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/sinh.php',
+ 'e90135ab8e787795a509ed7147de207d' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/sqrt.php',
+ 'bb0a7923ffc6a90919cd64ec54ff06bc' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/tan.php',
+ '2d302f32ce0fd4e433dd91c5bb404a28' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/tanh.php',
+ '24dd4658a952171a4ee79218c4f9fd06' => __DIR__ . '/..' . '/markbaker/complex/classes/src/functions/theta.php',
+ 'e49b7876281d6f5bc39536dde96d1f4a' => __DIR__ . '/..' . '/markbaker/complex/classes/src/operations/add.php',
+ '47596e02b43cd6da7700134fd08f88cf' => __DIR__ . '/..' . '/markbaker/complex/classes/src/operations/subtract.php',
+ '883af48563631547925fa4c3b48ead07' => __DIR__ . '/..' . '/markbaker/complex/classes/src/operations/multiply.php',
+ 'f190e3308e6ca23234a2875edc985c03' => __DIR__ . '/..' . '/markbaker/complex/classes/src/operations/divideby.php',
+ 'ac9e33ce6841aa5bf5d16d465a2f03a7' => __DIR__ . '/..' . '/markbaker/complex/classes/src/operations/divideinto.php',
+ '3af723442581d6c310bf44543f9f5c60' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/adjoint.php',
+ 'd803221834c8b57fec95debb5406a797' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/antidiagonal.php',
+ '4714cafbd3be4c72c274a25eae9396bb' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/cofactors.php',
+ '89719dc7c77436609d1c1c31f0797b8f' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/determinant.php',
+ 'c28af79ec7730859d83f2d4310b8dd0b' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/diagonal.php',
+ 'c5d82bf1ac485e445f911e55789ab4e6' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/identity.php',
+ '0d2d594de24a247f7a33499e933aa21e' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/inverse.php',
+ 'f37c25880804a014ef40c8bffbab1b10' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/minors.php',
+ 'd6e4e42171df0dbea253b3067fefda38' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/trace.php',
+ '2c9b19fa954fd3e6fcc7e7a1383caddd' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Functions/transpose.php',
+ '0a538fc9b897450ec362480ebbebe94f' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Operations/add.php',
+ 'f0843f7f4089ec2343c7445544356385' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Operations/directsum.php',
+ 'ad3e8c29aa16d134661a414265677b61' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Operations/subtract.php',
+ '8d37dad4703fab45bfec9dd0bbf3278e' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Operations/multiply.php',
+ '4888a6f58c08148ebe17682f9ce9b2a8' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Operations/divideby.php',
+ 'eef6fa3879d3efa347cd24d5eb348f85' => __DIR__ . '/..' . '/markbaker/matrix/classes/src/Operations/divideinto.php',
+ '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
+ '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php',
+ 'dc1275c308c5b416beb314b6317daca2' => __DIR__ . '/..' . '/overtrue/pinyin/src/const.php',
+ 'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
+ '941748b3c8cae4466c827dfb5ca9602a' => __DIR__ . '/..' . '/rmccue/requests/library/Deprecated.php',
+ );
+
+ public static $prefixLengthsPsr4 = array (
+ 'p' =>
+ array (
+ 'phpseclib3\\' => 11,
+ ),
+ 'W' =>
+ array (
+ 'WpOrg\\Requests\\' => 15,
+ ),
+ 'S' =>
+ array (
+ 'Symfony\\Component\\Finder\\' => 25,
+ ),
+ 'P' =>
+ array (
+ 'Psr\\SimpleCache\\' => 16,
+ 'PhpOffice\\PhpWord\\' => 18,
+ 'PhpOffice\\PhpSpreadsheet\\' => 25,
+ 'ParagonIE\\ConstantTime\\' => 23,
+ 'PHPMailer\\PHPMailer\\' => 20,
+ ),
+ 'O' =>
+ array (
+ 'Overtrue\\Pinyin\\' => 16,
+ ),
+ 'M' =>
+ array (
+ 'Matrix\\' => 7,
+ ),
+ 'L' =>
+ array (
+ 'Laminas\\ZendFrameworkBridge\\' => 28,
+ 'Laminas\\Escaper\\' => 16,
+ ),
+ 'G' =>
+ array (
+ 'Gregwar\\' => 8,
+ 'GorHill\\FineDiff\\' => 17,
+ ),
+ 'C' =>
+ array (
+ 'Cron\\' => 5,
+ 'Complex\\' => 8,
+ ),
+ 'B' =>
+ array (
+ 'Box\\Spout\\' => 10,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'phpseclib3\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib',
+ ),
+ 'WpOrg\\Requests\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/rmccue/requests/src',
+ ),
+ 'Symfony\\Component\\Finder\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/symfony/finder',
+ ),
+ 'Psr\\SimpleCache\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/psr/simple-cache/src',
+ ),
+ 'PhpOffice\\PhpWord\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/phpoffice/phpword/src/PhpWord',
+ ),
+ 'PhpOffice\\PhpSpreadsheet\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/phpoffice/phpspreadsheet/src/PhpSpreadsheet',
+ ),
+ 'ParagonIE\\ConstantTime\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
+ ),
+ 'PHPMailer\\PHPMailer\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
+ ),
+ 'Overtrue\\Pinyin\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/overtrue/pinyin/src',
+ ),
+ 'Matrix\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/markbaker/matrix/classes/src',
+ ),
+ 'Laminas\\ZendFrameworkBridge\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/laminas/laminas-zendframework-bridge/src',
+ ),
+ 'Laminas\\Escaper\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/laminas/laminas-escaper/src',
+ ),
+ 'Gregwar\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/gregwar/captcha/src/Gregwar',
+ ),
+ 'GorHill\\FineDiff\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/gorhill/php-fine-diff/GorHill/FineDiff',
+ ),
+ 'Cron\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron',
+ ),
+ 'Complex\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/markbaker/complex/classes/src',
+ ),
+ 'Box\\Spout\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/box/spout/src/Spout',
+ ),
+ );
+
+ public static $prefixesPsr0 = array (
+ 'P' =>
+ array (
+ 'Parsedown' =>
+ array (
+ 0 => __DIR__ . '/..' . '/erusev/parsedown',
+ ),
+ 'PHPThumb\\Tests' =>
+ array (
+ 0 => __DIR__ . '/..' . '/masterexploder/phpthumb/tests',
+ ),
+ 'PHPThumb' =>
+ array (
+ 0 => __DIR__ . '/..' . '/masterexploder/phpthumb/src',
+ ),
+ ),
+ 'H' =>
+ array (
+ 'HTMLPurifier' =>
+ array (
+ 0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library',
+ ),
+ ),
+ 'D' =>
+ array (
+ 'Detection' =>
+ array (
+ 0 => __DIR__ . '/..' . '/mobiledetect/mobiledetectlib/namespaced',
+ ),
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ 'Mobile_Detect' => __DIR__ . '/..' . '/mobiledetect/mobiledetectlib/Mobile_Detect.php',
+ 'PclZip' => __DIR__ . '/..' . '/pclzip/pclzip/pclzip.lib.php',
+ 'Requests' => __DIR__ . '/..' . '/rmccue/requests/library/Requests.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInitafe523bf1918581efcc4a020da6c4d4e::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInitafe523bf1918581efcc4a020da6c4d4e::$prefixDirsPsr4;
+ $loader->prefixesPsr0 = ComposerStaticInitafe523bf1918581efcc4a020da6c4d4e::$prefixesPsr0;
+ $loader->classMap = ComposerStaticInitafe523bf1918581efcc4a020da6c4d4e::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/lib/vendor/composer/installed.json b/lib/vendor/composer/installed.json
new file mode 100644
index 0000000000..52a2ae0441
--- /dev/null
+++ b/lib/vendor/composer/installed.json
@@ -0,0 +1,1698 @@
+{
+ "packages": [
+ {
+ "name": "box/spout",
+ "version": "v2.7.3",
+ "version_normalized": "2.7.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/box/spout.git",
+ "reference": "3681a3421a868ab9a65da156c554f756541f452b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/box/spout/zipball/3681a3421a868ab9a65da156c554f756541f452b",
+ "reference": "3681a3421a868ab9a65da156c554f756541f452b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-xmlreader": "*",
+ "ext-zip": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.0"
+ },
+ "suggest": {
+ "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)",
+ "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
+ },
+ "time": "2017-09-25T19:44:35+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Box\\Spout\\": "src/Spout"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Adrien Loison",
+ "email": "adrien@box.com"
+ }
+ ],
+ "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
+ "homepage": "https://www.github.com/box/spout",
+ "keywords": [
+ "OOXML",
+ "csv",
+ "excel",
+ "memory",
+ "odf",
+ "ods",
+ "office",
+ "open",
+ "php",
+ "read",
+ "scale",
+ "spreadsheet",
+ "stream",
+ "write",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/box/spout/issues",
+ "source": "https://github.com/box/spout/tree/v2.7.3"
+ },
+ "abandoned": true,
+ "install-path": "../box/spout"
+ },
+ {
+ "name": "dragonmantank/cron-expression",
+ "version": "v2.3.1",
+ "version_normalized": "2.3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dragonmantank/cron-expression.git",
+ "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/65b2d8ee1f10915efb3b55597da3404f096acba2",
+ "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0"
+ },
+ "time": "2020-10-13T00:52:37+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Cron\\": "src/Cron/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Chris Tankersley",
+ "email": "chris@ctankersley.com",
+ "homepage": "https://github.com/dragonmantank"
+ }
+ ],
+ "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+ "keywords": [
+ "cron",
+ "schedule"
+ ],
+ "support": {
+ "issues": "https://github.com/dragonmantank/cron-expression/issues",
+ "source": "https://github.com/dragonmantank/cron-expression/tree/v2.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dragonmantank",
+ "type": "github"
+ }
+ ],
+ "install-path": "../dragonmantank/cron-expression"
+ },
+ {
+ "name": "erusev/parsedown",
+ "version": "1.7.4",
+ "version_normalized": "1.7.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/erusev/parsedown.git",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "time": "2019-12-30T22:54:17+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Parsedown": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Emanuil Rusev",
+ "email": "hello@erusev.com",
+ "homepage": "http://erusev.com"
+ }
+ ],
+ "description": "Parser for Markdown.",
+ "homepage": "http://parsedown.org",
+ "keywords": [
+ "markdown",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/erusev/parsedown/issues",
+ "source": "https://github.com/erusev/parsedown/tree/1.7.x"
+ },
+ "install-path": "../erusev/parsedown"
+ },
+ {
+ "name": "ezyang/htmlpurifier",
+ "version": "v4.18.0",
+ "version_normalized": "4.18.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezyang/htmlpurifier.git",
+ "reference": "cb56001e54359df7ae76dc522d08845dc741621b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b",
+ "reference": "cb56001e54359df7ae76dc522d08845dc741621b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "require-dev": {
+ "cerdic/css-tidy": "^1.7 || ^2.0",
+ "simpletest/simpletest": "dev-master"
+ },
+ "suggest": {
+ "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
+ "ext-bcmath": "Used for unit conversion and imagecrash protection",
+ "ext-iconv": "Converts text to and from non-UTF-8 encodings",
+ "ext-tidy": "Used for pretty-printing HTML"
+ },
+ "time": "2024-11-01T03:51:45+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "library/HTMLPurifier.composer.php"
+ ],
+ "psr-0": {
+ "HTMLPurifier": "library/"
+ },
+ "exclude-from-classmap": [
+ "/library/HTMLPurifier/Language/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Edward Z. Yang",
+ "email": "admin@htmlpurifier.org",
+ "homepage": "http://ezyang.com"
+ }
+ ],
+ "description": "Standards compliant HTML filter written in PHP",
+ "homepage": "http://htmlpurifier.org/",
+ "keywords": [
+ "html"
+ ],
+ "support": {
+ "issues": "https://github.com/ezyang/htmlpurifier/issues",
+ "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0"
+ },
+ "install-path": "../ezyang/htmlpurifier"
+ },
+ {
+ "name": "gorhill/php-fine-diff",
+ "version": "0.1.0",
+ "version_normalized": "0.1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/falk-graeser/PHP-FineDiff.git",
+ "reference": "b0c9ab136958882c501d461b0ad204f908e19392"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/falk-graeser/PHP-FineDiff/zipball/b0c9ab136958882c501d461b0ad204f908e19392",
+ "reference": "b0c9ab136958882c501d461b0ad204f908e19392",
+ "shasum": ""
+ },
+ "time": "2015-07-30T10:21:26+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "GorHill\\FineDiff\\": "GorHill/FineDiff"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Raymond Hill",
+ "email": "r.michael@queo-group.com",
+ "homepage": "http://www.queoflow.com/",
+ "role": "Developer"
+ },
+ {
+ "name": "Falk Gräser",
+ "email": "f.graeser@queo-group.com",
+ "homepage": "http://www.queoflow.com/",
+ "role": "The-Clean-Up-Guy"
+ }
+ ],
+ "homepage": "http://www.raymondhill.net/finediff/",
+ "keywords": [
+ "finediff"
+ ],
+ "support": {
+ "source": "https://github.com/falk-graeser/PHP-FineDiff/tree/0.1.0"
+ },
+ "install-path": "../gorhill/php-fine-diff"
+ },
+ {
+ "name": "gregwar/captcha",
+ "version": "v1.3.0",
+ "version_normalized": "1.3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Gregwar/Captcha.git",
+ "reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/4edbcd09fde4353b94ce550f43460eba73baf2cc",
+ "reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.3.0",
+ "symfony/finder": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6.4 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "time": "2025-06-23T12:25:54+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Gregwar\\": "src/Gregwar"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Passault",
+ "email": "g.passault@gmail.com",
+ "homepage": "http://www.gregwar.com/"
+ },
+ {
+ "name": "Jeremy Livingston",
+ "email": "jeremy.j.livingston@gmail.com"
+ }
+ ],
+ "description": "Captcha generator",
+ "homepage": "https://github.com/Gregwar/Captcha",
+ "keywords": [
+ "bot",
+ "captcha",
+ "spam"
+ ],
+ "support": {
+ "issues": "https://github.com/Gregwar/Captcha/issues",
+ "source": "https://github.com/Gregwar/Captcha/tree/v1.3.0"
+ },
+ "install-path": "../gregwar/captcha"
+ },
+ {
+ "name": "laminas/laminas-escaper",
+ "version": "2.6.1",
+ "version_normalized": "2.6.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-escaper.git",
+ "reference": "25f2a053eadfa92ddacb609dcbbc39362610da70"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/25f2a053eadfa92ddacb609dcbbc39362610da70",
+ "reference": "25f2a053eadfa92ddacb609dcbbc39362610da70",
+ "shasum": ""
+ },
+ "require": {
+ "laminas/laminas-zendframework-bridge": "^1.0",
+ "php": "^5.6 || ^7.0"
+ },
+ "replace": {
+ "zendframework/zend-escaper": "self.version"
+ },
+ "require-dev": {
+ "laminas/laminas-coding-standard": "~1.0.0",
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2"
+ },
+ "time": "2019-12-31T16:43:30+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6.x-dev",
+ "dev-develop": "2.7.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Laminas\\Escaper\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs",
+ "homepage": "https://laminas.dev",
+ "keywords": [
+ "escaper",
+ "laminas"
+ ],
+ "support": {
+ "chat": "https://laminas.dev/chat",
+ "docs": "https://docs.laminas.dev/laminas-escaper/",
+ "forum": "https://discourse.laminas.dev",
+ "issues": "https://github.com/laminas/laminas-escaper/issues",
+ "rss": "https://github.com/laminas/laminas-escaper/releases.atom",
+ "source": "https://github.com/laminas/laminas-escaper"
+ },
+ "install-path": "../laminas/laminas-escaper"
+ },
+ {
+ "name": "laminas/laminas-zendframework-bridge",
+ "version": "1.1.1",
+ "version_normalized": "1.1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laminas/laminas-zendframework-bridge.git",
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642",
+ "reference": "6ede70583e101030bcace4dcddd648f760ddf642",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "time": "2020-09-14T14:23:00+00:00",
+ "type": "library",
+ "extra": {
+ "laminas": {
+ "module": "Laminas\\ZendFrameworkBridge"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "src/autoload.php"
+ ],
+ "psr-4": {
+ "Laminas\\ZendFrameworkBridge\\": "src//"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Alias legacy ZF class names to Laminas Project equivalents.",
+ "keywords": [
+ "ZendFramework",
+ "autoloading",
+ "laminas",
+ "zf"
+ ],
+ "support": {
+ "forum": "https://discourse.laminas.dev/",
+ "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues",
+ "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom",
+ "source": "https://github.com/laminas/laminas-zendframework-bridge"
+ },
+ "funding": [
+ {
+ "url": "https://funding.communitybridge.org/projects/laminas-project",
+ "type": "community_bridge"
+ }
+ ],
+ "abandoned": true,
+ "install-path": "../laminas/laminas-zendframework-bridge"
+ },
+ {
+ "name": "markbaker/complex",
+ "version": "1.5.0",
+ "version_normalized": "1.5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/c3131244e29c08d44fefb49e0dd35021e9e39dd2",
+ "reference": "c3131244e29c08d44fefb49e0dd35021e9e39dd2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6.0|^7.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0|^5.0|^6.0|^7.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^4.8.35|^5.0|^6.0|^7.0",
+ "sebastian/phpcpd": "2.*",
+ "squizlabs/php_codesniffer": "^3.4.0"
+ },
+ "time": "2020-08-26T19:47:57+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "classes/src/functions/abs.php",
+ "classes/src/functions/acos.php",
+ "classes/src/functions/acosh.php",
+ "classes/src/functions/acot.php",
+ "classes/src/functions/acoth.php",
+ "classes/src/functions/acsc.php",
+ "classes/src/functions/acsch.php",
+ "classes/src/functions/argument.php",
+ "classes/src/functions/asec.php",
+ "classes/src/functions/asech.php",
+ "classes/src/functions/asin.php",
+ "classes/src/functions/asinh.php",
+ "classes/src/functions/atan.php",
+ "classes/src/functions/atanh.php",
+ "classes/src/functions/conjugate.php",
+ "classes/src/functions/cos.php",
+ "classes/src/functions/cosh.php",
+ "classes/src/functions/cot.php",
+ "classes/src/functions/coth.php",
+ "classes/src/functions/csc.php",
+ "classes/src/functions/csch.php",
+ "classes/src/functions/exp.php",
+ "classes/src/functions/inverse.php",
+ "classes/src/functions/ln.php",
+ "classes/src/functions/log2.php",
+ "classes/src/functions/log10.php",
+ "classes/src/functions/negative.php",
+ "classes/src/functions/pow.php",
+ "classes/src/functions/rho.php",
+ "classes/src/functions/sec.php",
+ "classes/src/functions/sech.php",
+ "classes/src/functions/sin.php",
+ "classes/src/functions/sinh.php",
+ "classes/src/functions/sqrt.php",
+ "classes/src/functions/tan.php",
+ "classes/src/functions/tanh.php",
+ "classes/src/functions/theta.php",
+ "classes/src/operations/add.php",
+ "classes/src/operations/subtract.php",
+ "classes/src/operations/multiply.php",
+ "classes/src/operations/divideby.php",
+ "classes/src/operations/divideinto.php"
+ ],
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/1.5.0"
+ },
+ "install-path": "../markbaker/complex"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "1.2.3",
+ "version_normalized": "1.2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "44bb1ab01811116f01fe216ab37d921dccc6c10d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/44bb1ab01811116f01fe216ab37d921dccc6c10d",
+ "reference": "44bb1ab01811116f01fe216ab37d921dccc6c10d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6.0|^7.0.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "dev-master",
+ "phploc/phploc": "^4",
+ "phpmd/phpmd": "dev-master",
+ "phpunit/phpunit": "^5.7|^6.0|7.0",
+ "sebastian/phpcpd": "^3.0",
+ "squizlabs/php_codesniffer": "^3.0@dev"
+ },
+ "time": "2021-01-26T14:36:01+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "classes/src/Functions/adjoint.php",
+ "classes/src/Functions/antidiagonal.php",
+ "classes/src/Functions/cofactors.php",
+ "classes/src/Functions/determinant.php",
+ "classes/src/Functions/diagonal.php",
+ "classes/src/Functions/identity.php",
+ "classes/src/Functions/inverse.php",
+ "classes/src/Functions/minors.php",
+ "classes/src/Functions/trace.php",
+ "classes/src/Functions/transpose.php",
+ "classes/src/Operations/add.php",
+ "classes/src/Operations/directsum.php",
+ "classes/src/Operations/subtract.php",
+ "classes/src/Operations/multiply.php",
+ "classes/src/Operations/divideby.php",
+ "classes/src/Operations/divideinto.php"
+ ],
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/1.2.3"
+ },
+ "install-path": "../markbaker/matrix"
+ },
+ {
+ "name": "masterexploder/phpthumb",
+ "version": "2.1",
+ "version_normalized": "2.1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/masterexploder/PHPThumb.git",
+ "reference": "01f6aa65aa3d136e175f67da2b4ea3e316770d02"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/masterexploder/PHPThumb/zipball/01f6aa65aa3d136e175f67da2b4ea3e316770d02",
+ "reference": "01f6aa65aa3d136e175f67da2b4ea3e316770d02",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "time": "2015-09-08T22:55:44+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "PHPThumb": "src",
+ "PHPThumb\\Tests": "tests"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jarrod Nettles",
+ "email": "jarrod.nettles@icloud.com"
+ },
+ {
+ "name": "Ian Selby",
+ "email": "ian@gxdlabs.com"
+ }
+ ],
+ "description": "A library for manipulating images in PHP.",
+ "homepage": "https://github.com/masterexploder/PHPThumb",
+ "keywords": [
+ "image",
+ "resize",
+ "rotate"
+ ],
+ "support": {
+ "issues": "https://github.com/masterexploder/PHPThumb/issues",
+ "source": "https://github.com/masterexploder/PHPThumb/tree/2.1"
+ },
+ "install-path": "../masterexploder/phpthumb"
+ },
+ {
+ "name": "mobiledetect/mobiledetectlib",
+ "version": "2.8.45",
+ "version_normalized": "2.8.45.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/serbanghita/Mobile-Detect.git",
+ "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96aaebcf4f50d3d2692ab81d2c5132e425bca266",
+ "reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8.36"
+ },
+ "time": "2023-11-07T21:57:25+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Detection": "namespaced/"
+ },
+ "classmap": [
+ "Mobile_Detect.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Serban Ghita",
+ "email": "serbanghita@gmail.com",
+ "homepage": "http://mobiledetect.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
+ "homepage": "https://github.com/serbanghita/Mobile-Detect",
+ "keywords": [
+ "detect mobile devices",
+ "mobile",
+ "mobile detect",
+ "mobile detector",
+ "php mobile detect"
+ ],
+ "support": {
+ "issues": "https://github.com/serbanghita/Mobile-Detect/issues",
+ "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.45"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/serbanghita",
+ "type": "github"
+ }
+ ],
+ "install-path": "../mobiledetect/mobiledetectlib"
+ },
+ {
+ "name": "mustangostang/spyc",
+ "version": "0.6.3",
+ "version_normalized": "0.6.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mustangostang/spyc.git",
+ "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mustangostang/spyc/zipball/4627c838b16550b666d15aeae1e5289dd5b77da0",
+ "reference": "4627c838b16550b666d15aeae1e5289dd5b77da0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.3.*@dev"
+ },
+ "time": "2019-09-10T13:16:29+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.5.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "Spyc.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "mustangostang",
+ "email": "vlad.andersen@gmail.com"
+ }
+ ],
+ "description": "A simple YAML loader/dumper class for PHP",
+ "homepage": "https://github.com/mustangostang/spyc/",
+ "keywords": [
+ "spyc",
+ "yaml",
+ "yml"
+ ],
+ "support": {
+ "issues": "https://github.com/mustangostang/spyc/issues",
+ "source": "https://github.com/mustangostang/spyc/tree/0.6.3"
+ },
+ "install-path": "../mustangostang/spyc"
+ },
+ {
+ "name": "overtrue/pinyin",
+ "version": "4.0.5",
+ "version_normalized": "4.0.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/overtrue/pinyin.git",
+ "reference": "94fdb3ea6eca5677afd9548111d95bedcd5a4086"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/overtrue/pinyin/zipball/94fdb3ea6eca5677afd9548111d95bedcd5a4086",
+ "reference": "94fdb3ea6eca5677afd9548111d95bedcd5a4086",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~7.5"
+ },
+ "time": "2019-09-20T12:43:45+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "src/const.php"
+ ],
+ "psr-4": {
+ "Overtrue\\Pinyin\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Carlos",
+ "homepage": "http://github.com/overtrue"
+ }
+ ],
+ "description": "Chinese to pinyin translator.",
+ "homepage": "https://github.com/overtrue/pinyin",
+ "keywords": [
+ "Chinese",
+ "Pinyin",
+ "cn2pinyin"
+ ],
+ "support": {
+ "issues": "https://github.com/overtrue/pinyin/issues",
+ "source": "https://github.com/overtrue/pinyin/tree/master"
+ },
+ "install-path": "../overtrue/pinyin"
+ },
+ {
+ "name": "paragonie/constant_time_encoding",
+ "version": "v2.7.0",
+ "version_normalized": "2.7.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/constant_time_encoding.git",
+ "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105",
+ "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6|^7|^8|^9",
+ "vimeo/psalm": "^1|^2|^3|^4"
+ },
+ "time": "2024-05-08T12:18:48+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com",
+ "role": "Maintainer"
+ },
+ {
+ "name": "Steve 'Sc00bz' Thomas",
+ "email": "steve@tobtu.com",
+ "homepage": "https://www.tobtu.com",
+ "role": "Original Developer"
+ }
+ ],
+ "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+ "keywords": [
+ "base16",
+ "base32",
+ "base32_decode",
+ "base32_encode",
+ "base64",
+ "base64_decode",
+ "base64_encode",
+ "bin2hex",
+ "encoding",
+ "hex",
+ "hex2bin",
+ "rfc4648"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+ "source": "https://github.com/paragonie/constant_time_encoding"
+ },
+ "install-path": "../paragonie/constant_time_encoding"
+ },
+ {
+ "name": "paragonie/random_compat",
+ "version": "v9.99.100",
+ "version_normalized": "9.99.100.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/random_compat.git",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">= 7"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*|5.*",
+ "vimeo/psalm": "^1"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ },
+ "time": "2020-10-15T08:29:30+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "polyfill",
+ "pseudorandom",
+ "random"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/random_compat/issues",
+ "source": "https://github.com/paragonie/random_compat"
+ },
+ "install-path": "../paragonie/random_compat"
+ },
+ {
+ "name": "pclzip/pclzip",
+ "version": "2.8.2",
+ "version_normalized": "2.8.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ivanlanin/pclzip.git",
+ "reference": "19dd1de9d3f5fc4d7d70175b4c344dee329f45fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ivanlanin/pclzip/zipball/19dd1de9d3f5fc4d7d70175b4c344dee329f45fd",
+ "reference": "19dd1de9d3f5fc4d7d70175b4c344dee329f45fd",
+ "shasum": ""
+ },
+ "time": "2014-06-05T11:42:24+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "classmap": [
+ "pclzip.lib.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1"
+ ],
+ "authors": [
+ {
+ "name": "Vincent Blavet"
+ }
+ ],
+ "description": "A PHP library that offers compression and extraction functions for Zip formatted archives",
+ "homepage": "http://www.phpconcept.net/pclzip",
+ "keywords": [
+ "php",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/ivanlanin/pclzip/issues",
+ "source": "https://github.com/ivanlanin/pclzip/tree/master"
+ },
+ "install-path": "../pclzip/pclzip"
+ },
+ {
+ "name": "phpmailer/phpmailer",
+ "version": "v6.10.0",
+ "version_normalized": "6.10.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPMailer/PHPMailer.git",
+ "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
+ "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-hash": "*",
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "doctrine/annotations": "^1.2.6 || ^1.13.3",
+ "php-parallel-lint/php-console-highlighter": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "phpcompatibility/php-compatibility": "^9.3.5",
+ "roave/security-advisories": "dev-latest",
+ "squizlabs/php_codesniffer": "^3.7.2",
+ "yoast/phpunit-polyfills": "^1.0.4"
+ },
+ "suggest": {
+ "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+ "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+ "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+ "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+ "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+ "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+ "psr/log": "For optional PSR-3 debug logging",
+ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+ "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+ },
+ "time": "2025-04-24T15:19:31+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "PHPMailer\\PHPMailer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-only"
+ ],
+ "authors": [
+ {
+ "name": "Marcus Bointon",
+ "email": "phpmailer@synchromedia.co.uk"
+ },
+ {
+ "name": "Jim Jagielski",
+ "email": "jimjag@gmail.com"
+ },
+ {
+ "name": "Andy Prevost",
+ "email": "codeworxtech@users.sourceforge.net"
+ },
+ {
+ "name": "Brent R. Matzelle"
+ }
+ ],
+ "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+ "support": {
+ "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Synchro",
+ "type": "github"
+ }
+ ],
+ "install-path": "../phpmailer/phpmailer"
+ },
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "1.8.2",
+ "version_normalized": "1.8.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "0c1346a1956347590b7db09533966307d20cb7cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/0c1346a1956347590b7db09533966307d20cb7cc",
+ "reference": "0c1346a1956347590b7db09533966307d20cb7cc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "markbaker/complex": "^1.4",
+ "markbaker/matrix": "^1.1",
+ "php": "^5.6|^7.0",
+ "psr/simple-cache": "^1.0"
+ },
+ "require-dev": {
+ "doctrine/instantiator": "^1.0.0",
+ "dompdf/dompdf": "^0.8.0",
+ "friendsofphp/php-cs-fixer": "@stable",
+ "jpgraph/jpgraph": "^4.0",
+ "mpdf/mpdf": "^7.0.0",
+ "phpcompatibility/php-compatibility": "^8.0",
+ "phpunit/phpunit": "^5.7",
+ "squizlabs/php_codesniffer": "^3.3",
+ "tecnickcom/tcpdf": "^6.2"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+ "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+ },
+ "time": "2019-07-08T21:21:25+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ },
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.8.2"
+ },
+ "install-path": "../phpoffice/phpspreadsheet"
+ },
+ {
+ "name": "phpoffice/phpword",
+ "version": "0.18.3",
+ "version_normalized": "0.18.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PHPWord.git",
+ "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/be0190cd5d8f95b4be08d5853b107aa4e352759a",
+ "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-xml": "*",
+ "laminas/laminas-escaper": "^2.2",
+ "php": "^5.3.3 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "dompdf/dompdf": "0.8.* || 1.0.*",
+ "ext-gd": "*",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^2.2",
+ "mpdf/mpdf": "5.7.4 || 6.* || 7.* || 8.*",
+ "php-coveralls/php-coveralls": "1.1.0 || ^2.0",
+ "phploc/phploc": "2.* || 3.* || 4.* || 5.* || 6.* || 7.*",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^4.8.36 || ^7.0",
+ "squizlabs/php_codesniffer": "^2.9 || ^3.5",
+ "tecnickcom/tcpdf": "6.*"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Allows writing PDF",
+ "ext-gd2": "Allows adding images",
+ "ext-xmlwriter": "Allows writing OOXML and ODF",
+ "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
+ "ext-zip": "Allows writing OOXML and ODF"
+ },
+ "time": "2022-02-17T15:40:03+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-develop": "0.19-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpWord\\": "src/PhpWord"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker"
+ },
+ {
+ "name": "Gabriel Bull",
+ "email": "me@gabrielbull.com",
+ "homepage": "http://gabrielbull.com/"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net/blog/"
+ },
+ {
+ "name": "Ivan Lanin",
+ "homepage": "http://ivan.lanin.org"
+ },
+ {
+ "name": "Roman Syroeshko",
+ "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
+ },
+ {
+ "name": "Antoine de Troostembergh"
+ }
+ ],
+ "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)",
+ "homepage": "http://phpoffice.github.io",
+ "keywords": [
+ "ISO IEC 29500",
+ "OOXML",
+ "Office Open XML",
+ "OpenDocument",
+ "OpenXML",
+ "PhpOffice",
+ "PhpWord",
+ "Rich Text Format",
+ "WordprocessingML",
+ "doc",
+ "docx",
+ "html",
+ "odf",
+ "odt",
+ "office",
+ "pdf",
+ "php",
+ "reader",
+ "rtf",
+ "template",
+ "template processor",
+ "word",
+ "writer"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PHPWord/issues",
+ "source": "https://github.com/PHPOffice/PHPWord/tree/0.18.3"
+ },
+ "install-path": "../phpoffice/phpword"
+ },
+ {
+ "name": "phpseclib/phpseclib",
+ "version": "3.0.46",
+ "version_normalized": "3.0.46.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpseclib/phpseclib.git",
+ "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6",
+ "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6",
+ "shasum": ""
+ },
+ "require": {
+ "paragonie/constant_time_encoding": "^1|^2|^3",
+ "paragonie/random_compat": "^1.4|^2.0|^9.99.99",
+ "php": ">=5.6.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*"
+ },
+ "suggest": {
+ "ext-dom": "Install the DOM extension to load XML formatted public keys.",
+ "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
+ "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
+ "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
+ "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
+ },
+ "time": "2025-06-26T16:29:55+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "phpseclib/bootstrap.php"
+ ],
+ "psr-4": {
+ "phpseclib3\\": "phpseclib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jim Wigginton",
+ "email": "terrafrost@php.net",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Patrick Monnerat",
+ "email": "pm@datasphere.ch",
+ "role": "Developer"
+ },
+ {
+ "name": "Andreas Fischer",
+ "email": "bantu@phpbb.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Hans-Jürgen Petrich",
+ "email": "petrich@tronic-media.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Graham Campbell",
+ "email": "graham@alt-three.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
+ "homepage": "http://phpseclib.sourceforge.net",
+ "keywords": [
+ "BigInteger",
+ "aes",
+ "asn.1",
+ "asn1",
+ "blowfish",
+ "crypto",
+ "cryptography",
+ "encryption",
+ "rsa",
+ "security",
+ "sftp",
+ "signature",
+ "signing",
+ "ssh",
+ "twofish",
+ "x.509",
+ "x509"
+ ],
+ "support": {
+ "issues": "https://github.com/phpseclib/phpseclib/issues",
+ "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/terrafrost",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/phpseclib",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../phpseclib/phpseclib"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "1.0.1",
+ "version_normalized": "1.0.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "time": "2017-10-23T01:57:42+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
+ },
+ "install-path": "../psr/simple-cache"
+ },
+ {
+ "name": "rmccue/requests",
+ "version": "v2.0.15",
+ "version_normalized": "2.0.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/WordPress/Requests.git",
+ "reference": "877cd66169755899682f1595e057334b40d9d149"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/WordPress/Requests/zipball/877cd66169755899682f1595e057334b40d9d149",
+ "reference": "877cd66169755899682f1595e057334b40d9d149",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7",
+ "php-parallel-lint/php-console-highlighter": "^0.5.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.1",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "requests/test-server": "dev-main",
+ "roave/security-advisories": "dev-latest",
+ "squizlabs/php_codesniffer": "^3.6",
+ "wp-coding-standards/wpcs": "^2.0",
+ "yoast/phpunit-polyfills": "^1.0.0"
+ },
+ "suggest": {
+ "art4/requests-psr18-adapter": "For using Requests as a PSR-18 HTTP Client",
+ "ext-curl": "For improved performance",
+ "ext-openssl": "For secure transport support",
+ "ext-zlib": "For improved performance when decompressing encoded streams"
+ },
+ "time": "2025-01-21T10:13:31+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "library/Deprecated.php"
+ ],
+ "psr-4": {
+ "WpOrg\\Requests\\": "src/"
+ },
+ "classmap": [
+ "library/Requests.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Ryan McCue",
+ "homepage": "https://rmccue.io/"
+ },
+ {
+ "name": "Alain Schlesser",
+ "homepage": "https://github.com/schlessera"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/WordPress/Requests/graphs/contributors"
+ }
+ ],
+ "description": "A HTTP library written in PHP, for human beings.",
+ "homepage": "https://requests.ryanmccue.info/",
+ "keywords": [
+ "curl",
+ "fsockopen",
+ "http",
+ "idna",
+ "ipv6",
+ "iri",
+ "sockets"
+ ],
+ "support": {
+ "docs": "https://requests.ryanmccue.info/",
+ "issues": "https://github.com/WordPress/Requests/issues",
+ "source": "https://github.com/WordPress/Requests"
+ },
+ "install-path": "../rmccue/requests"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v3.4.47",
+ "version_normalized": "3.4.47.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "b6b6ad3db3edb1b4b1c1896b1975fb684994de6e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/b6b6ad3db3edb1b4b1c1896b1975fb684994de6e",
+ "reference": "b6b6ad3db3edb1b4b1c1896b1975fb684994de6e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8"
+ },
+ "time": "2020-11-16T17:02:08+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Finder Component",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v3.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../symfony/finder"
+ }
+ ],
+ "dev": true,
+ "dev-package-names": []
+}
diff --git a/lib/vendor/composer/installed.php b/lib/vendor/composer/installed.php
new file mode 100644
index 0000000000..a554ceaf93
--- /dev/null
+++ b/lib/vendor/composer/installed.php
@@ -0,0 +1,245 @@
+ array(
+ 'pretty_version' => 'dev-main',
+ 'version' => 'dev-main',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'reference' => '14f290ae63a7ce26371b9598aeb5517f537e61c7',
+ 'name' => '__root__',
+ 'dev' => true,
+ ),
+ 'versions' => array(
+ '__root__' => array(
+ 'pretty_version' => 'dev-main',
+ 'version' => 'dev-main',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'reference' => '14f290ae63a7ce26371b9598aeb5517f537e61c7',
+ 'dev_requirement' => false,
+ ),
+ 'box/spout' => array(
+ 'pretty_version' => 'v2.7.3',
+ 'version' => '2.7.3.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../box/spout',
+ 'aliases' => array(),
+ 'reference' => '3681a3421a868ab9a65da156c554f756541f452b',
+ 'dev_requirement' => false,
+ ),
+ 'dragonmantank/cron-expression' => array(
+ 'pretty_version' => 'v2.3.1',
+ 'version' => '2.3.1.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../dragonmantank/cron-expression',
+ 'aliases' => array(),
+ 'reference' => '65b2d8ee1f10915efb3b55597da3404f096acba2',
+ 'dev_requirement' => false,
+ ),
+ 'erusev/parsedown' => array(
+ 'pretty_version' => '1.7.4',
+ 'version' => '1.7.4.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../erusev/parsedown',
+ 'aliases' => array(),
+ 'reference' => 'cb17b6477dfff935958ba01325f2e8a2bfa6dab3',
+ 'dev_requirement' => false,
+ ),
+ 'ezyang/htmlpurifier' => array(
+ 'pretty_version' => 'v4.18.0',
+ 'version' => '4.18.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
+ 'aliases' => array(),
+ 'reference' => 'cb56001e54359df7ae76dc522d08845dc741621b',
+ 'dev_requirement' => false,
+ ),
+ 'gorhill/php-fine-diff' => array(
+ 'pretty_version' => '0.1.0',
+ 'version' => '0.1.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../gorhill/php-fine-diff',
+ 'aliases' => array(),
+ 'reference' => 'b0c9ab136958882c501d461b0ad204f908e19392',
+ 'dev_requirement' => false,
+ ),
+ 'gregwar/captcha' => array(
+ 'pretty_version' => 'v1.3.0',
+ 'version' => '1.3.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../gregwar/captcha',
+ 'aliases' => array(),
+ 'reference' => '4edbcd09fde4353b94ce550f43460eba73baf2cc',
+ 'dev_requirement' => false,
+ ),
+ 'laminas/laminas-escaper' => array(
+ 'pretty_version' => '2.6.1',
+ 'version' => '2.6.1.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../laminas/laminas-escaper',
+ 'aliases' => array(),
+ 'reference' => '25f2a053eadfa92ddacb609dcbbc39362610da70',
+ 'dev_requirement' => false,
+ ),
+ 'laminas/laminas-zendframework-bridge' => array(
+ 'pretty_version' => '1.1.1',
+ 'version' => '1.1.1.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../laminas/laminas-zendframework-bridge',
+ 'aliases' => array(),
+ 'reference' => '6ede70583e101030bcace4dcddd648f760ddf642',
+ 'dev_requirement' => false,
+ ),
+ 'markbaker/complex' => array(
+ 'pretty_version' => '1.5.0',
+ 'version' => '1.5.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../markbaker/complex',
+ 'aliases' => array(),
+ 'reference' => 'c3131244e29c08d44fefb49e0dd35021e9e39dd2',
+ 'dev_requirement' => false,
+ ),
+ 'markbaker/matrix' => array(
+ 'pretty_version' => '1.2.3',
+ 'version' => '1.2.3.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../markbaker/matrix',
+ 'aliases' => array(),
+ 'reference' => '44bb1ab01811116f01fe216ab37d921dccc6c10d',
+ 'dev_requirement' => false,
+ ),
+ 'masterexploder/phpthumb' => array(
+ 'pretty_version' => '2.1',
+ 'version' => '2.1.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../masterexploder/phpthumb',
+ 'aliases' => array(),
+ 'reference' => '01f6aa65aa3d136e175f67da2b4ea3e316770d02',
+ 'dev_requirement' => false,
+ ),
+ 'mobiledetect/mobiledetectlib' => array(
+ 'pretty_version' => '2.8.45',
+ 'version' => '2.8.45.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../mobiledetect/mobiledetectlib',
+ 'aliases' => array(),
+ 'reference' => '96aaebcf4f50d3d2692ab81d2c5132e425bca266',
+ 'dev_requirement' => false,
+ ),
+ 'mustangostang/spyc' => array(
+ 'pretty_version' => '0.6.3',
+ 'version' => '0.6.3.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../mustangostang/spyc',
+ 'aliases' => array(),
+ 'reference' => '4627c838b16550b666d15aeae1e5289dd5b77da0',
+ 'dev_requirement' => false,
+ ),
+ 'overtrue/pinyin' => array(
+ 'pretty_version' => '4.0.5',
+ 'version' => '4.0.5.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../overtrue/pinyin',
+ 'aliases' => array(),
+ 'reference' => '94fdb3ea6eca5677afd9548111d95bedcd5a4086',
+ 'dev_requirement' => false,
+ ),
+ 'paragonie/constant_time_encoding' => array(
+ 'pretty_version' => 'v2.7.0',
+ 'version' => '2.7.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../paragonie/constant_time_encoding',
+ 'aliases' => array(),
+ 'reference' => '52a0d99e69f56b9ec27ace92ba56897fe6993105',
+ 'dev_requirement' => false,
+ ),
+ 'paragonie/random_compat' => array(
+ 'pretty_version' => 'v9.99.100',
+ 'version' => '9.99.100.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../paragonie/random_compat',
+ 'aliases' => array(),
+ 'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
+ 'dev_requirement' => false,
+ ),
+ 'pclzip/pclzip' => array(
+ 'pretty_version' => '2.8.2',
+ 'version' => '2.8.2.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../pclzip/pclzip',
+ 'aliases' => array(),
+ 'reference' => '19dd1de9d3f5fc4d7d70175b4c344dee329f45fd',
+ 'dev_requirement' => false,
+ ),
+ 'phpmailer/phpmailer' => array(
+ 'pretty_version' => 'v6.10.0',
+ 'version' => '6.10.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../phpmailer/phpmailer',
+ 'aliases' => array(),
+ 'reference' => 'bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144',
+ 'dev_requirement' => false,
+ ),
+ 'phpoffice/phpspreadsheet' => array(
+ 'pretty_version' => '1.8.2',
+ 'version' => '1.8.2.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet',
+ 'aliases' => array(),
+ 'reference' => '0c1346a1956347590b7db09533966307d20cb7cc',
+ 'dev_requirement' => false,
+ ),
+ 'phpoffice/phpword' => array(
+ 'pretty_version' => '0.18.3',
+ 'version' => '0.18.3.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../phpoffice/phpword',
+ 'aliases' => array(),
+ 'reference' => 'be0190cd5d8f95b4be08d5853b107aa4e352759a',
+ 'dev_requirement' => false,
+ ),
+ 'phpseclib/phpseclib' => array(
+ 'pretty_version' => '3.0.46',
+ 'version' => '3.0.46.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../phpseclib/phpseclib',
+ 'aliases' => array(),
+ 'reference' => '56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6',
+ 'dev_requirement' => false,
+ ),
+ 'psr/simple-cache' => array(
+ 'pretty_version' => '1.0.1',
+ 'version' => '1.0.1.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../psr/simple-cache',
+ 'aliases' => array(),
+ 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b',
+ 'dev_requirement' => false,
+ ),
+ 'rmccue/requests' => array(
+ 'pretty_version' => 'v2.0.15',
+ 'version' => '2.0.15.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../rmccue/requests',
+ 'aliases' => array(),
+ 'reference' => '877cd66169755899682f1595e057334b40d9d149',
+ 'dev_requirement' => false,
+ ),
+ 'symfony/finder' => array(
+ 'pretty_version' => 'v3.4.47',
+ 'version' => '3.4.47.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../symfony/finder',
+ 'aliases' => array(),
+ 'reference' => 'b6b6ad3db3edb1b4b1c1896b1975fb684994de6e',
+ 'dev_requirement' => false,
+ ),
+ 'zendframework/zend-escaper' => array(
+ 'dev_requirement' => false,
+ 'replaced' => array(
+ 0 => '2.6.1',
+ ),
+ ),
+ ),
+);
diff --git a/lib/vendor/composer/platform_check.php b/lib/vendor/composer/platform_check.php
new file mode 100644
index 0000000000..f79e574be7
--- /dev/null
+++ b/lib/vendor/composer/platform_check.php
@@ -0,0 +1,26 @@
+= 70000)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 7.0.0". You are running ' . PHP_VERSION . '.';
+}
+
+if ($issues) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+ } elseif (!headers_sent()) {
+ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
+ }
+ }
+ trigger_error(
+ 'Composer detected issues in your platform: ' . implode(' ', $issues),
+ E_USER_ERROR
+ );
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/CHANGELOG.md b/lib/vendor/dragonmantank/cron-expression/CHANGELOG.md
new file mode 100644
index 0000000000..4e207aaddb
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/CHANGELOG.md
@@ -0,0 +1,84 @@
+# Change Log
+
+## [2.3.1] - 2020-10-12
+### Added
+- Added support for PHP 8 (#92)
+### Changed
+- N/A
+### Fixed
+- N/A
+
+## [2.3.0] - 2019-03-30
+### Added
+- Added support for DateTimeImmutable via DateTimeInterface
+- Added support for PHP 7.3
+- Started listing projects that use the library
+### Changed
+- Errors should now report a human readable position in the cron expression, instead of starting at 0
+### Fixed
+- N/A
+
+## [2.2.0] - 2018-06-05
+### Added
+- Added support for steps larger than field ranges (#6)
+## Changed
+- N/A
+### Fixed
+- Fixed validation for numbers with leading 0s (#12)
+
+## [2.1.0] - 2018-04-06
+### Added
+- N/A
+### Changed
+- Upgraded to PHPUnit 6 (#2)
+### Fixed
+- Refactored timezones to deal with some inconsistent behavior (#3)
+- Allow ranges and lists in same expression (#5)
+- Fixed regression where literals were not converted to their numerical counterpart (#)
+
+## [2.0.0] - 2017-10-12
+### Added
+- N/A
+
+### Changed
+- Dropped support for PHP 5.x
+- Dropped support for the YEAR field, as it was not part of the cron standard
+
+### Fixed
+- Reworked validation for all the field types
+- Stepping should now work for 1-indexed fields like Month (#153)
+
+## [1.2.0] - 2017-01-22
+### Added
+- Added IDE, CodeSniffer, and StyleCI.IO support
+
+### Changed
+- Switched to PSR-4 Autoloading
+
+### Fixed
+- 0 step expressions are handled better
+- Fixed `DayOfMonth` validation to be more strict
+- Typos
+
+## [1.1.0] - 2016-01-26
+### Added
+- Support for non-hourly offset timezones
+- Checks for valid expressions
+
+### Changed
+- Max Iterations no longer hardcoded for `getRunDate()`
+- Supports DateTimeImmutable for newer PHP verions
+
+### Fixed
+- Fixed looping bug for PHP 7 when determining the last specified weekday of a month
+
+## [1.0.3] - 2013-11-23
+### Added
+- Now supports expressions with any number of extra spaces, tabs, or newlines
+
+### Changed
+- Using static instead of self in `CronExpression::factory`
+
+### Fixed
+- Fixes issue [#28](https://github.com/mtdowling/cron-expression/issues/28) where PHP increments of ranges were failing due to PHP casting hyphens to 0
+- Only set default timezone if the given $currentTime is not a DateTime instance ([#34](https://github.com/mtdowling/cron-expression/issues/34))
diff --git a/lib/vendor/dragonmantank/cron-expression/LICENSE b/lib/vendor/dragonmantank/cron-expression/LICENSE
new file mode 100644
index 0000000000..3e38bbc88d
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011 Michael Dowling , 2016 Chris Tankersley , and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/vendor/dragonmantank/cron-expression/README.md b/lib/vendor/dragonmantank/cron-expression/README.md
new file mode 100644
index 0000000000..8e8021b2a1
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/README.md
@@ -0,0 +1,78 @@
+PHP Cron Expression Parser
+==========================
+
+[](https://packagist.org/packages/dragonmantank/cron-expression) [](https://packagist.org/packages/dragonmantank/cron-expression) [](http://travis-ci.org/dragonmantank/cron-expression)
+
+The PHP cron expression parser can parse a CRON expression, determine if it is
+due to run, calculate the next run date of the expression, and calculate the previous
+run date of the expression. You can calculate dates far into the future or past by
+skipping **n** number of matching dates.
+
+The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9),
+lists (e.g. 1,2,3), **W** to find the nearest weekday for a given day of the month, **L** to
+find the last day of the month, **L** to find the last given weekday of a month, and hash
+(#) to find the nth weekday of a given month.
+
+More information about this fork can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork.
+
+Installing
+==========
+
+Add the dependency to your project:
+
+```bash
+composer require dragonmantank/cron-expression
+```
+
+Usage
+=====
+```php
+isDue();
+echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
+echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s');
+
+// Works with complex expressions
+$cron = Cron\CronExpression::factory('3-59/15 6-12 */15 1 2-5');
+echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
+
+// Calculate a run date two iterations into the future
+$cron = Cron\CronExpression::factory('@daily');
+echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s');
+
+// Calculate a run date relative to a specific time
+$cron = Cron\CronExpression::factory('@monthly');
+echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s');
+```
+
+CRON Expressions
+================
+
+A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows:
+
+ * * * * *
+ - - - - -
+ | | | | |
+ | | | | |
+ | | | | +----- day of week (0 - 7) (Sunday=0 or 7)
+ | | | +---------- month (1 - 12)
+ | | +--------------- day of month (1 - 31)
+ | +-------------------- hour (0 - 23)
+ +------------------------- min (0 - 59)
+
+Requirements
+============
+
+- PHP 7.0+
+- PHPUnit is required to run the unit tests
+- Composer is required to run the unit tests
+
+Projects that Use cron-expression
+=================================
+* Part of the [Laravel Framework](https://github.com/laravel/framework/)
+* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle)
\ No newline at end of file
diff --git a/lib/vendor/dragonmantank/cron-expression/composer.json b/lib/vendor/dragonmantank/cron-expression/composer.json
new file mode 100644
index 0000000000..6fcf8184cd
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/composer.json
@@ -0,0 +1,40 @@
+{
+ "name": "dragonmantank/cron-expression",
+ "type": "library",
+ "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+ "keywords": ["cron", "schedule"],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Chris Tankersley",
+ "email": "chris@ctankersley.com",
+ "homepage": "https://github.com/dragonmantank"
+ }
+ ],
+ "require": {
+ "php": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cron\\": "src/Cron/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\": "tests/Cron/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php
new file mode 100644
index 0000000000..8b1072ab2c
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php
@@ -0,0 +1,286 @@
+fullRange = range($this->rangeStart, $this->rangeEnd);
+ }
+
+ /**
+ * Check to see if a field is satisfied by a value
+ *
+ * @param string $dateValue Date value to check
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isSatisfied($dateValue, $value)
+ {
+ if ($this->isIncrementsOfRanges($value)) {
+ return $this->isInIncrementsOfRanges($dateValue, $value);
+ } elseif ($this->isRange($value)) {
+ return $this->isInRange($dateValue, $value);
+ }
+
+ return $value == '*' || $dateValue == $value;
+ }
+
+ /**
+ * Check if a value is a range
+ *
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isRange($value)
+ {
+ return strpos($value, '-') !== false;
+ }
+
+ /**
+ * Check if a value is an increments of ranges
+ *
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isIncrementsOfRanges($value)
+ {
+ return strpos($value, '/') !== false;
+ }
+
+ /**
+ * Test if a value is within a range
+ *
+ * @param string $dateValue Set date value
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isInRange($dateValue, $value)
+ {
+ $parts = array_map(function($value) {
+ $value = trim($value);
+ $value = $this->convertLiterals($value);
+ return $value;
+ },
+ explode('-', $value, 2)
+ );
+
+
+ return $dateValue >= $parts[0] && $dateValue <= $parts[1];
+ }
+
+ /**
+ * Test if a value is within an increments of ranges (offset[-to]/step size)
+ *
+ * @param string $dateValue Set date value
+ * @param string $value Value to test
+ *
+ * @return bool
+ */
+ public function isInIncrementsOfRanges($dateValue, $value)
+ {
+ $chunks = array_map('trim', explode('/', $value, 2));
+ $range = $chunks[0];
+ $step = isset($chunks[1]) ? $chunks[1] : 0;
+
+ // No step or 0 steps aren't cool
+ if (is_null($step) || '0' === $step || 0 === $step) {
+ return false;
+ }
+
+ // Expand the * to a full range
+ if ('*' == $range) {
+ $range = $this->rangeStart . '-' . $this->rangeEnd;
+ }
+
+ // Generate the requested small range
+ $rangeChunks = explode('-', $range, 2);
+ $rangeStart = $rangeChunks[0];
+ $rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart;
+
+ if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) {
+ throw new \OutOfRangeException('Invalid range start requested');
+ }
+
+ if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) {
+ throw new \OutOfRangeException('Invalid range end requested');
+ }
+
+ // Steps larger than the range need to wrap around and be handled slightly differently than smaller steps
+ if ($step >= $this->rangeEnd) {
+ $thisRange = [$this->fullRange[$step % count($this->fullRange)]];
+ } else {
+ $thisRange = range($rangeStart, $rangeEnd, $step);
+ }
+
+ return in_array($dateValue, $thisRange);
+ }
+
+ /**
+ * Returns a range of values for the given cron expression
+ *
+ * @param string $expression The expression to evaluate
+ * @param int $max Maximum offset for range
+ *
+ * @return array
+ */
+ public function getRangeForExpression($expression, $max)
+ {
+ $values = array();
+ $expression = $this->convertLiterals($expression);
+
+ if (strpos($expression, ',') !== false) {
+ $ranges = explode(',', $expression);
+ $values = [];
+ foreach ($ranges as $range) {
+ $expanded = $this->getRangeForExpression($range, $this->rangeEnd);
+ $values = array_merge($values, $expanded);
+ }
+ return $values;
+ }
+
+ if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) {
+ if (!$this->isIncrementsOfRanges($expression)) {
+ list ($offset, $to) = explode('-', $expression);
+ $offset = $this->convertLiterals($offset);
+ $to = $this->convertLiterals($to);
+ $stepSize = 1;
+ }
+ else {
+ $range = array_map('trim', explode('/', $expression, 2));
+ $stepSize = isset($range[1]) ? $range[1] : 0;
+ $range = $range[0];
+ $range = explode('-', $range, 2);
+ $offset = $range[0];
+ $to = isset($range[1]) ? $range[1] : $max;
+ }
+ $offset = $offset == '*' ? $this->rangeStart : $offset;
+ if ($stepSize >= $this->rangeEnd) {
+ $values = [$this->fullRange[$stepSize % count($this->fullRange)]];
+ } else {
+ for ($i = $offset; $i <= $to; $i += $stepSize) {
+ $values[] = (int)$i;
+ }
+ }
+ sort($values);
+ }
+ else {
+ $values = array($expression);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Convert literal
+ *
+ * @param string $value
+ * @return string
+ */
+ protected function convertLiterals($value)
+ {
+ if (count($this->literals)) {
+ $key = array_search($value, $this->literals);
+ if ($key !== false) {
+ return (string) $key;
+ }
+ }
+
+ return $value;
+ }
+
+ /**
+ * Checks to see if a value is valid for the field
+ *
+ * @param string $value
+ * @return bool
+ */
+ public function validate($value)
+ {
+ $value = $this->convertLiterals($value);
+
+ // All fields allow * as a valid value
+ if ('*' === $value) {
+ return true;
+ }
+
+ if (strpos($value, '/') !== false) {
+ list($range, $step) = explode('/', $value);
+ return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
+ }
+
+ // Validate each chunk of a list individually
+ if (strpos($value, ',') !== false) {
+ foreach (explode(',', $value) as $listItem) {
+ if (!$this->validate($listItem)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ if (strpos($value, '-') !== false) {
+ if (substr_count($value, '-') > 1) {
+ return false;
+ }
+
+ $chunks = explode('-', $value);
+ $chunks[0] = $this->convertLiterals($chunks[0]);
+ $chunks[1] = $this->convertLiterals($chunks[1]);
+
+ if ('*' == $chunks[0] || '*' == $chunks[1]) {
+ return false;
+ }
+
+ return $this->validate($chunks[0]) && $this->validate($chunks[1]);
+ }
+
+ if (!is_numeric($value)) {
+ return false;
+ }
+
+ if (is_float($value) || strpos($value, '.') !== false) {
+ return false;
+ }
+
+ // We should have a numeric by now, so coerce this into an integer
+ $value = (int) $value;
+
+ return in_array($value, $this->fullRange, true);
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php
new file mode 100644
index 0000000000..594b435825
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php
@@ -0,0 +1,413 @@
+ '0 0 1 1 *',
+ '@annually' => '0 0 1 1 *',
+ '@monthly' => '0 0 1 * *',
+ '@weekly' => '0 0 * * 0',
+ '@daily' => '0 0 * * *',
+ '@hourly' => '0 * * * *'
+ );
+
+ if (isset($mappings[$expression])) {
+ $expression = $mappings[$expression];
+ }
+
+ return new static($expression, $fieldFactory ?: new FieldFactory());
+ }
+
+ /**
+ * Validate a CronExpression.
+ *
+ * @param string $expression The CRON expression to validate.
+ *
+ * @return bool True if a valid CRON expression was passed. False if not.
+ * @see \Cron\CronExpression::factory
+ */
+ public static function isValidExpression($expression)
+ {
+ try {
+ self::factory($expression);
+ } catch (InvalidArgumentException $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse a CRON expression
+ *
+ * @param string $expression CRON expression (e.g. '8 * * * *')
+ * @param FieldFactory|null $fieldFactory Factory to create cron fields
+ */
+ public function __construct($expression, FieldFactory $fieldFactory = null)
+ {
+ $this->fieldFactory = $fieldFactory;
+ $this->setExpression($expression);
+ }
+
+ /**
+ * Set or change the CRON expression
+ *
+ * @param string $value CRON expression (e.g. 8 * * * *)
+ *
+ * @return CronExpression
+ * @throws \InvalidArgumentException if not a valid CRON expression
+ */
+ public function setExpression($value)
+ {
+ $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
+ if (count($this->cronParts) < 5) {
+ throw new InvalidArgumentException(
+ $value . ' is not a valid CRON expression'
+ );
+ }
+
+ foreach ($this->cronParts as $position => $part) {
+ $this->setPart($position, $part);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set part of the CRON expression
+ *
+ * @param int $position The position of the CRON expression to set
+ * @param string $value The value to set
+ *
+ * @return CronExpression
+ * @throws \InvalidArgumentException if the value is not valid for the part
+ */
+ public function setPart($position, $value)
+ {
+ if (!$this->fieldFactory->getField($position)->validate($value)) {
+ throw new InvalidArgumentException(
+ 'Invalid CRON field value ' . $value . ' at position ' . $position
+ );
+ }
+
+ $this->cronParts[$position] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Set max iteration count for searching next run dates
+ *
+ * @param int $maxIterationCount Max iteration count when searching for next run date
+ *
+ * @return CronExpression
+ */
+ public function setMaxIterationCount($maxIterationCount)
+ {
+ $this->maxIterationCount = $maxIterationCount;
+
+ return $this;
+ }
+
+ /**
+ * Get a next run date relative to the current date or a specific date
+ *
+ * @param string|\DateTimeInterface $currentTime Relative calculation date
+ * @param int $nth Number of matches to skip before returning a
+ * matching next run date. 0, the default, will return the
+ * current date and time if the next run date falls on the
+ * current date and time. Setting this value to 1 will
+ * skip the first match and go to the second match.
+ * Setting this value to 2 will skip the first 2
+ * matches and so on.
+ * @param bool $allowCurrentDate Set to TRUE to return the current date if
+ * it matches the cron expression.
+ * @param null|string $timeZone TimeZone to use instead of the system default
+ *
+ * @return \DateTime
+ * @throws \RuntimeException on too many iterations
+ */
+ public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
+ {
+ return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
+ }
+
+ /**
+ * Get a previous run date relative to the current date or a specific date
+ *
+ * @param string|\DateTimeInterface $currentTime Relative calculation date
+ * @param int $nth Number of matches to skip before returning
+ * @param bool $allowCurrentDate Set to TRUE to return the
+ * current date if it matches the cron expression
+ * @param null|string $timeZone TimeZone to use instead of the system default
+ *
+ * @return \DateTime
+ * @throws \RuntimeException on too many iterations
+ * @see \Cron\CronExpression::getNextRunDate
+ */
+ public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
+ {
+ return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone);
+ }
+
+ /**
+ * Get multiple run dates starting at the current date or a specific date
+ *
+ * @param int $total Set the total number of dates to calculate
+ * @param string|\DateTimeInterface $currentTime Relative calculation date
+ * @param bool $invert Set to TRUE to retrieve previous dates
+ * @param bool $allowCurrentDate Set to TRUE to return the
+ * current date if it matches the cron expression
+ * @param null|string $timeZone TimeZone to use instead of the system default
+ *
+ * @return \DateTime[] Returns an array of run dates
+ */
+ public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null)
+ {
+ $matches = array();
+ for ($i = 0; $i < max(0, $total); $i++) {
+ try {
+ $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone);
+ } catch (RuntimeException $e) {
+ break;
+ }
+ }
+
+ return $matches;
+ }
+
+ /**
+ * Get all or part of the CRON expression
+ *
+ * @param string $part Specify the part to retrieve or NULL to get the full
+ * cron schedule string.
+ *
+ * @return string|null Returns the CRON expression, a part of the
+ * CRON expression, or NULL if the part was specified but not found
+ */
+ public function getExpression($part = null)
+ {
+ if (null === $part) {
+ return implode(' ', $this->cronParts);
+ } elseif (array_key_exists($part, $this->cronParts)) {
+ return $this->cronParts[$part];
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper method to output the full expression.
+ *
+ * @return string Full CRON expression
+ */
+ public function __toString()
+ {
+ return $this->getExpression();
+ }
+
+ /**
+ * Determine if the cron is due to run based on the current date or a
+ * specific date. This method assumes that the current number of
+ * seconds are irrelevant, and should be called once per minute.
+ *
+ * @param string|\DateTimeInterface $currentTime Relative calculation date
+ * @param null|string $timeZone TimeZone to use instead of the system default
+ *
+ * @return bool Returns TRUE if the cron is due to run or FALSE if not
+ */
+ public function isDue($currentTime = 'now', $timeZone = null)
+ {
+ $timeZone = $this->determineTimeZone($currentTime, $timeZone);
+
+ if ('now' === $currentTime) {
+ $currentTime = new DateTime();
+ } elseif ($currentTime instanceof DateTime) {
+ //
+ } elseif ($currentTime instanceof DateTimeImmutable) {
+ $currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
+ } else {
+ $currentTime = new DateTime($currentTime);
+ }
+ $currentTime->setTimeZone(new DateTimeZone($timeZone));
+
+ // drop the seconds to 0
+ $currentTime = DateTime::createFromFormat('Y-m-d H:i', $currentTime->format('Y-m-d H:i'));
+
+ try {
+ return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp();
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Get the next or previous run date of the expression relative to a date
+ *
+ * @param string|\DateTimeInterface $currentTime Relative calculation date
+ * @param int $nth Number of matches to skip before returning
+ * @param bool $invert Set to TRUE to go backwards in time
+ * @param bool $allowCurrentDate Set to TRUE to return the
+ * current date if it matches the cron expression
+ * @param string|null $timeZone TimeZone to use instead of the system default
+ *
+ * @return \DateTime
+ * @throws \RuntimeException on too many iterations
+ */
+ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null)
+ {
+ $timeZone = $this->determineTimeZone($currentTime, $timeZone);
+
+ if ($currentTime instanceof DateTime) {
+ $currentDate = clone $currentTime;
+ } elseif ($currentTime instanceof DateTimeImmutable) {
+ $currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
+ } else {
+ $currentDate = new DateTime($currentTime ?: 'now');
+ }
+
+ $currentDate->setTimeZone(new DateTimeZone($timeZone));
+ $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
+ $nextRun = clone $currentDate;
+ $nth = (int) $nth;
+
+ // We don't have to satisfy * or null fields
+ $parts = array();
+ $fields = array();
+ foreach (self::$order as $position) {
+ $part = $this->getExpression($position);
+ if (null === $part || '*' === $part) {
+ continue;
+ }
+ $parts[$position] = $part;
+ $fields[$position] = $this->fieldFactory->getField($position);
+ }
+
+ // Set a hard limit to bail on an impossible date
+ for ($i = 0; $i < $this->maxIterationCount; $i++) {
+
+ foreach ($parts as $position => $part) {
+ $satisfied = false;
+ // Get the field object used to validate this part
+ $field = $fields[$position];
+ // Check if this is singular or a list
+ if (strpos($part, ',') === false) {
+ $satisfied = $field->isSatisfiedBy($nextRun, $part);
+ } else {
+ foreach (array_map('trim', explode(',', $part)) as $listPart) {
+ if ($field->isSatisfiedBy($nextRun, $listPart)) {
+ $satisfied = true;
+ break;
+ }
+ }
+ }
+
+ // If the field is not satisfied, then start over
+ if (!$satisfied) {
+ $field->increment($nextRun, $invert, $part);
+ continue 2;
+ }
+ }
+
+ // Skip this match if needed
+ if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
+ $this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null);
+ continue;
+ }
+
+ return $nextRun;
+ }
+
+ // @codeCoverageIgnoreStart
+ throw new RuntimeException('Impossible CRON expression');
+ // @codeCoverageIgnoreEnd
+ }
+
+ /**
+ * Workout what timeZone should be used.
+ *
+ * @param string|\DateTimeInterface $currentTime Relative calculation date
+ * @param string|null $timeZone TimeZone to use instead of the system default
+ *
+ * @return string
+ */
+ protected function determineTimeZone($currentTime, $timeZone)
+ {
+ if (! is_null($timeZone)) {
+ return $timeZone;
+ }
+
+ if ($currentTime instanceOf DateTimeInterface) {
+ return $currentTime->getTimeZone()->getName();
+ }
+
+ return date_default_timezone_get();
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php
new file mode 100644
index 0000000000..d4552e06be
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php
@@ -0,0 +1,145 @@
+
+ */
+class DayOfMonthField extends AbstractField
+{
+ /**
+ * @inheritDoc
+ */
+ protected $rangeStart = 1;
+
+ /**
+ * @inheritDoc
+ */
+ protected $rangeEnd = 31;
+
+ /**
+ * Get the nearest day of the week for a given day in a month
+ *
+ * @param int $currentYear Current year
+ * @param int $currentMonth Current month
+ * @param int $targetDay Target day of the month
+ *
+ * @return \DateTime Returns the nearest date
+ */
+ private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
+ {
+ $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
+ $target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday");
+ $currentWeekday = (int) $target->format('N');
+
+ if ($currentWeekday < 6) {
+ return $target;
+ }
+
+ $lastDayOfMonth = $target->format('t');
+
+ foreach (array(-1, 1, -2, 2) as $i) {
+ $adjusted = $targetDay + $i;
+ if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
+ $target->setDate($currentYear, $currentMonth, $adjusted);
+ if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
+ return $target;
+ }
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isSatisfiedBy(DateTimeInterface $date, $value)
+ {
+ // ? states that the field value is to be skipped
+ if ($value == '?') {
+ return true;
+ }
+
+ $fieldValue = $date->format('d');
+
+ // Check to see if this is the last day of the month
+ if ($value == 'L') {
+ return $fieldValue == $date->format('t');
+ }
+
+ // Check to see if this is the nearest weekday to a particular value
+ if (strpos($value, 'W')) {
+ // Parse the target day
+ $targetDay = substr($value, 0, strpos($value, 'W'));
+ // Find out if the current day is the nearest day of the week
+ return $date->format('j') == self::getNearestWeekday(
+ $date->format('Y'),
+ $date->format('m'),
+ $targetDay
+ )->format('j');
+ }
+
+ return $this->isSatisfied($date->format('d'), $value);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param \DateTime|\DateTimeImmutable &$date
+ */
+ public function increment(DateTimeInterface &$date, $invert = false)
+ {
+ if ($invert) {
+ $date = $date->modify('previous day')->setTime(23, 59);
+ } else {
+ $date = $date->modify('next day')->setTime(0, 0);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate($value)
+ {
+ $basicChecks = parent::validate($value);
+
+ // Validate that a list don't have W or L
+ if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) {
+ return false;
+ }
+
+ if (!$basicChecks) {
+
+ if ($value === 'L') {
+ return true;
+ }
+
+ if (preg_match('/^(.*)W$/', $value, $matches)) {
+ return $this->validate($matches[1]);
+ }
+
+ return false;
+ }
+
+ return $basicChecks;
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php
new file mode 100644
index 0000000000..d4ba3156ec
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php
@@ -0,0 +1,196 @@
+ 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN'];
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->nthRange = range(1, 5);
+ parent::__construct();
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param \DateTime|\DateTimeImmutable $date
+ */
+ public function isSatisfiedBy(DateTimeInterface $date, $value)
+ {
+ if ($value == '?') {
+ return true;
+ }
+
+ // Convert text day of the week values to integers
+ $value = $this->convertLiterals($value);
+
+ $currentYear = $date->format('Y');
+ $currentMonth = $date->format('m');
+ $lastDayOfMonth = $date->format('t');
+
+ // Find out if this is the last specific weekday of the month
+ if (strpos($value, 'L')) {
+ $weekday = (int) $this->convertLiterals(substr($value, 0, strpos($value, 'L')));
+ $weekday %= 7;
+
+ $tdate = clone $date;
+ $tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
+ while ($tdate->format('w') != $weekday) {
+ $tdateClone = new DateTime();
+ $tdate = $tdateClone
+ ->setTimezone($tdate->getTimezone())
+ ->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
+ }
+
+ return $date->format('j') == $lastDayOfMonth;
+ }
+
+ // Handle # hash tokens
+ if (strpos($value, '#')) {
+ list($weekday, $nth) = explode('#', $value);
+
+ if (!is_numeric($nth)) {
+ throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given");
+ } else {
+ $nth = (int) $nth;
+ }
+
+ // 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601
+ if ($weekday === '0') {
+ $weekday = 7;
+ }
+
+ $weekday = $this->convertLiterals($weekday);
+
+ // Validate the hash fields
+ if ($weekday < 0 || $weekday > 7) {
+ throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given");
+ }
+
+ if (!in_array($nth, $this->nthRange)) {
+ throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given");
+ }
+
+ // The current weekday must match the targeted weekday to proceed
+ if ($date->format('N') != $weekday) {
+ return false;
+ }
+
+ $tdate = clone $date;
+ $tdate = $tdate->setDate($currentYear, $currentMonth, 1);
+ $dayCount = 0;
+ $currentDay = 1;
+ while ($currentDay < $lastDayOfMonth + 1) {
+ if ($tdate->format('N') == $weekday) {
+ if (++$dayCount >= $nth) {
+ break;
+ }
+ }
+ $tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay);
+ }
+
+ return $date->format('j') == $currentDay;
+ }
+
+ // Handle day of the week values
+ if (strpos($value, '-')) {
+ $parts = explode('-', $value);
+ if ($parts[0] == '7') {
+ $parts[0] = '0';
+ } elseif ($parts[1] == '0') {
+ $parts[1] = '7';
+ }
+ $value = implode('-', $parts);
+ }
+
+ // Test to see which Sunday to use -- 0 == 7 == Sunday
+ $format = in_array(7, str_split($value)) ? 'N' : 'w';
+ $fieldValue = $date->format($format);
+
+ return $this->isSatisfied($fieldValue, $value);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param \DateTime|\DateTimeImmutable &$date
+ */
+ public function increment(DateTimeInterface &$date, $invert = false)
+ {
+ if ($invert) {
+ $date = $date->modify('-1 day')->setTime(23, 59, 0);
+ } else {
+ $date = $date->modify('+1 day')->setTime(0, 0, 0);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate($value)
+ {
+ $basicChecks = parent::validate($value);
+
+ if (!$basicChecks) {
+ // Handle the # value
+ if (strpos($value, '#') !== false) {
+ $chunks = explode('#', $value);
+ $chunks[0] = $this->convertLiterals($chunks[0]);
+
+ if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) {
+ return true;
+ }
+ }
+
+ if (preg_match('/^(.*)L$/', $value, $matches)) {
+ return $this->validate($matches[1]);
+ }
+
+ return false;
+ }
+
+ return $basicChecks;
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php
new file mode 100644
index 0000000000..545e4b837c
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php
@@ -0,0 +1,54 @@
+fields[$position])) {
+ switch ($position) {
+ case 0:
+ $this->fields[$position] = new MinutesField();
+ break;
+ case 1:
+ $this->fields[$position] = new HoursField();
+ break;
+ case 2:
+ $this->fields[$position] = new DayOfMonthField();
+ break;
+ case 3:
+ $this->fields[$position] = new MonthField();
+ break;
+ case 4:
+ $this->fields[$position] = new DayOfWeekField();
+ break;
+ default:
+ throw new InvalidArgumentException(
+ ($position + 1) . ' is not a valid position'
+ );
+ }
+ }
+
+ return $this->fields[$position];
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/FieldInterface.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/FieldInterface.php
new file mode 100644
index 0000000000..f8366eae7b
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/FieldInterface.php
@@ -0,0 +1,41 @@
+isSatisfied($date->format('H'), $value);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param \DateTime|\DateTimeImmutable &$date
+ * @param string|null $parts
+ */
+ public function increment(DateTimeInterface &$date, $invert = false, $parts = null)
+ {
+ // Change timezone to UTC temporarily. This will
+ // allow us to go back or forwards and hour even
+ // if DST will be changed between the hours.
+ if (is_null($parts) || $parts == '*') {
+ $timezone = $date->getTimezone();
+ $date = $date->setTimezone(new DateTimeZone('UTC'));
+ $date = $date->modify(($invert ? '-' : '+') . '1 hour');
+ $date = $date->setTimezone($timezone);
+
+ $date = $date->setTime($date->format('H'), $invert ? 59 : 0);
+ return $this;
+ }
+
+ $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
+ $hours = array();
+ foreach ($parts as $part) {
+ $hours = array_merge($hours, $this->getRangeForExpression($part, 23));
+ }
+
+ $current_hour = $date->format('H');
+ $position = $invert ? count($hours) - 1 : 0;
+ if (count($hours) > 1) {
+ for ($i = 0; $i < count($hours) - 1; $i++) {
+ if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) ||
+ ($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) {
+ $position = $invert ? $i : $i + 1;
+ break;
+ }
+ }
+ }
+
+ $hour = $hours[$position];
+ if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) {
+ $date = $date->modify(($invert ? '-' : '+') . '1 day');
+ $date = $date->setTime($invert ? 23 : 0, $invert ? 59 : 0);
+ }
+ else {
+ $date = $date->setTime($hour, $invert ? 59 : 0);
+ }
+
+ return $this;
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php
new file mode 100644
index 0000000000..fecc9b6da7
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php
@@ -0,0 +1,75 @@
+isSatisfied($date->format('i'), $value);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param \DateTime|\DateTimeImmutable &$date
+ * @param string|null $parts
+ */
+ public function increment(DateTimeInterface &$date, $invert = false, $parts = null)
+ {
+ if (is_null($parts)) {
+ $date = $date->modify(($invert ? '-' : '+') . '1 minute');
+ return $this;
+ }
+
+ $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
+ $minutes = array();
+ foreach ($parts as $part) {
+ $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));
+ }
+
+ $current_minute = $date->format('i');
+ $position = $invert ? count($minutes) - 1 : 0;
+ if (count($minutes) > 1) {
+ for ($i = 0; $i < count($minutes) - 1; $i++) {
+ if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) ||
+ ($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) {
+ $position = $invert ? $i : $i + 1;
+ break;
+ }
+ }
+ }
+
+ if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) {
+ $date = $date->modify(($invert ? '-' : '+') . '1 hour');
+ $date = $date->setTime($date->format('H'), $invert ? 59 : 0);
+ }
+ else {
+ $date = $date->setTime($date->format('H'), $minutes[$position]);
+ }
+
+ return $this;
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php b/lib/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php
new file mode 100644
index 0000000000..afc9caff23
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php
@@ -0,0 +1,59 @@
+ 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL',
+ 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC'];
+
+ /**
+ * @inheritDoc
+ */
+ public function isSatisfiedBy(DateTimeInterface $date, $value)
+ {
+ if ($value == '?') {
+ return true;
+ }
+
+ $value = $this->convertLiterals($value);
+
+ return $this->isSatisfied($date->format('m'), $value);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param \DateTime|\DateTimeImmutable &$date
+ */
+ public function increment(DateTimeInterface &$date, $invert = false)
+ {
+ if ($invert) {
+ $date = $date->modify('last day of previous month')->setTime(23, 59);
+ } else {
+ $date = $date->modify('first day of next month')->setTime(0, 0);
+ }
+
+ return $this;
+ }
+
+
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/AbstractFieldTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/AbstractFieldTest.php
new file mode 100644
index 0000000000..38114392eb
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/AbstractFieldTest.php
@@ -0,0 +1,139 @@
+
+ */
+class AbstractFieldTest extends TestCase
+{
+ /**
+ * @covers \Cron\AbstractField::isRange
+ */
+ public function testTestsIfRange()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isRange('1-2'));
+ $this->assertFalse($f->isRange('2'));
+ }
+
+ /**
+ * @covers \Cron\AbstractField::isIncrementsOfRanges
+ */
+ public function testTestsIfIncrementsOfRanges()
+ {
+ $f = new DayOfWeekField();
+ $this->assertFalse($f->isIncrementsOfRanges('1-2'));
+ $this->assertTrue($f->isIncrementsOfRanges('1/2'));
+ $this->assertTrue($f->isIncrementsOfRanges('*/2'));
+ $this->assertTrue($f->isIncrementsOfRanges('3-12/2'));
+ }
+
+ /**
+ * @covers \Cron\AbstractField::isInRange
+ */
+ public function testTestsIfInRange()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isInRange('1', '1-2'));
+ $this->assertTrue($f->isInRange('2', '1-2'));
+ $this->assertTrue($f->isInRange('5', '4-12'));
+ $this->assertFalse($f->isInRange('3', '4-12'));
+ $this->assertFalse($f->isInRange('13', '4-12'));
+ }
+
+ /**
+ * @covers \Cron\AbstractField::isInIncrementsOfRanges
+ */
+ public function testTestsIfInIncrementsOfRangesOnZeroStartRange()
+ {
+ $f = new MinutesField();
+ $this->assertTrue($f->isInIncrementsOfRanges('3', '3-59/2'));
+ $this->assertTrue($f->isInIncrementsOfRanges('13', '3-59/2'));
+ $this->assertTrue($f->isInIncrementsOfRanges('15', '3-59/2'));
+ $this->assertTrue($f->isInIncrementsOfRanges('14', '*/2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('2', '3-59/13'));
+ $this->assertFalse($f->isInIncrementsOfRanges('14', '*/13'));
+ $this->assertFalse($f->isInIncrementsOfRanges('14', '3-59/2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('3', '2-59'));
+ $this->assertFalse($f->isInIncrementsOfRanges('3', '2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('3', '*'));
+ $this->assertFalse($f->isInIncrementsOfRanges('0', '*/0'));
+ $this->assertFalse($f->isInIncrementsOfRanges('1', '*/0'));
+
+ $this->assertTrue($f->isInIncrementsOfRanges('4', '4/1'));
+ $this->assertFalse($f->isInIncrementsOfRanges('14', '4/1'));
+ $this->assertFalse($f->isInIncrementsOfRanges('34', '4/1'));
+ }
+
+ /**
+ * @covers \Cron\AbstractField::isInIncrementsOfRanges
+ */
+ public function testTestsIfInIncrementsOfRangesOnOneStartRange()
+ {
+ $f = new MonthField();
+ $this->assertTrue($f->isInIncrementsOfRanges('3', '3-12/2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('13', '3-12/2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('15', '3-12/2'));
+ $this->assertTrue($f->isInIncrementsOfRanges('3', '*/2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('3', '*/3'));
+ $this->assertTrue($f->isInIncrementsOfRanges('7', '*/3'));
+ $this->assertFalse($f->isInIncrementsOfRanges('14', '3-12/2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('3', '2-12'));
+ $this->assertFalse($f->isInIncrementsOfRanges('3', '2'));
+ $this->assertFalse($f->isInIncrementsOfRanges('3', '*'));
+ $this->assertFalse($f->isInIncrementsOfRanges('0', '*/0'));
+ $this->assertFalse($f->isInIncrementsOfRanges('1', '*/0'));
+
+ $this->assertTrue($f->isInIncrementsOfRanges('4', '4/1'));
+ $this->assertFalse($f->isInIncrementsOfRanges('14', '4/1'));
+ $this->assertFalse($f->isInIncrementsOfRanges('34', '4/1'));
+ }
+
+ /**
+ * @covers \Cron\AbstractField::isSatisfied
+ */
+ public function testTestsIfSatisfied()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isSatisfied('12', '3-13'));
+ $this->assertFalse($f->isSatisfied('15', '3-7/2'));
+ $this->assertTrue($f->isSatisfied('12', '*'));
+ $this->assertTrue($f->isSatisfied('12', '12'));
+ $this->assertFalse($f->isSatisfied('12', '3-11'));
+ $this->assertFalse($f->isSatisfied('12', '3-7/2'));
+ $this->assertFalse($f->isSatisfied('12', '11'));
+ }
+
+ /**
+ * Allows ranges and lists to coexist in the same expression
+ *
+ * @see https://github.com/dragonmantank/cron-expression/issues/5
+ */
+ public function testAllowRangesAndLists()
+ {
+ $expression = '5-7,11-13';
+ $f = new HoursField();
+ $this->assertTrue($f->validate($expression));
+ }
+
+ /**
+ * Makes sure that various types of ranges expand out properly
+ *
+ * @see https://github.com/dragonmantank/cron-expression/issues/5
+ */
+ public function testGetRangeForExpressionExpandsCorrectly()
+ {
+ $f = new HoursField();
+ $this->assertSame([5, 6, 7, 11, 12, 13], $f->getRangeForExpression('5-7,11-13', 23));
+ $this->assertSame(['5', '6', '7', '11', '12', '13'], $f->getRangeForExpression('5,6,7,11,12,13', 23));
+ $this->assertSame([0, 6, 12, 18], $f->getRangeForExpression('*/6', 23));
+ $this->assertSame([5, 11], $f->getRangeForExpression('5-13/6', 23));
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/CronExpressionTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/CronExpressionTest.php
new file mode 100644
index 0000000000..8810d43da6
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/CronExpressionTest.php
@@ -0,0 +1,589 @@
+
+ */
+class CronExpressionTest extends TestCase
+{
+ /**
+ * @covers \Cron\CronExpression::factory
+ */
+ public function testFactoryRecognizesTemplates()
+ {
+ $this->assertSame('0 0 1 1 *', CronExpression::factory('@annually')->getExpression());
+ $this->assertSame('0 0 1 1 *', CronExpression::factory('@yearly')->getExpression());
+ $this->assertSame('0 0 * * 0', CronExpression::factory('@weekly')->getExpression());
+ }
+
+ /**
+ * @covers \Cron\CronExpression::__construct
+ * @covers \Cron\CronExpression::getExpression
+ * @covers \Cron\CronExpression::__toString
+ */
+ public function testParsesCronSchedule()
+ {
+ // '2010-09-10 12:00:00'
+ $cron = CronExpression::factory('1 2-4 * 4,5,6 */3');
+ $this->assertSame('1', $cron->getExpression(CronExpression::MINUTE));
+ $this->assertSame('2-4', $cron->getExpression(CronExpression::HOUR));
+ $this->assertSame('*', $cron->getExpression(CronExpression::DAY));
+ $this->assertSame('4,5,6', $cron->getExpression(CronExpression::MONTH));
+ $this->assertSame('*/3', $cron->getExpression(CronExpression::WEEKDAY));
+ $this->assertSame('1 2-4 * 4,5,6 */3', $cron->getExpression());
+ $this->assertSame('1 2-4 * 4,5,6 */3', (string) $cron);
+ $this->assertNull($cron->getExpression('foo'));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::__construct
+ * @covers \Cron\CronExpression::getExpression
+ * @covers \Cron\CronExpression::__toString
+ */
+ public function testParsesCronScheduleThrowsAnException()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid CRON field value A at position 0');
+
+ CronExpression::factory('A 1 2 3 4');
+ }
+
+ /**
+ * @covers \Cron\CronExpression::__construct
+ * @covers \Cron\CronExpression::getExpression
+ * @dataProvider scheduleWithDifferentSeparatorsProvider
+ */
+ public function testParsesCronScheduleWithAnySpaceCharsAsSeparators($schedule, array $expected)
+ {
+ $cron = CronExpression::factory($schedule);
+ $this->assertSame($expected[0], $cron->getExpression(CronExpression::MINUTE));
+ $this->assertSame($expected[1], $cron->getExpression(CronExpression::HOUR));
+ $this->assertSame($expected[2], $cron->getExpression(CronExpression::DAY));
+ $this->assertSame($expected[3], $cron->getExpression(CronExpression::MONTH));
+ $this->assertSame($expected[4], $cron->getExpression(CronExpression::WEEKDAY));
+ }
+
+ /**
+ * Data provider for testParsesCronScheduleWithAnySpaceCharsAsSeparators
+ *
+ * @return array
+ */
+ public static function scheduleWithDifferentSeparatorsProvider()
+ {
+ return array(
+ array("*\t*\t*\t*\t*\t", array('*', '*', '*', '*', '*', '*')),
+ array("* * * * * ", array('*', '*', '*', '*', '*', '*')),
+ array("* \t * \t * \t * \t * \t", array('*', '*', '*', '*', '*', '*')),
+ array("*\t \t*\t \t*\t \t*\t \t*\t \t", array('*', '*', '*', '*', '*', '*')),
+ );
+ }
+
+ /**
+ * @covers \Cron\CronExpression::__construct
+ * @covers \Cron\CronExpression::setExpression
+ * @covers \Cron\CronExpression::setPart
+ */
+ public function testInvalidCronsWillFail()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ // Only four values
+ $cron = CronExpression::factory('* * * 1');
+ }
+
+ /**
+ * @covers \Cron\CronExpression::setPart
+ */
+ public function testInvalidPartsWillFail()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ // Only four values
+ $cron = CronExpression::factory('* * * * *');
+ $cron->setPart(1, 'abc');
+ }
+
+ /**
+ * Data provider for cron schedule
+ *
+ * @return array
+ */
+ public function scheduleProvider()
+ {
+ return array(
+ array('*/2 */2 * * *', '2015-08-10 21:47:27', '2015-08-10 22:00:00', false),
+ array('* * * * *', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true),
+ array('* 20,21,22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true),
+ // Handles CSV values
+ array('* 20,22 * * *', '2015-08-10 21:50:00', '2015-08-10 22:00:00', false),
+ // CSV values can be complex
+ array('7-9 * */9 * *', '2015-08-10 22:02:33', '2015-08-10 22:07:00', false),
+ // 15th minute, of the second hour, every 15 days, in January, every Friday
+ array('1 * * * 7', '2015-08-10 21:47:27', '2015-08-16 00:01:00', false),
+ // Test with exact times
+ array('47 21 * * *', strtotime('2015-08-10 21:47:30'), '2015-08-10 21:47:00', true),
+ // Test Day of the week (issue #1)
+ // According cron implementation, 0|7 = sunday, 1 => monday, etc
+ array('* * * * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
+ array('* * * * 7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
+ array('* * * * 1', strtotime('2011-06-15 23:09:00'), '2011-06-20 00:00:00', false),
+ // Should return the sunday date as 7 equals 0
+ array('0 0 * * MON,SUN', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
+ array('0 0 * * 1,7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
+ array('0 0 * * 0-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
+ array('0 0 * * 7-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
+ array('0 0 * * 4-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
+ array('0 0 * * 7-3', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false),
+ array('0 0 * * 3-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false),
+ array('0 0 * * 3-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false),
+ // Test lists of values and ranges (Abhoryo)
+ array('0 0 * * 2-7', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false),
+ array('0 0 * * 2-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false),
+ array('0 0 * * 4-7', strtotime('2011-07-19 00:00:00'), '2011-07-21 00:00:00', false),
+ // Test increments of ranges
+ array('0-12/4 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true),
+ array('4-59/2 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true),
+ array('4-59/2 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:06:00', true),
+ array('4-59/3 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:07:00', false),
+ // Test Day of the Week and the Day of the Month (issue #1)
+ array('0 0 1 1 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
+ array('0 0 1 JAN 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
+ array('0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false),
+ // Test the W day of the week modifier for day of the month field
+ array('0 0 2W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
+ array('0 0 1W * *', strtotime('2011-05-01 00:00:00'), '2011-05-02 00:00:00', false),
+ array('0 0 1W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
+ array('0 0 3W * *', strtotime('2011-07-01 00:00:00'), '2011-07-04 00:00:00', false),
+ array('0 0 16W * *', strtotime('2011-07-01 00:00:00'), '2011-07-15 00:00:00', false),
+ array('0 0 28W * *', strtotime('2011-07-01 00:00:00'), '2011-07-28 00:00:00', false),
+ array('0 0 30W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
+ array('0 0 31W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
+ // Test the last weekday of a month
+ array('* * * * 5L', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false),
+ array('* * * * 6L', strtotime('2011-07-01 00:00:00'), '2011-07-30 00:00:00', false),
+ array('* * * * 7L', strtotime('2011-07-01 00:00:00'), '2011-07-31 00:00:00', false),
+ array('* * * * 1L', strtotime('2011-07-24 00:00:00'), '2011-07-25 00:00:00', false),
+ array('* * * 1 5L', strtotime('2011-12-25 00:00:00'), '2012-01-27 00:00:00', false),
+ // Test the hash symbol for the nth weekday of a given month
+ array('* * * * 5#2', strtotime('2011-07-01 00:00:00'), '2011-07-08 00:00:00', false),
+ array('* * * * 5#1', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true),
+ array('* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false),
+
+ // Issue #7, documented example failed
+ ['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-31 06:03:00', false],
+
+ // https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403
+ ['* * * * MON-FRI', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-09 00:00:00'), false],
+ ['* * * * TUE', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-10 00:00:00'), false],
+ );
+ }
+
+ /**
+ * @covers \Cron\CronExpression::isDue
+ * @covers \Cron\CronExpression::getNextRunDate
+ * @covers \Cron\DayOfMonthField
+ * @covers \Cron\DayOfWeekField
+ * @covers \Cron\MinutesField
+ * @covers \Cron\HoursField
+ * @covers \Cron\MonthField
+ * @covers \Cron\CronExpression::getRunDate
+ * @dataProvider scheduleProvider
+ */
+ public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $isDue)
+ {
+ $relativeTimeString = is_int($relativeTime) ? date('Y-m-d H:i:s', $relativeTime) : $relativeTime;
+
+ // Test next run date
+ $cron = CronExpression::factory($schedule);
+ if (is_string($relativeTime)) {
+ $relativeTime = new DateTime($relativeTime);
+ } elseif (is_int($relativeTime)) {
+ $relativeTime = date('Y-m-d H:i:s', $relativeTime);
+ }
+
+ if (is_string($nextRun)) {
+ $nextRunDate = new DateTime($nextRun);
+ } elseif (is_int($nextRun)) {
+ $nextRunDate = new DateTime();
+ $nextRunDate->setTimestamp($nextRun);
+ }
+ $this->assertSame($isDue, $cron->isDue($relativeTime));
+ $next = $cron->getNextRunDate($relativeTime, 0, true);
+
+ $this->assertEquals($nextRunDate, $next);
+ }
+
+ /**
+ * @covers \Cron\CronExpression::isDue
+ */
+ public function testIsDueHandlesDifferentDates()
+ {
+ $cron = CronExpression::factory('* * * * *');
+ $this->assertTrue($cron->isDue());
+ $this->assertTrue($cron->isDue('now'));
+ $this->assertTrue($cron->isDue(new DateTime('now')));
+ $this->assertTrue($cron->isDue(date('Y-m-d H:i')));
+ $this->assertTrue($cron->isDue(new DateTimeImmutable('now')));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::isDue
+ */
+ public function testIsDueHandlesDifferentDefaultTimezones()
+ {
+ $originalTimezone = date_default_timezone_get();
+ $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00
+ $date = '2014-01-01 15:00'; //Wednesday
+
+ date_default_timezone_set('UTC');
+ $this->assertTrue($cron->isDue(new DateTime($date), 'UTC'));
+ $this->assertFalse($cron->isDue(new DateTime($date), 'Europe/Amsterdam'));
+ $this->assertFalse($cron->isDue(new DateTime($date), 'Asia/Tokyo'));
+
+ date_default_timezone_set('Europe/Amsterdam');
+ $this->assertFalse($cron->isDue(new DateTime($date), 'UTC'));
+ $this->assertTrue($cron->isDue(new DateTime($date), 'Europe/Amsterdam'));
+ $this->assertFalse($cron->isDue(new DateTime($date), 'Asia/Tokyo'));
+
+ date_default_timezone_set('Asia/Tokyo');
+ $this->assertFalse($cron->isDue(new DateTime($date), 'UTC'));
+ $this->assertFalse($cron->isDue(new DateTime($date), 'Europe/Amsterdam'));
+ $this->assertTrue($cron->isDue(new DateTime($date), 'Asia/Tokyo'));
+
+ date_default_timezone_set($originalTimezone);
+ }
+
+ /**
+ * @covers \Cron\CronExpression::isDue
+ */
+ public function testIsDueHandlesDifferentSuppliedTimezones()
+ {
+ $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00
+ $date = '2014-01-01 15:00'; //Wednesday
+
+ $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'UTC'));
+ $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'Europe/Amsterdam'));
+ $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'Asia/Tokyo'));
+
+ $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'UTC'));
+ $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'Europe/Amsterdam'));
+ $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'Asia/Tokyo'));
+
+ $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'UTC'));
+ $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'Europe/Amsterdam'));
+ $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'Asia/Tokyo'));
+ }
+
+ /**
+ * @covers Cron\CronExpression::isDue
+ */
+ public function testIsDueHandlesDifferentTimezonesAsArgument()
+ {
+ $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00
+ $date = '2014-01-01 15:00'; //Wednesday
+ $utc = new \DateTimeZone('UTC');
+ $amsterdam = new \DateTimeZone('Europe/Amsterdam');
+ $tokyo = new \DateTimeZone('Asia/Tokyo');
+ $this->assertTrue($cron->isDue(new DateTime($date, $utc), 'UTC'));
+ $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam), 'UTC'));
+ $this->assertFalse($cron->isDue(new DateTime($date, $tokyo), 'UTC'));
+ $this->assertFalse($cron->isDue(new DateTime($date, $utc), 'Europe/Amsterdam'));
+ $this->assertTrue($cron->isDue(new DateTime($date, $amsterdam), 'Europe/Amsterdam'));
+ $this->assertFalse($cron->isDue(new DateTime($date, $tokyo), 'Europe/Amsterdam'));
+ $this->assertFalse($cron->isDue(new DateTime($date, $utc), 'Asia/Tokyo'));
+ $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam), 'Asia/Tokyo'));
+ $this->assertTrue($cron->isDue(new DateTime($date, $tokyo), 'Asia/Tokyo'));
+ }
+
+ /**
+ * @covers Cron\CronExpression::isDue
+ */
+ public function testRecognisesTimezonesAsPartOfDateTime()
+ {
+ $cron = CronExpression::factory("0 7 * * *");
+ $tzCron = "America/New_York";
+ $tzServer = new \DateTimeZone("Europe/London");
+
+ $dtCurrent = \DateTime::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
+ $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron);
+ $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
+
+ $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
+ $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron);
+ $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
+
+ $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
+ $dtPrev = $cron->getPreviousRunDate($dtCurrent->format("c"), 0, true, $tzCron);
+ $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
+
+ $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer);
+ $dtPrev = $cron->getPreviousRunDate($dtCurrent->format("\@U"), 0, true, $tzCron);
+ $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e"));
+
+ }
+
+
+ /**
+ * @covers \Cron\CronExpression::getPreviousRunDate
+ */
+ public function testCanGetPreviousRunDates()
+ {
+ $cron = CronExpression::factory('* * * * *');
+ $next = $cron->getNextRunDate('now');
+ $two = $cron->getNextRunDate('now', 1);
+ $this->assertEquals($next, $cron->getPreviousRunDate($two));
+
+ $cron = CronExpression::factory('* */2 * * *');
+ $next = $cron->getNextRunDate('now');
+ $two = $cron->getNextRunDate('now', 1);
+ $this->assertEquals($next, $cron->getPreviousRunDate($two));
+
+ $cron = CronExpression::factory('* * * */2 *');
+ $next = $cron->getNextRunDate('now');
+ $two = $cron->getNextRunDate('now', 1);
+ $this->assertEquals($next, $cron->getPreviousRunDate($two));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::getMultipleRunDates
+ */
+ public function testProvidesMultipleRunDates()
+ {
+ $cron = CronExpression::factory('*/2 * * * *');
+ $this->assertEquals(array(
+ new DateTime('2008-11-09 00:00:00'),
+ new DateTime('2008-11-09 00:02:00'),
+ new DateTime('2008-11-09 00:04:00'),
+ new DateTime('2008-11-09 00:06:00')
+ ), $cron->getMultipleRunDates(4, '2008-11-09 00:00:00', false, true));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::getMultipleRunDates
+ * @covers \Cron\CronExpression::setMaxIterationCount
+ */
+ public function testProvidesMultipleRunDatesForTheFarFuture() {
+ // Fails with the default 1000 iteration limit
+ $cron = CronExpression::factory('0 0 12 1 *');
+ $cron->setMaxIterationCount(2000);
+ $this->assertEquals(array(
+ new DateTime('2016-01-12 00:00:00'),
+ new DateTime('2017-01-12 00:00:00'),
+ new DateTime('2018-01-12 00:00:00'),
+ new DateTime('2019-01-12 00:00:00'),
+ new DateTime('2020-01-12 00:00:00'),
+ new DateTime('2021-01-12 00:00:00'),
+ new DateTime('2022-01-12 00:00:00'),
+ new DateTime('2023-01-12 00:00:00'),
+ new DateTime('2024-01-12 00:00:00'),
+ ), $cron->getMultipleRunDates(9, '2015-04-28 00:00:00', false, true));
+ }
+
+ /**
+ * @covers \Cron\CronExpression
+ */
+ public function testCanIterateOverNextRuns()
+ {
+ $cron = CronExpression::factory('@weekly');
+ $nextRun = $cron->getNextRunDate("2008-11-09 08:00:00");
+ $this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00"));
+
+ // true is cast to 1
+ $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", true, true);
+ $this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00"));
+
+ // You can iterate over them
+ $nextRun = $cron->getNextRunDate($cron->getNextRunDate("2008-11-09 00:00:00", 1, true), 1, true);
+ $this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00"));
+
+ // You can skip more than one
+ $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 2, true);
+ $this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00"));
+ $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 3, true);
+ $this->assertEquals($nextRun, new DateTime("2008-11-30 00:00:00"));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::getRunDate
+ */
+ public function testGetRunDateHandlesDifferentDates()
+ {
+ $cron = CronExpression::factory('@weekly');
+ $date = new DateTime("2019-03-10 00:00:00");
+ $this->assertEquals($date, $cron->getNextRunDate("2019-03-03 08:00:00"));
+ $this->assertEquals($date, $cron->getNextRunDate(new DateTime("2019-03-03 08:00:00")));
+ $this->assertEquals($date, $cron->getNextRunDate(new DateTimeImmutable("2019-03-03 08:00:00")));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::getRunDate
+ */
+ public function testSkipsCurrentDateByDefault()
+ {
+ $cron = CronExpression::factory('* * * * *');
+ $current = new DateTime('now');
+ $next = $cron->getNextRunDate($current);
+ $nextPrev = $cron->getPreviousRunDate($next);
+ $this->assertSame($current->format('Y-m-d H:i:00'), $nextPrev->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::getRunDate
+ * @ticket 7
+ */
+ public function testStripsForSeconds()
+ {
+ $cron = CronExpression::factory('* * * * *');
+ $current = new DateTime('2011-09-27 10:10:54');
+ $this->assertSame('2011-09-27 10:11:00', $cron->getNextRunDate($current)->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::getRunDate
+ */
+ public function testFixesPhpBugInDateIntervalMonth()
+ {
+ $cron = CronExpression::factory('0 0 27 JAN *');
+ $this->assertSame('2011-01-27 00:00:00', $cron->getPreviousRunDate('2011-08-22 00:00:00')->format('Y-m-d H:i:s'));
+ }
+
+ public function testIssue29()
+ {
+ $cron = CronExpression::factory('@weekly');
+ $this->assertSame(
+ '2013-03-10 00:00:00',
+ $cron->getPreviousRunDate('2013-03-17 00:00:00')->format('Y-m-d H:i:s')
+ );
+ }
+
+ /**
+ * @see https://github.com/mtdowling/cron-expression/issues/20
+ */
+ public function testIssue20() {
+ $e = CronExpression::factory('* * * * MON#1');
+ $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
+ $this->assertFalse($e->isDue(new DateTime('2014-04-14 00:00:00')));
+ $this->assertFalse($e->isDue(new DateTime('2014-04-21 00:00:00')));
+
+ $e = CronExpression::factory('* * * * SAT#2');
+ $this->assertFalse($e->isDue(new DateTime('2014-04-05 00:00:00')));
+ $this->assertTrue($e->isDue(new DateTime('2014-04-12 00:00:00')));
+ $this->assertFalse($e->isDue(new DateTime('2014-04-19 00:00:00')));
+
+ $e = CronExpression::factory('* * * * SUN#3');
+ $this->assertFalse($e->isDue(new DateTime('2014-04-13 00:00:00')));
+ $this->assertTrue($e->isDue(new DateTime('2014-04-20 00:00:00')));
+ $this->assertFalse($e->isDue(new DateTime('2014-04-27 00:00:00')));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::getRunDate
+ */
+ public function testKeepOriginalTime()
+ {
+ $now = new \DateTime;
+ $strNow = $now->format(DateTime::ISO8601);
+ $cron = CronExpression::factory('0 0 * * *');
+ $cron->getPreviousRunDate($now);
+ $this->assertSame($strNow, $now->format(DateTime::ISO8601));
+ }
+
+ /**
+ * @covers \Cron\CronExpression::__construct
+ * @covers \Cron\CronExpression::factory
+ * @covers \Cron\CronExpression::isValidExpression
+ * @covers \Cron\CronExpression::setExpression
+ * @covers \Cron\CronExpression::setPart
+ */
+ public function testValidationWorks()
+ {
+ // Invalid. Only four values
+ $this->assertFalse(CronExpression::isValidExpression('* * * 1'));
+ // Valid
+ $this->assertTrue(CronExpression::isValidExpression('* * * * 1'));
+
+ // Issue #156, 13 is an invalid month
+ $this->assertFalse(CronExpression::isValidExpression("* * * 13 * "));
+
+ // Issue #155, 90 is an invalid second
+ $this->assertFalse(CronExpression::isValidExpression('90 * * * *'));
+
+ // Issue #154, 24 is an invalid hour
+ $this->assertFalse(CronExpression::isValidExpression("0 24 1 12 0"));
+
+ // Issue #125, this is just all sorts of wrong
+ $this->assertFalse(CronExpression::isValidExpression('990 14 * * mon-fri0345345'));
+
+ // see https://github.com/dragonmantank/cron-expression/issues/5
+ $this->assertTrue(CronExpression::isValidExpression('2,17,35,47 5-7,11-13 * * *'));
+ }
+
+ /**
+ * Makes sure that 00 is considered a valid value for 0-based fields
+ * cronie allows numbers with a leading 0, so adding support for this as well
+ *
+ * @see https://github.com/dragonmantank/cron-expression/issues/12
+ */
+ public function testDoubleZeroIsValid()
+ {
+ $this->assertTrue(CronExpression::isValidExpression('00 * * * *'));
+ $this->assertTrue(CronExpression::isValidExpression('01 * * * *'));
+ $this->assertTrue(CronExpression::isValidExpression('* 00 * * *'));
+ $this->assertTrue(CronExpression::isValidExpression('* 01 * * *'));
+
+ $e = CronExpression::factory('00 * * * *');
+ $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
+ $e = CronExpression::factory('01 * * * *');
+ $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:01:00')));
+
+ $e = CronExpression::factory('* 00 * * *');
+ $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
+ $e = CronExpression::factory('* 01 * * *');
+ $this->assertTrue($e->isDue(new DateTime('2014-04-07 01:00:00')));
+ }
+
+
+ /**
+ * Ranges with large steps should "wrap around" to the appropriate value
+ * cronie allows for steps that are larger than the range of a field, with it wrapping around like a ring buffer. We
+ * should do the same.
+ *
+ * @see https://github.com/dragonmantank/cron-expression/issues/6
+ */
+ public function testRangesWrapAroundWithLargeSteps()
+ {
+ $f = new MonthField();
+ $this->assertTrue($f->validate('*/123'));
+ $this->assertSame([4], $f->getRangeForExpression('*/123', 12));
+
+ $e = CronExpression::factory('* * * */123 *');
+ $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00')));
+
+ $nextRunDate = $e->getNextRunDate(new DateTime('2014-04-07 00:00:00'));
+ $this->assertSame('2014-04-07 00:01:00', $nextRunDate->format('Y-m-d H:i:s'));
+
+ $nextRunDate = $e->getNextRunDate(new DateTime('2014-05-07 00:00:00'));
+ $this->assertSame('2015-04-01 00:00:00', $nextRunDate->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * When there is an issue with a field, we should report the human readable position
+ *
+ * @see https://github.com/dragonmantank/cron-expression/issues/29
+ */
+ public function testFieldPositionIsHumanAdjusted()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage("6 is not a valid position");
+ $e = CronExpression::factory('0 * * * * ? *');
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/DayOfMonthFieldTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/DayOfMonthFieldTest.php
new file mode 100644
index 0000000000..2191b6bffa
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/DayOfMonthFieldTest.php
@@ -0,0 +1,77 @@
+
+ */
+class DayOfMonthFieldTest extends TestCase
+{
+ /**
+ * @covers \Cron\DayOfMonthField::validate
+ */
+ public function testValidatesField()
+ {
+ $f = new DayOfMonthField();
+ $this->assertTrue($f->validate('1'));
+ $this->assertTrue($f->validate('*'));
+ $this->assertTrue($f->validate('L'));
+ $this->assertTrue($f->validate('5W'));
+ $this->assertTrue($f->validate('01'));
+ $this->assertFalse($f->validate('5W,L'));
+ $this->assertFalse($f->validate('1.'));
+ }
+
+ /**
+ * @covers \Cron\DayOfMonthField::isSatisfiedBy
+ */
+ public function testChecksIfSatisfied()
+ {
+ $f = new DayOfMonthField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
+ }
+
+ /**
+ * @covers \Cron\DayOfMonthField::increment
+ */
+ public function testIncrementsDate()
+ {
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f = new DayOfMonthField();
+ $f->increment($d);
+ $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
+
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f->increment($d, true);
+ $this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\DayOfMonthField::increment
+ */
+ public function testIncrementsDateTimeImmutable()
+ {
+ $d = new DateTimeImmutable('2011-03-15 11:15:00');
+ $f = new DayOfMonthField();
+ $f->increment($d);
+ $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * Day of the month cannot accept a 0 value, it must be between 1 and 31
+ * See Github issue #120
+ *
+ * @since 2017-01-22
+ */
+ public function testDoesNotAccept0Date()
+ {
+ $f = new DayOfMonthField();
+ $this->assertFalse($f->validate(0));
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/DayOfWeekFieldTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/DayOfWeekFieldTest.php
new file mode 100644
index 0000000000..74e63c28ce
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/DayOfWeekFieldTest.php
@@ -0,0 +1,156 @@
+
+ */
+class DayOfWeekFieldTest extends TestCase
+{
+ /**
+ * @covers \Cron\DayOfWeekField::validate
+ */
+ public function testValidatesField()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->validate('1'));
+ $this->assertTrue($f->validate('01'));
+ $this->assertTrue($f->validate('00'));
+ $this->assertTrue($f->validate('*'));
+ $this->assertFalse($f->validate('*/3,1,1-12'));
+ $this->assertTrue($f->validate('SUN-2'));
+ $this->assertFalse($f->validate('1.'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::isSatisfiedBy
+ */
+ public function testChecksIfSatisfied()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::increment
+ */
+ public function testIncrementsDate()
+ {
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f = new DayOfWeekField();
+ $f->increment($d);
+ $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
+
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f->increment($d, true);
+ $this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::increment
+ */
+ public function testIncrementsDateTimeImmutable()
+ {
+ $d = new DateTimeImmutable('2011-03-15 11:15:00');
+ $f = new DayOfWeekField();
+ $f->increment($d);
+ $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::isSatisfiedBy
+ */
+ public function testValidatesHashValueWeekday()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Weekday must be a value between 0 and 7. 12 given');
+
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime(), '12#1'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::isSatisfiedBy
+ */
+ public function testValidatesHashValueNth()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('There are never more than 5 or less than 1 of a given weekday in a month');
+
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime(), '3#6'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::validate
+ */
+ public function testValidateWeekendHash()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->validate('MON#1'));
+ $this->assertTrue($f->validate('TUE#2'));
+ $this->assertTrue($f->validate('WED#3'));
+ $this->assertTrue($f->validate('THU#4'));
+ $this->assertTrue($f->validate('FRI#5'));
+ $this->assertTrue($f->validate('SAT#1'));
+ $this->assertTrue($f->validate('SUN#3'));
+ $this->assertTrue($f->validate('MON#1,MON#3'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::isSatisfiedBy
+ */
+ public function testHandlesZeroAndSevenDayOfTheWeekValues()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '0-2'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '6-0'));
+
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN#3'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '0#3'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '7#3'));
+ }
+
+ /**
+ * @covers \Cron\DayOfWeekField::isSatisfiedBy
+ */
+ public function testHandlesLastWeekdayOfTheMonth()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), 'FRIL'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), '5L'));
+ $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), 'FRIL'));
+ $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), '5L'));
+ }
+
+ /**
+ * @see https://github.com/mtdowling/cron-expression/issues/47
+ */
+ public function testIssue47() {
+ $f = new DayOfWeekField();
+ $this->assertFalse($f->validate('mon,'));
+ $this->assertFalse($f->validate('mon-'));
+ $this->assertFalse($f->validate('*/2,'));
+ $this->assertFalse($f->validate('-mon'));
+ $this->assertFalse($f->validate(',1'));
+ $this->assertFalse($f->validate('*-'));
+ $this->assertFalse($f->validate(',-'));
+ }
+
+ /**
+ * @see https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403
+ */
+ public function testLiteralsExpandProperly()
+ {
+ $f = new DayOfWeekField();
+ $this->assertTrue($f->validate('MON-FRI'));
+ $this->assertSame([1,2,3,4,5], $f->getRangeForExpression('MON-FRI', 7));
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/FieldFactoryTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/FieldFactoryTest.php
new file mode 100644
index 0000000000..e25d070751
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/FieldFactoryTest.php
@@ -0,0 +1,43 @@
+
+ */
+class FieldFactoryTest extends TestCase
+{
+ /**
+ * @covers \Cron\FieldFactory::getField
+ */
+ public function testRetrievesFieldInstances()
+ {
+ $mappings = array(
+ 0 => 'Cron\MinutesField',
+ 1 => 'Cron\HoursField',
+ 2 => 'Cron\DayOfMonthField',
+ 3 => 'Cron\MonthField',
+ 4 => 'Cron\DayOfWeekField',
+ );
+
+ $f = new FieldFactory();
+
+ foreach ($mappings as $position => $class) {
+ $this->assertSame($class, get_class($f->getField($position)));
+ }
+ }
+
+ /**
+ * @covers \Cron\FieldFactory::getField
+ */
+ public function testValidatesFieldPosition()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $f = new FieldFactory();
+ $f->getField(-1);
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/HoursFieldTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/HoursFieldTest.php
new file mode 100644
index 0000000000..1849f28bea
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/HoursFieldTest.php
@@ -0,0 +1,99 @@
+
+ */
+class HoursFieldTest extends TestCase
+{
+ /**
+ * @covers \Cron\HoursField::validate
+ */
+ public function testValidatesField()
+ {
+ $f = new HoursField();
+ $this->assertTrue($f->validate('1'));
+ $this->assertTrue($f->validate('00'));
+ $this->assertTrue($f->validate('01'));
+ $this->assertTrue($f->validate('*'));
+ $this->assertFalse($f->validate('*/3,1,1-12'));
+ }
+
+ /**
+ * @covers \Cron\HoursField::isSatisfiedBy
+ */
+ public function testChecksIfSatisfied()
+ {
+ $f = new HoursField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
+ }
+
+ /**
+ * @covers \Cron\HoursField::increment
+ */
+ public function testIncrementsDate()
+ {
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f = new HoursField();
+ $f->increment($d);
+ $this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
+
+ $d->setTime(11, 15, 0);
+ $f->increment($d, true);
+ $this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\HoursField::increment
+ */
+ public function testIncrementsDateTimeImmutable()
+ {
+ $d = new DateTimeImmutable('2011-03-15 11:15:00');
+ $f = new HoursField();
+ $f->increment($d);
+ $this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\HoursField::increment
+ */
+ public function testIncrementsDateWithThirtyMinuteOffsetTimezone()
+ {
+ $tz = date_default_timezone_get();
+ date_default_timezone_set('America/St_Johns');
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f = new HoursField();
+ $f->increment($d);
+ $this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
+
+ $d->setTime(11, 15, 0);
+ $f->increment($d, true);
+ $this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s'));
+ date_default_timezone_set($tz);
+ }
+
+ /**
+ * @covers \Cron\HoursField::increment
+ */
+ public function testIncrementDateWithFifteenMinuteOffsetTimezone()
+ {
+ $tz = date_default_timezone_get();
+ date_default_timezone_set('Asia/Kathmandu');
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f = new HoursField();
+ $f->increment($d);
+ $this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s'));
+
+ $d->setTime(11, 15, 0);
+ $f->increment($d, true);
+ $this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s'));
+ date_default_timezone_set($tz);
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/MinutesFieldTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/MinutesFieldTest.php
new file mode 100644
index 0000000000..41a536d684
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/MinutesFieldTest.php
@@ -0,0 +1,73 @@
+
+ */
+class MinutesFieldTest extends TestCase
+{
+ /**
+ * @covers \Cron\MinutesField::validate
+ */
+ public function testValidatesField()
+ {
+ $f = new MinutesField();
+ $this->assertTrue($f->validate('1'));
+ $this->assertTrue($f->validate('*'));
+ $this->assertFalse($f->validate('*/3,1,1-12'));
+ }
+
+ /**
+ * @covers \Cron\MinutesField::isSatisfiedBy
+ */
+ public function testChecksIfSatisfied()
+ {
+ $f = new MinutesField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
+ }
+
+ /**
+ * @covers \Cron\MinutesField::increment
+ */
+ public function testIncrementsDate()
+ {
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f = new MinutesField();
+ $f->increment($d);
+ $this->assertSame('2011-03-15 11:16:00', $d->format('Y-m-d H:i:s'));
+ $f->increment($d, true);
+ $this->assertSame('2011-03-15 11:15:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\MinutesField::increment
+ */
+ public function testIncrementsDateTimeImmutable()
+ {
+ $d = new DateTimeImmutable('2011-03-15 11:15:00');
+ $f = new MinutesField();
+ $f->increment($d);
+ $this->assertSame('2011-03-15 11:16:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * Various bad syntaxes that are reported to work, but shouldn't.
+ *
+ * @author Chris Tankersley
+ * @since 2017-08-18
+ */
+ public function testBadSyntaxesShouldNotValidate()
+ {
+ $f = new MinutesField();
+ $this->assertFalse($f->validate('*-1'));
+ $this->assertFalse($f->validate('1-2-3'));
+ $this->assertFalse($f->validate('-1'));
+ }
+}
diff --git a/lib/vendor/dragonmantank/cron-expression/tests/Cron/MonthFieldTest.php b/lib/vendor/dragonmantank/cron-expression/tests/Cron/MonthFieldTest.php
new file mode 100644
index 0000000000..f329f4c161
--- /dev/null
+++ b/lib/vendor/dragonmantank/cron-expression/tests/Cron/MonthFieldTest.php
@@ -0,0 +1,103 @@
+
+ */
+class MonthFieldTest extends TestCase
+{
+ /**
+ * @covers \Cron\MonthField::validate
+ */
+ public function testValidatesField()
+ {
+ $f = new MonthField();
+ $this->assertTrue($f->validate('12'));
+ $this->assertTrue($f->validate('*'));
+ $this->assertFalse($f->validate('*/10,2,1-12'));
+ $this->assertFalse($f->validate('1.fix-regexp'));
+ }
+
+ /**
+ * @covers \Cron\MonthField::isSatisfiedBy
+ */
+ public function testChecksIfSatisfied()
+ {
+ $f = new MonthField();
+ $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?'));
+ $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?'));
+ }
+
+ /**
+ * @covers \Cron\MonthField::increment
+ */
+ public function testIncrementsDate()
+ {
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f = new MonthField();
+ $f->increment($d);
+ $this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s'));
+
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f->increment($d, true);
+ $this->assertSame('2011-02-28 23:59:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\MonthField::increment
+ */
+ public function testIncrementsDateTimeImmutable()
+ {
+ $d = new DateTimeImmutable('2011-03-15 11:15:00');
+ $f = new MonthField();
+ $f->increment($d);
+ $this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\MonthField::increment
+ */
+ public function testIncrementsDateWithThirtyMinuteTimezone()
+ {
+ $tz = date_default_timezone_get();
+ date_default_timezone_set('America/St_Johns');
+ $d = new DateTime('2011-03-31 11:59:59');
+ $f = new MonthField();
+ $f->increment($d);
+ $this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s'));
+
+ $d = new DateTime('2011-03-15 11:15:00');
+ $f->increment($d, true);
+ $this->assertSame('2011-02-28 23:59:00', $d->format('Y-m-d H:i:s'));
+ date_default_timezone_set($tz);
+ }
+
+
+ /**
+ * @covers \Cron\MonthField::increment
+ */
+ public function testIncrementsYearAsNeeded()
+ {
+ $f = new MonthField();
+ $d = new DateTime('2011-12-15 00:00:00');
+ $f->increment($d);
+ $this->assertSame('2012-01-01 00:00:00', $d->format('Y-m-d H:i:s'));
+ }
+
+ /**
+ * @covers \Cron\MonthField::increment
+ */
+ public function testDecrementsYearAsNeeded()
+ {
+ $f = new MonthField();
+ $d = new DateTime('2011-01-15 00:00:00');
+ $f->increment($d, true);
+ $this->assertSame('2010-12-31 23:59:00', $d->format('Y-m-d H:i:s'));
+ }
+}
diff --git a/lib/vendor/erusev/parsedown/LICENSE.txt b/lib/vendor/erusev/parsedown/LICENSE.txt
new file mode 100644
index 0000000000..8e7c764d16
--- /dev/null
+++ b/lib/vendor/erusev/parsedown/LICENSE.txt
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-2018 Emanuil Rusev, erusev.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/vendor/erusev/parsedown/Parsedown.php b/lib/vendor/erusev/parsedown/Parsedown.php
new file mode 100644
index 0000000000..1b9d6d5bc2
--- /dev/null
+++ b/lib/vendor/erusev/parsedown/Parsedown.php
@@ -0,0 +1,1712 @@
+DefinitionData = array();
+
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ # remove surrounding line breaks
+ $text = trim($text, "\n");
+
+ # split text into lines
+ $lines = explode("\n", $text);
+
+ # iterate through lines to identify blocks
+ $markup = $this->lines($lines);
+
+ # trim line breaks
+ $markup = trim($markup, "\n");
+
+ return $markup;
+ }
+
+ #
+ # Setters
+ #
+
+ function setBreaksEnabled($breaksEnabled)
+ {
+ $this->breaksEnabled = $breaksEnabled;
+
+ return $this;
+ }
+
+ protected $breaksEnabled;
+
+ function setMarkupEscaped($markupEscaped)
+ {
+ $this->markupEscaped = $markupEscaped;
+
+ return $this;
+ }
+
+ protected $markupEscaped;
+
+ function setUrlsLinked($urlsLinked)
+ {
+ $this->urlsLinked = $urlsLinked;
+
+ return $this;
+ }
+
+ protected $urlsLinked = true;
+
+ function setSafeMode($safeMode)
+ {
+ $this->safeMode = (bool) $safeMode;
+
+ return $this;
+ }
+
+ protected $safeMode;
+
+ protected $safeLinksWhitelist = array(
+ 'http://',
+ 'https://',
+ 'ftp://',
+ 'ftps://',
+ 'mailto:',
+ 'data:image/png;base64,',
+ 'data:image/gif;base64,',
+ 'data:image/jpeg;base64,',
+ 'irc:',
+ 'ircs:',
+ 'git:',
+ 'ssh:',
+ 'news:',
+ 'steam:',
+ );
+
+ #
+ # Lines
+ #
+
+ protected $BlockTypes = array(
+ '#' => array('Header'),
+ '*' => array('Rule', 'List'),
+ '+' => array('List'),
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
+ '0' => array('List'),
+ '1' => array('List'),
+ '2' => array('List'),
+ '3' => array('List'),
+ '4' => array('List'),
+ '5' => array('List'),
+ '6' => array('List'),
+ '7' => array('List'),
+ '8' => array('List'),
+ '9' => array('List'),
+ ':' => array('Table'),
+ '<' => array('Comment', 'Markup'),
+ '=' => array('SetextHeader'),
+ '>' => array('Quote'),
+ '[' => array('Reference'),
+ '_' => array('Rule'),
+ '`' => array('FencedCode'),
+ '|' => array('Table'),
+ '~' => array('FencedCode'),
+ );
+
+ # ~
+
+ protected $unmarkedBlockTypes = array(
+ 'Code',
+ );
+
+ #
+ # Blocks
+ #
+
+ protected function lines(array $lines)
+ {
+ $CurrentBlock = null;
+
+ foreach ($lines as $line)
+ {
+ if (chop($line) === '')
+ {
+ if (isset($CurrentBlock))
+ {
+ $CurrentBlock['interrupted'] = true;
+ }
+
+ continue;
+ }
+
+ if (strpos($line, "\t") !== false)
+ {
+ $parts = explode("\t", $line);
+
+ $line = $parts[0];
+
+ unset($parts[0]);
+
+ foreach ($parts as $part)
+ {
+ $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
+
+ $line .= str_repeat(' ', $shortage);
+ $line .= $part;
+ }
+ }
+
+ $indent = 0;
+
+ while (isset($line[$indent]) and $line[$indent] === ' ')
+ {
+ $indent ++;
+ }
+
+ $text = $indent > 0 ? substr($line, $indent) : $line;
+
+ # ~
+
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']))
+ {
+ $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $CurrentBlock = $Block;
+
+ continue;
+ }
+ else
+ {
+ if ($this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+ }
+ }
+ }
+
+ # ~
+
+ $marker = $text[0];
+
+ # ~
+
+ $blockTypes = $this->unmarkedBlockTypes;
+
+ if (isset($this->BlockTypes[$marker]))
+ {
+ foreach ($this->BlockTypes[$marker] as $blockType)
+ {
+ $blockTypes []= $blockType;
+ }
+ }
+
+ #
+ # ~
+
+ foreach ($blockTypes as $blockType)
+ {
+ $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $Block['type'] = $blockType;
+
+ if ( ! isset($Block['identified']))
+ {
+ $Blocks []= $CurrentBlock;
+
+ $Block['identified'] = true;
+ }
+
+ if ($this->isBlockContinuable($blockType))
+ {
+ $Block['continuable'] = true;
+ }
+
+ $CurrentBlock = $Block;
+
+ continue 2;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
+ {
+ $CurrentBlock['element']['text'] .= "\n".$text;
+ }
+ else
+ {
+ $Blocks []= $CurrentBlock;
+
+ $CurrentBlock = $this->paragraph($Line);
+
+ $CurrentBlock['identified'] = true;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
+ }
+
+ # ~
+
+ $Blocks []= $CurrentBlock;
+
+ unset($Blocks[0]);
+
+ # ~
+
+ $markup = '';
+
+ foreach ($Blocks as $Block)
+ {
+ if (isset($Block['hidden']))
+ {
+ continue;
+ }
+
+ $markup .= "\n";
+ $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
+ }
+
+ $markup .= "\n";
+
+ # ~
+
+ return $markup;
+ }
+
+ protected function isBlockContinuable($Type)
+ {
+ return method_exists($this, 'block'.$Type.'Continue');
+ }
+
+ protected function isBlockCompletable($Type)
+ {
+ return method_exists($this, 'block'.$Type.'Complete');
+ }
+
+ #
+ # Code
+
+ protected function blockCode($Line, $Block = null)
+ {
+ if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['indent'] >= 4)
+ {
+ $text = substr($Line['body'], 4);
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'pre',
+ 'handler' => 'element',
+ 'text' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeContinue($Line, $Block)
+ {
+ if ($Line['indent'] >= 4)
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text']['text'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['text']['text'] .= "\n";
+
+ $text = substr($Line['body'], 4);
+
+ $Block['element']['text']['text'] .= $text;
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeComplete($Block)
+ {
+ $text = $Block['element']['text']['text'];
+
+ $Block['element']['text']['text'] = $text;
+
+ return $Block;
+ }
+
+ #
+ # Comment
+
+ protected function blockComment($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
+ {
+ $Block = array(
+ 'markup' => $Line['body'],
+ );
+
+ if (preg_match('/-->$/', $Line['text']))
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockCommentContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ $Block['markup'] .= "\n" . $Line['body'];
+
+ if (preg_match('/-->$/', $Line['text']))
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+
+ #
+ # Fenced Code
+
+ protected function blockFencedCode($Line)
+ {
+ if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches))
+ {
+ $Element = array(
+ 'name' => 'code',
+ 'text' => '',
+ );
+
+ if (isset($matches[1]))
+ {
+ /**
+ * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
+ * Every HTML element may have a class attribute specified.
+ * The attribute, if specified, must have a value that is a set
+ * of space-separated tokens representing the various classes
+ * that the element belongs to.
+ * [...]
+ * The space characters, for the purposes of this specification,
+ * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
+ * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
+ * U+000D CARRIAGE RETURN (CR).
+ */
+ $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r"));
+
+ $class = 'language-'.$language;
+
+ $Element['attributes'] = array(
+ 'class' => $class,
+ );
+ }
+
+ $Block = array(
+ 'char' => $Line['text'][0],
+ 'element' => array(
+ 'name' => 'pre',
+ 'handler' => 'element',
+ 'text' => $Element,
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockFencedCodeContinue($Line, $Block)
+ {
+ if (isset($Block['complete']))
+ {
+ return;
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text']['text'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
+ {
+ $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
+
+ $Block['complete'] = true;
+
+ return $Block;
+ }
+
+ $Block['element']['text']['text'] .= "\n".$Line['body'];
+
+ return $Block;
+ }
+
+ protected function blockFencedCodeComplete($Block)
+ {
+ $text = $Block['element']['text']['text'];
+
+ $Block['element']['text']['text'] = $text;
+
+ return $Block;
+ }
+
+ #
+ # Header
+
+ protected function blockHeader($Line)
+ {
+ if (isset($Line['text'][1]))
+ {
+ $level = 1;
+
+ while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
+ {
+ $level ++;
+ }
+
+ if ($level > 6)
+ {
+ return;
+ }
+
+ $text = trim($Line['text'], '# ');
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'h' . min(6, $level),
+ 'text' => $text,
+ 'handler' => 'line',
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # List
+
+ protected function blockList($Line)
+ {
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
+
+ if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'indent' => $Line['indent'],
+ 'pattern' => $pattern,
+ 'element' => array(
+ 'name' => $name,
+ 'handler' => 'elements',
+ ),
+ );
+
+ if($name === 'ol')
+ {
+ $listStart = stristr($matches[0], '.', true);
+
+ if($listStart !== '1')
+ {
+ $Block['element']['attributes'] = array('start' => $listStart);
+ }
+ }
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => 'li',
+ 'text' => array(
+ $matches[2],
+ ),
+ );
+
+ $Block['element']['text'] []= & $Block['li'];
+
+ return $Block;
+ }
+ }
+
+ protected function blockListContinue($Line, array $Block)
+ {
+ if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['li']['text'] []= '';
+
+ $Block['loose'] = true;
+
+ unset($Block['interrupted']);
+ }
+
+ unset($Block['li']);
+
+ $text = isset($matches[1]) ? $matches[1] : '';
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => 'li',
+ 'text' => array(
+ $text,
+ ),
+ );
+
+ $Block['element']['text'] []= & $Block['li'];
+
+ return $Block;
+ }
+
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
+ {
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+ $Block['li']['text'] []= $text;
+
+ return $Block;
+ }
+
+ if ($Line['indent'] > 0)
+ {
+ $Block['li']['text'] []= '';
+
+ $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+ $Block['li']['text'] []= $text;
+
+ unset($Block['interrupted']);
+
+ return $Block;
+ }
+ }
+
+ protected function blockListComplete(array $Block)
+ {
+ if (isset($Block['loose']))
+ {
+ foreach ($Block['element']['text'] as &$li)
+ {
+ if (end($li['text']) !== '')
+ {
+ $li['text'] []= '';
+ }
+ }
+ }
+
+ return $Block;
+ }
+
+ #
+ # Quote
+
+ protected function blockQuote($Line)
+ {
+ if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'blockquote',
+ 'handler' => 'lines',
+ 'text' => (array) $matches[1],
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockQuoteContinue($Line, array $Block)
+ {
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['text'] []= '';
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['text'] []= $matches[1];
+
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $Block['element']['text'] []= $Line['text'];
+
+ return $Block;
+ }
+ }
+
+ #
+ # Rule
+
+ protected function blockRule($Line)
+ {
+ if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'hr'
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Setext
+
+ protected function blockSetextHeader($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (chop($Line['text'], $Line['text'][0]) === '')
+ {
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+ return $Block;
+ }
+ }
+
+ #
+ # Markup
+
+ protected function blockMarkup($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
+ {
+ $element = strtolower($matches[1]);
+
+ if (in_array($element, $this->textLevelElements))
+ {
+ return;
+ }
+
+ $Block = array(
+ 'name' => $matches[1],
+ 'depth' => 0,
+ 'markup' => $Line['text'],
+ );
+
+ $length = strlen($matches[0]);
+
+ $remainder = substr($Line['text'], $length);
+
+ if (trim($remainder) === '')
+ {
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+ {
+ $Block['closed'] = true;
+
+ $Block['void'] = true;
+ }
+ }
+ else
+ {
+ if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
+ {
+ return;
+ }
+
+ if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
+ {
+ $Block['closed'] = true;
+ }
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockMarkupContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
+ {
+ $Block['depth'] ++;
+ }
+
+ if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
+ {
+ if ($Block['depth'] > 0)
+ {
+ $Block['depth'] --;
+ }
+ else
+ {
+ $Block['closed'] = true;
+ }
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['markup'] .= "\n";
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['markup'] .= "\n".$Line['body'];
+
+ return $Block;
+ }
+
+ #
+ # Reference
+
+ protected function blockReference($Line)
+ {
+ if (preg_match('/^\[(.+?)\]:[ ]*(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
+ {
+ $id = strtolower($matches[1]);
+
+ $Data = array(
+ 'url' => $matches[2],
+ 'title' => null,
+ );
+
+ if (isset($matches[3]))
+ {
+ $Data['title'] = $matches[3];
+ }
+
+ $this->DefinitionData['Reference'][$id] = $Data;
+
+ $Block = array(
+ 'hidden' => true,
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Table
+
+ protected function blockTable($Line, array $Block = null)
+ {
+ if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
+ {
+ $alignments = array();
+
+ $divider = $Line['text'];
+
+ $divider = trim($divider);
+ $divider = trim($divider, '|');
+
+ $dividerCells = explode('|', $divider);
+
+ foreach ($dividerCells as $dividerCell)
+ {
+ $dividerCell = trim($dividerCell);
+
+ if ($dividerCell === '')
+ {
+ continue;
+ }
+
+ $alignment = null;
+
+ if ($dividerCell[0] === ':')
+ {
+ $alignment = 'left';
+ }
+
+ if (substr($dividerCell, - 1) === ':')
+ {
+ $alignment = $alignment === 'left' ? 'center' : 'right';
+ }
+
+ $alignments []= $alignment;
+ }
+
+ # ~
+
+ $HeaderElements = array();
+
+ $header = $Block['element']['text'];
+
+ $header = trim($header);
+ $header = trim($header, '|');
+
+ $headerCells = explode('|', $header);
+
+ foreach ($headerCells as $index => $headerCell)
+ {
+ $headerCell = trim($headerCell);
+
+ $HeaderElement = array(
+ 'name' => 'th',
+ 'text' => $headerCell,
+ 'handler' => 'line',
+ );
+
+ if (isset($alignments[$index]))
+ {
+ $alignment = $alignments[$index];
+
+ $HeaderElement['attributes'] = array(
+ 'style' => 'text-align: '.$alignment.';',
+ );
+ }
+
+ $HeaderElements []= $HeaderElement;
+ }
+
+ # ~
+
+ $Block = array(
+ 'alignments' => $alignments,
+ 'identified' => true,
+ 'element' => array(
+ 'name' => 'table',
+ 'handler' => 'elements',
+ ),
+ );
+
+ $Block['element']['text'] []= array(
+ 'name' => 'thead',
+ 'handler' => 'elements',
+ );
+
+ $Block['element']['text'] []= array(
+ 'name' => 'tbody',
+ 'handler' => 'elements',
+ 'text' => array(),
+ );
+
+ $Block['element']['text'][0]['text'] []= array(
+ 'name' => 'tr',
+ 'handler' => 'elements',
+ 'text' => $HeaderElements,
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockTableContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
+ {
+ $Elements = array();
+
+ $row = $Line['text'];
+
+ $row = trim($row);
+ $row = trim($row, '|');
+
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
+
+ foreach ($matches[0] as $index => $cell)
+ {
+ $cell = trim($cell);
+
+ $Element = array(
+ 'name' => 'td',
+ 'handler' => 'line',
+ 'text' => $cell,
+ );
+
+ if (isset($Block['alignments'][$index]))
+ {
+ $Element['attributes'] = array(
+ 'style' => 'text-align: '.$Block['alignments'][$index].';',
+ );
+ }
+
+ $Elements []= $Element;
+ }
+
+ $Element = array(
+ 'name' => 'tr',
+ 'handler' => 'elements',
+ 'text' => $Elements,
+ );
+
+ $Block['element']['text'][1]['text'] []= $Element;
+
+ return $Block;
+ }
+ }
+
+ #
+ # ~
+ #
+
+ protected function paragraph($Line)
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'p',
+ 'text' => $Line['text'],
+ 'handler' => 'line',
+ ),
+ );
+
+ return $Block;
+ }
+
+ #
+ # Inline Elements
+ #
+
+ protected $InlineTypes = array(
+ '"' => array('SpecialCharacter'),
+ '!' => array('Image'),
+ '&' => array('SpecialCharacter'),
+ '*' => array('Emphasis'),
+ ':' => array('Url'),
+ '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
+ '>' => array('SpecialCharacter'),
+ '[' => array('Link'),
+ '_' => array('Emphasis'),
+ '`' => array('Code'),
+ '~' => array('Strikethrough'),
+ '\\' => array('EscapeSequence'),
+ );
+
+ # ~
+
+ protected $inlineMarkerList = '!"*_&[:<>`~\\';
+
+ #
+ # ~
+ #
+
+ public function line($text, $nonNestables=array())
+ {
+ $markup = '';
+
+ # $excerpt is based on the first occurrence of a marker
+
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
+ {
+ $marker = $excerpt[0];
+
+ $markerPosition = strpos($text, $marker);
+
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
+
+ foreach ($this->InlineTypes[$marker] as $inlineType)
+ {
+ # check to see if the current inline type is nestable in the current context
+
+ if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables))
+ {
+ continue;
+ }
+
+ $Inline = $this->{'inline'.$inlineType}($Excerpt);
+
+ if ( ! isset($Inline))
+ {
+ continue;
+ }
+
+ # makes sure that the inline belongs to "our" marker
+
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
+ {
+ continue;
+ }
+
+ # sets a default inline position
+
+ if ( ! isset($Inline['position']))
+ {
+ $Inline['position'] = $markerPosition;
+ }
+
+ # cause the new element to 'inherit' our non nestables
+
+ foreach ($nonNestables as $non_nestable)
+ {
+ $Inline['element']['nonNestables'][] = $non_nestable;
+ }
+
+ # the text that comes before the inline
+ $unmarkedText = substr($text, 0, $Inline['position']);
+
+ # compile the unmarked text
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ # compile the inline
+ $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
+
+ # remove the examined text
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+ continue 2;
+ }
+
+ # the marker does not belong to an inline
+
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+ $markup .= $this->unmarkedText($unmarkedText);
+
+ $text = substr($text, $markerPosition + 1);
+ }
+
+ $markup .= $this->unmarkedText($text);
+
+ return $markup;
+ }
+
+ #
+ # ~
+ #
+
+ protected function inlineCode($Excerpt)
+ {
+ $marker = $Excerpt['text'][0];
+
+ if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmailTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ if ( ! isset($matches[2]))
+ {
+ $url = 'mailto:' . $url;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[1],
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmphasis($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ $marker = $Excerpt['text'][0];
+
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'strong';
+ }
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'em';
+ }
+ else
+ {
+ return;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => $emphasis,
+ 'handler' => 'line',
+ 'text' => $matches[1],
+ ),
+ );
+ }
+
+ protected function inlineEscapeSequence($Excerpt)
+ {
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+ {
+ return array(
+ 'markup' => $Excerpt['text'][1],
+ 'extent' => 2,
+ );
+ }
+ }
+
+ protected function inlineImage($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+ {
+ return;
+ }
+
+ $Excerpt['text']= substr($Excerpt['text'], 1);
+
+ $Link = $this->inlineLink($Excerpt);
+
+ if ($Link === null)
+ {
+ return;
+ }
+
+ $Inline = array(
+ 'extent' => $Link['extent'] + 1,
+ 'element' => array(
+ 'name' => 'img',
+ 'attributes' => array(
+ 'src' => $Link['element']['attributes']['href'],
+ 'alt' => $Link['element']['text'],
+ ),
+ ),
+ );
+
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+ unset($Inline['element']['attributes']['href']);
+
+ return $Inline;
+ }
+
+ protected function inlineLink($Excerpt)
+ {
+ $Element = array(
+ 'name' => 'a',
+ 'handler' => 'line',
+ 'nonNestables' => array('Url', 'Link'),
+ 'text' => null,
+ 'attributes' => array(
+ 'href' => null,
+ 'title' => null,
+ ),
+ );
+
+ $extent = 0;
+
+ $remainder = $Excerpt['text'];
+
+ if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
+ {
+ $Element['text'] = $matches[1];
+
+ $extent += strlen($matches[0]);
+
+ $remainder = substr($remainder, $extent);
+ }
+ else
+ {
+ return;
+ }
+
+ if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
+ {
+ $Element['attributes']['href'] = $matches[1];
+
+ if (isset($matches[2]))
+ {
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+ }
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+ {
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
+ $definition = strtolower($definition);
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ $definition = strtolower($Element['text']);
+ }
+
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
+ {
+ return;
+ }
+
+ $Definition = $this->DefinitionData['Reference'][$definition];
+
+ $Element['attributes']['href'] = $Definition['url'];
+ $Element['attributes']['title'] = $Definition['title'];
+ }
+
+ return array(
+ 'extent' => $extent,
+ 'element' => $Element,
+ );
+ }
+
+ protected function inlineMarkup($Excerpt)
+ {
+ if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'markup' => $matches[0],
+ 'extent' => strlen($matches[0]),
+ );
+ }
+ }
+
+ protected function inlineSpecialCharacter($Excerpt)
+ {
+ if ($Excerpt['text'][0] === '&' and ! preg_match('/^?\w+;/', $Excerpt['text']))
+ {
+ return array(
+ 'markup' => '&',
+ 'extent' => 1,
+ );
+ }
+
+ $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
+
+ if (isset($SpecialCharacter[$Excerpt['text'][0]]))
+ {
+ return array(
+ 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
+ 'extent' => 1,
+ );
+ }
+ }
+
+ protected function inlineStrikethrough($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'del',
+ 'text' => $matches[1],
+ 'handler' => 'line',
+ ),
+ );
+ }
+ }
+
+ protected function inlineUrl($Excerpt)
+ {
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+ {
+ return;
+ }
+
+ if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
+ {
+ $url = $matches[0][0];
+
+ $Inline = array(
+ 'extent' => strlen($matches[0][0]),
+ 'position' => $matches[0][1],
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+
+ return $Inline;
+ }
+ }
+
+ protected function inlineUrlTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ # ~
+
+ protected function unmarkedText($text)
+ {
+ if ($this->breaksEnabled)
+ {
+ $text = preg_replace('/[ ]*\n/', " \n", $text);
+ }
+ else
+ {
+ $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', " \n", $text);
+ $text = str_replace(" \n", "\n", $text);
+ }
+
+ return $text;
+ }
+
+ #
+ # Handlers
+ #
+
+ protected function element(array $Element)
+ {
+ if ($this->safeMode)
+ {
+ $Element = $this->sanitiseElement($Element);
+ }
+
+ $markup = '<'.$Element['name'];
+
+ if (isset($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $name => $value)
+ {
+ if ($value === null)
+ {
+ continue;
+ }
+
+ $markup .= ' '.$name.'="'.self::escape($value).'"';
+ }
+ }
+
+ $permitRawHtml = false;
+
+ if (isset($Element['text']))
+ {
+ $text = $Element['text'];
+ }
+ // very strongly consider an alternative if you're writing an
+ // extension
+ elseif (isset($Element['rawHtml']))
+ {
+ $text = $Element['rawHtml'];
+ $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
+ $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
+ }
+
+ if (isset($text))
+ {
+ $markup .= '>';
+
+ if (!isset($Element['nonNestables']))
+ {
+ $Element['nonNestables'] = array();
+ }
+
+ if (isset($Element['handler']))
+ {
+ $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']);
+ }
+ elseif (!$permitRawHtml)
+ {
+ $markup .= self::escape($text, true);
+ }
+ else
+ {
+ $markup .= $text;
+ }
+
+ $markup .= ''.$Element['name'].'>';
+ }
+ else
+ {
+ $markup .= ' />';
+ }
+
+ return $markup;
+ }
+
+ protected function elements(array $Elements)
+ {
+ $markup = '';
+
+ foreach ($Elements as $Element)
+ {
+ $markup .= "\n" . $this->element($Element);
+ }
+
+ $markup .= "\n";
+
+ return $markup;
+ }
+
+ # ~
+
+ protected function li($lines)
+ {
+ $markup = $this->lines($lines);
+
+ $trimmedMarkup = trim($markup);
+
+ if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '
+```
+
+More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI).
+
+### Security
+
+Parsedown is capable of escaping user-input within the HTML that it generates. Additionally Parsedown will apply sanitisation to additional scripting vectors (such as scripting link destinations) that are introduced by the markdown syntax itself.
+
+To tell Parsedown that it is processing untrusted user-input, use the following:
+```php
+$parsedown = new Parsedown;
+$parsedown->setSafeMode(true);
+```
+
+If instead, you wish to allow HTML within untrusted user-input, but still want output to be free from XSS it is recommended that you make use of a HTML sanitiser that allows HTML tags to be whitelisted, like [HTML Purifier](http://htmlpurifier.org/).
+
+In both cases you should strongly consider employing defence-in-depth measures, like [deploying a Content-Security-Policy](https://scotthelme.co.uk/content-security-policy-an-introduction/) (a browser security feature) so that your page is likely to be safe even if an attacker finds a vulnerability in one of the first lines of defence above.
+
+#### Security of Parsedown Extensions
+
+Safe mode does not necessarily yield safe results when using extensions to Parsedown. Extensions should be evaluated on their own to determine their specific safety against XSS.
+
+### Escaping HTML
+> ⚠️ **WARNING:** This method isn't safe from XSS!
+
+If you wish to escape HTML **in trusted input**, you can use the following:
+```php
+$parsedown = new Parsedown;
+$parsedown->setMarkupEscaped(true);
+```
+
+Beware that this still allows users to insert unsafe scripting vectors, such as links like `[xss](javascript:alert%281%29)`.
+
+### Questions
+
+**How does Parsedown work?**
+
+It tries to read Markdown like a human. First, it looks at the lines. It’s interested in how the lines start. This helps it recognise blocks. It knows, for example, that if a line starts with a `-` then perhaps it belongs to a list. Once it recognises the blocks, it continues to the content. As it reads, it watches out for special characters. This helps it recognise inline elements (or inlines).
+
+We call this approach "line based". We believe that Parsedown is the first Markdown parser to use it. Since the release of Parsedown, other developers have used the same approach to develop other Markdown parsers in PHP and in other languages.
+
+**Is it compliant with CommonMark?**
+
+It passes most of the CommonMark tests. Most of the tests that don't pass deal with cases that are quite uncommon. Still, as CommonMark matures, compliance should improve.
+
+**Who uses it?**
+
+[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony demo](https://github.com/symfony/symfony-demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents).
+
+**How can I help?**
+
+Use it, star it, share it and if you feel generous, [donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2).
diff --git a/lib/vendor/erusev/parsedown/composer.json b/lib/vendor/erusev/parsedown/composer.json
new file mode 100644
index 0000000000..f8b40f8ce8
--- /dev/null
+++ b/lib/vendor/erusev/parsedown/composer.json
@@ -0,0 +1,33 @@
+{
+ "name": "erusev/parsedown",
+ "description": "Parser for Markdown.",
+ "keywords": ["markdown", "parser"],
+ "homepage": "http://parsedown.org",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Emanuil Rusev",
+ "email": "hello@erusev.com",
+ "homepage": "http://erusev.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "ext-mbstring": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "autoload": {
+ "psr-0": {"Parsedown": ""}
+ },
+ "autoload-dev": {
+ "psr-0": {
+ "TestParsedown": "test/",
+ "ParsedownTest": "test/",
+ "CommonMarkTest": "test/",
+ "CommonMarkTestWeak": "test/"
+ }
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/CREDITS b/lib/vendor/ezyang/htmlpurifier/CREDITS
new file mode 100644
index 0000000000..7921b45af7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/CREDITS
@@ -0,0 +1,9 @@
+
+CREDITS
+
+Almost everything written by Edward Z. Yang (Ambush Commander). Lots of thanks
+to the DevNetwork Community for their help (see docs/ref-devnetwork.html for
+more details), Feyd especially (namely IPv6 and optimization). Thanks to RSnake
+for letting me package his fantastic XSS cheatsheet for a smoketest.
+
+ vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/LICENSE b/lib/vendor/ezyang/htmlpurifier/LICENSE
new file mode 100644
index 0000000000..8c88a20d45
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/LICENSE
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+ vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/README.md b/lib/vendor/ezyang/htmlpurifier/README.md
new file mode 100644
index 0000000000..e6b7199c12
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/README.md
@@ -0,0 +1,29 @@
+HTML Purifier [](https://github.com/ezyang/htmlpurifier/actions/workflows/ci.yml)
+=============
+
+HTML Purifier is an HTML filtering solution that uses a unique combination
+of robust whitelists and aggressive parsing to ensure that not only are
+XSS attacks thwarted, but the resulting HTML is standards compliant.
+
+HTML Purifier is oriented towards richly formatted documents from
+untrusted sources that require CSS and a full tag-set. This library can
+be configured to accept a more restrictive set of tags, but it won't be
+as efficient as more bare-bones parsers. It will, however, do the job
+right, which may be more important.
+
+Places to go:
+
+* See INSTALL for a quick installation guide
+* See docs/ for developer-oriented documentation, code examples and
+ an in-depth installation guide.
+* See WYSIWYG for information on editors like TinyMCE and FCKeditor
+
+HTML Purifier can be found on the web at: [http://htmlpurifier.org/](http://htmlpurifier.org/)
+
+## Installation
+
+Package available on [Composer](https://packagist.org/packages/ezyang/htmlpurifier).
+
+If you're using Composer to manage dependencies, you can use
+
+ $ composer require ezyang/htmlpurifier
diff --git a/lib/vendor/ezyang/htmlpurifier/VERSION b/lib/vendor/ezyang/htmlpurifier/VERSION
new file mode 100644
index 0000000000..ef8ffbdd9b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/VERSION
@@ -0,0 +1 @@
+4.18.0
\ No newline at end of file
diff --git a/lib/vendor/ezyang/htmlpurifier/composer.json b/lib/vendor/ezyang/htmlpurifier/composer.json
new file mode 100644
index 0000000000..7799d1d22d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/composer.json
@@ -0,0 +1,45 @@
+{
+ "name": "ezyang/htmlpurifier",
+ "description": "Standards compliant HTML filter written in PHP",
+ "type": "library",
+ "keywords": ["html"],
+ "homepage": "http://htmlpurifier.org/",
+ "license": "LGPL-2.1-or-later",
+ "authors": [
+ {
+ "name": "Edward Z. Yang",
+ "email": "admin@htmlpurifier.org",
+ "homepage": "http://ezyang.com"
+ }
+ ],
+ "require": {
+ "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
+ },
+ "require-dev": {
+ "cerdic/css-tidy": "^1.7 || ^2.0",
+ "simpletest/simpletest": "dev-master"
+ },
+ "autoload": {
+ "psr-0": { "HTMLPurifier": "library/" },
+ "files": ["library/HTMLPurifier.composer.php"],
+ "exclude-from-classmap": [
+ "/library/HTMLPurifier/Language/"
+ ]
+ },
+ "suggest": {
+ "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
+ "ext-iconv": "Converts text to and from non-UTF-8 encodings",
+ "ext-bcmath": "Used for unit conversion and imagecrash protection",
+ "ext-tidy": "Used for pretty-printing HTML"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/ezyang/simpletest.git",
+ "no-api": true
+ }
+ ]
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php
new file mode 100644
index 0000000000..1960c399f8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php
@@ -0,0 +1,11 @@
+purify($html, $config);
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php
new file mode 100644
index 0000000000..4c713a33af
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.includes.php
@@ -0,0 +1,236 @@
+ $attributes) {
+ $allowed_elements[$element] = true;
+ foreach ($attributes as $attribute => $x) {
+ $allowed_attributes["$element.$attribute"] = true;
+ }
+ }
+ $config->set('HTML.AllowedElements', $allowed_elements);
+ $config->set('HTML.AllowedAttributes', $allowed_attributes);
+ if ($allowed_protocols !== null) {
+ $config->set('URI.AllowedSchemes', $allowed_protocols);
+ }
+ $purifier = new HTMLPurifier($config);
+ return $purifier->purify($string);
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php
new file mode 100644
index 0000000000..39b1b65319
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.path.php
@@ -0,0 +1,11 @@
+config = HTMLPurifier_Config::create($config);
+ $this->strategy = new HTMLPurifier_Strategy_Core();
+ }
+
+ /**
+ * Adds a filter to process the output. First come first serve
+ *
+ * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object
+ */
+ public function addFilter($filter)
+ {
+ trigger_error(
+ 'HTMLPurifier->addFilter() is deprecated, use configuration directives' .
+ ' in the Filter namespace or Filter.Custom',
+ E_USER_WARNING
+ );
+ $this->filters[] = $filter;
+ }
+
+ /**
+ * Filters an HTML snippet/document to be XSS-free and standards-compliant.
+ *
+ * @param string $html String of HTML to purify
+ * @param HTMLPurifier_Config $config Config object for this operation,
+ * if omitted, defaults to the config object specified during this
+ * object's construction. The parameter can also be any type
+ * that HTMLPurifier_Config::create() supports.
+ *
+ * @return string Purified HTML
+ */
+ public function purify($html, $config = null)
+ {
+ // :TODO: make the config merge in, instead of replace
+ $config = $config ? HTMLPurifier_Config::create($config) : $this->config;
+
+ // implementation is partially environment dependant, partially
+ // configuration dependant
+ $lexer = HTMLPurifier_Lexer::create($config);
+
+ $context = new HTMLPurifier_Context();
+
+ // setup HTML generator
+ $this->generator = new HTMLPurifier_Generator($config, $context);
+ $context->register('Generator', $this->generator);
+
+ // set up global context variables
+ if ($config->get('Core.CollectErrors')) {
+ // may get moved out if other facilities use it
+ $language_factory = HTMLPurifier_LanguageFactory::instance();
+ $language = $language_factory->create($config, $context);
+ $context->register('Locale', $language);
+
+ $error_collector = new HTMLPurifier_ErrorCollector($context);
+ $context->register('ErrorCollector', $error_collector);
+ }
+
+ // setup id_accumulator context, necessary due to the fact that
+ // AttrValidator can be called from many places
+ $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
+ $context->register('IDAccumulator', $id_accumulator);
+
+ $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context);
+
+ // setup filters
+ $filter_flags = $config->getBatch('Filter');
+ $custom_filters = $filter_flags['Custom'];
+ unset($filter_flags['Custom']);
+ $filters = array();
+ foreach ($filter_flags as $filter => $flag) {
+ if (!$flag) {
+ continue;
+ }
+ if (strpos($filter, '.') !== false) {
+ continue;
+ }
+ $class = "HTMLPurifier_Filter_$filter";
+ $filters[] = new $class;
+ }
+ foreach ($custom_filters as $filter) {
+ // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat
+ $filters[] = $filter;
+ }
+ $filters = array_merge($filters, $this->filters);
+ // maybe prepare(), but later
+
+ for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) {
+ $html = $filters[$i]->preFilter($html, $config, $context);
+ }
+
+ // purified HTML
+ $html =
+ $this->generator->generateFromTokens(
+ // list of tokens
+ $this->strategy->execute(
+ // list of un-purified tokens
+ $lexer->tokenizeHTML(
+ // un-purified HTML
+ $html,
+ $config,
+ $context
+ ),
+ $config,
+ $context
+ )
+ );
+
+ for ($i = $filter_size - 1; $i >= 0; $i--) {
+ $html = $filters[$i]->postFilter($html, $config, $context);
+ }
+
+ $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context);
+ $this->context =& $context;
+ return $html;
+ }
+
+ /**
+ * Filters an array of HTML snippets
+ *
+ * @param string[] $array_of_html Array of html snippets
+ * @param HTMLPurifier_Config $config Optional config object for this operation.
+ * See HTMLPurifier::purify() for more details.
+ *
+ * @return string[] Array of purified HTML
+ */
+ public function purifyArray($array_of_html, $config = null)
+ {
+ $context_array = array();
+ $array = array();
+ foreach($array_of_html as $key=>$value){
+ if (is_array($value)) {
+ $array[$key] = $this->purifyArray($value, $config);
+ } else {
+ $array[$key] = $this->purify($value, $config);
+ }
+ $context_array[$key] = $this->context;
+ }
+ $this->context = $context_array;
+ return $array;
+ }
+
+ /**
+ * Singleton for enforcing just one HTML Purifier in your system
+ *
+ * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype
+ * HTMLPurifier instance to overload singleton with,
+ * or HTMLPurifier_Config instance to configure the
+ * generated version with.
+ *
+ * @return HTMLPurifier
+ */
+ public static function instance($prototype = null)
+ {
+ if (!self::$instance || $prototype) {
+ if ($prototype instanceof HTMLPurifier) {
+ self::$instance = $prototype;
+ } elseif ($prototype) {
+ self::$instance = new HTMLPurifier($prototype);
+ } else {
+ self::$instance = new HTMLPurifier();
+ }
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Singleton for enforcing just one HTML Purifier in your system
+ *
+ * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype
+ * HTMLPurifier instance to overload singleton with,
+ * or HTMLPurifier_Config instance to configure the
+ * generated version with.
+ *
+ * @return HTMLPurifier
+ * @note Backwards compatibility, see instance()
+ */
+ public static function getInstance($prototype = null)
+ {
+ return HTMLPurifier::instance($prototype);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php
new file mode 100644
index 0000000000..8a417d2462
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier.safe-includes.php
@@ -0,0 +1,230 @@
+getHTMLDefinition();
+ $parent = new HTMLPurifier_Token_Start($definition->info_parent);
+ $stack = array($parent->toNode());
+ foreach ($tokens as $token) {
+ $token->skip = null; // [MUT]
+ $token->carryover = null; // [MUT]
+ if ($token instanceof HTMLPurifier_Token_End) {
+ $token->start = null; // [MUT]
+ $r = array_pop($stack);
+ //assert($r->name === $token->name);
+ //assert(empty($token->attr));
+ $r->endCol = $token->col;
+ $r->endLine = $token->line;
+ $r->endArmor = $token->armor;
+ continue;
+ }
+ $node = $token->toNode();
+ $stack[count($stack)-1]->children[] = $node;
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ $stack[] = $node;
+ }
+ }
+ //assert(count($stack) == 1);
+ return $stack[0];
+ }
+
+ public static function flatten($node, $config, $context) {
+ $level = 0;
+ $nodes = array($level => new HTMLPurifier_Queue(array($node)));
+ $closingTokens = array();
+ $tokens = array();
+ do {
+ while (!$nodes[$level]->isEmpty()) {
+ $node = $nodes[$level]->shift(); // FIFO
+ list($start, $end) = $node->toTokenPair();
+ if ($level > 0) {
+ $tokens[] = $start;
+ }
+ if ($end !== NULL) {
+ $closingTokens[$level][] = $end;
+ }
+ if ($node instanceof HTMLPurifier_Node_Element) {
+ $level++;
+ $nodes[$level] = new HTMLPurifier_Queue();
+ foreach ($node->children as $childNode) {
+ $nodes[$level]->push($childNode);
+ }
+ }
+ }
+ $level--;
+ if ($level && isset($closingTokens[$level])) {
+ while ($token = array_pop($closingTokens[$level])) {
+ $tokens[] = $token;
+ }
+ }
+ } while ($level > 0);
+ return $tokens;
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php
new file mode 100644
index 0000000000..c7b17cf144
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrCollections.php
@@ -0,0 +1,148 @@
+doConstruct($attr_types, $modules);
+ }
+
+ public function doConstruct($attr_types, $modules)
+ {
+ // load extensions from the modules
+ foreach ($modules as $module) {
+ foreach ($module->attr_collections as $coll_i => $coll) {
+ if (!isset($this->info[$coll_i])) {
+ $this->info[$coll_i] = array();
+ }
+ foreach ($coll as $attr_i => $attr) {
+ if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) {
+ // merge in includes
+ $this->info[$coll_i][$attr_i] = array_merge(
+ $this->info[$coll_i][$attr_i],
+ $attr
+ );
+ continue;
+ }
+ $this->info[$coll_i][$attr_i] = $attr;
+ }
+ }
+ }
+ // perform internal expansions and inclusions
+ foreach ($this->info as $name => $attr) {
+ // merge attribute collections that include others
+ $this->performInclusions($this->info[$name]);
+ // replace string identifiers with actual attribute objects
+ $this->expandIdentifiers($this->info[$name], $attr_types);
+ }
+ }
+
+ /**
+ * Takes a reference to an attribute associative array and performs
+ * all inclusions specified by the zero index.
+ * @param array &$attr Reference to attribute array
+ */
+ public function performInclusions(&$attr)
+ {
+ if (!isset($attr[0])) {
+ return;
+ }
+ $merge = $attr[0];
+ $seen = array(); // recursion guard
+ // loop through all the inclusions
+ for ($i = 0; isset($merge[$i]); $i++) {
+ if (isset($seen[$merge[$i]])) {
+ continue;
+ }
+ $seen[$merge[$i]] = true;
+ // foreach attribute of the inclusion, copy it over
+ if (!isset($this->info[$merge[$i]])) {
+ continue;
+ }
+ foreach ($this->info[$merge[$i]] as $key => $value) {
+ if (isset($attr[$key])) {
+ continue;
+ } // also catches more inclusions
+ $attr[$key] = $value;
+ }
+ if (isset($this->info[$merge[$i]][0])) {
+ // recursion
+ $merge = array_merge($merge, $this->info[$merge[$i]][0]);
+ }
+ }
+ unset($attr[0]);
+ }
+
+ /**
+ * Expands all string identifiers in an attribute array by replacing
+ * them with the appropriate values inside HTMLPurifier_AttrTypes
+ * @param array &$attr Reference to attribute array
+ * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance
+ */
+ public function expandIdentifiers(&$attr, $attr_types)
+ {
+ // because foreach will process new elements we add, make sure we
+ // skip duplicates
+ $processed = array();
+
+ foreach ($attr as $def_i => $def) {
+ // skip inclusions
+ if ($def_i === 0) {
+ continue;
+ }
+
+ if (isset($processed[$def_i])) {
+ continue;
+ }
+
+ // determine whether or not attribute is required
+ if ($required = (strpos($def_i, '*') !== false)) {
+ // rename the definition
+ unset($attr[$def_i]);
+ $def_i = trim($def_i, '*');
+ $attr[$def_i] = $def;
+ }
+
+ $processed[$def_i] = true;
+
+ // if we've already got a literal object, move on
+ if (is_object($def)) {
+ // preserve previous required
+ $attr[$def_i]->required = ($required || $attr[$def_i]->required);
+ continue;
+ }
+
+ if ($def === false) {
+ unset($attr[$def_i]);
+ continue;
+ }
+
+ if ($t = $attr_types->get($def)) {
+ $attr[$def_i] = $t;
+ $attr[$def_i]->required = $required;
+ } else {
+ unset($attr[$def_i]);
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php
new file mode 100644
index 0000000000..739646fa7c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef.php
@@ -0,0 +1,144 @@
+ by removing
+ * leading and trailing whitespace, ignoring line feeds, and replacing
+ * carriage returns and tabs with spaces. While most useful for HTML
+ * attributes specified as CDATA, it can also be applied to most CSS
+ * values.
+ *
+ * @note This method is not entirely standards compliant, as trim() removes
+ * more types of whitespace than specified in the spec. In practice,
+ * this is rarely a problem, as those extra characters usually have
+ * already been removed by HTMLPurifier_Encoder.
+ *
+ * @warning This processing is inconsistent with XML's whitespace handling
+ * as specified by section 3.3.3 and referenced XHTML 1.0 section
+ * 4.7. However, note that we are NOT necessarily
+ * parsing XML, thus, this behavior may still be correct. We
+ * assume that newlines have been normalized.
+ */
+ public function parseCDATA($string)
+ {
+ $string = trim($string);
+ $string = str_replace(array("\n", "\t", "\r"), ' ', $string);
+ return $string;
+ }
+
+ /**
+ * Factory method for creating this class from a string.
+ * @param string $string String construction info
+ * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string
+ */
+ public function make($string)
+ {
+ // default implementation, return a flyweight of this object.
+ // If $string has an effect on the returned object (i.e. you
+ // need to overload this method), it is best
+ // to clone or instantiate new copies. (Instantiation is safer.)
+ return $this;
+ }
+
+ /**
+ * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work
+ * properly. THIS IS A HACK!
+ * @param string $string a CSS colour definition
+ * @return string
+ */
+ protected function mungeRgb($string)
+ {
+ $p = '\s*(\d+(\.\d+)?([%]?))\s*';
+
+ if (preg_match('/(rgba|hsla)\(/', $string)) {
+ return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string);
+ }
+
+ return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string);
+ }
+
+ /**
+ * Parses a possibly escaped CSS string and returns the "pure"
+ * version of it.
+ */
+ protected function expandCSSEscape($string)
+ {
+ // flexibly parse it
+ $ret = '';
+ for ($i = 0, $c = strlen($string); $i < $c; $i++) {
+ if ($string[$i] === '\\') {
+ $i++;
+ if ($i >= $c) {
+ $ret .= '\\';
+ break;
+ }
+ if (ctype_xdigit($string[$i])) {
+ $code = $string[$i];
+ for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) {
+ if (!ctype_xdigit($string[$i])) {
+ break;
+ }
+ $code .= $string[$i];
+ }
+ // We have to be extremely careful when adding
+ // new characters, to make sure we're not breaking
+ // the encoding.
+ $char = HTMLPurifier_Encoder::unichr(hexdec($code));
+ if (HTMLPurifier_Encoder::cleanUTF8($char) === '') {
+ continue;
+ }
+ $ret .= $char;
+ if ($i < $c && trim($string[$i]) !== '') {
+ $i--;
+ }
+ continue;
+ }
+ if ($string[$i] === "\n") {
+ continue;
+ }
+ }
+ $ret .= $string[$i];
+ }
+ return $ret;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php
new file mode 100644
index 0000000000..af6b8a05d1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS.php
@@ -0,0 +1,140 @@
+parseCDATA($css);
+
+ $definition = $config->getCSSDefinition();
+ $allow_duplicates = $config->get("CSS.AllowDuplicates");
+
+ $universal_attrdef = new HTMLPurifier_AttrDef_Enum(
+ array(
+ 'initial',
+ 'inherit',
+ 'unset',
+ )
+ );
+
+ // According to the CSS2.1 spec, the places where a
+ // non-delimiting semicolon can appear are in strings
+ // escape sequences. So here is some dumb hack to
+ // handle quotes.
+ $len = strlen($css);
+ $accum = "";
+ $declarations = array();
+ $quoted = false;
+ for ($i = 0; $i < $len; $i++) {
+ $c = strcspn($css, ";'\"", $i);
+ $accum .= substr($css, $i, $c);
+ $i += $c;
+ if ($i == $len) break;
+ $d = $css[$i];
+ if ($quoted) {
+ $accum .= $d;
+ if ($d == $quoted) {
+ $quoted = false;
+ }
+ } else {
+ if ($d == ";") {
+ $declarations[] = $accum;
+ $accum = "";
+ } else {
+ $accum .= $d;
+ $quoted = $d;
+ }
+ }
+ }
+ if ($accum != "") $declarations[] = $accum;
+
+ $propvalues = array();
+ $new_declarations = '';
+
+ /**
+ * Name of the current CSS property being validated.
+ */
+ $property = false;
+ $context->register('CurrentCSSProperty', $property);
+
+ foreach ($declarations as $declaration) {
+ if (!$declaration) {
+ continue;
+ }
+ if (!strpos($declaration, ':')) {
+ continue;
+ }
+ list($property, $value) = explode(':', $declaration, 2);
+ $property = trim($property);
+ $value = trim($value);
+ $ok = false;
+ do {
+ if (isset($definition->info[$property])) {
+ $ok = true;
+ break;
+ }
+ if (ctype_lower($property)) {
+ break;
+ }
+ $property = strtolower($property);
+ if (isset($definition->info[$property])) {
+ $ok = true;
+ break;
+ }
+ } while (0);
+ if (!$ok) {
+ continue;
+ }
+ $result = $universal_attrdef->validate($value, $config, $context);
+ if ($result === false) {
+ $result = $definition->info[$property]->validate(
+ $value,
+ $config,
+ $context
+ );
+ }
+ if ($result === false) {
+ continue;
+ }
+ if ($allow_duplicates) {
+ $new_declarations .= "$property:$result;";
+ } else {
+ $propvalues[$property] = $result;
+ }
+ }
+
+ $context->destroy('CurrentCSSProperty');
+
+ // procedure does not write the new CSS simultaneously, so it's
+ // slightly inefficient, but it's the only way of getting rid of
+ // duplicates. Perhaps config to optimize it, but not now.
+
+ foreach ($propvalues as $prop => $value) {
+ $new_declarations .= "$prop:$value;";
+ }
+
+ return $new_declarations ? $new_declarations : false;
+
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php
new file mode 100644
index 0000000000..af2b83dff8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/AlphaValue.php
@@ -0,0 +1,34 @@
+ 1.0) {
+ $result = '1';
+ }
+ return $result;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php
new file mode 100644
index 0000000000..28c49883a7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Background.php
@@ -0,0 +1,113 @@
+getCSSDefinition();
+ $this->info['background-color'] = $def->info['background-color'];
+ $this->info['background-image'] = $def->info['background-image'];
+ $this->info['background-repeat'] = $def->info['background-repeat'];
+ $this->info['background-attachment'] = $def->info['background-attachment'];
+ $this->info['background-position'] = $def->info['background-position'];
+ $this->info['background-size'] = $def->info['background-size'];
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ // regular pre-processing
+ $string = $this->parseCDATA($string);
+ if ($string === '') {
+ return false;
+ }
+
+ // munge rgb() decl if necessary
+ $string = $this->mungeRgb($string);
+
+ // assumes URI doesn't have spaces in it
+ $bits = explode(' ', $string); // bits to process
+
+ $caught = array();
+ $caught['color'] = false;
+ $caught['image'] = false;
+ $caught['repeat'] = false;
+ $caught['attachment'] = false;
+ $caught['position'] = false;
+ $caught['size'] = false;
+
+ $i = 0; // number of catches
+
+ foreach ($bits as $bit) {
+ if ($bit === '') {
+ continue;
+ }
+ foreach ($caught as $key => $status) {
+ if ($key != 'position') {
+ if ($status !== false) {
+ continue;
+ }
+ $r = $this->info['background-' . $key]->validate($bit, $config, $context);
+ } else {
+ $r = $bit;
+ }
+ if ($r === false) {
+ continue;
+ }
+ if ($key == 'position') {
+ if ($caught[$key] === false) {
+ $caught[$key] = '';
+ }
+ $caught[$key] .= $r . ' ';
+ } else {
+ $caught[$key] = $r;
+ }
+ $i++;
+ break;
+ }
+ }
+
+ if (!$i) {
+ return false;
+ }
+ if ($caught['position'] !== false) {
+ $caught['position'] = $this->info['background-position']->
+ validate($caught['position'], $config, $context);
+ }
+
+ $ret = array();
+ foreach ($caught as $value) {
+ if ($value === false) {
+ continue;
+ }
+ $ret[] = $value;
+ }
+
+ if (empty($ret)) {
+ return false;
+ }
+ return implode(' ', $ret);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php
new file mode 100644
index 0000000000..4580ef5a91
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php
@@ -0,0 +1,157 @@
+ | | left | center | right
+ ]
+ [
+ | | top | center | bottom
+ ]?
+ ] |
+ [ // this signifies that the vertical and horizontal adjectives
+ // can be arbitrarily ordered, however, there can only be two,
+ // one of each, or none at all
+ [
+ left | center | right
+ ] ||
+ [
+ top | center | bottom
+ ]
+ ]
+ top, left = 0%
+ center, (none) = 50%
+ bottom, right = 100%
+*/
+
+/* QuirksMode says:
+ keyword + length/percentage must be ordered correctly, as per W3C
+
+ Internet Explorer and Opera, however, support arbitrary ordering. We
+ should fix it up.
+
+ Minor issue though, not strictly necessary.
+*/
+
+// control freaks may appreciate the ability to convert these to
+// percentages or something, but it's not necessary
+
+/**
+ * Validates the value of background-position.
+ */
+class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef
+{
+
+ /**
+ * @type HTMLPurifier_AttrDef_CSS_Length
+ */
+ protected $length;
+
+ /**
+ * @type HTMLPurifier_AttrDef_CSS_Percentage
+ */
+ protected $percentage;
+
+ public function __construct()
+ {
+ $this->length = new HTMLPurifier_AttrDef_CSS_Length();
+ $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage();
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $string = $this->parseCDATA($string);
+ $bits = explode(' ', $string);
+
+ $keywords = array();
+ $keywords['h'] = false; // left, right
+ $keywords['v'] = false; // top, bottom
+ $keywords['ch'] = false; // center (first word)
+ $keywords['cv'] = false; // center (second word)
+ $measures = array();
+
+ $i = 0;
+
+ $lookup = array(
+ 'top' => 'v',
+ 'bottom' => 'v',
+ 'left' => 'h',
+ 'right' => 'h',
+ 'center' => 'c'
+ );
+
+ foreach ($bits as $bit) {
+ if ($bit === '') {
+ continue;
+ }
+
+ // test for keyword
+ $lbit = ctype_lower($bit) ? $bit : strtolower($bit);
+ if (isset($lookup[$lbit])) {
+ $status = $lookup[$lbit];
+ if ($status == 'c') {
+ if ($i == 0) {
+ $status = 'ch';
+ } else {
+ $status = 'cv';
+ }
+ }
+ $keywords[$status] = $lbit;
+ $i++;
+ }
+
+ // test for length
+ $r = $this->length->validate($bit, $config, $context);
+ if ($r !== false) {
+ $measures[] = $r;
+ $i++;
+ }
+
+ // test for percentage
+ $r = $this->percentage->validate($bit, $config, $context);
+ if ($r !== false) {
+ $measures[] = $r;
+ $i++;
+ }
+ }
+
+ if (!$i) {
+ return false;
+ } // no valid values were caught
+
+ $ret = array();
+
+ // first keyword
+ if ($keywords['h']) {
+ $ret[] = $keywords['h'];
+ } elseif ($keywords['ch']) {
+ $ret[] = $keywords['ch'];
+ $keywords['cv'] = false; // prevent re-use: center = center center
+ } elseif (count($measures)) {
+ $ret[] = array_shift($measures);
+ }
+
+ if ($keywords['v']) {
+ $ret[] = $keywords['v'];
+ } elseif ($keywords['cv']) {
+ $ret[] = $keywords['cv'];
+ } elseif (count($measures)) {
+ $ret[] = array_shift($measures);
+ }
+
+ if (empty($ret)) {
+ return false;
+ }
+ return implode(' ', $ret);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php
new file mode 100644
index 0000000000..16243ba1ed
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Border.php
@@ -0,0 +1,56 @@
+getCSSDefinition();
+ $this->info['border-width'] = $def->info['border-width'];
+ $this->info['border-style'] = $def->info['border-style'];
+ $this->info['border-top-color'] = $def->info['border-top-color'];
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $string = $this->parseCDATA($string);
+ $string = $this->mungeRgb($string);
+ $bits = explode(' ', $string);
+ $done = array(); // segments we've finished
+ $ret = ''; // return value
+ foreach ($bits as $bit) {
+ foreach ($this->info as $propname => $validator) {
+ if (isset($done[$propname])) {
+ continue;
+ }
+ $r = $validator->validate($bit, $config, $context);
+ if ($r !== false) {
+ $ret .= $r . ' ';
+ $done[$propname] = true;
+ break;
+ }
+ }
+ }
+ return rtrim($ret);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php
new file mode 100644
index 0000000000..d7287a00c2
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Color.php
@@ -0,0 +1,161 @@
+alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue();
+ }
+
+ /**
+ * @param string $color
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($color, $config, $context)
+ {
+ static $colors = null;
+ if ($colors === null) {
+ $colors = $config->get('Core.ColorKeywords');
+ }
+
+ $color = trim($color);
+ if ($color === '') {
+ return false;
+ }
+
+ $lower = strtolower($color);
+ if (isset($colors[$lower])) {
+ return $colors[$lower];
+ }
+
+ if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) {
+ $length = strlen($color);
+ if (strpos($color, ')') !== $length - 1) {
+ return false;
+ }
+
+ // get used function : rgb, rgba, hsl or hsla
+ $function = $matches[1];
+
+ $parameters_size = 3;
+ $alpha_channel = false;
+ if (substr($function, -1) === 'a') {
+ $parameters_size = 4;
+ $alpha_channel = true;
+ }
+
+ /*
+ * Allowed types for values :
+ * parameter_position => [type => max_value]
+ */
+ $allowed_types = array(
+ 1 => array('percentage' => 100, 'integer' => 255),
+ 2 => array('percentage' => 100, 'integer' => 255),
+ 3 => array('percentage' => 100, 'integer' => 255),
+ );
+ $allow_different_types = false;
+
+ if (strpos($function, 'hsl') !== false) {
+ $allowed_types = array(
+ 1 => array('integer' => 360),
+ 2 => array('percentage' => 100),
+ 3 => array('percentage' => 100),
+ );
+ $allow_different_types = true;
+ }
+
+ $values = trim(str_replace($function, '', $color), ' ()');
+
+ $parts = explode(',', $values);
+ if (count($parts) !== $parameters_size) {
+ return false;
+ }
+
+ $type = false;
+ $new_parts = array();
+ $i = 0;
+
+ foreach ($parts as $part) {
+ $i++;
+ $part = trim($part);
+
+ if ($part === '') {
+ return false;
+ }
+
+ // different check for alpha channel
+ if ($alpha_channel === true && $i === count($parts)) {
+ $result = $this->alpha->validate($part, $config, $context);
+
+ if ($result === false) {
+ return false;
+ }
+
+ $new_parts[] = (string)$result;
+ continue;
+ }
+
+ if (substr($part, -1) === '%') {
+ $current_type = 'percentage';
+ } else {
+ $current_type = 'integer';
+ }
+
+ if (!array_key_exists($current_type, $allowed_types[$i])) {
+ return false;
+ }
+
+ if (!$type) {
+ $type = $current_type;
+ }
+
+ if ($allow_different_types === false && $type != $current_type) {
+ return false;
+ }
+
+ $max_value = $allowed_types[$i][$current_type];
+
+ if ($current_type == 'integer') {
+ // Return value between range 0 -> $max_value
+ $new_parts[] = (int)max(min($part, $max_value), 0);
+ } elseif ($current_type == 'percentage') {
+ $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%';
+ }
+ }
+
+ $new_values = implode(',', $new_parts);
+
+ $color = $function . '(' . $new_values . ')';
+ } else {
+ // hexadecimal handling
+ if ($color[0] === '#') {
+ $hex = substr($color, 1);
+ } else {
+ $hex = $color;
+ $color = '#' . $color;
+ }
+ $length = strlen($hex);
+ if ($length !== 3 && $length !== 6) {
+ return false;
+ }
+ if (!ctype_xdigit($hex)) {
+ return false;
+ }
+ }
+ return $color;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php
new file mode 100644
index 0000000000..9c1750554f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Composite.php
@@ -0,0 +1,48 @@
+defs = $defs;
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ foreach ($this->defs as $i => $def) {
+ $result = $this->defs[$i]->validate($string, $config, $context);
+ if ($result !== false) {
+ return $result;
+ }
+ }
+ return false;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php
new file mode 100644
index 0000000000..9d77cc9aaf
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php
@@ -0,0 +1,44 @@
+def = $def;
+ $this->element = $element;
+ }
+
+ /**
+ * Checks if CurrentToken is set and equal to $this->element
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $token = $context->get('CurrentToken', true);
+ if ($token && $token->name == $this->element) {
+ return false;
+ }
+ return $this->def->validate($string, $config, $context);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php
new file mode 100644
index 0000000000..bde4c3301f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Filter.php
@@ -0,0 +1,77 @@
+intValidator = new HTMLPurifier_AttrDef_Integer();
+ }
+
+ /**
+ * @param string $value
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($value, $config, $context)
+ {
+ $value = $this->parseCDATA($value);
+ if ($value === 'none') {
+ return $value;
+ }
+ // if we looped this we could support multiple filters
+ $function_length = strcspn($value, '(');
+ $function = trim(substr($value, 0, $function_length));
+ if ($function !== 'alpha' &&
+ $function !== 'Alpha' &&
+ $function !== 'progid:DXImageTransform.Microsoft.Alpha'
+ ) {
+ return false;
+ }
+ $cursor = $function_length + 1;
+ $parameters_length = strcspn($value, ')', $cursor);
+ $parameters = substr($value, $cursor, $parameters_length);
+ $params = explode(',', $parameters);
+ $ret_params = array();
+ $lookup = array();
+ foreach ($params as $param) {
+ list($key, $value) = explode('=', $param);
+ $key = trim($key);
+ $value = trim($value);
+ if (isset($lookup[$key])) {
+ continue;
+ }
+ if ($key !== 'opacity') {
+ continue;
+ }
+ $value = $this->intValidator->validate($value, $config, $context);
+ if ($value === false) {
+ continue;
+ }
+ $int = (int)$value;
+ if ($int > 100) {
+ $value = '100';
+ }
+ if ($int < 0) {
+ $value = '0';
+ }
+ $ret_params[] = "$key=$value";
+ $lookup[$key] = true;
+ }
+ $ret_parameters = implode(',', $ret_params);
+ $ret_function = "$function($ret_parameters)";
+ return $ret_function;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php
new file mode 100644
index 0000000000..579b97ef1c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Font.php
@@ -0,0 +1,176 @@
+getCSSDefinition();
+ $this->info['font-style'] = $def->info['font-style'];
+ $this->info['font-variant'] = $def->info['font-variant'];
+ $this->info['font-weight'] = $def->info['font-weight'];
+ $this->info['font-size'] = $def->info['font-size'];
+ $this->info['line-height'] = $def->info['line-height'];
+ $this->info['font-family'] = $def->info['font-family'];
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ static $system_fonts = array(
+ 'caption' => true,
+ 'icon' => true,
+ 'menu' => true,
+ 'message-box' => true,
+ 'small-caption' => true,
+ 'status-bar' => true
+ );
+
+ // regular pre-processing
+ $string = $this->parseCDATA($string);
+ if ($string === '') {
+ return false;
+ }
+
+ // check if it's one of the keywords
+ $lowercase_string = strtolower($string);
+ if (isset($system_fonts[$lowercase_string])) {
+ return $lowercase_string;
+ }
+
+ $bits = explode(' ', $string); // bits to process
+ $stage = 0; // this indicates what we're looking for
+ $caught = array(); // which stage 0 properties have we caught?
+ $stage_1 = array('font-style', 'font-variant', 'font-weight');
+ $final = ''; // output
+
+ for ($i = 0, $size = count($bits); $i < $size; $i++) {
+ if ($bits[$i] === '') {
+ continue;
+ }
+ switch ($stage) {
+ case 0: // attempting to catch font-style, font-variant or font-weight
+ foreach ($stage_1 as $validator_name) {
+ if (isset($caught[$validator_name])) {
+ continue;
+ }
+ $r = $this->info[$validator_name]->validate(
+ $bits[$i],
+ $config,
+ $context
+ );
+ if ($r !== false) {
+ $final .= $r . ' ';
+ $caught[$validator_name] = true;
+ break;
+ }
+ }
+ // all three caught, continue on
+ if (count($caught) >= 3) {
+ $stage = 1;
+ }
+ if ($r !== false) {
+ break;
+ }
+ case 1: // attempting to catch font-size and perhaps line-height
+ $found_slash = false;
+ if (strpos($bits[$i], '/') !== false) {
+ list($font_size, $line_height) =
+ explode('/', $bits[$i]);
+ if ($line_height === '') {
+ // ooh, there's a space after the slash!
+ $line_height = false;
+ $found_slash = true;
+ }
+ } else {
+ $font_size = $bits[$i];
+ $line_height = false;
+ }
+ $r = $this->info['font-size']->validate(
+ $font_size,
+ $config,
+ $context
+ );
+ if ($r !== false) {
+ $final .= $r;
+ // attempt to catch line-height
+ if ($line_height === false) {
+ // we need to scroll forward
+ for ($j = $i + 1; $j < $size; $j++) {
+ if ($bits[$j] === '') {
+ continue;
+ }
+ if ($bits[$j] === '/') {
+ if ($found_slash) {
+ return false;
+ } else {
+ $found_slash = true;
+ continue;
+ }
+ }
+ $line_height = $bits[$j];
+ break;
+ }
+ } else {
+ // slash already found
+ $found_slash = true;
+ $j = $i;
+ }
+ if ($found_slash) {
+ $i = $j;
+ $r = $this->info['line-height']->validate(
+ $line_height,
+ $config,
+ $context
+ );
+ if ($r !== false) {
+ $final .= '/' . $r;
+ }
+ }
+ $final .= ' ';
+ $stage = 2;
+ break;
+ }
+ return false;
+ case 2: // attempting to catch font-family
+ $font_family =
+ implode(' ', array_slice($bits, $i, $size - $i));
+ $r = $this->info['font-family']->validate(
+ $font_family,
+ $config,
+ $context
+ );
+ if ($r !== false) {
+ $final .= $r . ' ';
+ // processing completed successfully
+ return rtrim($final);
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php
new file mode 100644
index 0000000000..f1ff116361
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php
@@ -0,0 +1,217 @@
+mask = array_reduce($c, function ($carry, $value) {
+ return $carry . $value;
+ }, '_- ');
+
+ /*
+ PHP's internal strcspn implementation is
+ O(length of string * length of mask), making it inefficient
+ for large masks. However, it's still faster than
+ preg_match 8)
+ for (p = s1;;) {
+ spanp = s2;
+ do {
+ if (*spanp == c || p == s1_end) {
+ return p - s1;
+ }
+ } while (spanp++ < (s2_end - 1));
+ c = *++p;
+ }
+ */
+ // possible optimization: invert the mask.
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ static $generic_names = array(
+ 'serif' => true,
+ 'sans-serif' => true,
+ 'monospace' => true,
+ 'fantasy' => true,
+ 'cursive' => true
+ );
+ $allowed_fonts = $config->get('CSS.AllowedFonts');
+
+ // assume that no font names contain commas in them
+ $fonts = explode(',', $string);
+ $final = '';
+ foreach ($fonts as $font) {
+ $font = trim($font);
+ if ($font === '') {
+ continue;
+ }
+ // match a generic name
+ if (isset($generic_names[$font])) {
+ if ($allowed_fonts === null || isset($allowed_fonts[$font])) {
+ $final .= $font . ', ';
+ }
+ continue;
+ }
+ // match a quoted name
+ if ($font[0] === '"' || $font[0] === "'") {
+ $length = strlen($font);
+ if ($length <= 2) {
+ continue;
+ }
+ $quote = $font[0];
+ if ($font[$length - 1] !== $quote) {
+ continue;
+ }
+ $font = substr($font, 1, $length - 2);
+ }
+
+ $font = $this->expandCSSEscape($font);
+
+ // $font is a pure representation of the font name
+
+ if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) {
+ continue;
+ }
+
+ if (ctype_alnum($font) && $font !== '') {
+ // very simple font, allow it in unharmed
+ $final .= $font . ', ';
+ continue;
+ }
+
+ // bugger out on whitespace. form feed (0C) really
+ // shouldn't show up regardless
+ $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font);
+
+ // Here, there are various classes of characters which need
+ // to be treated differently:
+ // - Alphanumeric characters are essentially safe. We
+ // handled these above.
+ // - Spaces require quoting, though most parsers will do
+ // the right thing if there aren't any characters that
+ // can be misinterpreted
+ // - Dashes rarely occur, but they fairly unproblematic
+ // for parsing/rendering purposes.
+ // The above characters cover the majority of Western font
+ // names.
+ // - Arbitrary Unicode characters not in ASCII. Because
+ // most parsers give little thought to Unicode, treatment
+ // of these codepoints is basically uniform, even for
+ // punctuation-like codepoints. These characters can
+ // show up in non-Western pages and are supported by most
+ // major browsers, for example: "MS 明朝" is a
+ // legitimate font-name
+ // . See
+ // the CSS3 spec for more examples:
+ //
+ // You can see live samples of these on the Internet:
+ //
+ // However, most of these fonts have ASCII equivalents:
+ // for example, 'MS Mincho', and it's considered
+ // professional to use ASCII font names instead of
+ // Unicode font names. Thanks Takeshi Terada for
+ // providing this information.
+ // The following characters, to my knowledge, have not been
+ // used to name font names.
+ // - Single quote. While theoretically you might find a
+ // font name that has a single quote in its name (serving
+ // as an apostrophe, e.g. Dave's Scribble), I haven't
+ // been able to find any actual examples of this.
+ // Internet Explorer's cssText translation (which I
+ // believe is invoked by innerHTML) normalizes any
+ // quoting to single quotes, and fails to escape single
+ // quotes. (Note that this is not IE's behavior for all
+ // CSS properties, just some sort of special casing for
+ // font-family). So a single quote *cannot* be used
+ // safely in the font-family context if there will be an
+ // innerHTML/cssText translation. Note that Firefox 3.x
+ // does this too.
+ // - Double quote. In IE, these get normalized to
+ // single-quotes, no matter what the encoding. (Fun
+ // fact, in IE8, the 'content' CSS property gained
+ // support, where they special cased to preserve encoded
+ // double quotes, but still translate unadorned double
+ // quotes into single quotes.) So, because their
+ // fixpoint behavior is identical to single quotes, they
+ // cannot be allowed either. Firefox 3.x displays
+ // single-quote style behavior.
+ // - Backslashes are reduced by one (so \\ -> \) every
+ // iteration, so they cannot be used safely. This shows
+ // up in IE7, IE8 and FF3
+ // - Semicolons, commas and backticks are handled properly.
+ // - The rest of the ASCII punctuation is handled properly.
+ // We haven't checked what browsers do to unadorned
+ // versions, but this is not important as long as the
+ // browser doesn't /remove/ surrounding quotes (as IE does
+ // for HTML).
+ //
+ // With these results in hand, we conclude that there are
+ // various levels of safety:
+ // - Paranoid: alphanumeric, spaces and dashes(?)
+ // - International: Paranoid + non-ASCII Unicode
+ // - Edgy: Everything except quotes, backslashes
+ // - NoJS: Standards compliance, e.g. sod IE. Note that
+ // with some judicious character escaping (since certain
+ // types of escaping doesn't work) this is theoretically
+ // OK as long as innerHTML/cssText is not called.
+ // We believe that international is a reasonable default
+ // (that we will implement now), and once we do more
+ // extensive research, we may feel comfortable with dropping
+ // it down to edgy.
+
+ // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of
+ // str(c)spn assumes that the string was already well formed
+ // Unicode (which of course it is).
+ if (strspn($font, $this->mask) !== strlen($font)) {
+ continue;
+ }
+
+ // Historical:
+ // In the absence of innerHTML/cssText, these ugly
+ // transforms don't pose a security risk (as \\ and \"
+ // might--these escapes are not supported by most browsers).
+ // We could try to be clever and use single-quote wrapping
+ // when there is a double quote present, but I have choosen
+ // not to implement that. (NOTE: you can reduce the amount
+ // of escapes by one depending on what quoting style you use)
+ // $font = str_replace('\\', '\\5C ', $font);
+ // $font = str_replace('"', '\\22 ', $font);
+ // $font = str_replace("'", '\\27 ', $font);
+
+ // font possibly with spaces, requires quoting
+ $final .= "'$font', ";
+ }
+ $final = rtrim($final, ', ');
+ if ($final === '') {
+ return false;
+ }
+ return $final;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php
new file mode 100644
index 0000000000..973002c17f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ident.php
@@ -0,0 +1,32 @@
+def = $def;
+ $this->allow = $allow;
+ }
+
+ /**
+ * Intercepts and removes !important if necessary
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ // test for ! and important tokens
+ $string = trim($string);
+ $is_important = false;
+ // :TODO: optimization: test directly for !important and ! important
+ if (strlen($string) >= 9 && substr($string, -9) === 'important') {
+ $temp = rtrim(substr($string, 0, -9));
+ // use a temp, because we might want to restore important
+ if (strlen($temp) >= 1 && substr($temp, -1) === '!') {
+ $string = rtrim(substr($temp, 0, -1));
+ $is_important = true;
+ }
+ }
+ $string = $this->def->validate($string, $config, $context);
+ if ($this->allow && $is_important) {
+ $string .= ' !important';
+ }
+ return $string;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php
new file mode 100644
index 0000000000..f12453a04a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Length.php
@@ -0,0 +1,77 @@
+min = $min !== null ? HTMLPurifier_Length::make($min) : null;
+ $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null;
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $string = $this->parseCDATA($string);
+
+ // Optimizations
+ if ($string === '') {
+ return false;
+ }
+ if ($string === '0') {
+ return '0';
+ }
+ if (strlen($string) === 1) {
+ return false;
+ }
+
+ $length = HTMLPurifier_Length::make($string);
+ if (!$length->isValid()) {
+ return false;
+ }
+
+ if ($this->min) {
+ $c = $length->compareTo($this->min);
+ if ($c === false) {
+ return false;
+ }
+ if ($c < 0) {
+ return false;
+ }
+ }
+ if ($this->max) {
+ $c = $length->compareTo($this->max);
+ if ($c === false) {
+ return false;
+ }
+ if ($c > 0) {
+ return false;
+ }
+ }
+ return $length->toString();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php
new file mode 100644
index 0000000000..e74d42654e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/ListStyle.php
@@ -0,0 +1,112 @@
+getCSSDefinition();
+ $this->info['list-style-type'] = $def->info['list-style-type'];
+ $this->info['list-style-position'] = $def->info['list-style-position'];
+ $this->info['list-style-image'] = $def->info['list-style-image'];
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ // regular pre-processing
+ $string = $this->parseCDATA($string);
+ if ($string === '') {
+ return false;
+ }
+
+ // assumes URI doesn't have spaces in it
+ $bits = explode(' ', strtolower($string)); // bits to process
+
+ $caught = array();
+ $caught['type'] = false;
+ $caught['position'] = false;
+ $caught['image'] = false;
+
+ $i = 0; // number of catches
+ $none = false;
+
+ foreach ($bits as $bit) {
+ if ($i >= 3) {
+ return;
+ } // optimization bit
+ if ($bit === '') {
+ continue;
+ }
+ foreach ($caught as $key => $status) {
+ if ($status !== false) {
+ continue;
+ }
+ $r = $this->info['list-style-' . $key]->validate($bit, $config, $context);
+ if ($r === false) {
+ continue;
+ }
+ if ($r === 'none') {
+ if ($none) {
+ continue;
+ } else {
+ $none = true;
+ }
+ if ($key == 'image') {
+ continue;
+ }
+ }
+ $caught[$key] = $r;
+ $i++;
+ break;
+ }
+ }
+
+ if (!$i) {
+ return false;
+ }
+
+ $ret = array();
+
+ // construct type
+ if ($caught['type']) {
+ $ret[] = $caught['type'];
+ }
+
+ // construct image
+ if ($caught['image']) {
+ $ret[] = $caught['image'];
+ }
+
+ // construct position
+ if ($caught['position']) {
+ $ret[] = $caught['position'];
+ }
+
+ if (empty($ret)) {
+ return false;
+ }
+ return implode(' ', $ret);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php
new file mode 100644
index 0000000000..e707f871ca
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Multiple.php
@@ -0,0 +1,71 @@
+single = $single;
+ $this->max = $max;
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $string = $this->mungeRgb($this->parseCDATA($string));
+ if ($string === '') {
+ return false;
+ }
+ $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n
+ $length = count($parts);
+ $final = '';
+ for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) {
+ if (ctype_space($parts[$i])) {
+ continue;
+ }
+ $result = $this->single->validate($parts[$i], $config, $context);
+ if ($result !== false) {
+ $final .= $result . ' ';
+ $num++;
+ }
+ }
+ if ($final === '') {
+ return false;
+ }
+ return rtrim($final);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php
new file mode 100644
index 0000000000..ef49d20fdd
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Number.php
@@ -0,0 +1,90 @@
+non_negative = $non_negative;
+ }
+
+ /**
+ * @param string $number
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return string|bool
+ * @warning Some contexts do not pass $config, $context. These
+ * variables should not be used without checking HTMLPurifier_Length
+ */
+ public function validate($number, $config, $context)
+ {
+ $number = $this->parseCDATA($number);
+
+ if ($number === '') {
+ return false;
+ }
+ if ($number === '0') {
+ return '0';
+ }
+
+ $sign = '';
+ switch ($number[0]) {
+ case '-':
+ if ($this->non_negative) {
+ return false;
+ }
+ $sign = '-';
+ case '+':
+ $number = substr($number, 1);
+ }
+
+ if (ctype_digit($number)) {
+ $number = ltrim($number, '0');
+ return $number ? $sign . $number : '0';
+ }
+
+ // Period is the only non-numeric character allowed
+ if (strpos($number, '.') === false) {
+ return false;
+ }
+
+ list($left, $right) = explode('.', $number, 2);
+
+ if ($left === '' && $right === '') {
+ return false;
+ }
+ if ($left !== '' && !ctype_digit($left)) {
+ return false;
+ }
+
+ // Remove leading zeros until positive number or a zero stays left
+ if (ltrim($left, '0') != '') {
+ $left = ltrim($left, '0');
+ } else {
+ $left = '0';
+ }
+
+ $right = rtrim($right, '0');
+
+ if ($right === '') {
+ return $left ? $sign . $left : '0';
+ } elseif (!ctype_digit($right)) {
+ return false;
+ }
+ return $sign . $left . '.' . $right;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php
new file mode 100644
index 0000000000..f0f25c50a8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Percentage.php
@@ -0,0 +1,54 @@
+number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative);
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $string = $this->parseCDATA($string);
+
+ if ($string === '') {
+ return false;
+ }
+ $length = strlen($string);
+ if ($length === 1) {
+ return false;
+ }
+ if ($string[$length - 1] !== '%') {
+ return false;
+ }
+
+ $number = substr($string, 0, $length - 1);
+ $number = $this->number_def->validate($number, $config, $context);
+
+ if ($number === false) {
+ return false;
+ }
+ return "$number%";
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php
new file mode 100644
index 0000000000..e08e2c496f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/Ratio.php
@@ -0,0 +1,46 @@
+parseCDATA($ratio);
+
+ $parts = explode('/', $ratio, 2);
+ $length = count($parts);
+
+ if ($length < 1 || $length > 2) {
+ return false;
+ }
+
+ $num = new \HTMLPurifier_AttrDef_CSS_Number();
+
+ if ($length === 1) {
+ return $num->validate($parts[0], $config, $context);
+ }
+
+ $num1 = $num->validate($parts[0], $config, $context);
+ $num2 = $num->validate($parts[1], $config, $context);
+
+ if ($num1 === false || $num2 === false) {
+ return false;
+ }
+
+ return $num1 . '/' . $num2;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php
new file mode 100644
index 0000000000..5fd4b7f7b4
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/TextDecoration.php
@@ -0,0 +1,46 @@
+ true,
+ 'overline' => true,
+ 'underline' => true,
+ );
+
+ $string = strtolower($this->parseCDATA($string));
+
+ if ($string === 'none') {
+ return $string;
+ }
+
+ $parts = explode(' ', $string);
+ $final = '';
+ foreach ($parts as $part) {
+ if (isset($allowed_values[$part])) {
+ $final .= $part . ' ';
+ }
+ }
+ $final = rtrim($final);
+ if ($final === '') {
+ return false;
+ }
+ return $final;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php
new file mode 100644
index 0000000000..6617acace5
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/URI.php
@@ -0,0 +1,77 @@
+parseCDATA($uri_string);
+ if (strpos($uri_string, 'url(') !== 0) {
+ return false;
+ }
+ $uri_string = substr($uri_string, 4);
+ if (strlen($uri_string) == 0) {
+ return false;
+ }
+ $new_length = strlen($uri_string) - 1;
+ if ($uri_string[$new_length] != ')') {
+ return false;
+ }
+ $uri = trim(substr($uri_string, 0, $new_length));
+
+ if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) {
+ $quote = $uri[0];
+ $new_length = strlen($uri) - 1;
+ if ($uri[$new_length] !== $quote) {
+ return false;
+ }
+ $uri = substr($uri, 1, $new_length - 1);
+ }
+
+ $uri = $this->expandCSSEscape($uri);
+
+ $result = parent::validate($uri, $config, $context);
+
+ if ($result === false) {
+ return false;
+ }
+
+ // extra sanity check; should have been done by URI
+ $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result);
+
+ // suspicious characters are ()'; we're going to percent encode
+ // them for safety.
+ $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result);
+
+ // there's an extra bug where ampersands lose their escaping on
+ // an innerHTML cycle, so a very unlucky query parameter could
+ // then change the meaning of the URL. Unfortunately, there's
+ // not much we can do about that...
+ return "url(\"$result\")";
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php
new file mode 100644
index 0000000000..6698a00c01
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Clone.php
@@ -0,0 +1,44 @@
+clone = $clone;
+ }
+
+ /**
+ * @param string $v
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($v, $config, $context)
+ {
+ return $this->clone->validate($v, $config, $context);
+ }
+
+ /**
+ * @param string $string
+ * @return HTMLPurifier_AttrDef
+ */
+ public function make($string)
+ {
+ return clone $this->clone;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php
new file mode 100644
index 0000000000..8abda7f6e2
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Enum.php
@@ -0,0 +1,73 @@
+valid_values = array_flip($valid_values);
+ $this->case_sensitive = $case_sensitive;
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $string = trim($string);
+ if (!$this->case_sensitive) {
+ // we may want to do full case-insensitive libraries
+ $string = ctype_lower($string) ? $string : strtolower($string);
+ }
+ $result = isset($this->valid_values[$string]);
+
+ return $result ? $string : false;
+ }
+
+ /**
+ * @param string $string In form of comma-delimited list of case-insensitive
+ * valid values. Example: "foo,bar,baz". Prepend "s:" to make
+ * case sensitive
+ * @return HTMLPurifier_AttrDef_Enum
+ */
+ public function make($string)
+ {
+ if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') {
+ $string = substr($string, 2);
+ $sensitive = true;
+ } else {
+ $sensitive = false;
+ }
+ $values = explode(',', $string);
+ return new HTMLPurifier_AttrDef_Enum($values, $sensitive);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php
new file mode 100644
index 0000000000..be3bbc8dc0
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Bool.php
@@ -0,0 +1,48 @@
+name = $name;
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $string Name of attribute
+ * @return HTMLPurifier_AttrDef_HTML_Bool
+ */
+ public function make($string)
+ {
+ return new HTMLPurifier_AttrDef_HTML_Bool($string);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php
new file mode 100644
index 0000000000..d5013488fc
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Class.php
@@ -0,0 +1,48 @@
+getDefinition('HTML')->doctype->name;
+ if ($name == "XHTML 1.1" || $name == "XHTML 2.0") {
+ return parent::split($string, $config, $context);
+ } else {
+ return preg_split('/\s+/', $string);
+ }
+ }
+
+ /**
+ * @param array $tokens
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ protected function filter($tokens, $config, $context)
+ {
+ $allowed = $config->get('Attr.AllowedClasses');
+ $forbidden = $config->get('Attr.ForbiddenClasses');
+ $ret = array();
+ foreach ($tokens as $token) {
+ if (($allowed === null || isset($allowed[$token])) &&
+ !isset($forbidden[$token]) &&
+ // We need this O(n) check because of PHP's array
+ // implementation that casts -0 to 0.
+ !in_array($token, $ret, true)
+ ) {
+ $ret[] = $token;
+ }
+ }
+ return $ret;
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php
new file mode 100644
index 0000000000..946ebb7820
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Color.php
@@ -0,0 +1,51 @@
+get('Core.ColorKeywords');
+ }
+
+ $string = trim($string);
+
+ if (empty($string)) {
+ return false;
+ }
+ $lower = strtolower($string);
+ if (isset($colors[$lower])) {
+ return $colors[$lower];
+ }
+ if ($string[0] === '#') {
+ $hex = substr($string, 1);
+ } else {
+ $hex = $string;
+ }
+
+ $length = strlen($hex);
+ if ($length !== 3 && $length !== 6) {
+ return false;
+ }
+ if (!ctype_xdigit($hex)) {
+ return false;
+ }
+ if ($length === 3) {
+ $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
+ }
+ return "#$hex";
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ContentEditable.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ContentEditable.php
new file mode 100644
index 0000000000..5b03d3e378
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ContentEditable.php
@@ -0,0 +1,16 @@
+get('HTML.Trusted')) {
+ $allowed = array('', 'true', 'false');
+ }
+
+ $enum = new HTMLPurifier_AttrDef_Enum($allowed);
+
+ return $enum->validate($string, $config, $context);
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php
new file mode 100644
index 0000000000..d79ba12b3f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/FrameTarget.php
@@ -0,0 +1,38 @@
+valid_values === false) {
+ $this->valid_values = $config->get('Attr.AllowedFrameTargets');
+ }
+ return parent::validate($string, $config, $context);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php
new file mode 100644
index 0000000000..4ba45610fe
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/ID.php
@@ -0,0 +1,113 @@
+selector = $selector;
+ }
+
+ /**
+ * @param string $id
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($id, $config, $context)
+ {
+ if (!$this->selector && !$config->get('Attr.EnableID')) {
+ return false;
+ }
+
+ $id = trim($id); // trim it first
+
+ if ($id === '') {
+ return false;
+ }
+
+ $prefix = $config->get('Attr.IDPrefix');
+ if ($prefix !== '') {
+ $prefix .= $config->get('Attr.IDPrefixLocal');
+ // prevent re-appending the prefix
+ if (strpos($id, $prefix) !== 0) {
+ $id = $prefix . $id;
+ }
+ } elseif ($config->get('Attr.IDPrefixLocal') !== '') {
+ trigger_error(
+ '%Attr.IDPrefixLocal cannot be used unless ' .
+ '%Attr.IDPrefix is set',
+ E_USER_WARNING
+ );
+ }
+
+ if (!$this->selector) {
+ $id_accumulator =& $context->get('IDAccumulator');
+ if (isset($id_accumulator->ids[$id])) {
+ return false;
+ }
+ }
+
+ // we purposely avoid using regex, hopefully this is faster
+
+ if ($config->get('Attr.ID.HTML5') === true) {
+ if (preg_match('/[\t\n\x0b\x0c ]/', $id)) {
+ return false;
+ }
+ } else {
+ if (ctype_alpha($id)) {
+ // OK
+ } else {
+ if (!ctype_alpha(@$id[0])) {
+ return false;
+ }
+ // primitive style of regexps, I suppose
+ $trim = trim(
+ $id,
+ 'A..Za..z0..9:-._'
+ );
+ if ($trim !== '') {
+ return false;
+ }
+ }
+ }
+
+ $regexp = $config->get('Attr.IDBlacklistRegexp');
+ if ($regexp && preg_match($regexp, $id)) {
+ return false;
+ }
+
+ if (!$this->selector) {
+ $id_accumulator->add($id);
+ }
+
+ // if no change was made to the ID, return the result
+ // else, return the new id if stripping whitespace made it
+ // valid, or return false.
+ return $id;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php
new file mode 100644
index 0000000000..1c4006fbbd
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Length.php
@@ -0,0 +1,56 @@
+ 100) {
+ return '100%';
+ }
+ return ((string)$points) . '%';
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php
new file mode 100644
index 0000000000..63fa04c15c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/LinkTypes.php
@@ -0,0 +1,72 @@
+ 'AllowedRel',
+ 'rev' => 'AllowedRev'
+ );
+ if (!isset($configLookup[$name])) {
+ trigger_error(
+ 'Unrecognized attribute name for link ' .
+ 'relationship.',
+ E_USER_ERROR
+ );
+ return;
+ }
+ $this->name = $configLookup[$name];
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $allowed = $config->get('Attr.' . $this->name);
+ if (empty($allowed)) {
+ return false;
+ }
+
+ $string = $this->parseCDATA($string);
+ $parts = explode(' ', $string);
+
+ // lookup to prevent duplicates
+ $ret_lookup = array();
+ foreach ($parts as $part) {
+ $part = strtolower(trim($part));
+ if (!isset($allowed[$part])) {
+ continue;
+ }
+ $ret_lookup[$part] = true;
+ }
+
+ if (empty($ret_lookup)) {
+ return false;
+ }
+ $string = implode(' ', array_keys($ret_lookup));
+ return $string;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php
new file mode 100644
index 0000000000..bbb20f2f80
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/MultiLength.php
@@ -0,0 +1,60 @@
+split($string, $config, $context);
+ $tokens = $this->filter($tokens, $config, $context);
+ if (empty($tokens)) {
+ return false;
+ }
+ return implode(' ', $tokens);
+ }
+
+ /**
+ * Splits a space separated list of tokens into its constituent parts.
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ protected function split($string, $config, $context)
+ {
+ // OPTIMIZABLE!
+ // do the preg_match, capture all subpatterns for reformulation
+
+ // we don't support U+00A1 and up codepoints or
+ // escaping because I don't know how to do that with regexps
+ // and plus it would complicate optimization efforts (you never
+ // see that anyway).
+ $pattern = '/(?:(?<=\s)|\A)' . // look behind for space or string start
+ '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)' .
+ '(?:(?=\s)|\z)/'; // look ahead for space or string end
+ preg_match_all($pattern, $string, $matches);
+ return $matches[1];
+ }
+
+ /**
+ * Template method for removing certain tokens based on arbitrary criteria.
+ * @note If we wanted to be really functional, we'd do an array_filter
+ * with a callback. But... we're not.
+ * @param array $tokens
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ protected function filter($tokens, $config, $context)
+ {
+ return $tokens;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php
new file mode 100644
index 0000000000..a1d019e095
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/HTML/Pixels.php
@@ -0,0 +1,76 @@
+max = $max;
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $string = trim($string);
+ if ($string === '0') {
+ return $string;
+ }
+ if ($string === '') {
+ return false;
+ }
+ $length = strlen($string);
+ if (substr($string, $length - 2) == 'px') {
+ $string = substr($string, 0, $length - 2);
+ }
+ if (!is_numeric($string)) {
+ return false;
+ }
+ $int = (int)$string;
+
+ if ($int < 0) {
+ return '0';
+ }
+
+ // upper-bound value, extremely high values can
+ // crash operating systems, see
+ // WARNING, above link WILL crash you if you're using Windows
+
+ if ($this->max !== null && $int > $this->max) {
+ return (string)$this->max;
+ }
+ return (string)$int;
+ }
+
+ /**
+ * @param string $string
+ * @return HTMLPurifier_AttrDef
+ */
+ public function make($string)
+ {
+ if ($string === '') {
+ $max = null;
+ } else {
+ $max = (int)$string;
+ }
+ $class = get_class($this);
+ return new $class($max);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php
new file mode 100644
index 0000000000..400e707d2f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Integer.php
@@ -0,0 +1,91 @@
+negative = $negative;
+ $this->zero = $zero;
+ $this->positive = $positive;
+ }
+
+ /**
+ * @param string $integer
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($integer, $config, $context)
+ {
+ $integer = $this->parseCDATA($integer);
+ if ($integer === '') {
+ return false;
+ }
+
+ // we could possibly simply typecast it to integer, but there are
+ // certain fringe cases that must not return an integer.
+
+ // clip leading sign
+ if ($this->negative && $integer[0] === '-') {
+ $digits = substr($integer, 1);
+ if ($digits === '0') {
+ $integer = '0';
+ } // rm minus sign for zero
+ } elseif ($this->positive && $integer[0] === '+') {
+ $digits = $integer = substr($integer, 1); // rm unnecessary plus
+ } else {
+ $digits = $integer;
+ }
+
+ // test if it's numeric
+ if (!ctype_digit($digits)) {
+ return false;
+ }
+
+ // perform scope tests
+ if (!$this->zero && $integer == 0) {
+ return false;
+ }
+ if (!$this->positive && $integer > 0) {
+ return false;
+ }
+ if (!$this->negative && $integer < 0) {
+ return false;
+ }
+
+ return $integer;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php
new file mode 100644
index 0000000000..2a55cea642
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Lang.php
@@ -0,0 +1,86 @@
+ 8 || !ctype_alnum($subtags[1])) {
+ return $new_string;
+ }
+ if (!ctype_lower($subtags[1])) {
+ $subtags[1] = strtolower($subtags[1]);
+ }
+
+ $new_string .= '-' . $subtags[1];
+ if ($num_subtags == 2) {
+ return $new_string;
+ }
+
+ // process all other subtags, index 2 and up
+ for ($i = 2; $i < $num_subtags; $i++) {
+ $length = strlen($subtags[$i]);
+ if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) {
+ return $new_string;
+ }
+ if (!ctype_lower($subtags[$i])) {
+ $subtags[$i] = strtolower($subtags[$i]);
+ }
+ $new_string .= '-' . $subtags[$i];
+ }
+ return $new_string;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php
new file mode 100644
index 0000000000..c7eb3199a4
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Switch.php
@@ -0,0 +1,53 @@
+tag = $tag;
+ $this->withTag = $with_tag;
+ $this->withoutTag = $without_tag;
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $token = $context->get('CurrentToken', true);
+ if (!$token || $token->name !== $this->tag) {
+ return $this->withoutTag->validate($string, $config, $context);
+ } else {
+ return $this->withTag->validate($string, $config, $context);
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php
new file mode 100644
index 0000000000..4553a4ea9b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/Text.php
@@ -0,0 +1,21 @@
+parseCDATA($string);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php
new file mode 100644
index 0000000000..c1cd89772c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI.php
@@ -0,0 +1,111 @@
+parser = new HTMLPurifier_URIParser();
+ $this->embedsResource = (bool)$embeds_resource;
+ }
+
+ /**
+ * @param string $string
+ * @return HTMLPurifier_AttrDef_URI
+ */
+ public function make($string)
+ {
+ $embeds = ($string === 'embedded');
+ return new HTMLPurifier_AttrDef_URI($embeds);
+ }
+
+ /**
+ * @param string $uri
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($uri, $config, $context)
+ {
+ if ($config->get('URI.Disable')) {
+ return false;
+ }
+
+ $uri = $this->parseCDATA($uri);
+
+ // parse the URI
+ $uri = $this->parser->parse($uri);
+ if ($uri === false) {
+ return false;
+ }
+
+ // add embedded flag to context for validators
+ $context->register('EmbeddedURI', $this->embedsResource);
+
+ $ok = false;
+ do {
+
+ // generic validation
+ $result = $uri->validate($config, $context);
+ if (!$result) {
+ break;
+ }
+
+ // chained filtering
+ $uri_def = $config->getDefinition('URI');
+ $result = $uri_def->filter($uri, $config, $context);
+ if (!$result) {
+ break;
+ }
+
+ // scheme-specific validation
+ $scheme_obj = $uri->getSchemeObj($config, $context);
+ if (!$scheme_obj) {
+ break;
+ }
+ if ($this->embedsResource && !$scheme_obj->browsable) {
+ break;
+ }
+ $result = $scheme_obj->validate($uri, $config, $context);
+ if (!$result) {
+ break;
+ }
+
+ // Post chained filtering
+ $result = $uri_def->postFilter($uri, $config, $context);
+ if (!$result) {
+ break;
+ }
+
+ // survived gauntlet
+ $ok = true;
+
+ } while (false);
+
+ $context->destroy('EmbeddedURI');
+ if (!$ok) {
+ return false;
+ }
+ // back to string
+ return $uri->toString();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php
new file mode 100644
index 0000000000..daf32b7643
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Email.php
@@ -0,0 +1,20 @@
+"
+ // that needs more percent encoding to be done
+ if ($string == '') {
+ return false;
+ }
+ $string = trim($string);
+ $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string);
+ return $result ? $string : false;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php
new file mode 100644
index 0000000000..17a97c1ecd
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php
@@ -0,0 +1,136 @@
+ipv4 = new HTMLPurifier_AttrDef_URI_IPv4();
+ $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6();
+ }
+
+ /**
+ * @param string $string
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string
+ */
+ public function validate($string, $config, $context)
+ {
+ $length = strlen($string);
+ // empty hostname is OK; it's usually semantically equivalent:
+ // the default host as defined by a URI scheme is used:
+ //
+ // If the URI scheme defines a default for host, then that
+ // default applies when the host subcomponent is undefined
+ // or when the registered name is empty (zero length).
+ if ($string === '') {
+ return '';
+ }
+ if ($length > 1 && $string[0] === '[' && $string[$length - 1] === ']') {
+ //IPv6
+ $ip = substr($string, 1, $length - 2);
+ $valid = $this->ipv6->validate($ip, $config, $context);
+ if ($valid === false) {
+ return false;
+ }
+ return '[' . $valid . ']';
+ }
+
+ // need to do checks on unusual encodings too
+ $ipv4 = $this->ipv4->validate($string, $config, $context);
+ if ($ipv4 !== false) {
+ return $ipv4;
+ }
+
+ // A regular domain name.
+
+ // This doesn't match I18N domain names, but we don't have proper IRI support,
+ // so force users to insert Punycode.
+
+ // Underscores defined as Unreserved Characters in RFC 3986 are
+ // allowed in a URI. There are cases where we want to consider a
+ // URI containing "_" such as "_dmarc.example.com".
+ // Underscores are not allowed in the default. If you want to
+ // allow it, set Core.AllowHostnameUnderscore to true.
+ $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : '';
+
+ // Based off of RFC 1738, but amended so that
+ // as per RFC 3696, the top label need only not be all numeric.
+ // The productions describing this are:
+ $a = '[a-z]'; // alpha
+ $an = "[a-z0-9$underscore]"; // alphanum
+ $and = "[a-z0-9-$underscore]"; // alphanum | "-"
+ // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+ $domainlabel = "$an(?:$and*$an)?";
+ // AMENDED as per RFC 3696
+ // toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+ // side condition: not all numeric
+ $toplabel = "$an(?:$and*$an)?";
+ // hostname = *( domainlabel "." ) toplabel [ "." ]
+ if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) {
+ if (!ctype_digit($matches[1])) {
+ return $string;
+ }
+ }
+
+ // PHP 5.3 and later support this functionality natively
+ if (function_exists('idn_to_ascii')) {
+ if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) {
+ $string = idn_to_ascii($string, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
+ } else {
+ $string = idn_to_ascii($string);
+ }
+
+ // If we have Net_IDNA2 support, we can support IRIs by
+ // punycoding them. (This is the most portable thing to do,
+ // since otherwise we have to assume browsers support
+ } elseif ($config->get('Core.EnableIDNA') && class_exists('Net_IDNA2')) {
+ $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true));
+ // we need to encode each period separately
+ $parts = explode('.', $string);
+ try {
+ $new_parts = array();
+ foreach ($parts as $part) {
+ $encodable = false;
+ for ($i = 0, $c = strlen($part); $i < $c; $i++) {
+ if (ord($part[$i]) > 0x7a) {
+ $encodable = true;
+ break;
+ }
+ }
+ if (!$encodable) {
+ $new_parts[] = $part;
+ } else {
+ $new_parts[] = $idna->encode($part);
+ }
+ }
+ $string = implode('.', $new_parts);
+ } catch (Exception $e) {
+ // XXX error reporting
+ }
+ }
+ // Try again
+ if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) {
+ return $string;
+ }
+ return false;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php
new file mode 100644
index 0000000000..30ac16c9e7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv4.php
@@ -0,0 +1,45 @@
+ip4) {
+ $this->_loadRegex();
+ }
+
+ if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) {
+ return $aIP;
+ }
+ return false;
+ }
+
+ /**
+ * Lazy load function to prevent regex from being stuffed in
+ * cache.
+ */
+ protected function _loadRegex()
+ {
+ $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255
+ $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})";
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php
new file mode 100644
index 0000000000..f243793eeb
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/IPv6.php
@@ -0,0 +1,89 @@
+ip4) {
+ $this->_loadRegex();
+ }
+
+ $original = $aIP;
+
+ $hex = '[0-9a-fA-F]';
+ $blk = '(?:' . $hex . '{1,4})';
+ $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128
+
+ // prefix check
+ if (strpos($aIP, '/') !== false) {
+ if (preg_match('#' . $pre . '$#s', $aIP, $find)) {
+ $aIP = substr($aIP, 0, 0 - strlen($find[0]));
+ unset($find);
+ } else {
+ return false;
+ }
+ }
+
+ // IPv4-compatiblity check
+ if (preg_match('#(?<=:' . ')' . $this->ip4 . '$#s', $aIP, $find)) {
+ $aIP = substr($aIP, 0, 0 - strlen($find[0]));
+ $ip = explode('.', $find[0]);
+ $ip = array_map('dechex', $ip);
+ $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3];
+ unset($find, $ip);
+ }
+
+ // compression check
+ $aIP = explode('::', $aIP);
+ $c = count($aIP);
+ if ($c > 2) {
+ return false;
+ } elseif ($c == 2) {
+ list($first, $second) = $aIP;
+ $first = explode(':', $first);
+ $second = explode(':', $second);
+
+ if (count($first) + count($second) > 8) {
+ return false;
+ }
+
+ while (count($first) < 8) {
+ array_push($first, '0');
+ }
+
+ array_splice($first, 8 - count($second), 8, $second);
+ $aIP = $first;
+ unset($first, $second);
+ } else {
+ $aIP = explode(':', $aIP[0]);
+ }
+ $c = count($aIP);
+
+ if ($c != 8) {
+ return false;
+ }
+
+ // All the pieces should be 16-bit hex strings. Are they?
+ foreach ($aIP as $piece) {
+ if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece))) {
+ return false;
+ }
+ }
+ return $original;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php
new file mode 100644
index 0000000000..b428331f15
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform.php
@@ -0,0 +1,60 @@
+confiscateAttr($attr, 'background');
+ // some validation should happen here
+
+ $this->prependCSS($attr, "background-image:url($background);");
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php
new file mode 100644
index 0000000000..d66c04a5b8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BdoDir.php
@@ -0,0 +1,27 @@
+get('Attr.DefaultTextDir');
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php
new file mode 100644
index 0000000000..0f51fd2cec
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BgColor.php
@@ -0,0 +1,28 @@
+confiscateAttr($attr, 'bgcolor');
+ // some validation should happen here
+
+ $this->prependCSS($attr, "background-color:$bgcolor;");
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php
new file mode 100644
index 0000000000..f25cd01955
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/BoolToCSS.php
@@ -0,0 +1,47 @@
+attr = $attr;
+ $this->css = $css;
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr[$this->attr])) {
+ return $attr;
+ }
+ unset($attr[$this->attr]);
+ $this->prependCSS($attr, $this->css);
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php
new file mode 100644
index 0000000000..057dc017fa
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Border.php
@@ -0,0 +1,26 @@
+confiscateAttr($attr, 'border');
+ // some validation should happen here
+ $this->prependCSS($attr, "border:{$border_width}px solid;");
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php
new file mode 100644
index 0000000000..7ccd0e3fb7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/EnumToCSS.php
@@ -0,0 +1,68 @@
+attr = $attr;
+ $this->enumToCSS = $enum_to_css;
+ $this->caseSensitive = (bool)$case_sensitive;
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr[$this->attr])) {
+ return $attr;
+ }
+
+ $value = trim($attr[$this->attr]);
+ unset($attr[$this->attr]);
+
+ if (!$this->caseSensitive) {
+ $value = strtolower($value);
+ }
+
+ if (!isset($this->enumToCSS[$value])) {
+ return $attr;
+ }
+ $this->prependCSS($attr, $this->enumToCSS[$value]);
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php
new file mode 100644
index 0000000000..235ebb34b6
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgRequired.php
@@ -0,0 +1,47 @@
+get('Core.RemoveInvalidImg')) {
+ return $attr;
+ }
+ $attr['src'] = $config->get('Attr.DefaultInvalidImage');
+ $src = false;
+ }
+
+ if (!isset($attr['alt'])) {
+ if ($src) {
+ $alt = $config->get('Attr.DefaultImageAlt');
+ if ($alt === null) {
+ $attr['alt'] = basename($attr['src']);
+ } else {
+ $attr['alt'] = $alt;
+ }
+ } else {
+ $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt');
+ }
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php
new file mode 100644
index 0000000000..350b3358fc
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ImgSpace.php
@@ -0,0 +1,61 @@
+ array('left', 'right'),
+ 'vspace' => array('top', 'bottom')
+ );
+
+ /**
+ * @param string $attr
+ */
+ public function __construct($attr)
+ {
+ $this->attr = $attr;
+ if (!isset($this->css[$attr])) {
+ trigger_error(htmlspecialchars($attr) . ' is not valid space attribute');
+ }
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr[$this->attr])) {
+ return $attr;
+ }
+
+ $width = $this->confiscateAttr($attr, $this->attr);
+ // some validation could happen here
+
+ if (!isset($this->css[$this->attr])) {
+ return $attr;
+ }
+
+ $style = '';
+ foreach ($this->css[$this->attr] as $suffix) {
+ $property = "margin-$suffix";
+ $style .= "$property:{$width}px;";
+ }
+ $this->prependCSS($attr, $style);
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php
new file mode 100644
index 0000000000..3ab47ed8c9
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Input.php
@@ -0,0 +1,56 @@
+pixels = new HTMLPurifier_AttrDef_HTML_Pixels();
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr['type'])) {
+ $t = 'text';
+ } else {
+ $t = strtolower($attr['type']);
+ }
+ if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') {
+ unset($attr['checked']);
+ }
+ if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') {
+ unset($attr['maxlength']);
+ }
+ if (isset($attr['size']) && $t !== 'text' && $t !== 'password') {
+ $result = $this->pixels->validate($attr['size'], $config, $context);
+ if ($result === false) {
+ unset($attr['size']);
+ } else {
+ $attr['size'] = $result;
+ }
+ }
+ if (isset($attr['src']) && $t !== 'image') {
+ unset($attr['src']);
+ }
+ if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) {
+ $attr['value'] = '';
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php
new file mode 100644
index 0000000000..5b0aff0e40
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Lang.php
@@ -0,0 +1,31 @@
+name = $name;
+ $this->cssName = $css_name ? $css_name : $name;
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr[$this->name])) {
+ return $attr;
+ }
+ $length = $this->confiscateAttr($attr, $this->name);
+ if (ctype_digit($length)) {
+ $length .= 'px';
+ }
+ $this->prependCSS($attr, $this->cssName . ":$length;");
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php
new file mode 100644
index 0000000000..63cce6837a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Name.php
@@ -0,0 +1,33 @@
+get('HTML.Attr.Name.UseCDATA')) {
+ return $attr;
+ }
+ if (!isset($attr['name'])) {
+ return $attr;
+ }
+ $id = $this->confiscateAttr($attr, 'name');
+ if (isset($attr['id'])) {
+ return $attr;
+ }
+ $attr['id'] = $id;
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php
new file mode 100644
index 0000000000..5a1fdbbfcc
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php
@@ -0,0 +1,46 @@
+idDef = new HTMLPurifier_AttrDef_HTML_ID();
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr['name'])) {
+ return $attr;
+ }
+ $name = $attr['name'];
+ if (isset($attr['id']) && $attr['id'] === $name) {
+ return $attr;
+ }
+ $result = $this->idDef->validate($name, $config, $context);
+ if ($result === false) {
+ unset($attr['name']);
+ } else {
+ $attr['name'] = $result;
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php
new file mode 100644
index 0000000000..1057ebee1b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/Nofollow.php
@@ -0,0 +1,52 @@
+parser = new HTMLPurifier_URIParser();
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr['href'])) {
+ return $attr;
+ }
+
+ // XXX Kind of inefficient
+ $url = $this->parser->parse($attr['href']);
+ $scheme = $url->getSchemeObj($config, $context);
+
+ if ($scheme->browsable && !$url->isLocal($config, $context)) {
+ if (isset($attr['rel'])) {
+ $rels = explode(' ', $attr['rel']);
+ if (!in_array('nofollow', $rels)) {
+ $rels[] = 'nofollow';
+ }
+ $attr['rel'] = implode(' ', $rels);
+ } else {
+ $attr['rel'] = 'nofollow';
+ }
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php
new file mode 100644
index 0000000000..231c81a3f0
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/SafeEmbed.php
@@ -0,0 +1,25 @@
+uri = new HTMLPurifier_AttrDef_URI(true); // embedded
+ $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent'));
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ // If we add support for other objects, we'll need to alter the
+ // transforms.
+ switch ($attr['name']) {
+ // application/x-shockwave-flash
+ // Keep this synchronized with Injector/SafeObject.php
+ case 'allowScriptAccess':
+ $attr['value'] = 'never';
+ break;
+ case 'allowNetworking':
+ $attr['value'] = 'internal';
+ break;
+ case 'allowFullScreen':
+ if ($config->get('HTML.FlashAllowFullScreen')) {
+ $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false';
+ } else {
+ $attr['value'] = 'false';
+ }
+ break;
+ case 'wmode':
+ $attr['value'] = $this->wmode->validate($attr['value'], $config, $context);
+ break;
+ case 'movie':
+ case 'src':
+ $attr['name'] = "movie";
+ $attr['value'] = $this->uri->validate($attr['value'], $config, $context);
+ break;
+ case 'flashvars':
+ // we're going to allow arbitrary inputs to the SWF, on
+ // the reasoning that it could only hack the SWF, not us.
+ break;
+ // add other cases to support other param name/value pairs
+ default:
+ $attr['name'] = $attr['value'] = null;
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php
new file mode 100644
index 0000000000..b7057bbf8e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/ScriptRequired.php
@@ -0,0 +1,23 @@
+
+ */
+class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform
+{
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr['type'])) {
+ $attr['type'] = 'text/javascript';
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php
new file mode 100644
index 0000000000..cc30ab8c38
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetBlank.php
@@ -0,0 +1,49 @@
+parser = new HTMLPurifier_URIParser();
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr['href'])) {
+ return $attr;
+ }
+
+ // XXX Kind of inefficient
+ $url = $this->parser->parse($attr['href']);
+
+ // Ignore invalid schemes (e.g. `javascript:`)
+ if (!($scheme = $url->getSchemeObj($config, $context))) {
+ return $attr;
+ }
+
+ if ($scheme->browsable && !$url->isBenign($config, $context)) {
+ $attr['target'] = '_blank';
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php
new file mode 100644
index 0000000000..1db3c6c09e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/TargetNoopener.php
@@ -0,0 +1,37 @@
+
+ */
+class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform
+{
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ // Calculated from Firefox
+ if (!isset($attr['cols'])) {
+ $attr['cols'] = '22';
+ }
+ if (!isset($attr['rows'])) {
+ $attr['rows'] = '3';
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php
new file mode 100644
index 0000000000..e4429e86d8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTypes.php
@@ -0,0 +1,97 @@
+info['Enum'] = new HTMLPurifier_AttrDef_Enum();
+ $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool();
+
+ $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID();
+ $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length();
+ $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength();
+ $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens();
+ $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels();
+ $this->info['Text'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['URI'] = new HTMLPurifier_AttrDef_URI();
+ $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang();
+ $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color();
+ $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right');
+ $this->info['LAlign'] = self::makeEnum('top,bottom,left,right');
+ $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget();
+ $this->info['ContentEditable'] = new HTMLPurifier_AttrDef_HTML_ContentEditable();
+
+ // unimplemented aliases
+ $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text();
+ $this->info['Character'] = new HTMLPurifier_AttrDef_Text();
+
+ // "proprietary" types
+ $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class();
+
+ // number is really a positive integer (one or more digits)
+ // FIXME: ^^ not always, see start and value of list items
+ $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true);
+ }
+
+ private static function makeEnum($in)
+ {
+ return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in)));
+ }
+
+ /**
+ * Retrieves a type
+ * @param string $type String type name
+ * @return HTMLPurifier_AttrDef Object AttrDef for type
+ */
+ public function get($type)
+ {
+ // determine if there is any extra info tacked on
+ if (strpos($type, '#') !== false) {
+ list($type, $string) = explode('#', $type, 2);
+ } else {
+ $string = '';
+ }
+
+ if (!isset($this->info[$type])) {
+ trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR);
+ return;
+ }
+ return $this->info[$type]->make($string);
+ }
+
+ /**
+ * Sets a new implementation for a type
+ * @param string $type String type name
+ * @param HTMLPurifier_AttrDef $impl Object AttrDef for type
+ */
+ public function set($type, $impl)
+ {
+ $this->info[$type] = $impl;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php
new file mode 100644
index 0000000000..f97dc93edd
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php
@@ -0,0 +1,178 @@
+getHTMLDefinition();
+ $e =& $context->get('ErrorCollector', true);
+
+ // initialize IDAccumulator if necessary
+ $ok =& $context->get('IDAccumulator', true);
+ if (!$ok) {
+ $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context);
+ $context->register('IDAccumulator', $id_accumulator);
+ }
+
+ // initialize CurrentToken if necessary
+ $current_token =& $context->get('CurrentToken', true);
+ if (!$current_token) {
+ $context->register('CurrentToken', $token);
+ }
+
+ if (!$token instanceof HTMLPurifier_Token_Start &&
+ !$token instanceof HTMLPurifier_Token_Empty
+ ) {
+ return;
+ }
+
+ // create alias to global definition array, see also $defs
+ // DEFINITION CALL
+ $d_defs = $definition->info_global_attr;
+
+ // don't update token until the very end, to ensure an atomic update
+ $attr = $token->attr;
+
+ // do global transformations (pre)
+ // nothing currently utilizes this
+ foreach ($definition->info_attr_transform_pre as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) {
+ $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+ }
+
+ // do local transformations only applicable to this element (pre)
+ // ex.
to
+ foreach ($definition->info[$token->name]->attr_transform_pre as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) {
+ $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+ }
+
+ // create alias to this element's attribute definition array, see
+ // also $d_defs (global attribute definition array)
+ // DEFINITION CALL
+ $defs = $definition->info[$token->name]->attr;
+
+ $attr_key = false;
+ $context->register('CurrentAttr', $attr_key);
+
+ // iterate through all the attribute keypairs
+ // Watch out for name collisions: $key has previously been used
+ foreach ($attr as $attr_key => $value) {
+
+ // call the definition
+ if (isset($defs[$attr_key])) {
+ // there is a local definition defined
+ if ($defs[$attr_key] === false) {
+ // We've explicitly been told not to allow this element.
+ // This is usually when there's a global definition
+ // that must be overridden.
+ // Theoretically speaking, we could have a
+ // AttrDef_DenyAll, but this is faster!
+ $result = false;
+ } else {
+ // validate according to the element's definition
+ $result = $defs[$attr_key]->validate(
+ $value,
+ $config,
+ $context
+ );
+ }
+ } elseif (isset($d_defs[$attr_key])) {
+ // there is a global definition defined, validate according
+ // to the global definition
+ $result = $d_defs[$attr_key]->validate(
+ $value,
+ $config,
+ $context
+ );
+ } else {
+ // system never heard of the attribute? DELETE!
+ $result = false;
+ }
+
+ // put the results into effect
+ if ($result === false || $result === null) {
+ // this is a generic error message that should replaced
+ // with more specific ones when possible
+ if ($e) {
+ $e->send(E_ERROR, 'AttrValidator: Attribute removed');
+ }
+
+ // remove the attribute
+ unset($attr[$attr_key]);
+ } elseif (is_string($result)) {
+ // generally, if a substitution is happening, there
+ // was some sort of implicit correction going on. We'll
+ // delegate it to the attribute classes to say exactly what.
+
+ // simple substitution
+ $attr[$attr_key] = $result;
+ } else {
+ // nothing happens
+ }
+
+ // we'd also want slightly more complicated substitution
+ // involving an array as the return value,
+ // although we're not sure how colliding attributes would
+ // resolve (certain ones would be completely overriden,
+ // others would prepend themselves).
+ }
+
+ $context->destroy('CurrentAttr');
+
+ // post transforms
+
+ // global (error reporting untested)
+ foreach ($definition->info_attr_transform_post as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) {
+ $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+ }
+
+ // local (error reporting untested)
+ foreach ($definition->info[$token->name]->attr_transform_post as $transform) {
+ $attr = $transform->transform($o = $attr, $config, $context);
+ if ($e) {
+ if ($attr != $o) {
+ $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
+ }
+ }
+ }
+
+ $token->attr = $attr;
+
+ // destroy CurrentToken if we made it ourselves
+ if (!$current_token) {
+ $context->destroy('CurrentToken');
+ }
+
+ }
+
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php
new file mode 100644
index 0000000000..bd8f9984f8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Bootstrap.php
@@ -0,0 +1,91 @@
+
+if (!defined('PHP_EOL')) {
+ switch (strtoupper(substr(PHP_OS, 0, 3))) {
+ case 'WIN':
+ define('PHP_EOL', "\r\n");
+ break;
+ case 'DAR':
+ define('PHP_EOL', "\r");
+ break;
+ default:
+ define('PHP_EOL', "\n");
+ }
+}
+
+/**
+ * Bootstrap class that contains meta-functionality for HTML Purifier such as
+ * the autoload function.
+ *
+ * @note
+ * This class may be used without any other files from HTML Purifier.
+ */
+class HTMLPurifier_Bootstrap
+{
+
+ /**
+ * Autoload function for HTML Purifier
+ * @param string $class Class to load
+ * @return bool
+ */
+ public static function autoload($class)
+ {
+ $file = HTMLPurifier_Bootstrap::getPath($class);
+ if (!$file) {
+ return false;
+ }
+ // Technically speaking, it should be ok and more efficient to
+ // just do 'require', but Antonio Parraga reports that with
+ // Zend extensions such as Zend debugger and APC, this invariant
+ // may be broken. Since we have efficient alternatives, pay
+ // the cost here and avoid the bug.
+ require_once HTMLPURIFIER_PREFIX . '/' . $file;
+ return true;
+ }
+
+ /**
+ * Returns the path for a specific class.
+ * @param string $class Class path to get
+ * @return string
+ */
+ public static function getPath($class)
+ {
+ if (strncmp('HTMLPurifier', $class, 12) !== 0) {
+ return false;
+ }
+ // Custom implementations
+ if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) {
+ $code = str_replace('_', '-', substr($class, 22));
+ $file = 'HTMLPurifier/Language/classes/' . $code . '.php';
+ } else {
+ $file = str_replace('_', '/', $class) . '.php';
+ }
+ if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) {
+ return false;
+ }
+ return $file;
+ }
+
+ /**
+ * "Pre-registers" our autoloader on the SPL stack.
+ */
+ public static function registerAutoload()
+ {
+ $autoload = array('HTMLPurifier_Bootstrap', 'autoload');
+ if (spl_autoload_functions() === false) {
+ spl_autoload_register($autoload);
+ } else {
+ // prepend flag exists, no need for shenanigans
+ spl_autoload_register($autoload, true, true);
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php
new file mode 100644
index 0000000000..3732c076a6
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/CSSDefinition.php
@@ -0,0 +1,565 @@
+info['text-align'] = new HTMLPurifier_AttrDef_Enum(
+ ['left', 'right', 'center', 'justify'],
+ false
+ );
+
+ $border_style =
+ $this->info['border-bottom-style'] =
+ $this->info['border-right-style'] =
+ $this->info['border-left-style'] =
+ $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum(
+ [
+ 'none',
+ 'hidden',
+ 'dotted',
+ 'dashed',
+ 'solid',
+ 'double',
+ 'groove',
+ 'ridge',
+ 'inset',
+ 'outset'
+ ],
+ false
+ );
+
+ $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style);
+
+ $this->info['clear'] = new HTMLPurifier_AttrDef_Enum(
+ ['none', 'left', 'right', 'both'],
+ false
+ );
+ $this->info['float'] = new HTMLPurifier_AttrDef_Enum(
+ ['none', 'left', 'right'],
+ false
+ );
+ $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum(
+ ['normal', 'italic', 'oblique'],
+ false
+ );
+ $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum(
+ ['normal', 'small-caps'],
+ false
+ );
+
+ $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(['none']),
+ new HTMLPurifier_AttrDef_CSS_URI()
+ ]
+ );
+
+ $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum(
+ ['inside', 'outside'],
+ false
+ );
+ $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum(
+ [
+ 'disc',
+ 'circle',
+ 'square',
+ 'decimal',
+ 'lower-roman',
+ 'upper-roman',
+ 'lower-alpha',
+ 'upper-alpha',
+ 'none'
+ ],
+ false
+ );
+ $this->info['list-style-image'] = $uri_or_none;
+
+ $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config);
+
+ $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum(
+ ['capitalize', 'uppercase', 'lowercase', 'none'],
+ false
+ );
+ $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color();
+
+ $this->info['background-image'] = $uri_or_none;
+ $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum(
+ ['repeat', 'repeat-x', 'repeat-y', 'no-repeat']
+ );
+ $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum(
+ ['scroll', 'fixed']
+ );
+ $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition();
+
+ $this->info['background-size'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(
+ [
+ 'auto',
+ 'cover',
+ 'contain',
+ ]
+ ),
+ new HTMLPurifier_AttrDef_CSS_Percentage(),
+ new HTMLPurifier_AttrDef_CSS_Length()
+ ]
+ );
+
+ $border_color =
+ $this->info['border-top-color'] =
+ $this->info['border-bottom-color'] =
+ $this->info['border-left-color'] =
+ $this->info['border-right-color'] =
+ $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(['transparent']),
+ new HTMLPurifier_AttrDef_CSS_Color()
+ ]
+ );
+
+ $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config);
+
+ $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color);
+
+ $border_width =
+ $this->info['border-top-width'] =
+ $this->info['border-bottom-width'] =
+ $this->info['border-left-width'] =
+ $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(['thin', 'medium', 'thick']),
+ new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative
+ ]
+ );
+
+ $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width);
+
+ $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(['normal']),
+ new HTMLPurifier_AttrDef_CSS_Length()
+ ]
+ );
+
+ $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(['normal']),
+ new HTMLPurifier_AttrDef_CSS_Length()
+ ]
+ );
+
+ $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(
+ [
+ 'xx-small',
+ 'x-small',
+ 'small',
+ 'medium',
+ 'large',
+ 'x-large',
+ 'xx-large',
+ 'larger',
+ 'smaller'
+ ]
+ ),
+ new HTMLPurifier_AttrDef_CSS_Percentage(),
+ new HTMLPurifier_AttrDef_CSS_Length()
+ ]
+ );
+
+ $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(['normal']),
+ new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true)
+ ]
+ );
+
+ $margin =
+ $this->info['margin-top'] =
+ $this->info['margin-bottom'] =
+ $this->info['margin-left'] =
+ $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage(),
+ new HTMLPurifier_AttrDef_Enum(['auto'])
+ ]
+ );
+
+ $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin);
+
+ // non-negative
+ $padding =
+ $this->info['padding-top'] =
+ $this->info['padding-bottom'] =
+ $this->info['padding-left'] =
+ $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true)
+ ]
+ );
+
+ $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding);
+
+ $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage()
+ ]
+ );
+
+ $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true),
+ new HTMLPurifier_AttrDef_Enum(['auto'])
+ ]
+ );
+ $trusted_min_wh = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true),
+ ]
+ );
+ $trusted_max_wh = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length('0'),
+ new HTMLPurifier_AttrDef_CSS_Percentage(true),
+ new HTMLPurifier_AttrDef_Enum(['none'])
+ ]
+ );
+ $max = $config->get('CSS.MaxImgLength');
+
+ $this->info['width'] =
+ $this->info['height'] =
+ $max === null ?
+ $trusted_wh :
+ new HTMLPurifier_AttrDef_Switch(
+ 'img',
+ // For img tags:
+ new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length('0', $max),
+ new HTMLPurifier_AttrDef_Enum(['auto'])
+ ]
+ ),
+ // For everyone else:
+ $trusted_wh
+ );
+ $this->info['min-width'] =
+ $this->info['min-height'] =
+ $max === null ?
+ $trusted_min_wh :
+ new HTMLPurifier_AttrDef_Switch(
+ 'img',
+ // For img tags:
+ new HTMLPurifier_AttrDef_CSS_Length('0', $max),
+ // For everyone else:
+ $trusted_min_wh
+ );
+ $this->info['max-width'] =
+ $this->info['max-height'] =
+ $max === null ?
+ $trusted_max_wh :
+ new HTMLPurifier_AttrDef_Switch(
+ 'img',
+ // For img tags:
+ new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length('0', $max),
+ new HTMLPurifier_AttrDef_Enum(['none'])
+ ]
+ ),
+ // For everyone else:
+ $trusted_max_wh
+ );
+
+ $this->info['aspect-ratio'] = new HTMLPurifier_AttrDef_CSS_Multiple(
+ new HTMLPurifier_AttrDef_CSS_Composite([
+ new HTMLPurifier_AttrDef_CSS_Ratio(),
+ new HTMLPurifier_AttrDef_Enum(['auto']),
+ ])
+ );
+
+ // text-decoration and related shorthands
+ $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration();
+
+ $this->info['text-decoration-line'] = new HTMLPurifier_AttrDef_Enum(
+ ['none', 'underline', 'overline', 'line-through']
+ );
+
+ $this->info['text-decoration-style'] = new HTMLPurifier_AttrDef_Enum(
+ ['solid', 'double', 'dotted', 'dashed', 'wavy']
+ );
+
+ $this->info['text-decoration-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+
+ $this->info['text-decoration-thickness'] = new HTMLPurifier_AttrDef_CSS_Composite([
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage(),
+ new HTMLPurifier_AttrDef_Enum(['auto', 'from-font'])
+ ]);
+
+ $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily();
+
+ // this could use specialized code
+ $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum(
+ [
+ 'normal',
+ 'bold',
+ 'bolder',
+ 'lighter',
+ '100',
+ '200',
+ '300',
+ '400',
+ '500',
+ '600',
+ '700',
+ '800',
+ '900'
+ ],
+ false
+ );
+
+ // MUST be called after other font properties, as it references
+ // a CSSDefinition object
+ $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config);
+
+ // same here
+ $this->info['border'] =
+ $this->info['border-bottom'] =
+ $this->info['border-top'] =
+ $this->info['border-left'] =
+ $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config);
+
+ $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum(
+ ['collapse', 'separate']
+ );
+
+ $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum(
+ ['top', 'bottom']
+ );
+
+ $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum(
+ ['auto', 'fixed']
+ );
+
+ $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Enum(
+ [
+ 'baseline',
+ 'sub',
+ 'super',
+ 'top',
+ 'text-top',
+ 'middle',
+ 'bottom',
+ 'text-bottom'
+ ]
+ ),
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage()
+ ]
+ );
+
+ $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2);
+
+ // These CSS properties don't work on many browsers, but we live
+ // in THE FUTURE!
+ $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(
+ ['nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line']
+ );
+
+ if ($config->get('CSS.Proprietary')) {
+ $this->doSetupProprietary($config);
+ }
+
+ if ($config->get('CSS.AllowTricky')) {
+ $this->doSetupTricky($config);
+ }
+
+ if ($config->get('CSS.Trusted')) {
+ $this->doSetupTrusted($config);
+ }
+
+ $allow_important = $config->get('CSS.AllowImportant');
+ // wrap all attr-defs with decorator that handles !important
+ foreach ($this->info as $k => $v) {
+ $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important);
+ }
+
+ $this->setupConfigStuff($config);
+ }
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ protected function doSetupProprietary($config)
+ {
+ // Internet Explorer only scrollbar colors
+ $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+ $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color();
+
+ // vendor specific prefixes of opacity
+ $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
+ $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
+
+ // only opacity, for now
+ $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter();
+
+ // more CSS3
+ $this->info['page-break-after'] =
+ $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum(
+ [
+ 'auto',
+ 'always',
+ 'avoid',
+ 'left',
+ 'right'
+ ]
+ );
+ $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(['auto', 'avoid']);
+
+ $border_radius = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative
+ new HTMLPurifier_AttrDef_CSS_Length('0') // disallow negative
+ ]);
+
+ $this->info['border-top-left-radius'] =
+ $this->info['border-top-right-radius'] =
+ $this->info['border-bottom-right-radius'] =
+ $this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 2);
+ // TODO: support SLASH syntax
+ $this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 4);
+
+ }
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ protected function doSetupTricky($config)
+ {
+ $this->info['display'] = new HTMLPurifier_AttrDef_Enum(
+ [
+ 'inline',
+ 'block',
+ 'list-item',
+ 'run-in',
+ 'compact',
+ 'marker',
+ 'table',
+ 'inline-block',
+ 'inline-table',
+ 'table-row-group',
+ 'table-header-group',
+ 'table-footer-group',
+ 'table-row',
+ 'table-column-group',
+ 'table-column',
+ 'table-cell',
+ 'table-caption',
+ 'none'
+ ]
+ );
+ $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum(
+ ['visible', 'hidden', 'collapse']
+ );
+ $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(['visible', 'hidden', 'auto', 'scroll']);
+ $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue();
+ }
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ protected function doSetupTrusted($config)
+ {
+ $this->info['position'] = new HTMLPurifier_AttrDef_Enum(
+ ['static', 'relative', 'absolute', 'fixed']
+ );
+ $this->info['top'] =
+ $this->info['left'] =
+ $this->info['right'] =
+ $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_CSS_Length(),
+ new HTMLPurifier_AttrDef_CSS_Percentage(),
+ new HTMLPurifier_AttrDef_Enum(['auto']),
+ ]
+ );
+ $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite(
+ [
+ new HTMLPurifier_AttrDef_Integer(),
+ new HTMLPurifier_AttrDef_Enum(['auto']),
+ ]
+ );
+ }
+
+ /**
+ * Performs extra config-based processing. Based off of
+ * HTMLPurifier_HTMLDefinition.
+ * @param HTMLPurifier_Config $config
+ * @todo Refactor duplicate elements into common class (probably using
+ * composition, not inheritance).
+ */
+ protected function setupConfigStuff($config)
+ {
+ // setup allowed elements
+ $support = "(for information on implementing this, see the " .
+ "support forums) ";
+ $allowed_properties = $config->get('CSS.AllowedProperties');
+ if ($allowed_properties !== null) {
+ foreach ($this->info as $name => $d) {
+ if (!isset($allowed_properties[$name])) {
+ unset($this->info[$name]);
+ }
+ unset($allowed_properties[$name]);
+ }
+ // emit errors
+ foreach ($allowed_properties as $name => $d) {
+ // :TODO: Is this htmlspecialchars() call really necessary?
+ $name = htmlspecialchars($name);
+ trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING);
+ }
+ }
+
+ $forbidden_properties = $config->get('CSS.ForbiddenProperties');
+ if ($forbidden_properties !== null) {
+ foreach ($this->info as $name => $d) {
+ if (isset($forbidden_properties[$name])) {
+ unset($this->info[$name]);
+ }
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php
new file mode 100644
index 0000000000..8eb17b82e1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef.php
@@ -0,0 +1,52 @@
+elements;
+ }
+
+ /**
+ * Validates nodes according to definition and returns modification.
+ *
+ * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node
+ * @param HTMLPurifier_Config $config HTMLPurifier_Config object
+ * @param HTMLPurifier_Context $context HTMLPurifier_Context object
+ * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children
+ */
+ abstract public function validateChildren($children, $config, $context);
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php
new file mode 100644
index 0000000000..7439be26b6
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Chameleon.php
@@ -0,0 +1,67 @@
+inline = new HTMLPurifier_ChildDef_Optional($inline);
+ $this->block = new HTMLPurifier_ChildDef_Optional($block);
+ $this->elements = $this->block->elements;
+ }
+
+ /**
+ * @param HTMLPurifier_Node[] $children
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool
+ */
+ public function validateChildren($children, $config, $context)
+ {
+ if ($context->get('IsInline') === false) {
+ return $this->block->validateChildren(
+ $children,
+ $config,
+ $context
+ );
+ } else {
+ return $this->inline->validateChildren(
+ $children,
+ $config,
+ $context
+ );
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php
new file mode 100644
index 0000000000..f515888a1d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Custom.php
@@ -0,0 +1,102 @@
+dtd_regex = $dtd_regex;
+ $this->_compileRegex();
+ }
+
+ /**
+ * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex)
+ */
+ protected function _compileRegex()
+ {
+ $raw = str_replace(' ', '', $this->dtd_regex);
+ if ($raw[0] != '(') {
+ $raw = "($raw)";
+ }
+ $el = '[#a-zA-Z0-9_.-]+';
+ $reg = $raw;
+
+ // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M
+ // DOING! Seriously: if there's problems, please report them.
+
+ // collect all elements into the $elements array
+ preg_match_all("/$el/", $reg, $matches);
+ foreach ($matches[0] as $match) {
+ $this->elements[$match] = true;
+ }
+
+ // setup all elements as parentheticals with leading commas
+ $reg = preg_replace("/$el/", '(,\\0)', $reg);
+
+ // remove commas when they were not solicited
+ $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg);
+
+ // remove all non-paranthetical commas: they are handled by first regex
+ $reg = preg_replace("/,\(/", '(', $reg);
+
+ $this->_pcre_regex = $reg;
+ }
+
+ /**
+ * @param HTMLPurifier_Node[] $children
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool
+ */
+ public function validateChildren($children, $config, $context)
+ {
+ $list_of_children = '';
+ $nesting = 0; // depth into the nest
+ foreach ($children as $node) {
+ if (!empty($node->is_whitespace)) {
+ continue;
+ }
+ $list_of_children .= $node->name . ',';
+ }
+ // add leading comma to deal with stray comma declarations
+ $list_of_children = ',' . rtrim($list_of_children, ',');
+ $okay =
+ preg_match(
+ '/^,?' . $this->_pcre_regex . '$/',
+ $list_of_children
+ );
+ return (bool)$okay;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php
new file mode 100644
index 0000000000..a8a6cbdd2c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Empty.php
@@ -0,0 +1,38 @@
+ true, 'ul' => true, 'ol' => true);
+
+ public $whitespace;
+
+ /**
+ * @param array $children
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function validateChildren($children, $config, $context)
+ {
+ // Flag for subclasses
+ $this->whitespace = false;
+
+ // if there are no tokens, delete parent node
+ if (empty($children)) {
+ return false;
+ }
+
+ // if li is not allowed, delete parent node
+ if (!isset($config->getHTMLDefinition()->info['li'])) {
+ trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING);
+ return false;
+ }
+
+ // the new set of children
+ $result = array();
+
+ // a little sanity check to make sure it's not ALL whitespace
+ $all_whitespace = true;
+
+ $current_li = null;
+
+ foreach ($children as $node) {
+ if (!empty($node->is_whitespace)) {
+ $result[] = $node;
+ continue;
+ }
+ $all_whitespace = false; // phew, we're not talking about whitespace
+
+ if ($node->name === 'li') {
+ // good
+ $current_li = $node;
+ $result[] = $node;
+ } else {
+ // we want to tuck this into the previous li
+ // Invariant: we expect the node to be ol/ul
+ // ToDo: Make this more robust in the case of not ol/ul
+ // by distinguishing between existing li and li created
+ // to handle non-list elements; non-list elements should
+ // not be appended to an existing li; only li created
+ // for non-list. This distinction is not currently made.
+ if ($current_li === null) {
+ $current_li = new HTMLPurifier_Node_Element('li');
+ $result[] = $current_li;
+ }
+ $current_li->children[] = $node;
+ $current_li->empty = false; // XXX fascinating! Check for this error elsewhere ToDo
+ }
+ }
+ if (empty($result)) {
+ return false;
+ }
+ if ($all_whitespace) {
+ return false;
+ }
+ return $result;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php
new file mode 100644
index 0000000000..b9468063b1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Optional.php
@@ -0,0 +1,45 @@
+whitespace) {
+ return $children;
+ } else {
+ return array();
+ }
+ }
+ return $result;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php
new file mode 100644
index 0000000000..0d1c8f5f39
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Required.php
@@ -0,0 +1,118 @@
+ $x) {
+ $elements[$i] = true;
+ if (empty($i)) {
+ unset($elements[$i]);
+ } // remove blank
+ }
+ }
+ $this->elements = $elements;
+ }
+
+ /**
+ * @type bool
+ */
+ public $allow_empty = false;
+
+ /**
+ * @type string
+ */
+ public $type = 'required';
+
+ /**
+ * @param array $children
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function validateChildren($children, $config, $context)
+ {
+ // Flag for subclasses
+ $this->whitespace = false;
+
+ // if there are no tokens, delete parent node
+ if (empty($children)) {
+ return false;
+ }
+
+ // the new set of children
+ $result = array();
+
+ // whether or not parsed character data is allowed
+ // this controls whether or not we silently drop a tag
+ // or generate escaped HTML from it
+ $pcdata_allowed = isset($this->elements['#PCDATA']);
+
+ // a little sanity check to make sure it's not ALL whitespace
+ $all_whitespace = true;
+
+ $stack = array_reverse($children);
+ while (!empty($stack)) {
+ $node = array_pop($stack);
+ if (!empty($node->is_whitespace)) {
+ $result[] = $node;
+ continue;
+ }
+ $all_whitespace = false; // phew, we're not talking about whitespace
+
+ if (!isset($this->elements[$node->name])) {
+ // special case text
+ // XXX One of these ought to be redundant or something
+ if ($pcdata_allowed && $node instanceof HTMLPurifier_Node_Text) {
+ $result[] = $node;
+ continue;
+ }
+ // spill the child contents in
+ // ToDo: Make configurable
+ if ($node instanceof HTMLPurifier_Node_Element) {
+ for ($i = count($node->children) - 1; $i >= 0; $i--) {
+ $stack[] = $node->children[$i];
+ }
+ continue;
+ }
+ continue;
+ }
+ $result[] = $node;
+ }
+ if (empty($result)) {
+ return false;
+ }
+ if ($all_whitespace) {
+ $this->whitespace = true;
+ return false;
+ }
+ return $result;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php
new file mode 100644
index 0000000000..3270a46e1b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/StrictBlockquote.php
@@ -0,0 +1,110 @@
+init($config);
+ return $this->fake_elements;
+ }
+
+ /**
+ * @param array $children
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function validateChildren($children, $config, $context)
+ {
+ $this->init($config);
+
+ // trick the parent class into thinking it allows more
+ $this->elements = $this->fake_elements;
+ $result = parent::validateChildren($children, $config, $context);
+ $this->elements = $this->real_elements;
+
+ if ($result === false) {
+ return array();
+ }
+ if ($result === true) {
+ $result = $children;
+ }
+
+ $def = $config->getHTMLDefinition();
+ $block_wrap_name = $def->info_block_wrapper;
+ $block_wrap = false;
+ $ret = array();
+
+ foreach ($result as $node) {
+ if ($block_wrap === false) {
+ if (($node instanceof HTMLPurifier_Node_Text && !$node->is_whitespace) ||
+ ($node instanceof HTMLPurifier_Node_Element && !isset($this->elements[$node->name]))) {
+ $block_wrap = new HTMLPurifier_Node_Element($def->info_block_wrapper);
+ $ret[] = $block_wrap;
+ }
+ } else {
+ if ($node instanceof HTMLPurifier_Node_Element && isset($this->elements[$node->name])) {
+ $block_wrap = false;
+
+ }
+ }
+ if ($block_wrap) {
+ $block_wrap->children[] = $node;
+ } else {
+ $ret[] = $node;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ private function init($config)
+ {
+ if (!$this->init) {
+ $def = $config->getHTMLDefinition();
+ // allow all inline elements
+ $this->real_elements = $this->elements;
+ $this->fake_elements = $def->info_content_sets['Flow'];
+ $this->fake_elements['#PCDATA'] = true;
+ $this->init = true;
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php
new file mode 100644
index 0000000000..d92205b321
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/Table.php
@@ -0,0 +1,227 @@
+ true,
+ 'tbody' => true,
+ 'thead' => true,
+ 'tfoot' => true,
+ 'caption' => true,
+ 'colgroup' => true,
+ 'col' => true
+ );
+
+ public function __construct()
+ {
+ }
+
+ /**
+ * @param array $children
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function validateChildren($children, $config, $context)
+ {
+ if (empty($children)) {
+ return false;
+ }
+
+ // only one of these elements is allowed in a table
+ $caption = false;
+ $thead = false;
+ $tfoot = false;
+
+ // whitespace
+ $initial_ws = array();
+ $after_caption_ws = array();
+ $after_thead_ws = array();
+ $after_tfoot_ws = array();
+
+ // as many of these as you want
+ $cols = array();
+ $content = array();
+
+ $tbody_mode = false; // if true, then we need to wrap any stray
+ //
s with a
.
+
+ $ws_accum =& $initial_ws;
+
+ foreach ($children as $node) {
+ if ($node instanceof HTMLPurifier_Node_Comment) {
+ $ws_accum[] = $node;
+ continue;
+ }
+ switch ($node->name) {
+ case 'tbody':
+ $tbody_mode = true;
+ // fall through
+ case 'tr':
+ $content[] = $node;
+ $ws_accum =& $content;
+ break;
+ case 'caption':
+ // there can only be one caption!
+ if ($caption !== false) break;
+ $caption = $node;
+ $ws_accum =& $after_caption_ws;
+ break;
+ case 'thead':
+ $tbody_mode = true;
+ // XXX This breaks rendering properties with
+ // Firefox, which never floats a to
+ // the top. Ever. (Our scheme will float the
+ // first to the top.) So maybe
+ // s that are not first should be
+ // turned into ? Very tricky, indeed.
+ if ($thead === false) {
+ $thead = $node;
+ $ws_accum =& $after_thead_ws;
+ } else {
+ // Oops, there's a second one! What
+ // should we do? Current behavior is to
+ // transmutate the first and last entries into
+ // tbody tags, and then put into content.
+ // Maybe a better idea is to *attach
+ // it* to the existing thead or tfoot?
+ // We don't do this, because Firefox
+ // doesn't float an extra tfoot to the
+ // bottom like it does for the first one.
+ $node->name = 'tbody';
+ $content[] = $node;
+ $ws_accum =& $content;
+ }
+ break;
+ case 'tfoot':
+ // see above for some aveats
+ $tbody_mode = true;
+ if ($tfoot === false) {
+ $tfoot = $node;
+ $ws_accum =& $after_tfoot_ws;
+ } else {
+ $node->name = 'tbody';
+ $content[] = $node;
+ $ws_accum =& $content;
+ }
+ break;
+ case 'colgroup':
+ case 'col':
+ $cols[] = $node;
+ $ws_accum =& $cols;
+ break;
+ case '#PCDATA':
+ // How is whitespace handled? We treat is as sticky to
+ // the *end* of the previous element. So all of the
+ // nonsense we have worked on is to keep things
+ // together.
+ if (!empty($node->is_whitespace)) {
+ $ws_accum[] = $node;
+ }
+ break;
+ }
+ }
+
+ if (empty($content) && $thead === false && $tfoot === false) {
+ return false;
+ }
+
+ $ret = $initial_ws;
+ if ($caption !== false) {
+ $ret[] = $caption;
+ $ret = array_merge($ret, $after_caption_ws);
+ }
+ if ($cols !== false) {
+ $ret = array_merge($ret, $cols);
+ }
+ if ($thead !== false) {
+ $ret[] = $thead;
+ $ret = array_merge($ret, $after_thead_ws);
+ }
+ if ($tfoot !== false) {
+ $ret[] = $tfoot;
+ $ret = array_merge($ret, $after_tfoot_ws);
+ }
+
+ if ($tbody_mode) {
+ // we have to shuffle tr into tbody
+ $current_tr_tbody = null;
+
+ foreach($content as $node) {
+ if (!isset($node->name)) {
+ continue;
+ }
+ switch ($node->name) {
+ case 'tbody':
+ $current_tr_tbody = null;
+ $ret[] = $node;
+ break;
+ case 'tr':
+ if ($current_tr_tbody === null) {
+ $current_tr_tbody = new HTMLPurifier_Node_Element('tbody');
+ $ret[] = $current_tr_tbody;
+ }
+ $current_tr_tbody->children[] = $node;
+ break;
+ case '#PCDATA':
+ //assert($node->is_whitespace);
+ if ($current_tr_tbody === null) {
+ $ret[] = $node;
+ } else {
+ $current_tr_tbody->children[] = $node;
+ }
+ break;
+ }
+ }
+ } else {
+ $ret = array_merge($ret, $content);
+ }
+
+ return $ret;
+
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php
new file mode 100644
index 0000000000..e6566e802b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Config.php
@@ -0,0 +1,920 @@
+defaultPlist;
+ $this->plist = new HTMLPurifier_PropertyList($parent);
+ $this->def = $definition; // keep a copy around for checking
+ $this->parser = new HTMLPurifier_VarParser_Flexible();
+ }
+
+ /**
+ * Convenience constructor that creates a config object based on a mixed var
+ * @param mixed $config Variable that defines the state of the config
+ * object. Can be: a HTMLPurifier_Config() object,
+ * an array of directives based on loadArray(),
+ * or a string filename of an ini file.
+ * @param HTMLPurifier_ConfigSchema $schema Schema object
+ * @return HTMLPurifier_Config Configured object
+ */
+ public static function create($config, $schema = null)
+ {
+ if ($config instanceof HTMLPurifier_Config) {
+ // pass-through
+ return $config;
+ }
+ if (!$schema) {
+ $ret = HTMLPurifier_Config::createDefault();
+ } else {
+ $ret = new HTMLPurifier_Config($schema);
+ }
+ if (is_string($config)) {
+ $ret->loadIni($config);
+ } elseif (is_array($config)) $ret->loadArray($config);
+ return $ret;
+ }
+
+ /**
+ * Creates a new config object that inherits from a previous one.
+ * @param HTMLPurifier_Config $config Configuration object to inherit from.
+ * @return HTMLPurifier_Config object with $config as its parent.
+ */
+ public static function inherit(HTMLPurifier_Config $config)
+ {
+ return new HTMLPurifier_Config($config->def, $config->plist);
+ }
+
+ /**
+ * Convenience constructor that creates a default configuration object.
+ * @return HTMLPurifier_Config default object.
+ */
+ public static function createDefault()
+ {
+ $definition = HTMLPurifier_ConfigSchema::instance();
+ $config = new HTMLPurifier_Config($definition);
+ return $config;
+ }
+
+ /**
+ * Retrieves a value from the configuration.
+ *
+ * @param string $key String key
+ * @param mixed $a
+ *
+ * @return mixed
+ */
+ public function get($key, $a = null)
+ {
+ if ($a !== null) {
+ $this->triggerError(
+ "Using deprecated API: use \$config->get('$key.$a') instead",
+ E_USER_WARNING
+ );
+ $key = "$key.$a";
+ }
+ if (!$this->finalized) {
+ $this->autoFinalize();
+ }
+ if (!isset($this->def->info[$key])) {
+ // can't add % due to SimpleTest bug
+ $this->triggerError(
+ 'Cannot retrieve value of undefined directive ' . htmlspecialchars($key),
+ E_USER_WARNING
+ );
+ return;
+ }
+ if (isset($this->def->info[$key]->isAlias)) {
+ $d = $this->def->info[$key];
+ $this->triggerError(
+ 'Cannot get value from aliased directive, use real name ' . $d->key,
+ E_USER_ERROR
+ );
+ return;
+ }
+ if ($this->lock) {
+ list($ns) = explode('.', $key);
+ if ($ns !== $this->lock) {
+ $this->triggerError(
+ 'Cannot get value of namespace ' . $ns . ' when lock for ' .
+ $this->lock .
+ ' is active, this probably indicates a Definition setup method ' .
+ 'is accessing directives that are not within its namespace',
+ E_USER_ERROR
+ );
+ return;
+ }
+ }
+ return $this->plist->get($key);
+ }
+
+ /**
+ * Retrieves an array of directives to values from a given namespace
+ *
+ * @param string $namespace String namespace
+ *
+ * @return array
+ */
+ public function getBatch($namespace)
+ {
+ if (!$this->finalized) {
+ $this->autoFinalize();
+ }
+ $full = $this->getAll();
+ if (!isset($full[$namespace])) {
+ $this->triggerError(
+ 'Cannot retrieve undefined namespace ' .
+ htmlspecialchars($namespace),
+ E_USER_WARNING
+ );
+ return;
+ }
+ return $full[$namespace];
+ }
+
+ /**
+ * Returns a SHA-1 signature of a segment of the configuration object
+ * that uniquely identifies that particular configuration
+ *
+ * @param string $namespace Namespace to get serial for
+ *
+ * @return string
+ * @note Revision is handled specially and is removed from the batch
+ * before processing!
+ */
+ public function getBatchSerial($namespace)
+ {
+ if (empty($this->serials[$namespace])) {
+ $batch = $this->getBatch($namespace);
+ unset($batch['DefinitionRev']);
+ $this->serials[$namespace] = sha1(serialize($batch));
+ }
+ return $this->serials[$namespace];
+ }
+
+ /**
+ * Returns a SHA-1 signature for the entire configuration object
+ * that uniquely identifies that particular configuration
+ *
+ * @return string
+ */
+ public function getSerial()
+ {
+ if (empty($this->serial)) {
+ $this->serial = sha1(serialize($this->getAll()));
+ }
+ return $this->serial;
+ }
+
+ /**
+ * Retrieves all directives, organized by namespace
+ *
+ * @warning This is a pretty inefficient function, avoid if you can
+ */
+ public function getAll()
+ {
+ if (!$this->finalized) {
+ $this->autoFinalize();
+ }
+ $ret = array();
+ foreach ($this->plist->squash() as $name => $value) {
+ list($ns, $key) = explode('.', $name, 2);
+ $ret[$ns][$key] = $value;
+ }
+ return $ret;
+ }
+
+ /**
+ * Sets a value to configuration.
+ *
+ * @param string $key key
+ * @param mixed $value value
+ * @param mixed $a
+ */
+ public function set($key, $value, $a = null)
+ {
+ if (strpos($key, '.') === false) {
+ $namespace = $key;
+ $directive = $value;
+ $value = $a;
+ $key = "$key.$directive";
+ $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE);
+ } else {
+ list($namespace) = explode('.', $key);
+ }
+ if ($this->isFinalized('Cannot set directive after finalization')) {
+ return;
+ }
+ if (!isset($this->def->info[$key])) {
+ $this->triggerError(
+ 'Cannot set undefined directive ' . htmlspecialchars($key) . ' to value',
+ E_USER_WARNING
+ );
+ return;
+ }
+ $def = $this->def->info[$key];
+
+ if (isset($def->isAlias)) {
+ if ($this->aliasMode) {
+ $this->triggerError(
+ 'Double-aliases not allowed, please fix '.
+ 'ConfigSchema bug with' . $key,
+ E_USER_ERROR
+ );
+ return;
+ }
+ $this->aliasMode = true;
+ $this->set($def->key, $value);
+ $this->aliasMode = false;
+ $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE);
+ return;
+ }
+
+ // Raw type might be negative when using the fully optimized form
+ // of stdClass, which indicates allow_null == true
+ $rtype = is_int($def) ? $def : $def->type;
+ if ($rtype < 0) {
+ $type = -$rtype;
+ $allow_null = true;
+ } else {
+ $type = $rtype;
+ $allow_null = isset($def->allow_null);
+ }
+
+ try {
+ $value = $this->parser->parse($value, $type, $allow_null);
+ } catch (HTMLPurifier_VarParserException $e) {
+ $this->triggerError(
+ 'Value for ' . $key . ' is of invalid type, should be ' .
+ HTMLPurifier_VarParser::getTypeName($type),
+ E_USER_WARNING
+ );
+ return;
+ }
+ if (is_string($value) && is_object($def)) {
+ // resolve value alias if defined
+ if (isset($def->aliases[$value])) {
+ $value = $def->aliases[$value];
+ }
+ // check to see if the value is allowed
+ if (isset($def->allowed) && !isset($def->allowed[$value])) {
+ $this->triggerError(
+ 'Value not supported, valid values are: ' .
+ $this->_listify($def->allowed),
+ E_USER_WARNING
+ );
+ return;
+ }
+ }
+ $this->plist->set($key, $value);
+
+ // reset definitions if the directives they depend on changed
+ // this is a very costly process, so it's discouraged
+ // with finalization
+ if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') {
+ $this->definitions[$namespace] = null;
+ }
+
+ $this->serials[$namespace] = false;
+ }
+
+ /**
+ * Convenience function for error reporting
+ *
+ * @param array $lookup
+ *
+ * @return string
+ */
+ private function _listify($lookup)
+ {
+ $list = array();
+ foreach ($lookup as $name => $b) {
+ $list[] = $name;
+ }
+ return implode(', ', $list);
+ }
+
+ /**
+ * Retrieves object reference to the HTML definition.
+ *
+ * @param bool $raw Return a copy that has not been setup yet. Must be
+ * called before it's been setup, otherwise won't work.
+ * @param bool $optimized If true, this method may return null, to
+ * indicate that a cached version of the modified
+ * definition object is available and no further edits
+ * are necessary. Consider using
+ * maybeGetRawHTMLDefinition, which is more explicitly
+ * named, instead.
+ *
+ * @return HTMLPurifier_HTMLDefinition|null
+ */
+ public function getHTMLDefinition($raw = false, $optimized = false)
+ {
+ return $this->getDefinition('HTML', $raw, $optimized);
+ }
+
+ /**
+ * Retrieves object reference to the CSS definition
+ *
+ * @param bool $raw Return a copy that has not been setup yet. Must be
+ * called before it's been setup, otherwise won't work.
+ * @param bool $optimized If true, this method may return null, to
+ * indicate that a cached version of the modified
+ * definition object is available and no further edits
+ * are necessary. Consider using
+ * maybeGetRawCSSDefinition, which is more explicitly
+ * named, instead.
+ *
+ * @return HTMLPurifier_CSSDefinition|null
+ */
+ public function getCSSDefinition($raw = false, $optimized = false)
+ {
+ return $this->getDefinition('CSS', $raw, $optimized);
+ }
+
+ /**
+ * Retrieves object reference to the URI definition
+ *
+ * @param bool $raw Return a copy that has not been setup yet. Must be
+ * called before it's been setup, otherwise won't work.
+ * @param bool $optimized If true, this method may return null, to
+ * indicate that a cached version of the modified
+ * definition object is available and no further edits
+ * are necessary. Consider using
+ * maybeGetRawURIDefinition, which is more explicitly
+ * named, instead.
+ *
+ * @return HTMLPurifier_URIDefinition|null
+ */
+ public function getURIDefinition($raw = false, $optimized = false)
+ {
+ return $this->getDefinition('URI', $raw, $optimized);
+ }
+
+ /**
+ * Retrieves a definition
+ *
+ * @param string $type Type of definition: HTML, CSS, etc
+ * @param bool $raw Whether or not definition should be returned raw
+ * @param bool $optimized Only has an effect when $raw is true. Whether
+ * or not to return null if the result is already present in
+ * the cache. This is off by default for backwards
+ * compatibility reasons, but you need to do things this
+ * way in order to ensure that caching is done properly.
+ * Check out enduser-customize.html for more details.
+ * We probably won't ever change this default, as much as the
+ * maybe semantics is the "right thing to do."
+ *
+ * @throws HTMLPurifier_Exception
+ * @return HTMLPurifier_Definition|null
+ */
+ public function getDefinition($type, $raw = false, $optimized = false)
+ {
+ if ($optimized && !$raw) {
+ throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false");
+ }
+ if (!$this->finalized) {
+ $this->autoFinalize();
+ }
+ // temporarily suspend locks, so we can handle recursive definition calls
+ $lock = $this->lock;
+ $this->lock = null;
+ $factory = HTMLPurifier_DefinitionCacheFactory::instance();
+ $cache = $factory->create($type, $this);
+ $this->lock = $lock;
+ if (!$raw) {
+ // full definition
+ // ---------------
+ // check if definition is in memory
+ if (!empty($this->definitions[$type])) {
+ $def = $this->definitions[$type];
+ // check if the definition is setup
+ if ($def->setup) {
+ return $def;
+ } else {
+ $def->setup($this);
+ if ($def->optimized) {
+ $cache->add($def, $this);
+ }
+ return $def;
+ }
+ }
+ // check if definition is in cache
+ $def = $cache->get($this);
+ if ($def) {
+ // definition in cache, save to memory and return it
+ $this->definitions[$type] = $def;
+ return $def;
+ }
+ // initialize it
+ $def = $this->initDefinition($type);
+ // set it up
+ $this->lock = $type;
+ $def->setup($this);
+ $this->lock = null;
+ // save in cache
+ $cache->add($def, $this);
+ // return it
+ return $def;
+ } else {
+ // raw definition
+ // --------------
+ // check preconditions
+ $def = null;
+ if ($optimized) {
+ if (is_null($this->get($type . '.DefinitionID'))) {
+ // fatally error out if definition ID not set
+ throw new HTMLPurifier_Exception(
+ "Cannot retrieve raw version without specifying %$type.DefinitionID"
+ );
+ }
+ }
+ if (!empty($this->definitions[$type])) {
+ $def = $this->definitions[$type];
+ if ($def->setup && !$optimized) {
+ $extra = $this->chatty ?
+ " (try moving this code block earlier in your initialization)" :
+ "";
+ throw new HTMLPurifier_Exception(
+ "Cannot retrieve raw definition after it has already been setup" .
+ $extra
+ );
+ }
+ if ($def->optimized === null) {
+ $extra = $this->chatty ? " (try flushing your cache)" : "";
+ throw new HTMLPurifier_Exception(
+ "Optimization status of definition is unknown" . $extra
+ );
+ }
+ if ($def->optimized !== $optimized) {
+ $msg = $optimized ? "optimized" : "unoptimized";
+ $extra = $this->chatty ?
+ " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)"
+ : "";
+ throw new HTMLPurifier_Exception(
+ "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra
+ );
+ }
+ }
+ // check if definition was in memory
+ if ($def) {
+ if ($def->setup) {
+ // invariant: $optimized === true (checked above)
+ return null;
+ } else {
+ return $def;
+ }
+ }
+ // if optimized, check if definition was in cache
+ // (because we do the memory check first, this formulation
+ // is prone to cache slamming, but I think
+ // guaranteeing that either /all/ of the raw
+ // setup code or /none/ of it is run is more important.)
+ if ($optimized) {
+ // This code path only gets run once; once we put
+ // something in $definitions (which is guaranteed by the
+ // trailing code), we always short-circuit above.
+ $def = $cache->get($this);
+ if ($def) {
+ // save the full definition for later, but don't
+ // return it yet
+ $this->definitions[$type] = $def;
+ return null;
+ }
+ }
+ // check invariants for creation
+ if (!$optimized) {
+ if (!is_null($this->get($type . '.DefinitionID'))) {
+ if ($this->chatty) {
+ $this->triggerError(
+ 'Due to a documentation error in previous version of HTML Purifier, your ' .
+ 'definitions are not being cached. If this is OK, you can remove the ' .
+ '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' .
+ 'modify your code to use maybeGetRawDefinition, and test if the returned ' .
+ 'value is null before making any edits (if it is null, that means that a ' .
+ 'cached version is available, and no raw operations are necessary). See ' .
+ '' .
+ 'Customize for more details',
+ E_USER_WARNING
+ );
+ } else {
+ $this->triggerError(
+ "Useless DefinitionID declaration",
+ E_USER_WARNING
+ );
+ }
+ }
+ }
+ // initialize it
+ $def = $this->initDefinition($type);
+ $def->optimized = $optimized;
+ return $def;
+ }
+ throw new HTMLPurifier_Exception("The impossible happened!");
+ }
+
+ /**
+ * Initialise definition
+ *
+ * @param string $type What type of definition to create
+ *
+ * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition
+ * @throws HTMLPurifier_Exception
+ */
+ private function initDefinition($type)
+ {
+ // quick checks failed, let's create the object
+ if ($type == 'HTML') {
+ $def = new HTMLPurifier_HTMLDefinition();
+ } elseif ($type == 'CSS') {
+ $def = new HTMLPurifier_CSSDefinition();
+ } elseif ($type == 'URI') {
+ $def = new HTMLPurifier_URIDefinition();
+ } else {
+ throw new HTMLPurifier_Exception(
+ "Definition of $type type not supported"
+ );
+ }
+ $this->definitions[$type] = $def;
+ return $def;
+ }
+
+ public function maybeGetRawDefinition($name)
+ {
+ return $this->getDefinition($name, true, true);
+ }
+
+ /**
+ * @return HTMLPurifier_HTMLDefinition|null
+ */
+ public function maybeGetRawHTMLDefinition()
+ {
+ return $this->getDefinition('HTML', true, true);
+ }
+
+ /**
+ * @return HTMLPurifier_CSSDefinition|null
+ */
+ public function maybeGetRawCSSDefinition()
+ {
+ return $this->getDefinition('CSS', true, true);
+ }
+
+ /**
+ * @return HTMLPurifier_URIDefinition|null
+ */
+ public function maybeGetRawURIDefinition()
+ {
+ return $this->getDefinition('URI', true, true);
+ }
+
+ /**
+ * Loads configuration values from an array with the following structure:
+ * Namespace.Directive => Value
+ *
+ * @param array $config_array Configuration associative array
+ */
+ public function loadArray($config_array)
+ {
+ if ($this->isFinalized('Cannot load directives after finalization')) {
+ return;
+ }
+ foreach ($config_array as $key => $value) {
+ $key = str_replace('_', '.', $key);
+ if (strpos($key, '.') !== false) {
+ $this->set($key, $value);
+ } else {
+ $namespace = $key;
+ $namespace_values = $value;
+ foreach ($namespace_values as $directive => $value2) {
+ $this->set($namespace .'.'. $directive, $value2);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a list of array(namespace, directive) for all directives
+ * that are allowed in a web-form context as per an allowed
+ * namespaces/directives list.
+ *
+ * @param array $allowed List of allowed namespaces/directives
+ * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy
+ *
+ * @return array
+ */
+ public static function getAllowedDirectivesForForm($allowed, $schema = null)
+ {
+ if (!$schema) {
+ $schema = HTMLPurifier_ConfigSchema::instance();
+ }
+ if ($allowed !== true) {
+ if (is_string($allowed)) {
+ $allowed = array($allowed);
+ }
+ $allowed_ns = array();
+ $allowed_directives = array();
+ $blacklisted_directives = array();
+ foreach ($allowed as $ns_or_directive) {
+ if (strpos($ns_or_directive, '.') !== false) {
+ // directive
+ if ($ns_or_directive[0] == '-') {
+ $blacklisted_directives[substr($ns_or_directive, 1)] = true;
+ } else {
+ $allowed_directives[$ns_or_directive] = true;
+ }
+ } else {
+ // namespace
+ $allowed_ns[$ns_or_directive] = true;
+ }
+ }
+ }
+ $ret = array();
+ foreach ($schema->info as $key => $def) {
+ list($ns, $directive) = explode('.', $key, 2);
+ if ($allowed !== true) {
+ if (isset($blacklisted_directives["$ns.$directive"])) {
+ continue;
+ }
+ if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) {
+ continue;
+ }
+ }
+ if (isset($def->isAlias)) {
+ continue;
+ }
+ if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') {
+ continue;
+ }
+ $ret[] = array($ns, $directive);
+ }
+ return $ret;
+ }
+
+ /**
+ * Loads configuration values from $_GET/$_POST that were posted
+ * via ConfigForm
+ *
+ * @param array $array $_GET or $_POST array to import
+ * @param string|bool $index Index/name that the config variables are in
+ * @param array|bool $allowed List of allowed namespaces/directives
+ * @param bool $mq_fix Boolean whether or not to enable magic quotes fix
+ * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy
+ *
+ * @return mixed
+ */
+ public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null)
+ {
+ $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema);
+ $config = HTMLPurifier_Config::create($ret, $schema);
+ return $config;
+ }
+
+ /**
+ * Merges in configuration values from $_GET/$_POST to object. NOT STATIC.
+ *
+ * @param array $array $_GET or $_POST array to import
+ * @param string|bool $index Index/name that the config variables are in
+ * @param array|bool $allowed List of allowed namespaces/directives
+ * @param bool $mq_fix Boolean whether or not to enable magic quotes fix
+ */
+ public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true)
+ {
+ $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def);
+ $this->loadArray($ret);
+ }
+
+ /**
+ * Prepares an array from a form into something usable for the more
+ * strict parts of HTMLPurifier_Config
+ *
+ * @param array $array $_GET or $_POST array to import
+ * @param string|bool $index Index/name that the config variables are in
+ * @param array|bool $allowed List of allowed namespaces/directives
+ * @param bool $mq_fix Boolean whether or not to enable magic quotes fix
+ * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy
+ *
+ * @return array
+ */
+ public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null)
+ {
+ if ($index !== false) {
+ $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array();
+ }
+ $mq = $mq_fix && version_compare(PHP_VERSION, '7.4.0', '<') && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
+
+ $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema);
+ $ret = array();
+ foreach ($allowed as $key) {
+ list($ns, $directive) = $key;
+ $skey = "$ns.$directive";
+ if (!empty($array["Null_$skey"])) {
+ $ret[$ns][$directive] = null;
+ continue;
+ }
+ if (!isset($array[$skey])) {
+ continue;
+ }
+ $value = $mq ? stripslashes($array[$skey]) : $array[$skey];
+ $ret[$ns][$directive] = $value;
+ }
+ return $ret;
+ }
+
+ /**
+ * Loads configuration values from an ini file
+ *
+ * @param string $filename Name of ini file
+ */
+ public function loadIni($filename)
+ {
+ if ($this->isFinalized('Cannot load directives after finalization')) {
+ return;
+ }
+ $array = parse_ini_file($filename, true);
+ $this->loadArray($array);
+ }
+
+ /**
+ * Checks whether or not the configuration object is finalized.
+ *
+ * @param string|bool $error String error message, or false for no error
+ *
+ * @return bool
+ */
+ public function isFinalized($error = false)
+ {
+ if ($this->finalized && $error) {
+ $this->triggerError($error, E_USER_ERROR);
+ }
+ return $this->finalized;
+ }
+
+ /**
+ * Finalizes configuration only if auto finalize is on and not
+ * already finalized
+ */
+ public function autoFinalize()
+ {
+ if ($this->autoFinalize) {
+ $this->finalize();
+ } else {
+ $this->plist->squash(true);
+ }
+ }
+
+ /**
+ * Finalizes a configuration object, prohibiting further change
+ */
+ public function finalize()
+ {
+ $this->finalized = true;
+ $this->parser = null;
+ }
+
+ /**
+ * Produces a nicely formatted error message by supplying the
+ * stack frame information OUTSIDE of HTMLPurifier_Config.
+ *
+ * @param string $msg An error message
+ * @param int $no An error number
+ */
+ protected function triggerError($msg, $no)
+ {
+ // determine previous stack frame
+ $extra = '';
+ if ($this->chatty) {
+ $trace = debug_backtrace();
+ // zip(tail(trace), trace) -- but PHP is not Haskell har har
+ for ($i = 0, $c = count($trace); $i < $c - 1; $i++) {
+ // XXX this is not correct on some versions of HTML Purifier
+ if (isset($trace[$i + 1]['class']) && $trace[$i + 1]['class'] === 'HTMLPurifier_Config') {
+ continue;
+ }
+ $frame = $trace[$i];
+ $extra = " invoked on line {$frame['line']} in file {$frame['file']}";
+ break;
+ }
+ }
+ trigger_error($msg . $extra, $no);
+ }
+
+ /**
+ * Returns a serialized form of the configuration object that can
+ * be reconstituted.
+ *
+ * @return string
+ */
+ public function serialize()
+ {
+ $this->getDefinition('HTML');
+ $this->getDefinition('CSS');
+ $this->getDefinition('URI');
+ return serialize($this);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php
new file mode 100644
index 0000000000..c3fe8cd4ab
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema.php
@@ -0,0 +1,176 @@
+ array(
+ * 'Directive' => new stdClass(),
+ * )
+ * )
+ *
+ * The stdClass may have the following properties:
+ *
+ * - If isAlias isn't set:
+ * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions
+ * - allow_null: If set, this directive allows null values
+ * - aliases: If set, an associative array of value aliases to real values
+ * - allowed: If set, a lookup array of allowed (string) values
+ * - If isAlias is set:
+ * - namespace: Namespace this directive aliases to
+ * - name: Directive name this directive aliases to
+ *
+ * In certain degenerate cases, stdClass will actually be an integer. In
+ * that case, the value is equivalent to an stdClass with the type
+ * property set to the integer. If the integer is negative, type is
+ * equal to the absolute value of integer, and allow_null is true.
+ *
+ * This class is friendly with HTMLPurifier_Config. If you need introspection
+ * about the schema, you're better of using the ConfigSchema_Interchange,
+ * which uses more memory but has much richer information.
+ * @type array
+ */
+ public $info = array();
+
+ /**
+ * Application-wide singleton
+ * @type HTMLPurifier_ConfigSchema
+ */
+ protected static $singleton;
+
+ public function __construct()
+ {
+ $this->defaultPlist = new HTMLPurifier_PropertyList();
+ }
+
+ /**
+ * Unserializes the default ConfigSchema.
+ * @return HTMLPurifier_ConfigSchema
+ */
+ public static function makeFromSerial()
+ {
+ $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser');
+ $r = unserialize($contents);
+ if (!$r) {
+ $hash = sha1($contents);
+ trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR);
+ }
+ return $r;
+ }
+
+ /**
+ * Retrieves an instance of the application-wide configuration definition.
+ * @param HTMLPurifier_ConfigSchema $prototype
+ * @return HTMLPurifier_ConfigSchema
+ */
+ public static function instance($prototype = null)
+ {
+ if ($prototype !== null) {
+ HTMLPurifier_ConfigSchema::$singleton = $prototype;
+ } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) {
+ HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial();
+ }
+ return HTMLPurifier_ConfigSchema::$singleton;
+ }
+
+ /**
+ * Defines a directive for configuration
+ * @warning Will fail of directive's namespace is defined.
+ * @warning This method's signature is slightly different from the legacy
+ * define() static method! Beware!
+ * @param string $key Name of directive
+ * @param mixed $default Default value of directive
+ * @param string $type Allowed type of the directive. See
+ * HTMLPurifier_VarParser::$types for allowed values
+ * @param bool $allow_null Whether or not to allow null values
+ */
+ public function add($key, $default, $type, $allow_null)
+ {
+ $obj = new stdClass();
+ $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type];
+ if ($allow_null) {
+ $obj->allow_null = true;
+ }
+ $this->info[$key] = $obj;
+ $this->defaults[$key] = $default;
+ $this->defaultPlist->set($key, $default);
+ }
+
+ /**
+ * Defines a directive value alias.
+ *
+ * Directive value aliases are convenient for developers because it lets
+ * them set a directive to several values and get the same result.
+ * @param string $key Name of Directive
+ * @param array $aliases Hash of aliased values to the real alias
+ */
+ public function addValueAliases($key, $aliases)
+ {
+ if (!isset($this->info[$key]->aliases)) {
+ $this->info[$key]->aliases = array();
+ }
+ foreach ($aliases as $alias => $real) {
+ $this->info[$key]->aliases[$alias] = $real;
+ }
+ }
+
+ /**
+ * Defines a set of allowed values for a directive.
+ * @warning This is slightly different from the corresponding static
+ * method definition.
+ * @param string $key Name of directive
+ * @param array $allowed Lookup array of allowed values
+ */
+ public function addAllowedValues($key, $allowed)
+ {
+ $this->info[$key]->allowed = $allowed;
+ }
+
+ /**
+ * Defines a directive alias for backwards compatibility
+ * @param string $key Directive that will be aliased
+ * @param string $new_key Directive that the alias will be to
+ */
+ public function addAlias($key, $new_key)
+ {
+ $obj = new stdClass;
+ $obj->key = $new_key;
+ $obj->isAlias = true;
+ $this->info[$key] = $obj;
+ }
+
+ /**
+ * Replaces any stdClass that only has the type property with type integer.
+ */
+ public function postProcess()
+ {
+ foreach ($this->info as $key => $v) {
+ if (count((array) $v) == 1) {
+ $this->info[$key] = $v->type;
+ } elseif (count((array) $v) == 2 && isset($v->allow_null)) {
+ $this->info[$key] = -$v->type;
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php
new file mode 100644
index 0000000000..d5906cd46d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php
@@ -0,0 +1,48 @@
+directives as $d) {
+ $schema->add(
+ $d->id->key,
+ $d->default,
+ $d->type,
+ $d->typeAllowsNull
+ );
+ if ($d->allowed !== null) {
+ $schema->addAllowedValues(
+ $d->id->key,
+ $d->allowed
+ );
+ }
+ foreach ($d->aliases as $alias) {
+ $schema->addAlias(
+ $alias->key,
+ $d->id->key
+ );
+ }
+ if ($d->valueAliases !== null) {
+ $schema->addValueAliases(
+ $d->id->key,
+ $d->valueAliases
+ );
+ }
+ }
+ $schema->postProcess();
+ return $schema;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php
new file mode 100644
index 0000000000..5fa56f7ddb
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Builder/Xml.php
@@ -0,0 +1,144 @@
+startElement('div');
+
+ $purifier = HTMLPurifier::getInstance();
+ $html = $purifier->purify($html);
+ $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
+ $this->writeRaw($html);
+
+ $this->endElement(); // div
+ }
+
+ /**
+ * @param mixed $var
+ * @return string
+ */
+ protected function export($var)
+ {
+ if ($var === array()) {
+ return 'array()';
+ }
+ return var_export($var, true);
+ }
+
+ /**
+ * @param HTMLPurifier_ConfigSchema_Interchange $interchange
+ */
+ public function build($interchange)
+ {
+ // global access, only use as last resort
+ $this->interchange = $interchange;
+
+ $this->setIndent(true);
+ $this->startDocument('1.0', 'UTF-8');
+ $this->startElement('configdoc');
+ $this->writeElement('title', $interchange->name);
+
+ foreach ($interchange->directives as $directive) {
+ $this->buildDirective($directive);
+ }
+
+ if ($this->namespace) {
+ $this->endElement();
+ } // namespace
+
+ $this->endElement(); // configdoc
+ $this->flush();
+ }
+
+ /**
+ * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive
+ */
+ public function buildDirective($directive)
+ {
+ // Kludge, although I suppose having a notion of a "root namespace"
+ // certainly makes things look nicer when documentation is built.
+ // Depends on things being sorted.
+ if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) {
+ if ($this->namespace) {
+ $this->endElement();
+ } // namespace
+ $this->namespace = $directive->id->getRootNamespace();
+ $this->startElement('namespace');
+ $this->writeAttribute('id', $this->namespace);
+ $this->writeElement('name', $this->namespace);
+ }
+
+ $this->startElement('directive');
+ $this->writeAttribute('id', $directive->id->toString());
+
+ $this->writeElement('name', $directive->id->getDirective());
+
+ $this->startElement('aliases');
+ foreach ($directive->aliases as $alias) {
+ $this->writeElement('alias', $alias->toString());
+ }
+ $this->endElement(); // aliases
+
+ $this->startElement('constraints');
+ if ($directive->version) {
+ $this->writeElement('version', $directive->version);
+ }
+ $this->startElement('type');
+ if ($directive->typeAllowsNull) {
+ $this->writeAttribute('allow-null', 'yes');
+ }
+ $this->text($directive->type);
+ $this->endElement(); // type
+ if ($directive->allowed) {
+ $this->startElement('allowed');
+ foreach ($directive->allowed as $value => $x) {
+ $this->writeElement('value', $value);
+ }
+ $this->endElement(); // allowed
+ }
+ $this->writeElement('default', $this->export($directive->default));
+ $this->writeAttribute('xml:space', 'preserve');
+ if ($directive->external) {
+ $this->startElement('external');
+ foreach ($directive->external as $project) {
+ $this->writeElement('project', $project);
+ }
+ $this->endElement();
+ }
+ $this->endElement(); // constraints
+
+ if ($directive->deprecatedVersion) {
+ $this->startElement('deprecated');
+ $this->writeElement('version', $directive->deprecatedVersion);
+ $this->writeElement('use', $directive->deprecatedUse->toString());
+ $this->endElement(); // deprecated
+ }
+
+ $this->startElement('description');
+ $this->writeHTMLDiv($directive->description);
+ $this->endElement(); // description
+
+ $this->endElement(); // directive
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php
new file mode 100644
index 0000000000..2671516c58
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Exception.php
@@ -0,0 +1,11 @@
+ array(directive info)
+ * @type HTMLPurifier_ConfigSchema_Interchange_Directive[]
+ */
+ public $directives = array();
+
+ /**
+ * Adds a directive array to $directives
+ * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive
+ * @throws HTMLPurifier_ConfigSchema_Exception
+ */
+ public function addDirective($directive)
+ {
+ if (isset($this->directives[$i = $directive->id->toString()])) {
+ throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'");
+ }
+ $this->directives[$i] = $directive;
+ }
+
+ /**
+ * Convenience function to perform standard validation. Throws exception
+ * on failed validation.
+ */
+ public function validate()
+ {
+ $validator = new HTMLPurifier_ConfigSchema_Validator();
+ return $validator->validate($this);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php
new file mode 100644
index 0000000000..127a39a673
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Directive.php
@@ -0,0 +1,89 @@
+ true).
+ * Null if all values are allowed.
+ * @type array
+ */
+ public $allowed;
+
+ /**
+ * List of aliases for the directive.
+ * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))).
+ * @type HTMLPurifier_ConfigSchema_Interchange_Id[]
+ */
+ public $aliases = array();
+
+ /**
+ * Hash of value aliases, e.g. array('alt' => 'real'). Null if value
+ * aliasing is disabled (necessary for non-scalar types).
+ * @type array
+ */
+ public $valueAliases;
+
+ /**
+ * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'.
+ * Null if the directive has always existed.
+ * @type string
+ */
+ public $version;
+
+ /**
+ * ID of directive that supercedes this old directive.
+ * Null if not deprecated.
+ * @type HTMLPurifier_ConfigSchema_Interchange_Id
+ */
+ public $deprecatedUse;
+
+ /**
+ * Version of HTML Purifier this directive was deprecated. Null if not
+ * deprecated.
+ * @type string
+ */
+ public $deprecatedVersion;
+
+ /**
+ * List of external projects this directive depends on, e.g. array('CSSTidy').
+ * @type array
+ */
+ public $external = array();
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php
new file mode 100644
index 0000000000..126f09d957
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Interchange/Id.php
@@ -0,0 +1,58 @@
+key = $key;
+ }
+
+ /**
+ * @return string
+ * @warning This is NOT magic, to ensure that people don't abuse SPL and
+ * cause problems for PHP 5.0 support.
+ */
+ public function toString()
+ {
+ return $this->key;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRootNamespace()
+ {
+ return substr($this->key, 0, strpos($this->key, "."));
+ }
+
+ /**
+ * @return string
+ */
+ public function getDirective()
+ {
+ return substr($this->key, strpos($this->key, ".") + 1);
+ }
+
+ /**
+ * @param string $id
+ * @return HTMLPurifier_ConfigSchema_Interchange_Id
+ */
+ public static function make($id)
+ {
+ return new HTMLPurifier_ConfigSchema_Interchange_Id($id);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php
new file mode 100644
index 0000000000..655e6dd1b9
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/InterchangeBuilder.php
@@ -0,0 +1,226 @@
+varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native();
+ }
+
+ /**
+ * @param string $dir
+ * @return HTMLPurifier_ConfigSchema_Interchange
+ */
+ public static function buildFromDirectory($dir = null)
+ {
+ $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder();
+ $interchange = new HTMLPurifier_ConfigSchema_Interchange();
+ return $builder->buildDir($interchange, $dir);
+ }
+
+ /**
+ * @param HTMLPurifier_ConfigSchema_Interchange $interchange
+ * @param string $dir
+ * @return HTMLPurifier_ConfigSchema_Interchange
+ */
+ public function buildDir($interchange, $dir = null)
+ {
+ if (!$dir) {
+ $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema';
+ }
+ if (file_exists($dir . '/info.ini')) {
+ $info = parse_ini_file($dir . '/info.ini');
+ $interchange->name = $info['name'];
+ }
+
+ $files = array();
+ $dh = opendir($dir);
+ while (false !== ($file = readdir($dh))) {
+ if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') {
+ continue;
+ }
+ $files[] = $file;
+ }
+ closedir($dh);
+
+ sort($files);
+ foreach ($files as $file) {
+ $this->buildFile($interchange, $dir . '/' . $file);
+ }
+ return $interchange;
+ }
+
+ /**
+ * @param HTMLPurifier_ConfigSchema_Interchange $interchange
+ * @param string $file
+ */
+ public function buildFile($interchange, $file)
+ {
+ $parser = new HTMLPurifier_StringHashParser();
+ $this->build(
+ $interchange,
+ new HTMLPurifier_StringHash($parser->parseFile($file))
+ );
+ }
+
+ /**
+ * Builds an interchange object based on a hash.
+ * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build
+ * @param HTMLPurifier_StringHash $hash source data
+ * @throws HTMLPurifier_ConfigSchema_Exception
+ */
+ public function build($interchange, $hash)
+ {
+ if (!$hash instanceof HTMLPurifier_StringHash) {
+ $hash = new HTMLPurifier_StringHash($hash);
+ }
+ if (!isset($hash['ID'])) {
+ throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID');
+ }
+ if (strpos($hash['ID'], '.') === false) {
+ if (count($hash) == 2 && isset($hash['DESCRIPTION'])) {
+ $hash->offsetGet('DESCRIPTION'); // prevent complaining
+ } else {
+ throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace');
+ }
+ } else {
+ $this->buildDirective($interchange, $hash);
+ }
+ $this->_findUnused($hash);
+ }
+
+ /**
+ * @param HTMLPurifier_ConfigSchema_Interchange $interchange
+ * @param HTMLPurifier_StringHash $hash
+ * @throws HTMLPurifier_ConfigSchema_Exception
+ */
+ public function buildDirective($interchange, $hash)
+ {
+ $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive();
+
+ // These are required elements:
+ $directive->id = $this->id($hash->offsetGet('ID'));
+ $id = $directive->id->toString(); // convenience
+
+ if (isset($hash['TYPE'])) {
+ $type = explode('/', $hash->offsetGet('TYPE'));
+ if (isset($type[1])) {
+ $directive->typeAllowsNull = true;
+ }
+ $directive->type = $type[0];
+ } else {
+ throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined");
+ }
+
+ if (isset($hash['DEFAULT'])) {
+ try {
+ $directive->default = $this->varParser->parse(
+ $hash->offsetGet('DEFAULT'),
+ $directive->type,
+ $directive->typeAllowsNull
+ );
+ } catch (HTMLPurifier_VarParserException $e) {
+ throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'");
+ }
+ }
+
+ if (isset($hash['DESCRIPTION'])) {
+ $directive->description = $hash->offsetGet('DESCRIPTION');
+ }
+
+ if (isset($hash['ALLOWED'])) {
+ $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED')));
+ }
+
+ if (isset($hash['VALUE-ALIASES'])) {
+ $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES'));
+ }
+
+ if (isset($hash['ALIASES'])) {
+ $raw_aliases = trim($hash->offsetGet('ALIASES'));
+ $aliases = preg_split('/\s*,\s*/', $raw_aliases);
+ foreach ($aliases as $alias) {
+ $directive->aliases[] = $this->id($alias);
+ }
+ }
+
+ if (isset($hash['VERSION'])) {
+ $directive->version = $hash->offsetGet('VERSION');
+ }
+
+ if (isset($hash['DEPRECATED-USE'])) {
+ $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE'));
+ }
+
+ if (isset($hash['DEPRECATED-VERSION'])) {
+ $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION');
+ }
+
+ if (isset($hash['EXTERNAL'])) {
+ $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL')));
+ }
+
+ $interchange->addDirective($directive);
+ }
+
+ /**
+ * Evaluates an array PHP code string without array() wrapper
+ * @param string $contents
+ */
+ protected function evalArray($contents)
+ {
+ return eval('return array(' . $contents . ');');
+ }
+
+ /**
+ * Converts an array list into a lookup array.
+ * @param array $array
+ * @return array
+ */
+ protected function lookup($array)
+ {
+ $ret = array();
+ foreach ($array as $val) {
+ $ret[$val] = true;
+ }
+ return $ret;
+ }
+
+ /**
+ * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id
+ * object based on a string Id.
+ * @param string $id
+ * @return HTMLPurifier_ConfigSchema_Interchange_Id
+ */
+ protected function id($id)
+ {
+ return HTMLPurifier_ConfigSchema_Interchange_Id::make($id);
+ }
+
+ /**
+ * Triggers errors for any unused keys passed in the hash; such keys
+ * may indicate typos, missing values, etc.
+ * @param HTMLPurifier_StringHash $hash Hash to check.
+ */
+ protected function _findUnused($hash)
+ {
+ $accessed = $hash->getAccessed();
+ foreach ($hash as $k => $v) {
+ if (!isset($accessed[$k])) {
+ trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE);
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php
new file mode 100644
index 0000000000..fb31277889
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/Validator.php
@@ -0,0 +1,248 @@
+parser = new HTMLPurifier_VarParser();
+ }
+
+ /**
+ * Validates a fully-formed interchange object.
+ * @param HTMLPurifier_ConfigSchema_Interchange $interchange
+ * @return bool
+ */
+ public function validate($interchange)
+ {
+ $this->interchange = $interchange;
+ $this->aliases = array();
+ // PHP is a bit lax with integer <=> string conversions in
+ // arrays, so we don't use the identical !== comparison
+ foreach ($interchange->directives as $i => $directive) {
+ $id = $directive->id->toString();
+ if ($i != $id) {
+ $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'");
+ }
+ $this->validateDirective($directive);
+ }
+ return true;
+ }
+
+ /**
+ * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object.
+ * @param HTMLPurifier_ConfigSchema_Interchange_Id $id
+ */
+ public function validateId($id)
+ {
+ $id_string = $id->toString();
+ $this->context[] = "id '$id_string'";
+ if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) {
+ // handled by InterchangeBuilder
+ $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id');
+ }
+ // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.)
+ // we probably should check that it has at least one namespace
+ $this->with($id, 'key')
+ ->assertNotEmpty()
+ ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder
+ array_pop($this->context);
+ }
+
+ /**
+ * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object.
+ * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
+ */
+ public function validateDirective($d)
+ {
+ $id = $d->id->toString();
+ $this->context[] = "directive '$id'";
+ $this->validateId($d->id);
+
+ $this->with($d, 'description')
+ ->assertNotEmpty();
+
+ // BEGIN - handled by InterchangeBuilder
+ $this->with($d, 'type')
+ ->assertNotEmpty();
+ $this->with($d, 'typeAllowsNull')
+ ->assertIsBool();
+ try {
+ // This also tests validity of $d->type
+ $this->parser->parse($d->default, $d->type, $d->typeAllowsNull);
+ } catch (HTMLPurifier_VarParserException $e) {
+ $this->error('default', 'had error: ' . $e->getMessage());
+ }
+ // END - handled by InterchangeBuilder
+
+ if (!is_null($d->allowed) || !empty($d->valueAliases)) {
+ // allowed and valueAliases require that we be dealing with
+ // strings, so check for that early.
+ $d_int = HTMLPurifier_VarParser::$types[$d->type];
+ if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) {
+ $this->error('type', 'must be a string type when used with allowed or value aliases');
+ }
+ }
+
+ $this->validateDirectiveAllowed($d);
+ $this->validateDirectiveValueAliases($d);
+ $this->validateDirectiveAliases($d);
+
+ array_pop($this->context);
+ }
+
+ /**
+ * Extra validation if $allowed member variable of
+ * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
+ * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
+ */
+ public function validateDirectiveAllowed($d)
+ {
+ if (is_null($d->allowed)) {
+ return;
+ }
+ $this->with($d, 'allowed')
+ ->assertNotEmpty()
+ ->assertIsLookup(); // handled by InterchangeBuilder
+ if (is_string($d->default) && !isset($d->allowed[$d->default])) {
+ $this->error('default', 'must be an allowed value');
+ }
+ $this->context[] = 'allowed';
+ foreach ($d->allowed as $val => $x) {
+ if (!is_string($val)) {
+ $this->error("value $val", 'must be a string');
+ }
+ }
+ array_pop($this->context);
+ }
+
+ /**
+ * Extra validation if $valueAliases member variable of
+ * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
+ * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
+ */
+ public function validateDirectiveValueAliases($d)
+ {
+ if (is_null($d->valueAliases)) {
+ return;
+ }
+ $this->with($d, 'valueAliases')
+ ->assertIsArray(); // handled by InterchangeBuilder
+ $this->context[] = 'valueAliases';
+ foreach ($d->valueAliases as $alias => $real) {
+ if (!is_string($alias)) {
+ $this->error("alias $alias", 'must be a string');
+ }
+ if (!is_string($real)) {
+ $this->error("alias target $real from alias '$alias'", 'must be a string');
+ }
+ if ($alias === $real) {
+ $this->error("alias '$alias'", "must not be an alias to itself");
+ }
+ }
+ if (!is_null($d->allowed)) {
+ foreach ($d->valueAliases as $alias => $real) {
+ if (isset($d->allowed[$alias])) {
+ $this->error("alias '$alias'", 'must not be an allowed value');
+ } elseif (!isset($d->allowed[$real])) {
+ $this->error("alias '$alias'", 'must be an alias to an allowed value');
+ }
+ }
+ }
+ array_pop($this->context);
+ }
+
+ /**
+ * Extra validation if $aliases member variable of
+ * HTMLPurifier_ConfigSchema_Interchange_Directive is defined.
+ * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d
+ */
+ public function validateDirectiveAliases($d)
+ {
+ $this->with($d, 'aliases')
+ ->assertIsArray(); // handled by InterchangeBuilder
+ $this->context[] = 'aliases';
+ foreach ($d->aliases as $alias) {
+ $this->validateId($alias);
+ $s = $alias->toString();
+ if (isset($this->interchange->directives[$s])) {
+ $this->error("alias '$s'", 'collides with another directive');
+ }
+ if (isset($this->aliases[$s])) {
+ $other_directive = $this->aliases[$s];
+ $this->error("alias '$s'", "collides with alias for directive '$other_directive'");
+ }
+ $this->aliases[$s] = $d->id->toString();
+ }
+ array_pop($this->context);
+ }
+
+ // protected helper functions
+
+ /**
+ * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom
+ * for validating simple member variables of objects.
+ * @param $obj
+ * @param $member
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ protected function with($obj, $member)
+ {
+ return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member);
+ }
+
+ /**
+ * Emits an error, providing helpful context.
+ * @throws HTMLPurifier_ConfigSchema_Exception
+ */
+ protected function error($target, $msg)
+ {
+ if ($target !== false) {
+ $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext();
+ } else {
+ $prefix = ucfirst($this->getFormattedContext());
+ }
+ throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg));
+ }
+
+ /**
+ * Returns a formatted context string.
+ * @return string
+ */
+ protected function getFormattedContext()
+ {
+ return implode(' in ', array_reverse($this->context));
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php
new file mode 100644
index 0000000000..c9aa3644af
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/ValidatorAtom.php
@@ -0,0 +1,130 @@
+context = $context;
+ $this->obj = $obj;
+ $this->member = $member;
+ $this->contents =& $obj->$member;
+ }
+
+ /**
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ public function assertIsString()
+ {
+ if (!is_string($this->contents)) {
+ $this->error('must be a string');
+ }
+ return $this;
+ }
+
+ /**
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ public function assertIsBool()
+ {
+ if (!is_bool($this->contents)) {
+ $this->error('must be a boolean');
+ }
+ return $this;
+ }
+
+ /**
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ public function assertIsArray()
+ {
+ if (!is_array($this->contents)) {
+ $this->error('must be an array');
+ }
+ return $this;
+ }
+
+ /**
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ public function assertNotNull()
+ {
+ if ($this->contents === null) {
+ $this->error('must not be null');
+ }
+ return $this;
+ }
+
+ /**
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ public function assertAlnum()
+ {
+ $this->assertIsString();
+ if (!ctype_alnum($this->contents)) {
+ $this->error('must be alphanumeric');
+ }
+ return $this;
+ }
+
+ /**
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ public function assertNotEmpty()
+ {
+ if (empty($this->contents)) {
+ $this->error('must not be empty');
+ }
+ return $this;
+ }
+
+ /**
+ * @return HTMLPurifier_ConfigSchema_ValidatorAtom
+ */
+ public function assertIsLookup()
+ {
+ $this->assertIsArray();
+ foreach ($this->contents as $v) {
+ if ($v !== true) {
+ $this->error('must be a lookup array');
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * @param string $msg
+ * @throws HTMLPurifier_ConfigSchema_Exception
+ */
+ protected function error($msg)
+ {
+ throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser
new file mode 100644
index 0000000000..3cd756beef
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema.ser
@@ -0,0 +1 @@
+O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:128:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:17:"Core.RemoveBlanks";b:0;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";i:1200;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:20:"URI.SafeIframeRegexp";N;}s:12:"defaultPlist";O:25:"HTMLPurifier_PropertyList":3:{s:7:" * data";a:128:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";s:7:"#6B8E23";s:6:"orange";s:7:"#FFA500";s:9:"orangered";s:7:"#FF4500";s:6:"orchid";s:7:"#DA70D6";s:13:"palegoldenrod";s:7:"#EEE8AA";s:9:"palegreen";s:7:"#98FB98";s:13:"paleturquoise";s:7:"#AFEEEE";s:13:"palevioletred";s:7:"#DB7093";s:10:"papayawhip";s:7:"#FFEFD5";s:9:"peachpuff";s:7:"#FFDAB9";s:4:"peru";s:7:"#CD853F";s:4:"pink";s:7:"#FFC0CB";s:4:"plum";s:7:"#DDA0DD";s:10:"powderblue";s:7:"#B0E0E6";s:6:"purple";s:7:"#800080";s:13:"rebeccapurple";s:7:"#663399";s:3:"red";s:7:"#FF0000";s:9:"rosybrown";s:7:"#BC8F8F";s:9:"royalblue";s:7:"#4169E1";s:11:"saddlebrown";s:7:"#8B4513";s:6:"salmon";s:7:"#FA8072";s:10:"sandybrown";s:7:"#F4A460";s:8:"seagreen";s:7:"#2E8B57";s:8:"seashell";s:7:"#FFF5EE";s:6:"sienna";s:7:"#A0522D";s:6:"silver";s:7:"#C0C0C0";s:7:"skyblue";s:7:"#87CEEB";s:9:"slateblue";s:7:"#6A5ACD";s:9:"slategray";s:7:"#708090";s:9:"slategrey";s:7:"#708090";s:4:"snow";s:7:"#FFFAFA";s:11:"springgreen";s:7:"#00FF7F";s:9:"steelblue";s:7:"#4682B4";s:3:"tan";s:7:"#D2B48C";s:4:"teal";s:7:"#008080";s:7:"thistle";s:7:"#D8BFD8";s:6:"tomato";s:7:"#FF6347";s:9:"turquoise";s:7:"#40E0D0";s:6:"violet";s:7:"#EE82EE";s:5:"wheat";s:7:"#F5DEB3";s:5:"white";s:7:"#FFFFFF";s:10:"whitesmoke";s:7:"#F5F5F5";s:6:"yellow";s:7:"#FFFF00";s:11:"yellowgreen";s:7:"#9ACD32";}s:30:"Core.ConvertDocumentToFragment";b:1;s:36:"Core.DirectLexLineNumberSyncInterval";i:0;s:20:"Core.DisableExcludes";b:0;s:15:"Core.EnableIDNA";b:0;s:13:"Core.Encoding";s:5:"utf-8";s:26:"Core.EscapeInvalidChildren";b:0;s:22:"Core.EscapeInvalidTags";b:0;s:29:"Core.EscapeNonASCIICharacters";b:0;s:19:"Core.HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:13:"Core.Language";s:2:"en";s:24:"Core.LegacyEntityDecoder";b:0;s:14:"Core.LexerImpl";N;s:24:"Core.MaintainLineNumbers";N;s:22:"Core.NormalizeNewlines";b:1;s:17:"Core.RemoveBlanks";b:0;s:21:"Core.RemoveInvalidImg";b:1;s:33:"Core.RemoveProcessingInstructions";b:0;s:25:"Core.RemoveScriptContents";N;s:13:"Filter.Custom";a:0:{}s:34:"Filter.ExtractStyleBlocks.Escaping";b:1;s:31:"Filter.ExtractStyleBlocks.Scope";N;s:34:"Filter.ExtractStyleBlocks.TidyImpl";N;s:25:"Filter.ExtractStyleBlocks";b:0;s:14:"Filter.YouTube";b:0;s:12:"HTML.Allowed";N;s:22:"HTML.AllowedAttributes";N;s:20:"HTML.AllowedComments";a:0:{}s:26:"HTML.AllowedCommentsRegexp";N;s:20:"HTML.AllowedElements";N;s:19:"HTML.AllowedModules";N;s:23:"HTML.Attr.Name.UseCDATA";b:0;s:17:"HTML.BlockWrapper";s:1:"p";s:16:"HTML.CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:18:"HTML.CustomDoctype";N;s:17:"HTML.DefinitionID";N;s:18:"HTML.DefinitionRev";i:1;s:12:"HTML.Doctype";N;s:25:"HTML.FlashAllowFullScreen";b:0;s:24:"HTML.ForbiddenAttributes";a:0:{}s:22:"HTML.ForbiddenElements";a:0:{}s:10:"HTML.Forms";b:0;s:17:"HTML.MaxImgLength";i:1200;s:13:"HTML.Nofollow";b:0;s:11:"HTML.Parent";s:3:"div";s:16:"HTML.Proprietary";b:0;s:14:"HTML.SafeEmbed";b:0;s:15:"HTML.SafeIframe";b:0;s:15:"HTML.SafeObject";b:0;s:18:"HTML.SafeScripting";a:0:{}s:11:"HTML.Strict";b:0;s:16:"HTML.TargetBlank";b:0;s:19:"HTML.TargetNoopener";b:1;s:21:"HTML.TargetNoreferrer";b:1;s:12:"HTML.TidyAdd";a:0:{}s:14:"HTML.TidyLevel";s:6:"medium";s:15:"HTML.TidyRemove";a:0:{}s:12:"HTML.Trusted";b:0;s:10:"HTML.XHTML";b:1;s:28:"Output.CommentScriptContents";b:1;s:19:"Output.FixInnerHTML";b:1;s:18:"Output.FlashCompat";b:0;s:14:"Output.Newline";N;s:15:"Output.SortAttr";b:0;s:17:"Output.TidyFormat";b:0;s:17:"Test.ForceNoIconv";b:0;s:18:"URI.AllowedSchemes";a:7:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;s:3:"tel";b:1;}s:8:"URI.Base";N;s:17:"URI.DefaultScheme";s:4:"http";s:16:"URI.DefinitionID";N;s:17:"URI.DefinitionRev";i:1;s:11:"URI.Disable";b:0;s:19:"URI.DisableExternal";b:0;s:28:"URI.DisableExternalResources";b:0;s:20:"URI.DisableResources";b:0;s:8:"URI.Host";N;s:17:"URI.HostBlacklist";a:0:{}s:16:"URI.MakeAbsolute";b:0;s:9:"URI.Munge";N;s:18:"URI.MungeResources";b:0;s:18:"URI.MungeSecretKey";N;s:26:"URI.OverrideAllowedSchemes";b:1;s:20:"URI.SafeIframeRegexp";N;}s:9:" * parent";N;s:8:" * cache";N;}s:4:"info";a:141:{s:19:"Attr.AllowedClasses";i:-8;s:24:"Attr.AllowedFrameTargets";i:8;s:15:"Attr.AllowedRel";i:8;s:15:"Attr.AllowedRev";i:8;s:18:"Attr.ClassUseCDATA";i:-7;s:20:"Attr.DefaultImageAlt";i:-1;s:24:"Attr.DefaultInvalidImage";i:1;s:27:"Attr.DefaultInvalidImageAlt";i:1;s:19:"Attr.DefaultTextDir";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:2:{s:3:"ltr";b:1;s:3:"rtl";b:1;}}s:13:"Attr.EnableID";i:7;s:17:"HTML.EnableAttrID";O:8:"stdClass":2:{s:3:"key";s:13:"Attr.EnableID";s:7:"isAlias";b:1;}s:21:"Attr.ForbiddenClasses";i:8;s:13:"Attr.ID.HTML5";i:-7;s:16:"Attr.IDBlacklist";i:9;s:22:"Attr.IDBlacklistRegexp";i:-1;s:13:"Attr.IDPrefix";i:1;s:18:"Attr.IDPrefixLocal";i:1;s:24:"AutoFormat.AutoParagraph";i:7;s:17:"AutoFormat.Custom";i:9;s:25:"AutoFormat.DisplayLinkURI";i:7;s:18:"AutoFormat.Linkify";i:7;s:33:"AutoFormat.PurifierLinkify.DocURL";i:1;s:37:"AutoFormatParam.PurifierLinkifyDocURL";O:8:"stdClass":2:{s:3:"key";s:33:"AutoFormat.PurifierLinkify.DocURL";s:7:"isAlias";b:1;}s:26:"AutoFormat.PurifierLinkify";i:7;s:32:"AutoFormat.RemoveEmpty.Predicate";i:10;s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";i:8;s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";i:7;s:22:"AutoFormat.RemoveEmpty";i:7;s:39:"AutoFormat.RemoveSpansWithoutAttributes";i:7;s:19:"CSS.AllowDuplicates";i:7;s:18:"CSS.AllowImportant";i:7;s:15:"CSS.AllowTricky";i:7;s:16:"CSS.AllowedFonts";i:-8;s:21:"CSS.AllowedProperties";i:-8;s:17:"CSS.DefinitionRev";i:5;s:23:"CSS.ForbiddenProperties";i:8;s:16:"CSS.MaxImgLength";i:-1;s:15:"CSS.Proprietary";i:7;s:11:"CSS.Trusted";i:7;s:20:"Cache.DefinitionImpl";i:-1;s:20:"Core.DefinitionCache";O:8:"stdClass":2:{s:3:"key";s:20:"Cache.DefinitionImpl";s:7:"isAlias";b:1;}s:20:"Cache.SerializerPath";i:-1;s:27:"Cache.SerializerPermissions";i:-5;s:22:"Core.AggressivelyFixLt";i:7;s:29:"Core.AggressivelyRemoveScript";i:7;s:28:"Core.AllowHostnameUnderscore";i:7;s:23:"Core.AllowParseManyTags";i:7;s:18:"Core.CollectErrors";i:7;s:18:"Core.ColorKeywords";i:10;s:30:"Core.ConvertDocumentToFragment";i:7;s:24:"Core.AcceptFullDocuments";O:8:"stdClass":2:{s:3:"key";s:30:"Core.ConvertDocumentToFragment";s:7:"isAlias";b:1;}s:36:"Core.DirectLexLineNumberSyncInterval";i:5;s:20:"Core.DisableExcludes";i:7;s:15:"Core.EnableIDNA";i:7;s:13:"Core.Encoding";i:2;s:26:"Core.EscapeInvalidChildren";i:7;s:22:"Core.EscapeInvalidTags";i:7;s:29:"Core.EscapeNonASCIICharacters";i:7;s:19:"Core.HiddenElements";i:8;s:13:"Core.Language";i:1;s:24:"Core.LegacyEntityDecoder";i:7;s:14:"Core.LexerImpl";i:-11;s:24:"Core.MaintainLineNumbers";i:-7;s:22:"Core.NormalizeNewlines";i:7;s:17:"Core.RemoveBlanks";i:7;s:21:"Core.RemoveInvalidImg";i:7;s:33:"Core.RemoveProcessingInstructions";i:7;s:25:"Core.RemoveScriptContents";i:-7;s:13:"Filter.Custom";i:9;s:34:"Filter.ExtractStyleBlocks.Escaping";i:7;s:33:"Filter.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:38:"FilterParam.ExtractStyleBlocksEscaping";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.Escaping";s:7:"isAlias";b:1;}s:31:"Filter.ExtractStyleBlocks.Scope";i:-1;s:30:"Filter.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:35:"FilterParam.ExtractStyleBlocksScope";O:8:"stdClass":2:{s:3:"key";s:31:"Filter.ExtractStyleBlocks.Scope";s:7:"isAlias";b:1;}s:34:"Filter.ExtractStyleBlocks.TidyImpl";i:-11;s:38:"FilterParam.ExtractStyleBlocksTidyImpl";O:8:"stdClass":2:{s:3:"key";s:34:"Filter.ExtractStyleBlocks.TidyImpl";s:7:"isAlias";b:1;}s:25:"Filter.ExtractStyleBlocks";i:7;s:14:"Filter.YouTube";i:7;s:12:"HTML.Allowed";i:-4;s:22:"HTML.AllowedAttributes";i:-8;s:20:"HTML.AllowedComments";i:8;s:26:"HTML.AllowedCommentsRegexp";i:-1;s:20:"HTML.AllowedElements";i:-8;s:19:"HTML.AllowedModules";i:-8;s:23:"HTML.Attr.Name.UseCDATA";i:7;s:17:"HTML.BlockWrapper";i:1;s:16:"HTML.CoreModules";i:8;s:18:"HTML.CustomDoctype";i:-1;s:17:"HTML.DefinitionID";i:-1;s:18:"HTML.DefinitionRev";i:5;s:12:"HTML.Doctype";O:8:"stdClass":3:{s:4:"type";i:1;s:10:"allow_null";b:1;s:7:"allowed";a:5:{s:22:"HTML 4.01 Transitional";b:1;s:16:"HTML 4.01 Strict";b:1;s:22:"XHTML 1.0 Transitional";b:1;s:16:"XHTML 1.0 Strict";b:1;s:9:"XHTML 1.1";b:1;}}s:25:"HTML.FlashAllowFullScreen";i:7;s:24:"HTML.ForbiddenAttributes";i:8;s:22:"HTML.ForbiddenElements";i:8;s:10:"HTML.Forms";i:7;s:17:"HTML.MaxImgLength";i:-5;s:13:"HTML.Nofollow";i:7;s:11:"HTML.Parent";i:1;s:16:"HTML.Proprietary";i:7;s:14:"HTML.SafeEmbed";i:7;s:15:"HTML.SafeIframe";i:7;s:15:"HTML.SafeObject";i:7;s:18:"HTML.SafeScripting";i:8;s:11:"HTML.Strict";i:7;s:16:"HTML.TargetBlank";i:7;s:19:"HTML.TargetNoopener";i:7;s:21:"HTML.TargetNoreferrer";i:7;s:12:"HTML.TidyAdd";i:8;s:14:"HTML.TidyLevel";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:4:{s:4:"none";b:1;s:5:"light";b:1;s:6:"medium";b:1;s:5:"heavy";b:1;}}s:15:"HTML.TidyRemove";i:8;s:12:"HTML.Trusted";i:7;s:10:"HTML.XHTML";i:7;s:10:"Core.XHTML";O:8:"stdClass":2:{s:3:"key";s:10:"HTML.XHTML";s:7:"isAlias";b:1;}s:28:"Output.CommentScriptContents";i:7;s:26:"Core.CommentScriptContents";O:8:"stdClass":2:{s:3:"key";s:28:"Output.CommentScriptContents";s:7:"isAlias";b:1;}s:19:"Output.FixInnerHTML";i:7;s:18:"Output.FlashCompat";i:7;s:14:"Output.Newline";i:-1;s:15:"Output.SortAttr";i:7;s:17:"Output.TidyFormat";i:7;s:15:"Core.TidyFormat";O:8:"stdClass":2:{s:3:"key";s:17:"Output.TidyFormat";s:7:"isAlias";b:1;}s:17:"Test.ForceNoIconv";i:7;s:18:"URI.AllowedSchemes";i:8;s:8:"URI.Base";i:-1;s:17:"URI.DefaultScheme";i:-1;s:16:"URI.DefinitionID";i:-1;s:17:"URI.DefinitionRev";i:5;s:11:"URI.Disable";i:7;s:15:"Attr.DisableURI";O:8:"stdClass":2:{s:3:"key";s:11:"URI.Disable";s:7:"isAlias";b:1;}s:19:"URI.DisableExternal";i:7;s:28:"URI.DisableExternalResources";i:7;s:20:"URI.DisableResources";i:7;s:8:"URI.Host";i:-1;s:17:"URI.HostBlacklist";i:9;s:16:"URI.MakeAbsolute";i:7;s:9:"URI.Munge";i:-1;s:18:"URI.MungeResources";i:7;s:18:"URI.MungeSecretKey";i:-1;s:26:"URI.OverrideAllowedSchemes";i:7;s:20:"URI.SafeIframeRegexp";i:-1;}}
\ No newline at end of file
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt
new file mode 100644
index 0000000000..0517fed0a1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt
@@ -0,0 +1,8 @@
+Attr.AllowedClasses
+TYPE: lookup/null
+VERSION: 4.0.0
+DEFAULT: null
+--DESCRIPTION--
+List of allowed class values in the class attribute. By default, this is null,
+which means all classes are allowed.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt
new file mode 100644
index 0000000000..249edd647b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt
@@ -0,0 +1,12 @@
+Attr.AllowedFrameTargets
+TYPE: lookup
+DEFAULT: array()
+--DESCRIPTION--
+Lookup table of all allowed link frame targets. Some commonly used link
+targets include _blank, _self, _parent and _top. Values should be
+lowercase, as validation will be done in a case-sensitive manner despite
+W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute
+so this directive will have no effect in that doctype. XHTML 1.1 does not
+enable the Target module by default, you will have to manually enable it
+(see the module documentation for more details.)
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt
new file mode 100644
index 0000000000..9a8fa6a2e2
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt
@@ -0,0 +1,9 @@
+Attr.AllowedRel
+TYPE: lookup
+VERSION: 1.6.0
+DEFAULT: array()
+--DESCRIPTION--
+List of allowed forward document relationships in the rel attribute. Common
+values may be nofollow or print. By default, this is empty, meaning that no
+document relationships are allowed.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt
new file mode 100644
index 0000000000..b017883485
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt
@@ -0,0 +1,9 @@
+Attr.AllowedRev
+TYPE: lookup
+VERSION: 1.6.0
+DEFAULT: array()
+--DESCRIPTION--
+List of allowed reverse document relationships in the rev attribute. This
+attribute is a bit of an edge-case; if you don't know what it is for, stay
+away.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt
new file mode 100644
index 0000000000..e774b823b1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt
@@ -0,0 +1,19 @@
+Attr.ClassUseCDATA
+TYPE: bool/null
+DEFAULT: null
+VERSION: 4.0.0
+--DESCRIPTION--
+If null, class will auto-detect the doctype and, if matching XHTML 1.1 or
+XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise,
+it will use a relaxed CDATA definition. If true, the relaxed CDATA definition
+is forced; if false, the NMTOKENS definition is forced. To get behavior
+of HTML Purifier prior to 4.0.0, set this directive to false.
+
+Some rational behind the auto-detection:
+in previous versions of HTML Purifier, it was assumed that the form of
+class was NMTOKENS, as specified by the XHTML Modularization (representing
+XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however
+specify class as CDATA. HTML 5 effectively defines it as CDATA, but
+with the additional constraint that each name should be unique (this is not
+explicitly outlined in previous specifications).
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt
new file mode 100644
index 0000000000..533165e175
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt
@@ -0,0 +1,11 @@
+Attr.DefaultImageAlt
+TYPE: string/null
+DEFAULT: null
+VERSION: 3.2.0
+--DESCRIPTION--
+This is the content of the alt tag of an image if the user had not
+previously specified an alt attribute. This applies to all images without
+a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which
+only applies to invalid images, and overrides in the case of an invalid image.
+Default behavior with null is to use the basename of the src tag for the alt.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt
new file mode 100644
index 0000000000..9eb7e38469
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt
@@ -0,0 +1,9 @@
+Attr.DefaultInvalidImage
+TYPE: string
+DEFAULT: ''
+--DESCRIPTION--
+This is the default image an img tag will be pointed to if it does not have
+a valid src attribute. In future versions, we may allow the image tag to
+be removed completely, but due to design issues, this is not possible right
+now.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt
new file mode 100644
index 0000000000..2f17bf477a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt
@@ -0,0 +1,8 @@
+Attr.DefaultInvalidImageAlt
+TYPE: string
+DEFAULT: 'Invalid image'
+--DESCRIPTION--
+This is the content of the alt tag of an invalid image if the user had not
+previously specified an alt attribute. It has no effect when the image is
+valid but there was no alt attribute present.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt
new file mode 100644
index 0000000000..52654b53ae
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt
@@ -0,0 +1,10 @@
+Attr.DefaultTextDir
+TYPE: string
+DEFAULT: 'ltr'
+--DESCRIPTION--
+Defines the default text direction (ltr or rtl) of the document being
+parsed. This generally is the same as the value of the dir attribute in
+HTML, or ltr if that is not specified.
+--ALLOWED--
+'ltr', 'rtl'
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt
new file mode 100644
index 0000000000..6440d21032
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt
@@ -0,0 +1,16 @@
+Attr.EnableID
+TYPE: bool
+DEFAULT: false
+VERSION: 1.2.0
+--DESCRIPTION--
+Allows the ID attribute in HTML. This is disabled by default due to the
+fact that without proper configuration user input can easily break the
+validation of a webpage by specifying an ID that is already on the
+surrounding HTML. If you don't mind throwing caution to the wind, enable
+this directive, but I strongly recommend you also consider blacklisting IDs
+you use (%Attr.IDBlacklist) or prefixing all user supplied IDs
+(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of
+pre-1.2.0 versions.
+--ALIASES--
+HTML.EnableAttrID
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt
new file mode 100644
index 0000000000..f31d226f58
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt
@@ -0,0 +1,8 @@
+Attr.ForbiddenClasses
+TYPE: lookup
+VERSION: 4.0.0
+DEFAULT: array()
+--DESCRIPTION--
+List of forbidden class values in the class attribute. By default, this is
+empty, which means that no classes are forbidden. See also %Attr.AllowedClasses.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt
new file mode 100644
index 0000000000..735d4b7a10
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt
@@ -0,0 +1,10 @@
+Attr.ID.HTML5
+TYPE: bool/null
+DEFAULT: null
+VERSION: 4.8.0
+--DESCRIPTION--
+In HTML5, restrictions on the format of the id attribute have been significantly
+relaxed, such that any string is valid so long as it contains no spaces and
+is at least one character. In lieu of a general HTML5 compatibility flag,
+set this configuration directive to true to use the relaxed rules.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt
new file mode 100644
index 0000000000..5f2b5e3d2c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt
@@ -0,0 +1,5 @@
+Attr.IDBlacklist
+TYPE: list
+DEFAULT: array()
+DESCRIPTION: Array of IDs not allowed in the document.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt
new file mode 100644
index 0000000000..6f5824586e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt
@@ -0,0 +1,9 @@
+Attr.IDBlacklistRegexp
+TYPE: string/null
+VERSION: 1.6.0
+DEFAULT: NULL
+--DESCRIPTION--
+PCRE regular expression to be matched against all IDs. If the expression is
+matches, the ID is rejected. Use this with care: may cause significant
+degradation. ID matching is done after all other validation.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt
new file mode 100644
index 0000000000..cc49d43fd0
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt
@@ -0,0 +1,12 @@
+Attr.IDPrefix
+TYPE: string
+VERSION: 1.2.0
+DEFAULT: ''
+--DESCRIPTION--
+String to prefix to IDs. If you have no idea what IDs your pages may use,
+you may opt to simply add a prefix to all user-submitted ID attributes so
+that they are still usable, but will not conflict with core page IDs.
+Example: setting the directive to 'user_' will result in a user submitted
+'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true
+before using this.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt
new file mode 100644
index 0000000000..2c5924a7ad
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt
@@ -0,0 +1,14 @@
+Attr.IDPrefixLocal
+TYPE: string
+VERSION: 1.2.0
+DEFAULT: ''
+--DESCRIPTION--
+Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you
+need to allow multiple sets of user content on web page, you may need to
+have a seperate prefix that changes with each iteration. This way,
+seperately submitted user content displayed on the same page doesn't
+clobber each other. Ideal values are unique identifiers for the content it
+represents (i.e. the id of the row in the database). Be sure to add a
+seperator (like an underscore) at the end. Warning: this directive will
+not work unless %Attr.IDPrefix is set to a non-empty value!
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt
new file mode 100644
index 0000000000..d5caa1bb97
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt
@@ -0,0 +1,31 @@
+AutoFormat.AutoParagraph
+TYPE: bool
+VERSION: 2.0.1
+DEFAULT: false
+--DESCRIPTION--
+
+
+ This directive turns on auto-paragraphing, where double newlines are
+ converted in to paragraphs whenever possible. Auto-paragraphing:
+
+
+
Always applies to inline elements or text in the root node,
+
Applies to inline elements or text with double newlines in nodes
+ that allow paragraph tags,
+
Applies to double newlines in paragraph tags
+
+
+ p tags must be allowed for this directive to take effect.
+ We do not use br tags for paragraphing, as that is
+ semantically incorrect.
+
+
+ To prevent auto-paragraphing as a content-producer, refrain from using
+ double-newlines except to specify a new paragraph or in contexts where
+ it has special meaning (whitespace usually has no meaning except in
+ tags like pre, so this should not be difficult.) To prevent
+ the paragraphing of inline text adjacent to block elements, wrap them
+ in div tags (the behavior is slightly different outside of
+ the root node.)
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt
new file mode 100644
index 0000000000..2a476481af
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt
@@ -0,0 +1,12 @@
+AutoFormat.Custom
+TYPE: list
+VERSION: 2.0.1
+DEFAULT: array()
+--DESCRIPTION--
+
+
+ This directive can be used to add custom auto-format injectors.
+ Specify an array of injector names (class name minus the prefix)
+ or concrete implementations. Injector class must exist.
+
+ This directive turns on the in-text display of URIs in <a> tags, and disables
+ those links. For example, example becomes
+ example (http://example.com).
+
+ Location of configuration documentation to link to, let %s substitute
+ into the configuration's namespace and directive names sans the percent
+ sign.
+
+ Internal auto-formatter that converts configuration directives in
+ syntax %Namespace.Directive to links. a tags
+ with the href attribute must be allowed.
+
+ Given that an element has no contents, it will be removed by default, unless
+ this predicate dictates otherwise. The predicate can either be an associative
+ map from tag name to list of attributes that must be present for the element
+ to be considered preserved: thus, the default always preserves colgroup,
+ th and td, and also iframe if it
+ has a src.
+
+ When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp
+ are enabled, this directive defines what HTML elements should not be
+ removede if they have only a non-breaking space in them.
+
+ When enabled, HTML Purifier will treat any elements that contain only
+ non-breaking spaces as well as regular whitespace as empty, and remove
+ them when %AutoFormat.RemoveEmpty is enabled.
+
+
+ See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements
+ that don't have this behavior applied to them.
+
+ When enabled, HTML Purifier will attempt to remove empty elements that
+ contribute no semantic information to the document. The following types
+ of nodes will be removed:
+
+
+ Tags with no attributes and no content, and that are not empty
+ elements (remove <a></a> but not
+ <br />), and
+
+
+ Tags with no content, except for:
+
The colgroup element, or
+
+ Elements with the id or name attribute,
+ when those attributes are permitted on those elements.
+
+
+
+
+ Please be very careful when using this functionality; while it may not
+ seem that empty elements contain useful information, they can alter the
+ layout of a document given appropriate styling. This directive is most
+ useful when you are processing machine-generated HTML, please avoid using
+ it on regular user HTML.
+
+
+ Elements that contain only whitespace will be treated as empty. Non-breaking
+ spaces, however, do not count as whitespace. See
+ %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior.
+
+
+ This algorithm is not perfect; you may still notice some empty tags,
+ particularly if a node had elements, but those elements were later removed
+ because they were not permitted in that context, or tags that, after
+ being auto-closed by another tag, where empty. This is for safety reasons
+ to prevent clever code from breaking validation. The general rule of thumb:
+ if a tag looked empty on the way in, it will get removed; if HTML Purifier
+ made it empty, it will stay.
+
+ This directive causes span tags without any attributes
+ to be removed. It will also remove spans that had all attributes
+ removed during processing.
+
+ By default, HTML Purifier removes duplicate CSS properties,
+ like color:red; color:blue. If this is set to
+ true, duplicate properties are allowed.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt
new file mode 100644
index 0000000000..b324608f76
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt
@@ -0,0 +1,8 @@
+CSS.AllowImportant
+TYPE: bool
+DEFAULT: false
+VERSION: 3.1.0
+--DESCRIPTION--
+This parameter determines whether or not !important cascade modifiers should
+be allowed in user CSS. If false, !important will stripped.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt
new file mode 100644
index 0000000000..748be0eec8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt
@@ -0,0 +1,11 @@
+CSS.AllowTricky
+TYPE: bool
+DEFAULT: false
+VERSION: 3.1.0
+--DESCRIPTION--
+This parameter determines whether or not to allow "tricky" CSS properties and
+values. Tricky CSS properties/values can drastically modify page layout or
+be used for deceptive practices but do not directly constitute a security risk.
+For example, display:none; is considered a tricky property that
+will only be allowed if this directive is set to true.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt
new file mode 100644
index 0000000000..3fd4654065
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt
@@ -0,0 +1,12 @@
+CSS.AllowedFonts
+TYPE: lookup/null
+VERSION: 4.3.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+ Allows you to manually specify a set of allowed fonts. If
+ NULL, all fonts are allowed. This directive
+ affects generic names (serif, sans-serif, monospace, cursive,
+ fantasy) as well as specific font families.
+
+ If HTML Purifier's style attributes set is unsatisfactory for your needs,
+ you can overload it with your own list of tags to allow. Note that this
+ method is subtractive: it does its job by taking away from HTML Purifier
+ usual feature set, so you cannot add an attribute that HTML Purifier never
+ supported in the first place.
+
+
+ Warning: If another directive conflicts with the
+ elements here, that directive will win and override.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt
new file mode 100644
index 0000000000..5cb7dda3ba
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt
@@ -0,0 +1,11 @@
+CSS.DefinitionRev
+TYPE: int
+VERSION: 2.0.0
+DEFAULT: 1
+--DESCRIPTION--
+
+
+ Revision identifier for your custom definition. See
+ %HTML.DefinitionRev for details.
+
+ This is the logical inverse of %CSS.AllowedProperties, and it will
+ override that directive or any other directive. If possible,
+ %CSS.AllowedProperties is recommended over this directive,
+ because it can sometimes be difficult to tell whether or not you've
+ forbidden all of the CSS properties you truly would like to disallow.
+
+ This parameter sets the maximum allowed length on img tags,
+ effectively the width and height properties.
+ Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is
+ in place to prevent imagecrash attacks, disable with null at your own risk.
+ This directive is similar to %HTML.MaxImgLength, and both should be
+ concurrently edited, although there are
+ subtle differences in the input format (the CSS max is a number with
+ a unit).
+
+ Whether or not to allow safe, proprietary CSS values.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt
new file mode 100644
index 0000000000..e733a61e8a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt
@@ -0,0 +1,9 @@
+CSS.Trusted
+TYPE: bool
+VERSION: 4.2.1
+DEFAULT: false
+--DESCRIPTION--
+Indicates whether or not the user's CSS input is trusted or not. If the
+input is trusted, a more expansive set of allowed properties. See
+also %HTML.Trusted.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt
new file mode 100644
index 0000000000..c486724c88
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt
@@ -0,0 +1,14 @@
+Cache.DefinitionImpl
+TYPE: string/null
+VERSION: 2.0.0
+DEFAULT: 'Serializer'
+--DESCRIPTION--
+
+This directive defines which method to use when caching definitions,
+the complex data-type that makes HTML Purifier tick. Set to null
+to disable caching (not recommended, as you will see a definite
+performance degradation).
+
+--ALIASES--
+Core.DefinitionCache
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt
new file mode 100644
index 0000000000..54036507d6
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt
@@ -0,0 +1,13 @@
+Cache.SerializerPath
+TYPE: string/null
+VERSION: 2.0.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+
+ Absolute path with no trailing slash to store serialized definitions in.
+ Default is within the
+ HTML Purifier library inside DefinitionCache/Serializer. This
+ path must be writable by the webserver.
+
+ This directive enables aggressive pre-filter fixes HTML Purifier can
+ perform in order to ensure that open angled-brackets do not get killed
+ during parsing stage. Enabling this will result in two preg_replace_callback
+ calls and at least two preg_replace calls for every HTML document parsed;
+ if your users make very well-formed HTML, you can set this directive false.
+ This has no effect when DirectLex is used.
+
+
+ Notice: This directive's default turned from false to true
+ in HTML Purifier 3.2.0.
+
+ This directive enables aggressive pre-filter removal of
+ script tags. This is not necessary for security,
+ but it can help work around a bug in libxml where embedded
+ HTML elements inside script sections cause the parser to
+ choke. To revert to pre-4.9.0 behavior, set this to false.
+ This directive has no effect if %Core.Trusted is true,
+ %Core.RemoveScriptContents is false, or %Core.HiddenElements
+ does not contain script.
+
+ By RFC 1123, underscores are not permitted in host names.
+ (This is in contrast to the specification for DNS, RFC
+ 2181, which allows underscores.)
+ However, most browsers do the right thing when faced with
+ an underscore in the host name, and so some poorly written
+ websites are written with the expectation this should work.
+ Setting this parameter to true relaxes our allowed character
+ check so that underscores are permitted.
+
+ This directive allows parsing of many nested tags.
+ If you set true, relaxes any hardcoded limit from the parser.
+ However, in that case it may cause a Dos attack.
+ Be careful when enabling it.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt
new file mode 100644
index 0000000000..d7317911fa
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt
@@ -0,0 +1,12 @@
+Core.CollectErrors
+TYPE: bool
+VERSION: 2.0.0
+DEFAULT: false
+--DESCRIPTION--
+
+Whether or not to collect errors found while filtering the document. This
+is a useful way to give feedback to your users. Warning:
+Currently this feature is very patchy and experimental, with lots of
+possible error messages not yet implemented. It will not cause any
+problems, but it may not help your users either.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt
new file mode 100644
index 0000000000..a75844cd58
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt
@@ -0,0 +1,160 @@
+Core.ColorKeywords
+TYPE: hash
+VERSION: 2.0.0
+--DEFAULT--
+array (
+ 'aliceblue' => '#F0F8FF',
+ 'antiquewhite' => '#FAEBD7',
+ 'aqua' => '#00FFFF',
+ 'aquamarine' => '#7FFFD4',
+ 'azure' => '#F0FFFF',
+ 'beige' => '#F5F5DC',
+ 'bisque' => '#FFE4C4',
+ 'black' => '#000000',
+ 'blanchedalmond' => '#FFEBCD',
+ 'blue' => '#0000FF',
+ 'blueviolet' => '#8A2BE2',
+ 'brown' => '#A52A2A',
+ 'burlywood' => '#DEB887',
+ 'cadetblue' => '#5F9EA0',
+ 'chartreuse' => '#7FFF00',
+ 'chocolate' => '#D2691E',
+ 'coral' => '#FF7F50',
+ 'cornflowerblue' => '#6495ED',
+ 'cornsilk' => '#FFF8DC',
+ 'crimson' => '#DC143C',
+ 'cyan' => '#00FFFF',
+ 'darkblue' => '#00008B',
+ 'darkcyan' => '#008B8B',
+ 'darkgoldenrod' => '#B8860B',
+ 'darkgray' => '#A9A9A9',
+ 'darkgrey' => '#A9A9A9',
+ 'darkgreen' => '#006400',
+ 'darkkhaki' => '#BDB76B',
+ 'darkmagenta' => '#8B008B',
+ 'darkolivegreen' => '#556B2F',
+ 'darkorange' => '#FF8C00',
+ 'darkorchid' => '#9932CC',
+ 'darkred' => '#8B0000',
+ 'darksalmon' => '#E9967A',
+ 'darkseagreen' => '#8FBC8F',
+ 'darkslateblue' => '#483D8B',
+ 'darkslategray' => '#2F4F4F',
+ 'darkslategrey' => '#2F4F4F',
+ 'darkturquoise' => '#00CED1',
+ 'darkviolet' => '#9400D3',
+ 'deeppink' => '#FF1493',
+ 'deepskyblue' => '#00BFFF',
+ 'dimgray' => '#696969',
+ 'dimgrey' => '#696969',
+ 'dodgerblue' => '#1E90FF',
+ 'firebrick' => '#B22222',
+ 'floralwhite' => '#FFFAF0',
+ 'forestgreen' => '#228B22',
+ 'fuchsia' => '#FF00FF',
+ 'gainsboro' => '#DCDCDC',
+ 'ghostwhite' => '#F8F8FF',
+ 'gold' => '#FFD700',
+ 'goldenrod' => '#DAA520',
+ 'gray' => '#808080',
+ 'grey' => '#808080',
+ 'green' => '#008000',
+ 'greenyellow' => '#ADFF2F',
+ 'honeydew' => '#F0FFF0',
+ 'hotpink' => '#FF69B4',
+ 'indianred' => '#CD5C5C',
+ 'indigo' => '#4B0082',
+ 'ivory' => '#FFFFF0',
+ 'khaki' => '#F0E68C',
+ 'lavender' => '#E6E6FA',
+ 'lavenderblush' => '#FFF0F5',
+ 'lawngreen' => '#7CFC00',
+ 'lemonchiffon' => '#FFFACD',
+ 'lightblue' => '#ADD8E6',
+ 'lightcoral' => '#F08080',
+ 'lightcyan' => '#E0FFFF',
+ 'lightgoldenrodyellow' => '#FAFAD2',
+ 'lightgray' => '#D3D3D3',
+ 'lightgrey' => '#D3D3D3',
+ 'lightgreen' => '#90EE90',
+ 'lightpink' => '#FFB6C1',
+ 'lightsalmon' => '#FFA07A',
+ 'lightseagreen' => '#20B2AA',
+ 'lightskyblue' => '#87CEFA',
+ 'lightslategray' => '#778899',
+ 'lightslategrey' => '#778899',
+ 'lightsteelblue' => '#B0C4DE',
+ 'lightyellow' => '#FFFFE0',
+ 'lime' => '#00FF00',
+ 'limegreen' => '#32CD32',
+ 'linen' => '#FAF0E6',
+ 'magenta' => '#FF00FF',
+ 'maroon' => '#800000',
+ 'mediumaquamarine' => '#66CDAA',
+ 'mediumblue' => '#0000CD',
+ 'mediumorchid' => '#BA55D3',
+ 'mediumpurple' => '#9370DB',
+ 'mediumseagreen' => '#3CB371',
+ 'mediumslateblue' => '#7B68EE',
+ 'mediumspringgreen' => '#00FA9A',
+ 'mediumturquoise' => '#48D1CC',
+ 'mediumvioletred' => '#C71585',
+ 'midnightblue' => '#191970',
+ 'mintcream' => '#F5FFFA',
+ 'mistyrose' => '#FFE4E1',
+ 'moccasin' => '#FFE4B5',
+ 'navajowhite' => '#FFDEAD',
+ 'navy' => '#000080',
+ 'oldlace' => '#FDF5E6',
+ 'olive' => '#808000',
+ 'olivedrab' => '#6B8E23',
+ 'orange' => '#FFA500',
+ 'orangered' => '#FF4500',
+ 'orchid' => '#DA70D6',
+ 'palegoldenrod' => '#EEE8AA',
+ 'palegreen' => '#98FB98',
+ 'paleturquoise' => '#AFEEEE',
+ 'palevioletred' => '#DB7093',
+ 'papayawhip' => '#FFEFD5',
+ 'peachpuff' => '#FFDAB9',
+ 'peru' => '#CD853F',
+ 'pink' => '#FFC0CB',
+ 'plum' => '#DDA0DD',
+ 'powderblue' => '#B0E0E6',
+ 'purple' => '#800080',
+ 'rebeccapurple' => '#663399',
+ 'red' => '#FF0000',
+ 'rosybrown' => '#BC8F8F',
+ 'royalblue' => '#4169E1',
+ 'saddlebrown' => '#8B4513',
+ 'salmon' => '#FA8072',
+ 'sandybrown' => '#F4A460',
+ 'seagreen' => '#2E8B57',
+ 'seashell' => '#FFF5EE',
+ 'sienna' => '#A0522D',
+ 'silver' => '#C0C0C0',
+ 'skyblue' => '#87CEEB',
+ 'slateblue' => '#6A5ACD',
+ 'slategray' => '#708090',
+ 'slategrey' => '#708090',
+ 'snow' => '#FFFAFA',
+ 'springgreen' => '#00FF7F',
+ 'steelblue' => '#4682B4',
+ 'tan' => '#D2B48C',
+ 'teal' => '#008080',
+ 'thistle' => '#D8BFD8',
+ 'tomato' => '#FF6347',
+ 'turquoise' => '#40E0D0',
+ 'violet' => '#EE82EE',
+ 'wheat' => '#F5DEB3',
+ 'white' => '#FFFFFF',
+ 'whitesmoke' => '#F5F5F5',
+ 'yellow' => '#FFFF00',
+ 'yellowgreen' => '#9ACD32'
+)
+--DESCRIPTION--
+
+Lookup array of color names to six digit hexadecimal number corresponding
+to color, with preceding hash mark. Used when parsing colors. The lookup
+is done in a case-insensitive manner.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt
new file mode 100644
index 0000000000..64b114fce2
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt
@@ -0,0 +1,14 @@
+Core.ConvertDocumentToFragment
+TYPE: bool
+DEFAULT: true
+--DESCRIPTION--
+
+This parameter determines whether or not the filter should convert
+input that is a full document with html and body tags to a fragment
+of just the contents of a body tag. This parameter is simply something
+HTML Purifier can do during an edge-case: for most inputs, this
+processing is not necessary.
+
+--ALIASES--
+Core.AcceptFullDocuments
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt
new file mode 100644
index 0000000000..36f16e07ea
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt
@@ -0,0 +1,17 @@
+Core.DirectLexLineNumberSyncInterval
+TYPE: int
+VERSION: 2.0.0
+DEFAULT: 0
+--DESCRIPTION--
+
+
+ Specifies the number of tokens the DirectLex line number tracking
+ implementations should process before attempting to resyncronize the
+ current line count by manually counting all previous new-lines. When
+ at 0, this functionality is disabled. Lower values will decrease
+ performance, and this is only strictly necessary if the counting
+ algorithm is buggy (in which case you should report it as a bug).
+ This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is
+ not being used.
+
+ This directive disables SGML-style exclusions, e.g. the exclusion of
+ <object> in any descendant of a
+ <pre> tag. Disabling excludes will allow some
+ invalid documents to pass through HTML Purifier, but HTML Purifier
+ will also be less likely to accidentally remove large documents during
+ processing.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt
new file mode 100644
index 0000000000..ce243c35dc
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt
@@ -0,0 +1,9 @@
+Core.EnableIDNA
+TYPE: bool
+DEFAULT: false
+VERSION: 4.4.0
+--DESCRIPTION--
+Allows international domain names in URLs. This configuration option
+requires the PEAR Net_IDNA2 module to be installed. It operates by
+punycoding any internationalized host names for maximum portability.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt
new file mode 100644
index 0000000000..8bfb47c3ac
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt
@@ -0,0 +1,15 @@
+Core.Encoding
+TYPE: istring
+DEFAULT: 'utf-8'
+--DESCRIPTION--
+If for some reason you are unable to convert all webpages to UTF-8, you can
+use this directive as a stop-gap compatibility change to let HTML Purifier
+deal with non UTF-8 input. This technique has notable deficiencies:
+absolutely no characters outside of the selected character encoding will be
+preserved, not even the ones that have been ampersand escaped (this is due
+to a UTF-8 specific feature that automatically resolves all
+entities), making it pretty useless for anything except the most I18N-blind
+applications, although %Core.EscapeNonASCIICharacters offers fixes this
+trouble with another tradeoff. This directive only accepts ISO-8859-1 if
+iconv is not enabled.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt
new file mode 100644
index 0000000000..a3881be75c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt
@@ -0,0 +1,12 @@
+Core.EscapeInvalidChildren
+TYPE: bool
+DEFAULT: false
+--DESCRIPTION--
+
Warning: this configuration option is no longer does anything as of 4.6.0.
+
+
When true, a child is found that is not allowed in the context of the
+parent element will be transformed into text as if it were ASCII. When
+false, that element and all internal tags will be dropped, though text will
+be preserved. There is no option for dropping the element but preserving
+child nodes.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt
new file mode 100644
index 0000000000..a7a5b249bb
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt
@@ -0,0 +1,7 @@
+Core.EscapeInvalidTags
+TYPE: bool
+DEFAULT: false
+--DESCRIPTION--
+When true, invalid tags will be written back to the document as plain text.
+Otherwise, they are silently dropped.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt
new file mode 100644
index 0000000000..abb499948a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt
@@ -0,0 +1,13 @@
+Core.EscapeNonASCIICharacters
+TYPE: bool
+VERSION: 1.4.0
+DEFAULT: false
+--DESCRIPTION--
+This directive overcomes a deficiency in %Core.Encoding by blindly
+converting all non-ASCII characters into decimal numeric entities before
+converting it to its native encoding. This means that even characters that
+can be expressed in the non-UTF-8 encoding will be entity-ized, which can
+be a real downer for encodings like Big5. It also assumes that the ASCII
+repetoire is available, although this is the case for almost all encodings.
+Anyway, use UTF-8!
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt
new file mode 100644
index 0000000000..915391edb7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt
@@ -0,0 +1,19 @@
+Core.HiddenElements
+TYPE: lookup
+--DEFAULT--
+array (
+ 'script' => true,
+ 'style' => true,
+)
+--DESCRIPTION--
+
+
+ This directive is a lookup array of elements which should have their
+ contents removed when they are not allowed by the HTML definition.
+ For example, the contents of a script tag are not
+ normally shown in a document, so if script tags are to be removed,
+ their contents should be removed to. This is opposed to a b
+ tag, which defines some presentational changes but does not hide its
+ contents.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt
new file mode 100644
index 0000000000..233fca14f8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.Language.txt
@@ -0,0 +1,10 @@
+Core.Language
+TYPE: string
+VERSION: 2.0.0
+DEFAULT: 'en'
+--DESCRIPTION--
+
+ISO 639 language code for localizable things in HTML Purifier to use,
+which is mainly error reporting. There is currently only an English (en)
+translation, so this directive is currently useless.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt
new file mode 100644
index 0000000000..392b436493
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt
@@ -0,0 +1,36 @@
+Core.LegacyEntityDecoder
+TYPE: bool
+VERSION: 4.9.0
+DEFAULT: false
+--DESCRIPTION--
+
+ Prior to HTML Purifier 4.9.0, entities were decoded by performing
+ a global search replace for all entities whose decoded versions
+ did not have special meanings under HTML, and replaced them with
+ their decoded versions. We would match all entities, even if they did
+ not have a trailing semicolon, but only if there weren't any trailing
+ alphanumeric characters.
+
+
+
Original
Text
Attribute
+
¥
¥
¥
+
¥
¥
¥
+
¥a
¥a
¥a
+
¥=
¥=
¥=
+
+
+ In HTML Purifier 4.9.0, we changed the behavior of entity parsing
+ to match entities that had missing trailing semicolons in less
+ cases, to more closely match HTML5 parsing behavior:
+
+
+
Original
Text
Attribute
+
¥
¥
¥
+
¥
¥
¥
+
¥a
¥a
¥a
+
¥=
¥=
¥=
+
+
+ This flag reverts back to pre-HTML Purifier 4.9.0 behavior.
+
+ This parameter determines what lexer implementation can be used. The
+ valid values are:
+
+
+
null
+
+ Recommended, the lexer implementation will be auto-detected based on
+ your PHP-version and configuration.
+
+
string lexer identifier
+
+ This is a slim way of manually overridding the implementation.
+ Currently recognized values are: DOMLex (the default PHP5
+implementation)
+ and DirectLex (the default PHP4 implementation). Only use this if
+ you know what you are doing: usually, the auto-detection will
+ manage things for cases you aren't even aware of.
+
+
object lexer instance
+
+ Super-advanced: you can specify your own, custom, implementation that
+ implements the interface defined by HTMLPurifier_Lexer.
+ I may remove this option simply because I don't expect anyone
+ to use it.
+
+ If true, HTML Purifier will add line number information to all tokens.
+ This is useful when error reporting is turned on, but can result in
+ significant performance degradation and should not be used when
+ unnecessary. This directive must be used with the DirectLex lexer,
+ as the DOMLex lexer does not (yet) support this functionality.
+ If the value is null, an appropriate value will be selected based
+ on other configuration.
+
+ If set to true, blank nodes will be removed. This can be useful for maintaining
+ backwards compatibility when upgrading from previous versions of PHP.
+
+ This directive enables pre-emptive URI checking in img
+ tags, as the attribute validation strategy is not authorized to
+ remove elements from the document. Revert to pre-1.3.0 behavior by setting to false.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt
new file mode 100644
index 0000000000..3397d9f71f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt
@@ -0,0 +1,11 @@
+Core.RemoveProcessingInstructions
+TYPE: bool
+VERSION: 4.2.0
+DEFAULT: false
+--DESCRIPTION--
+Instead of escaping processing instructions in the form <? ...
+?>, remove it out-right. This may be useful if the HTML
+you are validating contains XML processing instruction gunk, however,
+it can also be user-unfriendly for people attempting to post PHP
+snippets.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt
new file mode 100644
index 0000000000..a4cd966df8
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt
@@ -0,0 +1,12 @@
+Core.RemoveScriptContents
+TYPE: bool/null
+DEFAULT: NULL
+VERSION: 2.0.0
+DEPRECATED-VERSION: 2.1.0
+DEPRECATED-USE: Core.HiddenElements
+--DESCRIPTION--
+
+ This directive enables HTML Purifier to remove not only script tags
+ but all of their contents.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt
new file mode 100644
index 0000000000..3db50ef204
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt
@@ -0,0 +1,11 @@
+Filter.Custom
+TYPE: list
+VERSION: 3.1.0
+DEFAULT: array()
+--DESCRIPTION--
+
+ This directive can be used to add custom filters; it is nearly the
+ equivalent of the now deprecated HTMLPurifier->addFilter()
+ method. Specify an array of concrete implementations.
+
+ Whether or not to escape the dangerous characters <, > and &
+ as \3C, \3E and \26, respectively. This is can be safely set to false
+ if the contents of StyleBlocks will be placed in an external stylesheet,
+ where there is no risk of it being interpreted as HTML.
+
+ If you would like users to be able to define external stylesheets, but
+ only allow them to specify CSS declarations for a specific node and
+ prevent them from fiddling with other elements, use this directive.
+ It accepts any valid CSS selector, and will prepend this to any
+ CSS declaration extracted from the document. For example, if this
+ directive is set to #user-content and a user uses the
+ selector a:hover, the final selector will be
+ #user-content a:hover.
+
+
+ The comma shorthand may be used; consider the above example, with
+ #user-content, #user-content2, the final selector will
+ be #user-content a:hover, #user-content2 a:hover.
+
+
+ Warning: It is possible for users to bypass this measure
+ using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML
+ Purifier, and I am working to get it fixed. Until then, HTML Purifier
+ performs a basic check to prevent this.
+
+ If left NULL, HTML Purifier will attempt to instantiate a csstidy
+ class to use for internal cleaning. This will usually be good enough.
+
+
+ However, for trusted user input, you can set this to false to
+ disable cleaning. In addition, you can supply your own concrete implementation
+ of Tidy's interface to use, although I don't know why you'd want to do that.
+
+ This directive turns on the style block extraction filter, which removes
+ style blocks from input HTML, cleans them up with CSSTidy,
+ and places them in the StyleBlocks context variable, for further
+ use by you, usually to be placed in an external stylesheet, or a
+ style block in the head of your document.
+
+
+ Sample usage:
+
+
';
+?>
+
+
+
+ Filter.ExtractStyleBlocks
+body {color:#F00;} Some text';
+
+ $config = HTMLPurifier_Config::createDefault();
+ $config->set('Filter', 'ExtractStyleBlocks', true);
+ $purifier = new HTMLPurifier($config);
+
+ $html = $purifier->purify($dirty);
+
+ // This implementation writes the stylesheets to the styles/ directory.
+ // You can also echo the styles inside the document, but it's a bit
+ // more difficult to make sure they get interpreted properly by
+ // browsers; try the usual CSS armoring techniques.
+ $styles = $purifier->context->get('StyleBlocks');
+ $dir = 'styles/';
+ if (!is_dir($dir)) mkdir($dir);
+ $hash = sha1($_GET['html']);
+ foreach ($styles as $i => $style) {
+ file_put_contents($name = $dir . $hash . "_$i");
+ echo '';
+ }
+?>
+
+
+
+
+
+
+
+]]>
+
+ Warning: It is possible for a user to mount an
+ imagecrash attack using this CSS. Counter-measures are difficult;
+ it is not simply enough to limit the range of CSS lengths (using
+ relative lengths with many nesting levels allows for large values
+ to be attained without actually specifying them in the stylesheet),
+ and the flexible nature of selectors makes it difficult to selectively
+ disable lengths on image tags (HTML Purifier, however, does disable
+ CSS width and height in inline styling). There are probably two effective
+ counter measures: an explicit width and height set to auto in all
+ images in your document (unlikely) or the disabling of width and
+ height (somewhat reasonable). Whether or not these measures should be
+ used is left to the reader.
+
+ Warning: Deprecated in favor of %HTML.SafeObject and
+ %Output.FlashCompat (turn both on to allow YouTube videos and other
+ Flash content).
+
+
+ This directive enables YouTube video embedding in HTML Purifier. Check
+ this document
+ on embedding videos for more information on what this filter does.
+
+ This is a preferred convenience directive that combines
+ %HTML.AllowedElements and %HTML.AllowedAttributes.
+ Specify elements and attributes that are allowed using:
+ element1[attr1|attr2],element2.... For example,
+ if you would like to only allow paragraphs and links, specify
+ a[href],p. You can specify attributes that apply
+ to all elements using an asterisk, e.g. *[lang].
+ You can also use newlines instead of commas to separate elements.
+
+
+ Warning:
+ All of the constraints on the component directives are still enforced.
+ The syntax is a subset of TinyMCE's valid_elements
+ whitelist: directly copy-pasting it here will probably result in
+ broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes
+ are set, this directive has no effect.
+
+ If HTML Purifier's attribute set is unsatisfactory, overload it!
+ The syntax is "tag.attr" or "*.attr" for the global attributes
+ (style, id, class, dir, lang, xml:lang).
+
+
+ Warning: If another directive conflicts with the
+ elements here, that directive will win and override. For
+ example, %HTML.EnableAttrID will take precedence over *.id in this
+ directive. You must set that directive to true before you can use
+ IDs at all.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt
new file mode 100644
index 0000000000..140e21423e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt
@@ -0,0 +1,10 @@
+HTML.AllowedComments
+TYPE: lookup
+VERSION: 4.4.0
+DEFAULT: array()
+--DESCRIPTION--
+A whitelist which indicates what explicit comment bodies should be
+allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp
+(these directives are union'ed together, so a comment is considered
+valid if any directive deems it valid.)
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt
new file mode 100644
index 0000000000..f22e977d43
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt
@@ -0,0 +1,15 @@
+HTML.AllowedCommentsRegexp
+TYPE: string/null
+VERSION: 4.4.0
+DEFAULT: NULL
+--DESCRIPTION--
+A regexp, which if it matches the body of a comment, indicates that
+it should be allowed. Trailing and leading spaces are removed prior
+to running this regular expression.
+Warning: Make sure you specify
+correct anchor metacharacters ^regex$, otherwise you may accept
+comments that you did not mean to! In particular, the regex /foo|bar/
+is probably not sufficiently strict, since it also allows foobar.
+See also %HTML.AllowedComments (these directives are union'ed together,
+so a comment is considered valid if any directive deems it valid.)
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt
new file mode 100644
index 0000000000..1d3fa7907d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt
@@ -0,0 +1,23 @@
+HTML.AllowedElements
+TYPE: lookup/null
+VERSION: 1.3.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+ If HTML Purifier's tag set is unsatisfactory for your needs, you can
+ overload it with your own list of tags to allow. If you change
+ this, you probably also want to change %HTML.AllowedAttributes; see
+ also %HTML.Allowed which lets you set allowed elements and
+ attributes at the same time.
+
+
+ If you attempt to allow an element that HTML Purifier does not know
+ about, HTML Purifier will raise an error. You will need to manually
+ tell HTML Purifier about this element by using the
+ advanced customization features.
+
+
+ Warning: If another directive conflicts with the
+ elements here, that directive will win and override.
+
+ A doctype comes with a set of usual modules to use. Without having
+ to mucking about with the doctypes, you can quickly activate or
+ disable these modules by specifying which modules you wish to allow
+ with this directive. This is most useful for unit testing specific
+ modules, although end users may find it useful for their own ends.
+
+
+ If you specify a module that does not exist, the manager will silently
+ fail to use it, so be careful! User-defined modules are not affected
+ by this directive. Modules defined in %HTML.CoreModules are not
+ affected by this directive.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt
new file mode 100644
index 0000000000..151fb7b826
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt
@@ -0,0 +1,11 @@
+HTML.Attr.Name.UseCDATA
+TYPE: bool
+DEFAULT: false
+VERSION: 4.0.0
+--DESCRIPTION--
+The W3C specification DTD defines the name attribute to be CDATA, not ID, due
+to limitations of DTD. In certain documents, this relaxed behavior is desired,
+whether it is to specify duplicate names, or to specify names that would be
+illegal IDs (for example, names that begin with a digit.) Set this configuration
+directive to true to use the relaxed parsing rules.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt
new file mode 100644
index 0000000000..45ae469ec9
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt
@@ -0,0 +1,18 @@
+HTML.BlockWrapper
+TYPE: string
+VERSION: 1.3.0
+DEFAULT: 'p'
+--DESCRIPTION--
+
+
+ String name of element to wrap inline elements that are inside a block
+ context. This only occurs in the children of blockquote in strict mode.
+
+
+ Example: by default value,
+ <blockquote>Foo</blockquote> would become
+ <blockquote><p>Foo</p></blockquote>.
+ The <p> tags can be replaced with whatever you desire,
+ as long as it is a block level element.
+
+ Certain modularized doctypes (XHTML, namely), have certain modules
+ that must be included for the doctype to be an conforming document
+ type: put those modules here. By default, XHTML's core modules
+ are used. You can set this to a blank array to disable core module
+ protection, but this is not recommended.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt
new file mode 100644
index 0000000000..6ed70b599f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt
@@ -0,0 +1,9 @@
+HTML.CustomDoctype
+TYPE: string/null
+VERSION: 2.0.1
+DEFAULT: NULL
+--DESCRIPTION--
+
+A custom doctype for power-users who defined their own document
+type. This directive only applies when %HTML.Doctype is blank.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt
new file mode 100644
index 0000000000..103db754a2
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt
@@ -0,0 +1,33 @@
+HTML.DefinitionID
+TYPE: string/null
+DEFAULT: NULL
+VERSION: 2.0.0
+--DESCRIPTION--
+
+
+ Unique identifier for a custom-built HTML definition. If you edit
+ the raw version of the HTMLDefinition, introducing changes that the
+ configuration object does not reflect, you must specify this variable.
+ If you change your custom edits, you should change this directive, or
+ clear your cache. Example:
+
+ In the above example, the configuration is still at the defaults, but
+ using the advanced API, an extra attribute has been added. The
+ configuration object normally has no way of knowing that this change
+ has taken place, so it needs an extra directive: %HTML.DefinitionID.
+ If someone else attempts to use the default configuration, these two
+ pieces of code will not clobber each other in the cache, since one has
+ an extra directive attached to it.
+
+
+ You must specify a value to this directive to use the
+ advanced API features.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt
new file mode 100644
index 0000000000..229ae0267a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt
@@ -0,0 +1,16 @@
+HTML.DefinitionRev
+TYPE: int
+VERSION: 2.0.0
+DEFAULT: 1
+--DESCRIPTION--
+
+
+ Revision identifier for your custom definition specified in
+ %HTML.DefinitionID. This serves the same purpose: uniquely identifying
+ your custom definition, but this one does so in a chronological
+ context: revision 3 is more up-to-date then revision 2. Thus, when
+ this gets incremented, the cache handling is smart enough to clean
+ up any older revisions of your definition as well as flush the
+ cache.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt
new file mode 100644
index 0000000000..9dab497f2f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt
@@ -0,0 +1,11 @@
+HTML.Doctype
+TYPE: string/null
+DEFAULT: NULL
+--DESCRIPTION--
+Doctype to use during filtering. Technically speaking this is not actually
+a doctype (as it does not identify a corresponding DTD), but we are using
+this name for sake of simplicity. When non-blank, this will override any
+older directives like %HTML.XHTML or %HTML.Strict.
+--ALLOWED--
+'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1'
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt
new file mode 100644
index 0000000000..7878dc0bf6
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt
@@ -0,0 +1,11 @@
+HTML.FlashAllowFullScreen
+TYPE: bool
+VERSION: 4.2.0
+DEFAULT: false
+--DESCRIPTION--
+
+ Whether or not to permit embedded Flash content from
+ %HTML.SafeObject to expand to the full screen. Corresponds to
+ the allowFullScreen parameter.
+
+ While this directive is similar to %HTML.AllowedAttributes, for
+ forwards-compatibility with XML, this attribute has a different syntax. Instead of
+ tag.attr, use tag@attr. To disallow href
+ attributes in a tags, set this directive to
+ a@href. You can also disallow an attribute globally with
+ attr or *@attr (either syntax is fine; the latter
+ is provided for consistency with %HTML.AllowedAttributes).
+
+
+ Warning: This directive complements %HTML.ForbiddenElements,
+ accordingly, check
+ out that directive for a discussion of why you
+ should think twice before using this directive.
+
+ This was, perhaps, the most requested feature ever in HTML
+ Purifier. Please don't abuse it! This is the logical inverse of
+ %HTML.AllowedElements, and it will override that directive, or any
+ other directive.
+
+
+ If possible, %HTML.Allowed is recommended over this directive, because it
+ can sometimes be difficult to tell whether or not you've forbidden all of
+ the behavior you would like to disallow. If you forbid img
+ with the expectation of preventing images on your site, you'll be in for
+ a nasty surprise when people start using the background-image
+ CSS property.
+
+ Whether or not to permit form elements in the user input, regardless of
+ %HTML.Trusted value. Please be very careful when using this functionality, as
+ enabling forms in untrusted documents may allow for phishing attacks.
+
+ This directive controls the maximum number of pixels in the width and
+ height attributes in img tags. This is
+ in place to prevent imagecrash attacks, disable with null at your own risk.
+ This directive is similar to %CSS.MaxImgLength, and both should be
+ concurrently edited, although there are
+ subtle differences in the input format (the HTML max is an integer).
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt
new file mode 100644
index 0000000000..700b30924a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt
@@ -0,0 +1,7 @@
+HTML.Nofollow
+TYPE: bool
+VERSION: 4.3.0
+DEFAULT: FALSE
+--DESCRIPTION--
+If enabled, nofollow rel attributes are added to all outgoing links.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt
new file mode 100644
index 0000000000..62e8e160c7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt
@@ -0,0 +1,12 @@
+HTML.Parent
+TYPE: string
+VERSION: 1.3.0
+DEFAULT: 'div'
+--DESCRIPTION--
+
+
+ String name of element that HTML fragment passed to library will be
+ inserted in. An interesting variation would be using span as the
+ parent element, meaning that only inline tags would be allowed.
+
+ Whether or not to allow proprietary elements and attributes in your
+ documents, as per HTMLPurifier_HTMLModule_Proprietary.
+ Warning: This can cause your documents to stop
+ validating!
+
+ Whether or not to permit embed tags in documents, with a number of extra
+ security features added to prevent script execution. This is similar to
+ what websites like MySpace do to embed tags. Embed is a proprietary
+ element and will cause your website to stop validating; you should
+ see if you can use %Output.FlashCompat with %HTML.SafeObject instead
+ first.
+ Whether or not to permit iframe tags in untrusted documents. This
+ directive must be accompanied by a whitelist of permitted iframes,
+ such as %URI.SafeIframeRegexp, otherwise it will fatally error.
+ This directive has no effect on strict doctypes, as iframes are not
+ valid.
+
+ Whether or not to permit object tags in documents, with a number of extra
+ security features added to prevent script execution. This is similar to
+ what websites like MySpace do to object tags. You should also enable
+ %Output.FlashCompat in order to generate Internet Explorer
+ compatibility code for your object tags.
+
+ Whether or not to permit script tags to external scripts in documents.
+ Inline scripting is not allowed, and the script must match an explicit whitelist.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt
new file mode 100644
index 0000000000..a8b1de56be
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt
@@ -0,0 +1,9 @@
+HTML.Strict
+TYPE: bool
+VERSION: 1.3.0
+DEFAULT: false
+DEPRECATED-VERSION: 1.7.0
+DEPRECATED-USE: HTML.Doctype
+--DESCRIPTION--
+Determines whether or not to use Transitional (loose) or Strict rulesets.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt
new file mode 100644
index 0000000000..587a16778b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt
@@ -0,0 +1,8 @@
+HTML.TargetBlank
+TYPE: bool
+VERSION: 4.4.0
+DEFAULT: FALSE
+--DESCRIPTION--
+If enabled, target=blank attributes are added to all outgoing links.
+(This includes links from an HTTPS version of a page to an HTTP version.)
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt
new file mode 100644
index 0000000000..dd514c0def
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt
@@ -0,0 +1,10 @@
+--# vim: et sw=4 sts=4
+HTML.TargetNoopener
+TYPE: bool
+VERSION: 4.8.0
+DEFAULT: TRUE
+--DESCRIPTION--
+If enabled, noopener rel attributes are added to links which have
+a target attribute associated with them. This prevents malicious
+destinations from overwriting the original window.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt
new file mode 100644
index 0000000000..cb5a0b0e5e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt
@@ -0,0 +1,9 @@
+HTML.TargetNoreferrer
+TYPE: bool
+VERSION: 4.8.0
+DEFAULT: TRUE
+--DESCRIPTION--
+If enabled, noreferrer rel attributes are added to links which have
+a target attribute associated with them. This prevents malicious
+destinations from overwriting the original window.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt
new file mode 100644
index 0000000000..b4c271b7fa
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt
@@ -0,0 +1,8 @@
+HTML.TidyAdd
+TYPE: lookup
+VERSION: 2.0.0
+DEFAULT: array()
+--DESCRIPTION--
+
+Fixes to add to the default set of Tidy fixes as per your level.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt
new file mode 100644
index 0000000000..4186ccd0d1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt
@@ -0,0 +1,24 @@
+HTML.TidyLevel
+TYPE: string
+VERSION: 2.0.0
+DEFAULT: 'medium'
+--DESCRIPTION--
+
+
General level of cleanliness the Tidy module should enforce.
+There are four allowed values:
+
+
none
+
No extra tidying should be done
+
light
+
Only fix elements that would be discarded otherwise due to
+ lack of support in doctype
+
medium
+
Enforce best practices
+
heavy
+
Transform all deprecated elements and attributes to standards
+ compliant equivalents
+
+
+--ALLOWED--
+'none', 'light', 'medium', 'heavy'
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt
new file mode 100644
index 0000000000..996762bd1d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt
@@ -0,0 +1,8 @@
+HTML.TidyRemove
+TYPE: lookup
+VERSION: 2.0.0
+DEFAULT: array()
+--DESCRIPTION--
+
+Fixes to remove from the default set of Tidy fixes as per your level.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt
new file mode 100644
index 0000000000..1db9237e9e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt
@@ -0,0 +1,9 @@
+HTML.Trusted
+TYPE: bool
+VERSION: 2.0.0
+DEFAULT: false
+--DESCRIPTION--
+Indicates whether or not the user input is trusted or not. If the input is
+trusted, a more expansive set of allowed tags and attributes will be used.
+See also %CSS.Trusted.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt
new file mode 100644
index 0000000000..2a47e384f4
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt
@@ -0,0 +1,11 @@
+HTML.XHTML
+TYPE: bool
+DEFAULT: true
+VERSION: 1.1.0
+DEPRECATED-VERSION: 1.7.0
+DEPRECATED-USE: HTML.Doctype
+--DESCRIPTION--
+Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor.
+--ALIASES--
+Core.XHTML
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt
new file mode 100644
index 0000000000..08921fde70
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt
@@ -0,0 +1,10 @@
+Output.CommentScriptContents
+TYPE: bool
+VERSION: 2.0.0
+DEFAULT: true
+--DESCRIPTION--
+Determines whether or not HTML Purifier should attempt to fix up the
+contents of script tags for legacy browsers with comments.
+--ALIASES--
+Core.CommentScriptContents
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt
new file mode 100644
index 0000000000..d6f0d9f295
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt
@@ -0,0 +1,15 @@
+Output.FixInnerHTML
+TYPE: bool
+VERSION: 4.3.0
+DEFAULT: true
+--DESCRIPTION--
+
+ If true, HTML Purifier will protect against Internet Explorer's
+ mishandling of the innerHTML attribute by appending
+ a space to any attribute that does not contain angled brackets, spaces
+ or quotes, but contains a backtick. This slightly changes the
+ semantics of any given attribute, so if this is unacceptable and
+ you do not use innerHTML on any of your pages, you can
+ turn this directive off.
+
+ If true, HTML Purifier will generate Internet Explorer compatibility
+ code for all object code. This is highly recommended if you enable
+ %HTML.SafeObject.
+
+ Newline string to format final output with. If left null, HTML Purifier
+ will auto-detect the default newline type of the system and use that;
+ you can manually override it here. Remember, \r\n is Windows, \r
+ is Mac, and \n is Unix.
+
+ If true, HTML Purifier will sort attributes by name before writing them back
+ to the document, converting a tag like: <el b="" a="" c="" />
+ to <el a="" b="" c="" />. This is a workaround for
+ a bug in FCKeditor which causes it to swap attributes order, adding noise
+ to text diffs. If you're not seeing this bug, chances are, you don't need
+ this directive.
+
+ Determines whether or not to run Tidy on the final output for pretty
+ formatting reasons, such as indentation and wrap.
+
+
+ This can greatly improve readability for editors who are hand-editing
+ the HTML, but is by no means necessary as HTML Purifier has already
+ fixed all major errors the HTML may have had. Tidy is a non-default
+ extension, and this directive will silently fail if Tidy is not
+ available.
+
+
+ If you are looking to make the overall look of your page's source
+ better, I recommend running Tidy on the entire page rather than just
+ user-content (after all, the indentation relative to the containing
+ blocks will be incorrect).
+
+--ALIASES--
+Core.TidyFormat
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt
new file mode 100644
index 0000000000..071bc0295d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt
@@ -0,0 +1,7 @@
+Test.ForceNoIconv
+TYPE: bool
+DEFAULT: false
+--DESCRIPTION--
+When set to true, HTMLPurifier_Encoder will act as if iconv does not exist
+and use only pure PHP implementations.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt
new file mode 100644
index 0000000000..eb97307e20
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt
@@ -0,0 +1,18 @@
+URI.AllowedSchemes
+TYPE: lookup
+--DEFAULT--
+array (
+ 'http' => true,
+ 'https' => true,
+ 'mailto' => true,
+ 'ftp' => true,
+ 'nntp' => true,
+ 'news' => true,
+ 'tel' => true,
+)
+--DESCRIPTION--
+Whitelist that defines the schemes that a URI is allowed to have. This
+prevents XSS attacks from using pseudo-schemes like javascript or mocha.
+There is also support for the data and file
+URI schemes, but they are not enabled by default.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt
new file mode 100644
index 0000000000..876f0680cf
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.Base.txt
@@ -0,0 +1,17 @@
+URI.Base
+TYPE: string/null
+VERSION: 2.1.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+
+ The base URI is the URI of the document this purified HTML will be
+ inserted into. This information is important if HTML Purifier needs
+ to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute
+ is on. You may use a non-absolute URI for this value, but behavior
+ may vary (%URI.MakeAbsolute deals nicely with both absolute and
+ relative paths, but forwards-compatibility is not guaranteed).
+ Warning: If set, the scheme on this URI
+ overrides the one specified by %URI.DefaultScheme.
+
+ Disables all URIs in all forms. Not sure why you'd want to do that
+ (after all, the Internet's founded on the notion of a hyperlink).
+
+
+--ALIASES--
+Attr.DisableURI
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt
new file mode 100644
index 0000000000..13c122c8ce
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt
@@ -0,0 +1,11 @@
+URI.DisableExternal
+TYPE: bool
+VERSION: 1.2.0
+DEFAULT: false
+--DESCRIPTION--
+Disables links to external websites. This is a highly effective anti-spam
+and anti-pagerank-leech measure, but comes at a hefty price: nolinks or
+images outside of your domain will be allowed. Non-linkified URIs will
+still be preserved. If you want to be able to link to subdomains or use
+absolute URIs, specify %URI.Host for your website.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt
new file mode 100644
index 0000000000..abcc1efd61
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt
@@ -0,0 +1,13 @@
+URI.DisableExternalResources
+TYPE: bool
+VERSION: 1.3.0
+DEFAULT: false
+--DESCRIPTION--
+Disables the embedding of external resources, preventing users from
+embedding things like images from other hosts. This prevents access
+tracking (good for email viewers), bandwidth leeching, cross-site request
+forging, goatse.cx posting, and other nasties, but also results in a loss
+of end-user functionality (they can't directly post a pic they posted from
+Flickr anymore). Use it if you don't have a robust user-content moderation
+team.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt
new file mode 100644
index 0000000000..f891de4996
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt
@@ -0,0 +1,15 @@
+URI.DisableResources
+TYPE: bool
+VERSION: 4.2.0
+DEFAULT: false
+--DESCRIPTION--
+
+ Disables embedding resources, essentially meaning no pictures. You can
+ still link to them though. See %URI.DisableExternalResources for why
+ this might be a good idea.
+
+
+ Note: While this directive has been available since 1.3.0,
+ it didn't actually start doing anything until 4.2.0.
+
+ Defines the domain name of the server, so we can determine whether or
+ an absolute URI is from your website or not. Not strictly necessary,
+ as users should be using relative URIs to reference resources on your
+ website. It will, however, let you use absolute URIs to link to
+ subdomains of the domain you post here: i.e. example.com will allow
+ sub.example.com. However, higher up domains will still be excluded:
+ if you set %URI.Host to sub.example.com, example.com will be blocked.
+ Note: This directive overrides %URI.Base because
+ a given page may be on a sub-domain, but you wish HTML Purifier to be
+ more relaxed and allow some of the parent domains too.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt
new file mode 100644
index 0000000000..0b6df7625d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt
@@ -0,0 +1,9 @@
+URI.HostBlacklist
+TYPE: list
+VERSION: 1.3.0
+DEFAULT: array()
+--DESCRIPTION--
+List of strings that are forbidden in the host of any URI. Use it to kill
+domain names of spam, etc. Note that it will catch anything in the domain,
+so moo.com will catch moo.com.example.com.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt
new file mode 100644
index 0000000000..4214900a59
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt
@@ -0,0 +1,13 @@
+URI.MakeAbsolute
+TYPE: bool
+VERSION: 2.1.0
+DEFAULT: false
+--DESCRIPTION--
+
+
+ Converts all URIs into absolute forms. This is useful when the HTML
+ being filtered assumes a specific base path, but will actually be
+ viewed in a different context (and setting an alternate base URI is
+ not possible). %URI.Base must be set for this directive to work.
+
+ Munges all browsable (usually http, https and ftp)
+ absolute URIs into another URI, usually a URI redirection service.
+ This directive accepts a URI, formatted with a %s where
+ the url-encoded original URI should be inserted (sample:
+ http://www.google.com/url?q=%s).
+
+
+ Uses for this directive:
+
+
+
+ Prevent PageRank leaks, while being fairly transparent
+ to users (you may also want to add some client side JavaScript to
+ override the text in the statusbar). Notice:
+ Many security experts believe that this form of protection does not deter spam-bots.
+
+
+ Redirect users to a splash page telling them they are leaving your
+ website. While this is poor usability practice, it is often mandated
+ in corporate environments.
+
+
+
+ Prior to HTML Purifier 3.1.1, this directive also enabled the munging
+ of browsable external resources, which could break things if your redirection
+ script was a splash page or used meta tags. To revert to
+ previous behavior, please use %URI.MungeResources.
+
+
+ You may want to also use %URI.MungeSecretKey along with this directive
+ in order to enforce what URIs your redirector script allows. Open
+ redirector scripts can be a security risk and negatively affect the
+ reputation of your domain name.
+
+
+ Starting with HTML Purifier 3.1.1, there is also these substitutions:
+
+
+
+
+
Key
+
Description
+
Example <a href="">
+
+
+
+
+
%r
+
1 - The URI embeds a resource (blank) - The URI is merely a link
+
+
+
+
%n
+
The name of the tag this URI came from
+
a
+
+
+
%m
+
The name of the attribute this URI came from
+
href
+
+
+
%p
+
The name of the CSS property this URI came from, or blank if irrelevant
+
+
+
+
+
+ Admittedly, these letters are somewhat arbitrary; the only stipulation
+ was that they couldn't be a through f. r is for resource (I would have preferred
+ e, but you take what you can get), n is for name, m
+ was picked because it came after n (and I couldn't use a), p is for
+ property.
+
+ If true, any URI munging directives like %URI.Munge
+ will also apply to embedded resources, such as <img src="">.
+ Be careful enabling this directive if you have a redirector script
+ that does not use the Location HTTP header; all of your images
+ and other embedded resources will break.
+
+
+ Warning: It is strongly advised you use this in conjunction
+ %URI.MungeSecretKey to mitigate the security risk of an open redirector.
+
+ This directive enables secure checksum generation along with %URI.Munge.
+ It should be set to a secure key that is not shared with anyone else.
+ The checksum can be placed in the URI using %t. Use of this checksum
+ affords an additional level of protection by allowing a redirector
+ to check if a URI has passed through HTML Purifier with this line:
+
+ If the output is TRUE, the redirector script should accept the URI.
+
+
+
+ Please note that it would still be possible for an attacker to procure
+ secure hashes en-mass by abusing your website's Preview feature or the
+ like, but this service affords an additional level of protection
+ that should be combined with website blacklisting.
+
+
+
+ Remember this has no effect if %URI.Munge is not on.
+
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt
new file mode 100644
index 0000000000..23331a4e79
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt
@@ -0,0 +1,9 @@
+URI.OverrideAllowedSchemes
+TYPE: bool
+DEFAULT: true
+--DESCRIPTION--
+If this is set to true (which it is by default), you can override
+%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the
+registry. If false, you will also have to update that directive in order
+to add more schemes.
+--# vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt
new file mode 100644
index 0000000000..79084832be
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt
@@ -0,0 +1,22 @@
+URI.SafeIframeRegexp
+TYPE: string/null
+VERSION: 4.4.0
+DEFAULT: NULL
+--DESCRIPTION--
+
+ A PCRE regular expression that will be matched against an iframe URI. This is
+ a relatively inflexible scheme, but works well enough for the most common
+ use-case of iframes: embedded video. This directive only has an effect if
+ %HTML.SafeIframe is enabled. Here are some example values:
+
%^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
+
+
+ Note that this directive does not give you enough granularity to, say, disable
+ all autoplay videos. Pipe up on the HTML Purifier forums if this
+ is a capability you want.
+
';
+ // W3C uses an icon to indicate the severity of the error.
+ $error = $this->locale->getErrorName($severity);
+ $string .= "$error ";
+ if (!is_null($line) && !is_null($col)) {
+ $string .= "Line $line, Column $col: ";
+ } else {
+ $string .= 'End of Document: ';
+ }
+ $string .= '' . $this->generator->escape($msg) . ' ';
+ $string .= '
';
+ // Here, have a marker for the character on the column appropriate.
+ // Be sure to clip extremely long lines.
+ //$string .= '
';
+ //$string .= '';
+ //$string .= '
';
+ $ret[] = $string;
+ }
+ foreach ($current->children as $array) {
+ $context[] = $current;
+ $stack = array_merge($stack, array_reverse($array, true));
+ for ($i = count($array); $i > 0; $i--) {
+ $context_stack[] = $context;
+ }
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php
new file mode 100644
index 0000000000..cf869d3212
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/ErrorStruct.php
@@ -0,0 +1,74 @@
+children[$type][$id])) {
+ $this->children[$type][$id] = new HTMLPurifier_ErrorStruct();
+ $this->children[$type][$id]->type = $type;
+ }
+ return $this->children[$type][$id];
+ }
+
+ /**
+ * @param int $severity
+ * @param string $message
+ */
+ public function addError($severity, $message)
+ {
+ $this->errors[] = array($severity, $message);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php
new file mode 100644
index 0000000000..be85b4c560
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Exception.php
@@ -0,0 +1,12 @@
+preFilter,
+ * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter,
+ * 1->postFilter.
+ *
+ * @note Methods are not declared abstract as it is perfectly legitimate
+ * for an implementation not to want anything to happen on a step
+ */
+
+class HTMLPurifier_Filter
+{
+
+ /**
+ * Name of the filter for identification purposes.
+ * @type string
+ */
+ public $name;
+
+ /**
+ * Pre-processor function, handles HTML before HTML Purifier
+ * @param string $html
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return string
+ */
+ public function preFilter($html, $config, $context)
+ {
+ return $html;
+ }
+
+ /**
+ * Post-processor function, handles HTML after HTML Purifier
+ * @param string $html
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return string
+ */
+ public function postFilter($html, $config, $context)
+ {
+ return $html;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php
new file mode 100644
index 0000000000..e7e3cac1ad
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/ExtractStyleBlocks.php
@@ -0,0 +1,362 @@
+ blocks from input HTML, cleans them up
+ * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks')
+ * so they can be used elsewhere in the document.
+ *
+ * @note
+ * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for
+ * sample usage.
+ *
+ * @note
+ * This filter can also be used on stylesheets not included in the
+ * document--something purists would probably prefer. Just directly
+ * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS()
+ */
+class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter
+{
+ /**
+ * @type string
+ */
+ public $name = 'ExtractStyleBlocks';
+
+ /**
+ * @type array
+ */
+ private $_styleMatches = array();
+
+ /**
+ * @type csstidy
+ */
+ private $_tidy;
+
+ /**
+ * @type HTMLPurifier_AttrDef_HTML_ID
+ */
+ private $_id_attrdef;
+
+ /**
+ * @type HTMLPurifier_AttrDef_CSS_Ident
+ */
+ private $_class_attrdef;
+
+ /**
+ * @type HTMLPurifier_AttrDef_Enum
+ */
+ private $_enum_attrdef;
+
+ /**
+ * @type HTMLPurifier_AttrDef_Enum
+ */
+ private $_universal_attrdef;
+
+ public function __construct()
+ {
+ $this->_tidy = new csstidy();
+ $this->_tidy->set_cfg('lowercase_s', false);
+ $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true);
+ $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident();
+ $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum(
+ array(
+ 'first-child',
+ 'link',
+ 'visited',
+ 'active',
+ 'hover',
+ 'focus'
+ )
+ );
+ $this->_universal_attrdef = new HTMLPurifier_AttrDef_Enum(
+ array(
+ 'initial',
+ 'inherit',
+ 'unset',
+ )
+ );
+ }
+
+ /**
+ * Save the contents of CSS blocks to style matches
+ * @param array $matches preg_replace style $matches array
+ */
+ protected function styleCallback($matches)
+ {
+ $this->_styleMatches[] = $matches[1];
+ }
+
+ /**
+ * Removes inline
+ // we must not grab foo in a font-family prop).
+ if ($config->get('Filter.ExtractStyleBlocks.Escaping')) {
+ $css = str_replace(
+ array('<', '>', '&'),
+ array('\3C ', '\3E ', '\26 '),
+ $css
+ );
+ }
+ return $css;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php
new file mode 100644
index 0000000000..276d8362fa
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Filter/YouTube.php
@@ -0,0 +1,65 @@
+]+>.+?' .
+ '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s';
+ $pre_replace = '\1';
+ return preg_replace($pre_regex, $pre_replace, $html);
+ }
+
+ /**
+ * @param string $html
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return string
+ */
+ public function postFilter($html, $config, $context)
+ {
+ $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#';
+ return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html);
+ }
+
+ /**
+ * @param $url
+ * @return string
+ */
+ protected function armorUrl($url)
+ {
+ return str_replace('--', '--', $url);
+ }
+
+ /**
+ * @param array $matches
+ * @return string
+ */
+ protected function postFilterCallback($matches)
+ {
+ $url = $this->armorUrl($matches[1]);
+ return '';
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php
new file mode 100644
index 0000000000..eb56e2dfa2
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Generator.php
@@ -0,0 +1,286 @@
+ tags.
+ * @type bool
+ */
+ private $_scriptFix = false;
+
+ /**
+ * Cache of HTMLDefinition during HTML output to determine whether or
+ * not attributes should be minimized.
+ * @type HTMLPurifier_HTMLDefinition
+ */
+ private $_def;
+
+ /**
+ * Cache of %Output.SortAttr.
+ * @type bool
+ */
+ private $_sortAttr;
+
+ /**
+ * Cache of %Output.FlashCompat.
+ * @type bool
+ */
+ private $_flashCompat;
+
+ /**
+ * Cache of %Output.FixInnerHTML.
+ * @type bool
+ */
+ private $_innerHTMLFix;
+
+ /**
+ * Stack for keeping track of object information when outputting IE
+ * compatibility code.
+ * @type array
+ */
+ private $_flashStack = array();
+
+ /**
+ * Configuration for the generator
+ * @type HTMLPurifier_Config
+ */
+ protected $config;
+
+ /**
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ */
+ public function __construct($config, $context)
+ {
+ $this->config = $config;
+ $this->_scriptFix = $config->get('Output.CommentScriptContents');
+ $this->_innerHTMLFix = $config->get('Output.FixInnerHTML');
+ $this->_sortAttr = $config->get('Output.SortAttr');
+ $this->_flashCompat = $config->get('Output.FlashCompat');
+ $this->_def = $config->getHTMLDefinition();
+ $this->_xhtml = $this->_def->doctype->xml;
+ }
+
+ /**
+ * Generates HTML from an array of tokens.
+ * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token
+ * @return string Generated HTML
+ */
+ public function generateFromTokens($tokens)
+ {
+ if (!$tokens) {
+ return '';
+ }
+
+ // Basic algorithm
+ $html = '';
+ for ($i = 0, $size = count($tokens); $i < $size; $i++) {
+ if ($this->_scriptFix && $tokens[$i]->name === 'script'
+ && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) {
+ // script special case
+ // the contents of the script block must be ONE token
+ // for this to work.
+ $html .= $this->generateFromToken($tokens[$i++]);
+ $html .= $this->generateScriptFromToken($tokens[$i++]);
+ }
+ $html .= $this->generateFromToken($tokens[$i]);
+ }
+
+ // Tidy cleanup
+ if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) {
+ $tidy = new Tidy;
+ $tidy->parseString(
+ $html,
+ array(
+ 'indent'=> true,
+ 'output-xhtml' => $this->_xhtml,
+ 'show-body-only' => true,
+ 'indent-spaces' => 2,
+ 'wrap' => 68,
+ ),
+ 'utf8'
+ );
+ $tidy->cleanRepair();
+ $html = (string) $tidy; // explicit cast necessary
+ }
+
+ // Normalize newlines to system defined value
+ if ($this->config->get('Core.NormalizeNewlines')) {
+ $nl = $this->config->get('Output.Newline');
+ if ($nl === null) {
+ $nl = PHP_EOL;
+ }
+ if ($nl !== "\n") {
+ $html = str_replace("\n", $nl, $html);
+ }
+ }
+ return $html;
+ }
+
+ /**
+ * Generates HTML from a single token.
+ * @param HTMLPurifier_Token $token HTMLPurifier_Token object.
+ * @return string Generated HTML
+ */
+ public function generateFromToken($token)
+ {
+ if (!$token instanceof HTMLPurifier_Token) {
+ trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING);
+ return '';
+
+ } elseif ($token instanceof HTMLPurifier_Token_Start) {
+ $attr = $this->generateAttributes($token->attr, $token->name);
+ if ($this->_flashCompat) {
+ if ($token->name == "object") {
+ $flash = new stdClass();
+ $flash->attr = $token->attr;
+ $flash->param = array();
+ $this->_flashStack[] = $flash;
+ }
+ }
+ return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>';
+
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ $_extra = '';
+ if ($this->_flashCompat) {
+ if ($token->name == "object" && !empty($this->_flashStack)) {
+ // doesn't do anything for now
+ }
+ }
+ return $_extra . '' . $token->name . '>';
+
+ } elseif ($token instanceof HTMLPurifier_Token_Empty) {
+ if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) {
+ $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value'];
+ }
+ $attr = $this->generateAttributes($token->attr, $token->name);
+ return '<' . $token->name . ($attr ? ' ' : '') . $attr .
+ ( $this->_xhtml ? ' /': '' ) // v.
+ . '>';
+
+ } elseif ($token instanceof HTMLPurifier_Token_Text) {
+ return $this->escape($token->data, ENT_NOQUOTES);
+
+ } elseif ($token instanceof HTMLPurifier_Token_Comment) {
+ return '';
+ } else {
+ return '';
+
+ }
+ }
+
+ /**
+ * Special case processor for the contents of script tags
+ * @param HTMLPurifier_Token $token HTMLPurifier_Token object.
+ * @return string
+ * @warning This runs into problems if there's already a literal
+ * --> somewhere inside the script contents.
+ */
+ public function generateScriptFromToken($token)
+ {
+ if (!$token instanceof HTMLPurifier_Token_Text) {
+ return $this->generateFromToken($token);
+ }
+ // Thanks
+ $data = preg_replace('#//\s*$#', '', $token->data);
+ return '';
+ }
+
+ /**
+ * Generates attribute declarations from attribute array.
+ * @note This does not include the leading or trailing space.
+ * @param array $assoc_array_of_attributes Attribute array
+ * @param string $element Name of element attributes are for, used to check
+ * attribute minimization.
+ * @return string Generated HTML fragment for insertion.
+ */
+ public function generateAttributes($assoc_array_of_attributes, $element = '')
+ {
+ $html = '';
+ if ($this->_sortAttr) {
+ ksort($assoc_array_of_attributes);
+ }
+ foreach ($assoc_array_of_attributes as $key => $value) {
+ if (!$this->_xhtml) {
+ // Remove namespaced attributes
+ if (strpos($key, ':') !== false) {
+ continue;
+ }
+ // Check if we should minimize the attribute: val="val" -> val
+ if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) {
+ $html .= $key . ' ';
+ continue;
+ }
+ }
+ // Workaround for Internet Explorer innerHTML bug.
+ // Essentially, Internet Explorer, when calculating
+ // innerHTML, omits quotes if there are no instances of
+ // angled brackets, quotes or spaces. However, when parsing
+ // HTML (for example, when you assign to innerHTML), it
+ // treats backticks as quotes. Thus,
+ //
+ // becomes
+ //
+ // becomes
+ //
+ // Fortunately, all we need to do is trigger an appropriate
+ // quoting style, which we do by adding an extra space.
+ // This also is consistent with the W3C spec, which states
+ // that user agents may ignore leading or trailing
+ // whitespace (in fact, most don't, at least for attributes
+ // like alt, but an extra space at the end is barely
+ // noticeable). Still, we have a configuration knob for
+ // this, since this transformation is not necesary if you
+ // don't process user input with innerHTML or you don't plan
+ // on supporting Internet Explorer.
+ if ($this->_innerHTMLFix) {
+ if (strpos($value, '`') !== false) {
+ // check if correct quoting style would not already be
+ // triggered
+ if (strcspn($value, '"\' <>') === strlen($value)) {
+ // protect!
+ $value .= ' ';
+ }
+ }
+ }
+ $html .= $key.'="'.$this->escape($value).'" ';
+ }
+ return rtrim($html);
+ }
+
+ /**
+ * Escapes raw text data.
+ * @todo This really ought to be protected, but until we have a facility
+ * for properly generating HTML here w/o using tokens, it stays
+ * public.
+ * @param string $string String data to escape for HTML.
+ * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is
+ * permissible for non-attribute output.
+ * @return string escaped data.
+ */
+ public function escape($string, $quote = null)
+ {
+ // Workaround for APC bug on Mac Leopard reported by sidepodcast
+ // http://htmlpurifier.org/phorum/read.php?3,4823,4846
+ if ($quote === null) {
+ $quote = ENT_COMPAT;
+ }
+ return htmlspecialchars($string, $quote, 'UTF-8');
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php
new file mode 100644
index 0000000000..9b7b334dd9
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLDefinition.php
@@ -0,0 +1,493 @@
+getAnonymousModule();
+ if (!isset($module->info[$element_name])) {
+ $element = $module->addBlankElement($element_name);
+ } else {
+ $element = $module->info[$element_name];
+ }
+ $element->attr[$attr_name] = $def;
+ }
+
+ /**
+ * Adds a custom element to your HTML definition
+ * @see HTMLPurifier_HTMLModule::addElement() for detailed
+ * parameter and return value descriptions.
+ */
+ public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array())
+ {
+ $module = $this->getAnonymousModule();
+ // assume that if the user is calling this, the element
+ // is safe. This may not be a good idea
+ $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes);
+ return $element;
+ }
+
+ /**
+ * Adds a blank element to your HTML definition, for overriding
+ * existing behavior
+ * @param string $element_name
+ * @return HTMLPurifier_ElementDef
+ * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed
+ * parameter and return value descriptions.
+ */
+ public function addBlankElement($element_name)
+ {
+ $module = $this->getAnonymousModule();
+ $element = $module->addBlankElement($element_name);
+ return $element;
+ }
+
+ /**
+ * Retrieves a reference to the anonymous module, so you can
+ * bust out advanced features without having to make your own
+ * module.
+ * @return HTMLPurifier_HTMLModule
+ */
+ public function getAnonymousModule()
+ {
+ if (!$this->_anonModule) {
+ $this->_anonModule = new HTMLPurifier_HTMLModule();
+ $this->_anonModule->name = 'Anonymous';
+ }
+ return $this->_anonModule;
+ }
+
+ private $_anonModule = null;
+
+ // PUBLIC BUT INTERNAL VARIABLES --------------------------------------
+
+ /**
+ * @type string
+ */
+ public $type = 'HTML';
+
+ /**
+ * @type HTMLPurifier_HTMLModuleManager
+ */
+ public $manager;
+
+ /**
+ * Performs low-cost, preliminary initialization.
+ */
+ public function __construct()
+ {
+ $this->manager = new HTMLPurifier_HTMLModuleManager();
+ }
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ protected function doSetup($config)
+ {
+ $this->processModules($config);
+ $this->setupConfigStuff($config);
+ unset($this->manager);
+
+ // cleanup some of the element definitions
+ foreach ($this->info as $k => $v) {
+ unset($this->info[$k]->content_model);
+ unset($this->info[$k]->content_model_type);
+ }
+ }
+
+ /**
+ * Extract out the information from the manager
+ * @param HTMLPurifier_Config $config
+ */
+ protected function processModules($config)
+ {
+ if ($this->_anonModule) {
+ // for user specific changes
+ // this is late-loaded so we don't have to deal with PHP4
+ // reference wonky-ness
+ $this->manager->addModule($this->_anonModule);
+ unset($this->_anonModule);
+ }
+
+ $this->manager->setup($config);
+ $this->doctype = $this->manager->doctype;
+
+ foreach ($this->manager->modules as $module) {
+ foreach ($module->info_tag_transform as $k => $v) {
+ if ($v === false) {
+ unset($this->info_tag_transform[$k]);
+ } else {
+ $this->info_tag_transform[$k] = $v;
+ }
+ }
+ foreach ($module->info_attr_transform_pre as $k => $v) {
+ if ($v === false) {
+ unset($this->info_attr_transform_pre[$k]);
+ } else {
+ $this->info_attr_transform_pre[$k] = $v;
+ }
+ }
+ foreach ($module->info_attr_transform_post as $k => $v) {
+ if ($v === false) {
+ unset($this->info_attr_transform_post[$k]);
+ } else {
+ $this->info_attr_transform_post[$k] = $v;
+ }
+ }
+ foreach ($module->info_injector as $k => $v) {
+ if ($v === false) {
+ unset($this->info_injector[$k]);
+ } else {
+ $this->info_injector[$k] = $v;
+ }
+ }
+ }
+ $this->info = $this->manager->getElements();
+ $this->info_content_sets = $this->manager->contentSets->lookup;
+ }
+
+ /**
+ * Sets up stuff based on config. We need a better way of doing this.
+ * @param HTMLPurifier_Config $config
+ */
+ protected function setupConfigStuff($config)
+ {
+ $block_wrapper = $config->get('HTML.BlockWrapper');
+ if (isset($this->info_content_sets['Block'][$block_wrapper])) {
+ $this->info_block_wrapper = $block_wrapper;
+ } else {
+ trigger_error(
+ 'Cannot use non-block element as block wrapper',
+ E_USER_ERROR
+ );
+ }
+
+ $parent = $config->get('HTML.Parent');
+ $def = $this->manager->getElement($parent, true);
+ if ($def) {
+ $this->info_parent = $parent;
+ $this->info_parent_def = $def;
+ } else {
+ trigger_error(
+ 'Cannot use unrecognized element as parent',
+ E_USER_ERROR
+ );
+ $this->info_parent_def = $this->manager->getElement($this->info_parent, true);
+ }
+
+ // support template text
+ $support = "(for information on implementing this, see the support forums) ";
+
+ // setup allowed elements -----------------------------------------
+
+ $allowed_elements = $config->get('HTML.AllowedElements');
+ $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early
+
+ if (!is_array($allowed_elements) && !is_array($allowed_attributes)) {
+ $allowed = $config->get('HTML.Allowed');
+ if (is_string($allowed)) {
+ list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed);
+ }
+ }
+
+ if (is_array($allowed_elements)) {
+ foreach ($this->info as $name => $d) {
+ if (!isset($allowed_elements[$name])) {
+ unset($this->info[$name]);
+ }
+ unset($allowed_elements[$name]);
+ }
+ // emit errors
+ foreach ($allowed_elements as $element => $d) {
+ $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful!
+ trigger_error("Element '$element' is not supported $support", E_USER_WARNING);
+ }
+ }
+
+ // setup allowed attributes ---------------------------------------
+
+ $allowed_attributes_mutable = $allowed_attributes; // by copy!
+ if (is_array($allowed_attributes)) {
+ // This actually doesn't do anything, since we went away from
+ // global attributes. It's possible that userland code uses
+ // it, but HTMLModuleManager doesn't!
+ foreach ($this->info_global_attr as $attr => $x) {
+ $keys = array($attr, "*@$attr", "*.$attr");
+ $delete = true;
+ foreach ($keys as $key) {
+ if ($delete && isset($allowed_attributes[$key])) {
+ $delete = false;
+ }
+ if (isset($allowed_attributes_mutable[$key])) {
+ unset($allowed_attributes_mutable[$key]);
+ }
+ }
+ if ($delete) {
+ unset($this->info_global_attr[$attr]);
+ }
+ }
+
+ foreach ($this->info as $tag => $info) {
+ foreach ($info->attr as $attr => $x) {
+ $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr");
+ $delete = true;
+ foreach ($keys as $key) {
+ if ($delete && isset($allowed_attributes[$key])) {
+ $delete = false;
+ }
+ if (isset($allowed_attributes_mutable[$key])) {
+ unset($allowed_attributes_mutable[$key]);
+ }
+ }
+ if ($delete) {
+ if ($this->info[$tag]->attr[$attr]->required) {
+ trigger_error(
+ "Required attribute '$attr' in element '$tag' " .
+ "was not allowed, which means '$tag' will not be allowed either",
+ E_USER_WARNING
+ );
+ }
+ unset($this->info[$tag]->attr[$attr]);
+ }
+ }
+ }
+ // emit errors
+ foreach ($allowed_attributes_mutable as $elattr => $d) {
+ $bits = preg_split('/[.@]/', $elattr, 2);
+ $c = count($bits);
+ switch ($c) {
+ case 2:
+ if ($bits[0] !== '*') {
+ $element = htmlspecialchars($bits[0]);
+ $attribute = htmlspecialchars($bits[1]);
+ if (!isset($this->info[$element])) {
+ trigger_error(
+ "Cannot allow attribute '$attribute' if element " .
+ "'$element' is not allowed/supported $support"
+ );
+ } else {
+ trigger_error(
+ "Attribute '$attribute' in element '$element' not supported $support",
+ E_USER_WARNING
+ );
+ }
+ break;
+ }
+ // otherwise fall through
+ case 1:
+ $attribute = htmlspecialchars($bits[0]);
+ trigger_error(
+ "Global attribute '$attribute' is not ".
+ "supported in any elements $support",
+ E_USER_WARNING
+ );
+ break;
+ }
+ }
+ }
+
+ // setup forbidden elements ---------------------------------------
+
+ $forbidden_elements = $config->get('HTML.ForbiddenElements');
+ $forbidden_attributes = $config->get('HTML.ForbiddenAttributes');
+
+ foreach ($this->info as $tag => $info) {
+ if (isset($forbidden_elements[$tag])) {
+ unset($this->info[$tag]);
+ continue;
+ }
+ foreach ($info->attr as $attr => $x) {
+ if (isset($forbidden_attributes["$tag@$attr"]) ||
+ isset($forbidden_attributes["*@$attr"]) ||
+ isset($forbidden_attributes[$attr])
+ ) {
+ unset($this->info[$tag]->attr[$attr]);
+ continue;
+ } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually
+ // $tag.$attr are not user supplied, so no worries!
+ trigger_error(
+ "Error with $tag.$attr: tag.attr syntax not supported for " .
+ "HTML.ForbiddenAttributes; use tag@attr instead",
+ E_USER_WARNING
+ );
+ }
+ }
+ }
+ foreach ($forbidden_attributes as $key => $v) {
+ if (strlen($key) < 2) {
+ continue;
+ }
+ if ($key[0] != '*') {
+ continue;
+ }
+ if ($key[1] == '.') {
+ trigger_error(
+ "Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead",
+ E_USER_WARNING
+ );
+ }
+ }
+
+ // setup injectors -----------------------------------------------------
+ foreach ($this->info_injector as $i => $injector) {
+ if ($injector->checkNeeded($config) !== false) {
+ // remove injector that does not have it's required
+ // elements/attributes present, and is thus not needed.
+ unset($this->info_injector[$i]);
+ }
+ }
+ }
+
+ /**
+ * Parses a TinyMCE-flavored Allowed Elements and Attributes list into
+ * separate lists for processing. Format is element[attr1|attr2],element2...
+ * @warning Although it's largely drawn from TinyMCE's implementation,
+ * it is different, and you'll probably have to modify your lists
+ * @param array $list String list to parse
+ * @return array
+ * @todo Give this its own class, probably static interface
+ */
+ public function parseTinyMCEAllowedList($list)
+ {
+ $list = str_replace(array(' ', "\t"), '', $list);
+
+ $elements = array();
+ $attributes = array();
+
+ $chunks = preg_split('/(,|[\n\r]+)/', $list);
+ foreach ($chunks as $chunk) {
+ if (empty($chunk)) {
+ continue;
+ }
+ // remove TinyMCE element control characters
+ if (!strpos($chunk, '[')) {
+ $element = $chunk;
+ $attr = false;
+ } else {
+ list($element, $attr) = explode('[', $chunk);
+ }
+ if ($element !== '*') {
+ $elements[$element] = true;
+ }
+ if (!$attr) {
+ continue;
+ }
+ $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ]
+ $attr = explode('|', $attr);
+ foreach ($attr as $key) {
+ $attributes["$element.$key"] = true;
+ }
+ }
+ return array($elements, $attributes);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php
new file mode 100644
index 0000000000..9dbb987290
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule.php
@@ -0,0 +1,285 @@
+info, since the object's data is only info,
+ * with extra behavior associated with it.
+ * @type array
+ */
+ public $attr_collections = array();
+
+ /**
+ * Associative array of deprecated tag name to HTMLPurifier_TagTransform.
+ * @type array
+ */
+ public $info_tag_transform = array();
+
+ /**
+ * List of HTMLPurifier_AttrTransform to be performed before validation.
+ * @type array
+ */
+ public $info_attr_transform_pre = array();
+
+ /**
+ * List of HTMLPurifier_AttrTransform to be performed after validation.
+ * @type array
+ */
+ public $info_attr_transform_post = array();
+
+ /**
+ * List of HTMLPurifier_Injector to be performed during well-formedness fixing.
+ * An injector will only be invoked if all of it's pre-requisites are met;
+ * if an injector fails setup, there will be no error; it will simply be
+ * silently disabled.
+ * @type array
+ */
+ public $info_injector = array();
+
+ /**
+ * Boolean flag that indicates whether or not getChildDef is implemented.
+ * For optimization reasons: may save a call to a function. Be sure
+ * to set it if you do implement getChildDef(), otherwise it will have
+ * no effect!
+ * @type bool
+ */
+ public $defines_child_def = false;
+
+ /**
+ * Boolean flag whether or not this module is safe. If it is not safe, all
+ * of its members are unsafe. Modules are safe by default (this might be
+ * slightly dangerous, but it doesn't make much sense to force HTML Purifier,
+ * which is based off of safe HTML, to explicitly say, "This is safe," even
+ * though there are modules which are "unsafe")
+ *
+ * @type bool
+ * @note Previously, safety could be applied at an element level granularity.
+ * We've removed this ability, so in order to add "unsafe" elements
+ * or attributes, a dedicated module with this property set to false
+ * must be used.
+ */
+ public $safe = true;
+
+ /**
+ * Retrieves a proper HTMLPurifier_ChildDef subclass based on
+ * content_model and content_model_type member variables of
+ * the HTMLPurifier_ElementDef class. There is a similar function
+ * in HTMLPurifier_HTMLDefinition.
+ * @param HTMLPurifier_ElementDef $def
+ * @return HTMLPurifier_ChildDef subclass
+ */
+ public function getChildDef($def)
+ {
+ return false;
+ }
+
+ // -- Convenience -----------------------------------------------------
+
+ /**
+ * Convenience function that sets up a new element
+ * @param string $element Name of element to add
+ * @param string|bool $type What content set should element be registered to?
+ * Set as false to skip this step.
+ * @param string|HTMLPurifier_ChildDef $contents Allowed children in form of:
+ * "$content_model_type: $content_model"
+ * @param array|string $attr_includes What attribute collections to register to
+ * element?
+ * @param array $attr What unique attributes does the element define?
+ * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters.
+ * @return HTMLPurifier_ElementDef Created element definition object, so you
+ * can set advanced parameters
+ */
+ public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array())
+ {
+ $this->elements[] = $element;
+ // parse content_model
+ list($content_model_type, $content_model) = $this->parseContents($contents);
+ // merge in attribute inclusions
+ $this->mergeInAttrIncludes($attr, $attr_includes);
+ // add element to content sets
+ if ($type) {
+ $this->addElementToContentSet($element, $type);
+ }
+ // create element
+ $this->info[$element] = HTMLPurifier_ElementDef::create(
+ $content_model,
+ $content_model_type,
+ $attr
+ );
+ // literal object $contents means direct child manipulation
+ if (!is_string($contents)) {
+ $this->info[$element]->child = $contents;
+ }
+ return $this->info[$element];
+ }
+
+ /**
+ * Convenience function that creates a totally blank, non-standalone
+ * element.
+ * @param string $element Name of element to create
+ * @return HTMLPurifier_ElementDef Created element
+ */
+ public function addBlankElement($element)
+ {
+ if (!isset($this->info[$element])) {
+ $this->elements[] = $element;
+ $this->info[$element] = new HTMLPurifier_ElementDef();
+ $this->info[$element]->standalone = false;
+ } else {
+ trigger_error("Definition for $element already exists in module, cannot redefine");
+ }
+ return $this->info[$element];
+ }
+
+ /**
+ * Convenience function that registers an element to a content set
+ * @param string $element Element to register
+ * @param string $type Name content set (warning: case sensitive, usually upper-case
+ * first letter)
+ */
+ public function addElementToContentSet($element, $type)
+ {
+ if (!isset($this->content_sets[$type])) {
+ $this->content_sets[$type] = '';
+ } else {
+ $this->content_sets[$type] .= ' | ';
+ }
+ $this->content_sets[$type] .= $element;
+ }
+
+ /**
+ * Convenience function that transforms single-string contents
+ * into separate content model and content model type
+ * @param string $contents Allowed children in form of:
+ * "$content_model_type: $content_model"
+ * @return array
+ * @note If contents is an object, an array of two nulls will be
+ * returned, and the callee needs to take the original $contents
+ * and use it directly.
+ */
+ public function parseContents($contents)
+ {
+ if (!is_string($contents)) {
+ return array(null, null);
+ } // defer
+ switch ($contents) {
+ // check for shorthand content model forms
+ case 'Empty':
+ return array('empty', '');
+ case 'Inline':
+ return array('optional', 'Inline | #PCDATA');
+ case 'Flow':
+ return array('optional', 'Flow | #PCDATA');
+ }
+ list($content_model_type, $content_model) = explode(':', $contents);
+ $content_model_type = strtolower(trim($content_model_type));
+ $content_model = trim($content_model);
+ return array($content_model_type, $content_model);
+ }
+
+ /**
+ * Convenience function that merges a list of attribute includes into
+ * an attribute array.
+ * @param array $attr Reference to attr array to modify
+ * @param array $attr_includes Array of includes / string include to merge in
+ */
+ public function mergeInAttrIncludes(&$attr, $attr_includes)
+ {
+ if (!is_array($attr_includes)) {
+ if (empty($attr_includes)) {
+ $attr_includes = array();
+ } else {
+ $attr_includes = array($attr_includes);
+ }
+ }
+ $attr[0] = $attr_includes;
+ }
+
+ /**
+ * Convenience function that generates a lookup table with boolean
+ * true as value.
+ * @param string $list List of values to turn into a lookup
+ * @note You can also pass an arbitrary number of arguments in
+ * place of the regular argument
+ * @return array array equivalent of list
+ */
+ public function makeLookup($list)
+ {
+ $args = func_get_args();
+ if (is_string($list)) {
+ $list = $args;
+ }
+ $ret = array();
+ foreach ($list as $value) {
+ if (is_null($value)) {
+ continue;
+ }
+ $ret[$value] = true;
+ }
+ return $ret;
+ }
+
+ /**
+ * Lazy load construction of the module after determining whether
+ * or not it's needed, and also when a finalized configuration object
+ * is available.
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php
new file mode 100644
index 0000000000..1e67c790d0
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Bdo.php
@@ -0,0 +1,44 @@
+ array('dir' => false)
+ );
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ $bdo = $this->addElement(
+ 'bdo',
+ 'Inline',
+ 'Inline',
+ array('Core', 'Lang'),
+ array(
+ 'dir' => 'Enum#ltr,rtl', // required
+ // The Abstract Module specification has the attribute
+ // inclusions wrong for bdo: bdo allows Lang
+ )
+ );
+ $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir();
+
+ $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl';
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php
new file mode 100644
index 0000000000..7220c14cc7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/CommonAttributes.php
@@ -0,0 +1,32 @@
+ array(
+ 0 => array('Style'),
+ // 'xml:space' => false,
+ 'class' => 'Class',
+ 'id' => 'ID',
+ 'title' => 'CDATA',
+ 'contenteditable' => 'ContentEditable',
+ ),
+ 'Lang' => array(),
+ 'I18N' => array(
+ 0 => array('Lang'), // proprietary, for xml:lang/lang
+ ),
+ 'Common' => array(
+ 0 => array('Core', 'I18N')
+ )
+ );
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php
new file mode 100644
index 0000000000..a9042a3577
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Edit.php
@@ -0,0 +1,55 @@
+ 'URI',
+ // 'datetime' => 'Datetime', // not implemented
+ );
+ $this->addElement('del', 'Inline', $contents, 'Common', $attr);
+ $this->addElement('ins', 'Inline', $contents, 'Common', $attr);
+ }
+
+ // HTML 4.01 specifies that ins/del must not contain block
+ // elements when used in an inline context, chameleon is
+ // a complicated workaround to acheive this effect
+
+ // Inline context ! Block context (exclamation mark is
+ // separator, see getChildDef for parsing)
+
+ /**
+ * @type bool
+ */
+ public $defines_child_def = true;
+
+ /**
+ * @param HTMLPurifier_ElementDef $def
+ * @return HTMLPurifier_ChildDef_Chameleon
+ */
+ public function getChildDef($def)
+ {
+ if ($def->content_model_type != 'chameleon') {
+ return false;
+ }
+ $value = explode('!', $def->content_model);
+ return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php
new file mode 100644
index 0000000000..eb0edcffd5
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Forms.php
@@ -0,0 +1,194 @@
+ 'Form',
+ 'Inline' => 'Formctrl',
+ );
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ if ($config->get('HTML.Forms')) {
+ $this->safe = true;
+ }
+
+ $form = $this->addElement(
+ 'form',
+ 'Form',
+ 'Required: Heading | List | Block | fieldset',
+ 'Common',
+ array(
+ 'accept' => 'ContentTypes',
+ 'accept-charset' => 'Charsets',
+ 'action*' => 'URI',
+ 'method' => 'Enum#get,post',
+ // really ContentType, but these two are the only ones used today
+ 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data',
+ )
+ );
+ $form->excludes = array('form' => true);
+
+ $input = $this->addElement(
+ 'input',
+ 'Formctrl',
+ 'Empty',
+ 'Common',
+ array(
+ 'accept' => 'ContentTypes',
+ 'accesskey' => 'Character',
+ 'alt' => 'Text',
+ 'checked' => 'Bool#checked',
+ 'disabled' => 'Bool#disabled',
+ 'maxlength' => 'Number',
+ 'name' => 'CDATA',
+ 'readonly' => 'Bool#readonly',
+ 'size' => 'Number',
+ 'src' => 'URI#embedded',
+ 'tabindex' => 'Number',
+ 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image',
+ 'value' => 'CDATA',
+ )
+ );
+ $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input();
+
+ $this->addElement(
+ 'select',
+ 'Formctrl',
+ 'Required: optgroup | option',
+ 'Common',
+ array(
+ 'disabled' => 'Bool#disabled',
+ 'multiple' => 'Bool#multiple',
+ 'name' => 'CDATA',
+ 'size' => 'Number',
+ 'tabindex' => 'Number',
+ )
+ );
+
+ $this->addElement(
+ 'option',
+ false,
+ 'Optional: #PCDATA',
+ 'Common',
+ array(
+ 'disabled' => 'Bool#disabled',
+ 'label' => 'Text',
+ 'selected' => 'Bool#selected',
+ 'value' => 'CDATA',
+ )
+ );
+ // It's illegal for there to be more than one selected, but not
+ // be multiple. Also, no selected means undefined behavior. This might
+ // be difficult to implement; perhaps an injector, or a context variable.
+
+ $textarea = $this->addElement(
+ 'textarea',
+ 'Formctrl',
+ 'Optional: #PCDATA',
+ 'Common',
+ array(
+ 'accesskey' => 'Character',
+ 'cols*' => 'Number',
+ 'disabled' => 'Bool#disabled',
+ 'name' => 'CDATA',
+ 'readonly' => 'Bool#readonly',
+ 'rows*' => 'Number',
+ 'tabindex' => 'Number',
+ )
+ );
+ $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea();
+
+ $button = $this->addElement(
+ 'button',
+ 'Formctrl',
+ 'Optional: #PCDATA | Heading | List | Block | Inline',
+ 'Common',
+ array(
+ 'accesskey' => 'Character',
+ 'disabled' => 'Bool#disabled',
+ 'name' => 'CDATA',
+ 'tabindex' => 'Number',
+ 'type' => 'Enum#button,submit,reset',
+ 'value' => 'CDATA',
+ )
+ );
+
+ // For exclusions, ideally we'd specify content sets, not literal elements
+ $button->excludes = $this->makeLookup(
+ 'form',
+ 'fieldset', // Form
+ 'input',
+ 'select',
+ 'textarea',
+ 'label',
+ 'button', // Formctrl
+ 'a', // as per HTML 4.01 spec, this is omitted by modularization
+ 'isindex',
+ 'iframe' // legacy items
+ );
+
+ // Extra exclusion: img usemap="" is not permitted within this element.
+ // We'll omit this for now, since we don't have any good way of
+ // indicating it yet.
+
+ // This is HIGHLY user-unfriendly; we need a custom child-def for this
+ $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common');
+
+ $label = $this->addElement(
+ 'label',
+ 'Formctrl',
+ 'Optional: #PCDATA | Inline',
+ 'Common',
+ array(
+ 'accesskey' => 'Character',
+ // 'for' => 'IDREF', // IDREF not implemented, cannot allow
+ )
+ );
+ $label->excludes = array('label' => true);
+
+ $this->addElement(
+ 'legend',
+ false,
+ 'Optional: #PCDATA | Inline',
+ 'Common',
+ array(
+ 'accesskey' => 'Character',
+ )
+ );
+
+ $this->addElement(
+ 'optgroup',
+ false,
+ 'Required: option',
+ 'Common',
+ array(
+ 'disabled' => 'Bool#disabled',
+ 'label*' => 'Text',
+ )
+ );
+ // Don't forget an injector for . This one's a little complex
+ // because it maps to multiple elements.
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php
new file mode 100644
index 0000000000..72d7a31e68
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Hypertext.php
@@ -0,0 +1,40 @@
+addElement(
+ 'a',
+ 'Inline',
+ 'Inline',
+ 'Common',
+ array(
+ // 'accesskey' => 'Character',
+ // 'charset' => 'Charset',
+ 'href' => 'URI',
+ // 'hreflang' => 'LanguageCode',
+ 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'),
+ 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'),
+ // 'tabindex' => 'Number',
+ // 'type' => 'ContentType',
+ )
+ );
+ $a->formatting = true;
+ $a->excludes = array('a' => true);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php
new file mode 100644
index 0000000000..71dfc77447
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Iframe.php
@@ -0,0 +1,57 @@
+get('HTML.SafeIframe')) {
+ $this->safe = true;
+ }
+ $attrs = array(
+ 'src' => 'URI#embedded',
+ 'width' => 'Length',
+ 'height' => 'Length',
+ 'name' => 'ID',
+ 'scrolling' => 'Enum#yes,no,auto',
+ 'frameborder' => 'Enum#0,1',
+ 'longdesc' => 'URI',
+ 'marginheight' => 'Pixels',
+ 'marginwidth' => 'Pixels',
+ );
+
+ if ($config->get('HTML.Trusted')) {
+ $attrs['allowfullscreen'] = 'Bool#allowfullscreen';
+ }
+
+ $this->addElement(
+ 'iframe',
+ 'Inline',
+ 'Flow',
+ 'Common',
+ $attrs
+ );
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php
new file mode 100644
index 0000000000..0f5fdb3baf
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Image.php
@@ -0,0 +1,49 @@
+get('HTML.MaxImgLength');
+ $img = $this->addElement(
+ 'img',
+ 'Inline',
+ 'Empty',
+ 'Common',
+ array(
+ 'alt*' => 'Text',
+ // According to the spec, it's Length, but percents can
+ // be abused, so we allow only Pixels.
+ 'height' => 'Pixels#' . $max,
+ 'width' => 'Pixels#' . $max,
+ 'longdesc' => 'URI',
+ 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded
+ )
+ );
+ if ($max === null || $config->get('HTML.Trusted')) {
+ $img->attr['height'] =
+ $img->attr['width'] = 'Length';
+ }
+
+ // kind of strange, but splitting things up would be inefficient
+ $img->attr_transform_pre[] =
+ $img->attr_transform_post[] =
+ new HTMLPurifier_AttrTransform_ImgRequired();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php
new file mode 100644
index 0000000000..86b5299579
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Legacy.php
@@ -0,0 +1,186 @@
+addElement(
+ 'basefont',
+ 'Inline',
+ 'Empty',
+ null,
+ array(
+ 'color' => 'Color',
+ 'face' => 'Text', // extremely broad, we should
+ 'size' => 'Text', // tighten it
+ 'id' => 'ID'
+ )
+ );
+ $this->addElement('center', 'Block', 'Flow', 'Common');
+ $this->addElement(
+ 'dir',
+ 'Block',
+ 'Required: li',
+ 'Common',
+ array(
+ 'compact' => 'Bool#compact'
+ )
+ );
+ $this->addElement(
+ 'font',
+ 'Inline',
+ 'Inline',
+ array('Core', 'I18N'),
+ array(
+ 'color' => 'Color',
+ 'face' => 'Text', // extremely broad, we should
+ 'size' => 'Text', // tighten it
+ )
+ );
+ $this->addElement(
+ 'menu',
+ 'Block',
+ 'Required: li',
+ 'Common',
+ array(
+ 'compact' => 'Bool#compact'
+ )
+ );
+
+ $s = $this->addElement('s', 'Inline', 'Inline', 'Common');
+ $s->formatting = true;
+
+ $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common');
+ $strike->formatting = true;
+
+ $u = $this->addElement('u', 'Inline', 'Inline', 'Common');
+ $u->formatting = true;
+
+ // setup modifications to old elements
+
+ $align = 'Enum#left,right,center,justify';
+
+ $address = $this->addBlankElement('address');
+ $address->content_model = 'Inline | #PCDATA | p';
+ $address->content_model_type = 'optional';
+ $address->child = false;
+
+ $blockquote = $this->addBlankElement('blockquote');
+ $blockquote->content_model = 'Flow | #PCDATA';
+ $blockquote->content_model_type = 'optional';
+ $blockquote->child = false;
+
+ $br = $this->addBlankElement('br');
+ $br->attr['clear'] = 'Enum#left,all,right,none';
+
+ $caption = $this->addBlankElement('caption');
+ $caption->attr['align'] = 'Enum#top,bottom,left,right';
+
+ $div = $this->addBlankElement('div');
+ $div->attr['align'] = $align;
+
+ $dl = $this->addBlankElement('dl');
+ $dl->attr['compact'] = 'Bool#compact';
+
+ for ($i = 1; $i <= 6; $i++) {
+ $h = $this->addBlankElement("h$i");
+ $h->attr['align'] = $align;
+ }
+
+ $hr = $this->addBlankElement('hr');
+ $hr->attr['align'] = $align;
+ $hr->attr['noshade'] = 'Bool#noshade';
+ $hr->attr['size'] = 'Pixels';
+ $hr->attr['width'] = 'Length';
+
+ $img = $this->addBlankElement('img');
+ $img->attr['align'] = 'IAlign';
+ $img->attr['border'] = 'Pixels';
+ $img->attr['hspace'] = 'Pixels';
+ $img->attr['vspace'] = 'Pixels';
+
+ // figure out this integer business
+
+ $li = $this->addBlankElement('li');
+ $li->attr['value'] = new HTMLPurifier_AttrDef_Integer();
+ $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle';
+
+ $ol = $this->addBlankElement('ol');
+ $ol->attr['compact'] = 'Bool#compact';
+ $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer();
+ $ol->attr['type'] = 'Enum#s:1,i,I,a,A';
+
+ $p = $this->addBlankElement('p');
+ $p->attr['align'] = $align;
+
+ $pre = $this->addBlankElement('pre');
+ $pre->attr['width'] = 'Number';
+
+ // script omitted
+
+ $table = $this->addBlankElement('table');
+ $table->attr['align'] = 'Enum#left,center,right';
+ $table->attr['bgcolor'] = 'Color';
+
+ $tr = $this->addBlankElement('tr');
+ $tr->attr['bgcolor'] = 'Color';
+
+ $th = $this->addBlankElement('th');
+ $th->attr['bgcolor'] = 'Color';
+ $th->attr['height'] = 'Length';
+ $th->attr['nowrap'] = 'Bool#nowrap';
+ $th->attr['width'] = 'Length';
+
+ $td = $this->addBlankElement('td');
+ $td->attr['bgcolor'] = 'Color';
+ $td->attr['height'] = 'Length';
+ $td->attr['nowrap'] = 'Bool#nowrap';
+ $td->attr['width'] = 'Length';
+
+ $ul = $this->addBlankElement('ul');
+ $ul->attr['compact'] = 'Bool#compact';
+ $ul->attr['type'] = 'Enum#square,disc,circle';
+
+ // "safe" modifications to "unsafe" elements
+ // WARNING: If you want to add support for an unsafe, legacy
+ // attribute, make a new TrustedLegacy module with the trusted
+ // bit set appropriately
+
+ $form = $this->addBlankElement('form');
+ $form->content_model = 'Flow | #PCDATA';
+ $form->content_model_type = 'optional';
+ $form->attr['target'] = 'FrameTarget';
+
+ $input = $this->addBlankElement('input');
+ $input->attr['align'] = 'IAlign';
+
+ $legend = $this->addBlankElement('legend');
+ $legend->attr['align'] = 'LAlign';
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php
new file mode 100644
index 0000000000..7a20ff701c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/List.php
@@ -0,0 +1,51 @@
+ 'List');
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common');
+ $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common');
+ // XXX The wrap attribute is handled by MakeWellFormed. This is all
+ // quite unsatisfactory, because we generated this
+ // *specifically* for lists, and now a big chunk of the handling
+ // is done properly by the List ChildDef. So actually, we just
+ // want enough information to make autoclosing work properly,
+ // and then hand off the tricky stuff to the ChildDef.
+ $ol->wrap = 'li';
+ $ul->wrap = 'li';
+ $this->addElement('dl', 'List', 'Required: dt | dd', 'Common');
+
+ $this->addElement('li', false, 'Flow', 'Common');
+
+ $this->addElement('dd', false, 'Flow', 'Common');
+ $this->addElement('dt', false, 'Inline', 'Common');
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php
new file mode 100644
index 0000000000..60c0545154
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Name.php
@@ -0,0 +1,26 @@
+addBlankElement($name);
+ $element->attr['name'] = 'CDATA';
+ if (!$config->get('HTML.Attr.Name.UseCDATA')) {
+ $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync();
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php
new file mode 100644
index 0000000000..dc9410a895
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Nofollow.php
@@ -0,0 +1,25 @@
+addBlankElement('a');
+ $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php
new file mode 100644
index 0000000000..da722253ac
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php
@@ -0,0 +1,20 @@
+ array(
+ 'lang' => 'LanguageCode',
+ )
+ );
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php
new file mode 100644
index 0000000000..2f9efc5c88
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Object.php
@@ -0,0 +1,62 @@
+ to cater to legacy browsers: this
+ * module does not allow this sort of behavior
+ */
+class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule
+{
+ /**
+ * @type string
+ */
+ public $name = 'Object';
+
+ /**
+ * @type bool
+ */
+ public $safe = false;
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ $this->addElement(
+ 'object',
+ 'Inline',
+ 'Optional: #PCDATA | Flow | param',
+ 'Common',
+ array(
+ 'archive' => 'URI',
+ 'classid' => 'URI',
+ 'codebase' => 'URI',
+ 'codetype' => 'Text',
+ 'data' => 'URI',
+ 'declare' => 'Bool#declare',
+ 'height' => 'Length',
+ 'name' => 'CDATA',
+ 'standby' => 'Text',
+ 'tabindex' => 'Number',
+ 'type' => 'ContentType',
+ 'width' => 'Length'
+ )
+ );
+
+ $this->addElement(
+ 'param',
+ false,
+ 'Empty',
+ null,
+ array(
+ 'id' => 'ID',
+ 'name*' => 'Text',
+ 'type' => 'Text',
+ 'value' => 'Text',
+ 'valuetype' => 'Enum#data,ref,object'
+ )
+ );
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php
new file mode 100644
index 0000000000..6458ce9d88
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Presentation.php
@@ -0,0 +1,42 @@
+addElement('hr', 'Block', 'Empty', 'Common');
+ $this->addElement('sub', 'Inline', 'Inline', 'Common');
+ $this->addElement('sup', 'Inline', 'Inline', 'Common');
+ $b = $this->addElement('b', 'Inline', 'Inline', 'Common');
+ $b->formatting = true;
+ $big = $this->addElement('big', 'Inline', 'Inline', 'Common');
+ $big->formatting = true;
+ $i = $this->addElement('i', 'Inline', 'Inline', 'Common');
+ $i->formatting = true;
+ $small = $this->addElement('small', 'Inline', 'Inline', 'Common');
+ $small->formatting = true;
+ $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common');
+ $tt->formatting = true;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php
new file mode 100644
index 0000000000..5ee3c8e67f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Proprietary.php
@@ -0,0 +1,40 @@
+addElement(
+ 'marquee',
+ 'Inline',
+ 'Flow',
+ 'Common',
+ array(
+ 'direction' => 'Enum#left,right,up,down',
+ 'behavior' => 'Enum#alternate',
+ 'width' => 'Length',
+ 'height' => 'Length',
+ 'scrolldelay' => 'Number',
+ 'scrollamount' => 'Number',
+ 'loop' => 'Number',
+ 'bgcolor' => 'Color',
+ 'hspace' => 'Pixels',
+ 'vspace' => 'Pixels',
+ )
+ );
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php
new file mode 100644
index 0000000000..a0d48924da
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Ruby.php
@@ -0,0 +1,36 @@
+addElement(
+ 'ruby',
+ 'Inline',
+ 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))',
+ 'Common'
+ );
+ $this->addElement('rbc', false, 'Required: rb', 'Common');
+ $this->addElement('rtc', false, 'Required: rt', 'Common');
+ $rb = $this->addElement('rb', false, 'Inline', 'Common');
+ $rb->excludes = array('ruby' => true);
+ $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number'));
+ $rt->excludes = array('ruby' => true);
+ $this->addElement('rp', false, 'Optional: #PCDATA', 'Common');
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php
new file mode 100644
index 0000000000..04e6689ea6
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeEmbed.php
@@ -0,0 +1,40 @@
+get('HTML.MaxImgLength');
+ $embed = $this->addElement(
+ 'embed',
+ 'Inline',
+ 'Empty',
+ 'Common',
+ array(
+ 'src*' => 'URI#embedded',
+ 'type' => 'Enum#application/x-shockwave-flash',
+ 'width' => 'Pixels#' . $max,
+ 'height' => 'Pixels#' . $max,
+ 'allowscriptaccess' => 'Enum#never',
+ 'allownetworking' => 'Enum#internal',
+ 'flashvars' => 'Text',
+ 'wmode' => 'Enum#window,transparent,opaque',
+ 'name' => 'ID',
+ )
+ );
+ $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php
new file mode 100644
index 0000000000..1297f80a39
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeObject.php
@@ -0,0 +1,62 @@
+get('HTML.MaxImgLength');
+ $object = $this->addElement(
+ 'object',
+ 'Inline',
+ 'Optional: param | Flow | #PCDATA',
+ 'Common',
+ array(
+ // While technically not required by the spec, we're forcing
+ // it to this value.
+ 'type' => 'Enum#application/x-shockwave-flash',
+ 'width' => 'Pixels#' . $max,
+ 'height' => 'Pixels#' . $max,
+ 'data' => 'URI#embedded',
+ 'codebase' => new HTMLPurifier_AttrDef_Enum(
+ array(
+ 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0'
+ )
+ ),
+ )
+ );
+ $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject();
+
+ $param = $this->addElement(
+ 'param',
+ false,
+ 'Empty',
+ false,
+ array(
+ 'id' => 'ID',
+ 'name*' => 'Text',
+ 'value' => 'Text'
+ )
+ );
+ $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam();
+ $this->info_injector[] = 'SafeObject';
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php
new file mode 100644
index 0000000000..aea7584c37
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/SafeScripting.php
@@ -0,0 +1,40 @@
+get('HTML.SafeScripting');
+ $script = $this->addElement(
+ 'script',
+ 'Inline',
+ 'Optional:', // Not `Empty` to not allow to autoclose the tag @see https://www.w3.org/TR/html4/interact/scripts.html
+ null,
+ array(
+ // While technically not required by the spec, we're forcing
+ // it to this value.
+ 'type' => 'Enum#text/javascript',
+ 'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed), /*case sensitive*/ true)
+ )
+ );
+ $script->attr_transform_pre[] =
+ $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php
new file mode 100644
index 0000000000..8b28a7b7ea
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Scripting.php
@@ -0,0 +1,73 @@
+ 'script | noscript', 'Inline' => 'script | noscript');
+
+ /**
+ * @type bool
+ */
+ public $safe = false;
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ // TODO: create custom child-definition for noscript that
+ // auto-wraps stray #PCDATA in a similar manner to
+ // blockquote's custom definition (we would use it but
+ // blockquote's contents are optional while noscript's contents
+ // are required)
+
+ // TODO: convert this to new syntax, main problem is getting
+ // both content sets working
+
+ // In theory, this could be safe, but I don't see any reason to
+ // allow it.
+ $this->info['noscript'] = new HTMLPurifier_ElementDef();
+ $this->info['noscript']->attr = array(0 => array('Common'));
+ $this->info['noscript']->content_model = 'Heading | List | Block';
+ $this->info['noscript']->content_model_type = 'required';
+
+ $this->info['script'] = new HTMLPurifier_ElementDef();
+ $this->info['script']->attr = array(
+ 'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')),
+ 'src' => new HTMLPurifier_AttrDef_URI(true),
+ 'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript'))
+ );
+ $this->info['script']->content_model = '#PCDATA';
+ $this->info['script']->content_model_type = 'optional';
+ $this->info['script']->attr_transform_pre[] =
+ $this->info['script']->attr_transform_post[] =
+ new HTMLPurifier_AttrTransform_ScriptRequired();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php
new file mode 100644
index 0000000000..497b832ae2
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/StyleAttribute.php
@@ -0,0 +1,33 @@
+ array('style' => false), // see constructor
+ 'Core' => array(0 => array('Style'))
+ );
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php
new file mode 100644
index 0000000000..8a0b3b4616
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tables.php
@@ -0,0 +1,75 @@
+addElement('caption', false, 'Inline', 'Common');
+
+ $this->addElement(
+ 'table',
+ 'Block',
+ new HTMLPurifier_ChildDef_Table(),
+ 'Common',
+ array(
+ 'border' => 'Pixels',
+ 'cellpadding' => 'Length',
+ 'cellspacing' => 'Length',
+ 'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border',
+ 'rules' => 'Enum#none,groups,rows,cols,all',
+ 'summary' => 'Text',
+ 'width' => 'Length'
+ )
+ );
+
+ // common attributes
+ $cell_align = array(
+ 'align' => 'Enum#left,center,right,justify,char',
+ 'charoff' => 'Length',
+ 'valign' => 'Enum#top,middle,bottom,baseline',
+ );
+
+ $cell_t = array_merge(
+ array(
+ 'abbr' => 'Text',
+ 'colspan' => 'Number',
+ 'rowspan' => 'Number',
+ // Apparently, as of HTML5 this attribute only applies
+ // to 'th' elements.
+ 'scope' => 'Enum#row,col,rowgroup,colgroup',
+ ),
+ $cell_align
+ );
+ $this->addElement('td', false, 'Flow', 'Common', $cell_t);
+ $this->addElement('th', false, 'Flow', 'Common', $cell_t);
+
+ $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align);
+
+ $cell_col = array_merge(
+ array(
+ 'span' => 'Number',
+ 'width' => 'MultiLength',
+ ),
+ $cell_align
+ );
+ $this->addElement('col', false, 'Empty', 'Common', $cell_col);
+ $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col);
+
+ $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align);
+ $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align);
+ $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php
new file mode 100644
index 0000000000..b188ac9369
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Target.php
@@ -0,0 +1,28 @@
+addBlankElement($name);
+ $e->attr = array(
+ 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget()
+ );
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php
new file mode 100644
index 0000000000..58ccc68941
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetBlank.php
@@ -0,0 +1,24 @@
+addBlankElement('a');
+ $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlank();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php
new file mode 100644
index 0000000000..b967ff566b
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoopener.php
@@ -0,0 +1,21 @@
+addBlankElement('a');
+ $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoopener();
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php
new file mode 100644
index 0000000000..32484d6019
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/TargetNoreferrer.php
@@ -0,0 +1,21 @@
+addBlankElement('a');
+ $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetNoreferrer();
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php
new file mode 100644
index 0000000000..7a65e0048f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Text.php
@@ -0,0 +1,87 @@
+ 'Heading | Block | Inline'
+ );
+
+ /**
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ // Inline Phrasal -------------------------------------------------
+ $this->addElement('abbr', 'Inline', 'Inline', 'Common');
+ $this->addElement('acronym', 'Inline', 'Inline', 'Common');
+ $this->addElement('cite', 'Inline', 'Inline', 'Common');
+ $this->addElement('dfn', 'Inline', 'Inline', 'Common');
+ $this->addElement('kbd', 'Inline', 'Inline', 'Common');
+ $this->addElement('q', 'Inline', 'Inline', 'Common', array('cite' => 'URI'));
+ $this->addElement('samp', 'Inline', 'Inline', 'Common');
+ $this->addElement('var', 'Inline', 'Inline', 'Common');
+
+ $em = $this->addElement('em', 'Inline', 'Inline', 'Common');
+ $em->formatting = true;
+
+ $strong = $this->addElement('strong', 'Inline', 'Inline', 'Common');
+ $strong->formatting = true;
+
+ $code = $this->addElement('code', 'Inline', 'Inline', 'Common');
+ $code->formatting = true;
+
+ // Inline Structural ----------------------------------------------
+ $this->addElement('span', 'Inline', 'Inline', 'Common');
+ $this->addElement('br', 'Inline', 'Empty', 'Core');
+
+ // Block Phrasal --------------------------------------------------
+ $this->addElement('address', 'Block', 'Inline', 'Common');
+ $this->addElement('blockquote', 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI'));
+ $pre = $this->addElement('pre', 'Block', 'Inline', 'Common');
+ $pre->excludes = $this->makeLookup(
+ 'img',
+ 'big',
+ 'small',
+ 'object',
+ 'applet',
+ 'font',
+ 'basefont'
+ );
+ $this->addElement('h1', 'Heading', 'Inline', 'Common');
+ $this->addElement('h2', 'Heading', 'Inline', 'Common');
+ $this->addElement('h3', 'Heading', 'Inline', 'Common');
+ $this->addElement('h4', 'Heading', 'Inline', 'Common');
+ $this->addElement('h5', 'Heading', 'Inline', 'Common');
+ $this->addElement('h6', 'Heading', 'Inline', 'Common');
+
+ // Block Structural -----------------------------------------------
+ $p = $this->addElement('p', 'Block', 'Inline', 'Common');
+ $p->autoclose = array_flip(
+ array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul")
+ );
+
+ $this->addElement('div', 'Block', 'Flow', 'Common');
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php
new file mode 100644
index 0000000000..76fd93a6cb
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy.php
@@ -0,0 +1,228 @@
+ 'none', 'light', 'medium', 'heavy');
+
+ /**
+ * Default level to place all fixes in.
+ * Disabled by default.
+ * @type string
+ */
+ public $defaultLevel = null;
+
+ /**
+ * Lists of fixes used by getFixesForLevel().
+ * Format is:
+ * HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2');
+ * @type array
+ */
+ public $fixesForLevel = array(
+ 'light' => array(),
+ 'medium' => array(),
+ 'heavy' => array()
+ );
+
+ /**
+ * Lazy load constructs the module by determining the necessary
+ * fixes to create and then delegating to the populate() function.
+ * @param HTMLPurifier_Config $config
+ * @todo Wildcard matching and error reporting when an added or
+ * subtracted fix has no effect.
+ */
+ public function setup($config)
+ {
+ // create fixes, initialize fixesForLevel
+ $fixes = $this->makeFixes();
+ $this->makeFixesForLevel($fixes);
+
+ // figure out which fixes to use
+ $level = $config->get('HTML.TidyLevel');
+ $fixes_lookup = $this->getFixesForLevel($level);
+
+ // get custom fix declarations: these need namespace processing
+ $add_fixes = $config->get('HTML.TidyAdd');
+ $remove_fixes = $config->get('HTML.TidyRemove');
+
+ foreach ($fixes as $name => $fix) {
+ // needs to be refactored a little to implement globbing
+ if (isset($remove_fixes[$name]) ||
+ (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name]))) {
+ unset($fixes[$name]);
+ }
+ }
+
+ // populate this module with necessary fixes
+ $this->populate($fixes);
+ }
+
+ /**
+ * Retrieves all fixes per a level, returning fixes for that specific
+ * level as well as all levels below it.
+ * @param string $level level identifier, see $levels for valid values
+ * @return array Lookup up table of fixes
+ */
+ public function getFixesForLevel($level)
+ {
+ if ($level == $this->levels[0]) {
+ return array();
+ }
+ $activated_levels = array();
+ for ($i = 1, $c = count($this->levels); $i < $c; $i++) {
+ $activated_levels[] = $this->levels[$i];
+ if ($this->levels[$i] == $level) {
+ break;
+ }
+ }
+ if ($i == $c) {
+ trigger_error(
+ 'Tidy level ' . htmlspecialchars($level) . ' not recognized',
+ E_USER_WARNING
+ );
+ return array();
+ }
+ $ret = array();
+ foreach ($activated_levels as $level) {
+ foreach ($this->fixesForLevel[$level] as $fix) {
+ $ret[$fix] = true;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Dynamically populates the $fixesForLevel member variable using
+ * the fixes array. It may be custom overloaded, used in conjunction
+ * with $defaultLevel, or not used at all.
+ * @param array $fixes
+ */
+ public function makeFixesForLevel($fixes)
+ {
+ if (!isset($this->defaultLevel)) {
+ return;
+ }
+ if (!isset($this->fixesForLevel[$this->defaultLevel])) {
+ trigger_error(
+ 'Default level ' . $this->defaultLevel . ' does not exist',
+ E_USER_ERROR
+ );
+ return;
+ }
+ $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes);
+ }
+
+ /**
+ * Populates the module with transforms and other special-case code
+ * based on a list of fixes passed to it
+ * @param array $fixes Lookup table of fixes to activate
+ */
+ public function populate($fixes)
+ {
+ foreach ($fixes as $name => $fix) {
+ // determine what the fix is for
+ list($type, $params) = $this->getFixType($name);
+ switch ($type) {
+ case 'attr_transform_pre':
+ case 'attr_transform_post':
+ $attr = $params['attr'];
+ if (isset($params['element'])) {
+ $element = $params['element'];
+ if (empty($this->info[$element])) {
+ $e = $this->addBlankElement($element);
+ } else {
+ $e = $this->info[$element];
+ }
+ } else {
+ $type = "info_$type";
+ $e = $this;
+ }
+ $e->{$type}[$attr] = $fix;
+ break;
+ case 'tag_transform':
+ $this->info_tag_transform[$params['element']] = $fix;
+ break;
+ case 'child':
+ case 'content_model_type':
+ $element = $params['element'];
+ if (empty($this->info[$element])) {
+ $e = $this->addBlankElement($element);
+ } else {
+ $e = $this->info[$element];
+ }
+ $e->$type = $fix;
+ break;
+ default:
+ trigger_error("Fix type $type not supported", E_USER_ERROR);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parses a fix name and determines what kind of fix it is, as well
+ * as other information defined by the fix
+ * @param $name String name of fix
+ * @return array(string $fix_type, array $fix_parameters)
+ * @note $fix_parameters is type dependant, see populate() for usage
+ * of these parameters
+ */
+ public function getFixType($name)
+ {
+ // parse it
+ $property = $attr = null;
+ if (strpos($name, '#') !== false) {
+ list($name, $property) = explode('#', $name);
+ }
+ if (strpos($name, '@') !== false) {
+ list($name, $attr) = explode('@', $name);
+ }
+
+ // figure out the parameters
+ $params = array();
+ if ($name !== '') {
+ $params['element'] = $name;
+ }
+ if (!is_null($attr)) {
+ $params['attr'] = $attr;
+ }
+
+ // special case: attribute transform
+ if (!is_null($attr)) {
+ if (is_null($property)) {
+ $property = 'pre';
+ }
+ $type = 'attr_transform_' . $property;
+ return array($type, $params);
+ }
+
+ // special case: tag transform
+ if (is_null($property)) {
+ return array('tag_transform', $params);
+ }
+
+ return array($property, $params);
+
+ }
+
+ /**
+ * Defines all fixes the module will perform in a compact
+ * associative array of fix name to fix implementation.
+ * @return array
+ */
+ public function makeFixes()
+ {
+ return array();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php
new file mode 100644
index 0000000000..a995161b28
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Name.php
@@ -0,0 +1,33 @@
+content_model_type != 'strictblockquote') {
+ return parent::getChildDef($def);
+ }
+ return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php
new file mode 100644
index 0000000000..c095ad9745
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/Tidy/Transitional.php
@@ -0,0 +1,16 @@
+ 'text-align:left;',
+ 'right' => 'text-align:right;',
+ 'top' => 'caption-side:top;',
+ 'bottom' => 'caption-side:bottom;' // not supported by IE
+ )
+ );
+
+ // @align for img -------------------------------------------------
+ $r['img@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS(
+ 'align',
+ array(
+ 'left' => 'float:left;',
+ 'right' => 'float:right;',
+ 'top' => 'vertical-align:top;',
+ 'middle' => 'vertical-align:middle;',
+ 'bottom' => 'vertical-align:baseline;',
+ )
+ );
+
+ // @align for table -----------------------------------------------
+ $r['table@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS(
+ 'align',
+ array(
+ 'left' => 'float:left;',
+ 'center' => 'margin-left:auto;margin-right:auto;',
+ 'right' => 'float:right;'
+ )
+ );
+
+ // @align for hr -----------------------------------------------
+ $r['hr@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS(
+ 'align',
+ array(
+ // we use both text-align and margin because these work
+ // for different browsers (IE and Firefox, respectively)
+ // and the melange makes for a pretty cross-compatible
+ // solution
+ 'left' => 'margin-left:0;margin-right:auto;text-align:left;',
+ 'center' => 'margin-left:auto;margin-right:auto;text-align:center;',
+ 'right' => 'margin-left:auto;margin-right:0;text-align:right;'
+ )
+ );
+
+ // @align for h1, h2, h3, h4, h5, h6, p, div ----------------------
+ // {{{
+ $align_lookup = array();
+ $align_values = array('left', 'right', 'center', 'justify');
+ foreach ($align_values as $v) {
+ $align_lookup[$v] = "text-align:$v;";
+ }
+ // }}}
+ $r['h1@align'] =
+ $r['h2@align'] =
+ $r['h3@align'] =
+ $r['h4@align'] =
+ $r['h5@align'] =
+ $r['h6@align'] =
+ $r['p@align'] =
+ $r['div@align'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup);
+
+ // @bgcolor for table, tr, td, th ---------------------------------
+ $r['table@bgcolor'] =
+ $r['tr@bgcolor'] =
+ $r['td@bgcolor'] =
+ $r['th@bgcolor'] =
+ new HTMLPurifier_AttrTransform_BgColor();
+
+ // @border for img ------------------------------------------------
+ $r['img@border'] = new HTMLPurifier_AttrTransform_Border();
+
+ // @clear for br --------------------------------------------------
+ $r['br@clear'] =
+ new HTMLPurifier_AttrTransform_EnumToCSS(
+ 'clear',
+ array(
+ 'left' => 'clear:left;',
+ 'right' => 'clear:right;',
+ 'all' => 'clear:both;',
+ 'none' => 'clear:none;',
+ )
+ );
+
+ // @height for td, th ---------------------------------------------
+ $r['td@height'] =
+ $r['th@height'] =
+ new HTMLPurifier_AttrTransform_Length('height');
+
+ // @hspace for img ------------------------------------------------
+ $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace');
+
+ // @noshade for hr ------------------------------------------------
+ // this transformation is not precise but often good enough.
+ // different browsers use different styles to designate noshade
+ $r['hr@noshade'] =
+ new HTMLPurifier_AttrTransform_BoolToCSS(
+ 'noshade',
+ 'color:#808080;background-color:#808080;border:0;'
+ );
+
+ // @nowrap for td, th ---------------------------------------------
+ $r['td@nowrap'] =
+ $r['th@nowrap'] =
+ new HTMLPurifier_AttrTransform_BoolToCSS(
+ 'nowrap',
+ 'white-space:nowrap;'
+ );
+
+ // @size for hr --------------------------------------------------
+ $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height');
+
+ // @type for li, ol, ul -------------------------------------------
+ // {{{
+ $ul_types = array(
+ 'disc' => 'list-style-type:disc;',
+ 'square' => 'list-style-type:square;',
+ 'circle' => 'list-style-type:circle;'
+ );
+ $ol_types = array(
+ '1' => 'list-style-type:decimal;',
+ 'i' => 'list-style-type:lower-roman;',
+ 'I' => 'list-style-type:upper-roman;',
+ 'a' => 'list-style-type:lower-alpha;',
+ 'A' => 'list-style-type:upper-alpha;'
+ );
+ $li_types = $ul_types + $ol_types;
+ // }}}
+
+ $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types);
+ $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true);
+ $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true);
+
+ // @vspace for img ------------------------------------------------
+ $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace');
+
+ // @width for table, hr, td, th, col ------------------------------------------
+ $r['table@width'] =
+ $r['td@width'] =
+ $r['th@width'] =
+ $r['col@width'] =
+ $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width');
+
+ return $r;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php
new file mode 100644
index 0000000000..01dbe9deb6
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModule/XMLCommonAttributes.php
@@ -0,0 +1,20 @@
+ array(
+ 'xml:lang' => 'LanguageCode',
+ )
+ );
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php
new file mode 100644
index 0000000000..38c058fe23
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/HTMLModuleManager.php
@@ -0,0 +1,467 @@
+attrTypes = new HTMLPurifier_AttrTypes();
+ $this->doctypes = new HTMLPurifier_DoctypeRegistry();
+
+ // setup basic modules
+ $common = array(
+ 'CommonAttributes', 'Text', 'Hypertext', 'List',
+ 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image',
+ 'StyleAttribute',
+ // Unsafe:
+ 'Scripting', 'Object', 'Forms',
+ // Sorta legacy, but present in strict:
+ 'Name',
+ );
+ $transitional = array('Legacy', 'Target', 'Iframe');
+ $xml = array('XMLCommonAttributes');
+ $non_xml = array('NonXMLCommonAttributes');
+
+ // setup basic doctypes
+ $this->doctypes->register(
+ 'HTML 4.01 Transitional',
+ false,
+ array_merge($common, $transitional, $non_xml),
+ array('Tidy_Transitional', 'Tidy_Proprietary'),
+ array(),
+ '-//W3C//DTD HTML 4.01 Transitional//EN',
+ 'http://www.w3.org/TR/html4/loose.dtd'
+ );
+
+ $this->doctypes->register(
+ 'HTML 4.01 Strict',
+ false,
+ array_merge($common, $non_xml),
+ array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
+ array(),
+ '-//W3C//DTD HTML 4.01//EN',
+ 'http://www.w3.org/TR/html4/strict.dtd'
+ );
+
+ $this->doctypes->register(
+ 'XHTML 1.0 Transitional',
+ true,
+ array_merge($common, $transitional, $xml, $non_xml),
+ array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'),
+ array(),
+ '-//W3C//DTD XHTML 1.0 Transitional//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
+ );
+
+ $this->doctypes->register(
+ 'XHTML 1.0 Strict',
+ true,
+ array_merge($common, $xml, $non_xml),
+ array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'),
+ array(),
+ '-//W3C//DTD XHTML 1.0 Strict//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
+ );
+
+ $this->doctypes->register(
+ 'XHTML 1.1',
+ true,
+ // Iframe is a real XHTML 1.1 module, despite being
+ // "transitional"!
+ array_merge($common, $xml, array('Ruby', 'Iframe')),
+ array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1
+ array(),
+ '-//W3C//DTD XHTML 1.1//EN',
+ 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
+ );
+
+ }
+
+ /**
+ * Registers a module to the recognized module list, useful for
+ * overloading pre-existing modules.
+ * @param $module Mixed: string module name, with or without
+ * HTMLPurifier_HTMLModule prefix, or instance of
+ * subclass of HTMLPurifier_HTMLModule.
+ * @param $overload Boolean whether or not to overload previous modules.
+ * If this is not set, and you do overload a module,
+ * HTML Purifier will complain with a warning.
+ * @note This function will not call autoload, you must instantiate
+ * (and thus invoke) autoload outside the method.
+ * @note If a string is passed as a module name, different variants
+ * will be tested in this order:
+ * - Check for HTMLPurifier_HTMLModule_$name
+ * - Check all prefixes with $name in order they were added
+ * - Check for literal object name
+ * - Throw fatal error
+ * If your object name collides with an internal class, specify
+ * your module manually. All modules must have been included
+ * externally: registerModule will not perform inclusions for you!
+ */
+ public function registerModule($module, $overload = false)
+ {
+ if (is_string($module)) {
+ // attempt to load the module
+ $original_module = $module;
+ $ok = false;
+ foreach ($this->prefixes as $prefix) {
+ $module = $prefix . $original_module;
+ if (class_exists($module)) {
+ $ok = true;
+ break;
+ }
+ }
+ if (!$ok) {
+ $module = $original_module;
+ if (!class_exists($module)) {
+ trigger_error(
+ $original_module . ' module does not exist',
+ E_USER_ERROR
+ );
+ return;
+ }
+ }
+ $module = new $module();
+ }
+ if (empty($module->name)) {
+ trigger_error('Module instance of ' . get_class($module) . ' must have name');
+ return;
+ }
+ if (!$overload && isset($this->registeredModules[$module->name])) {
+ trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING);
+ }
+ $this->registeredModules[$module->name] = $module;
+ }
+
+ /**
+ * Adds a module to the current doctype by first registering it,
+ * and then tacking it on to the active doctype
+ */
+ public function addModule($module)
+ {
+ $this->registerModule($module);
+ if (is_object($module)) {
+ $module = $module->name;
+ }
+ $this->userModules[] = $module;
+ }
+
+ /**
+ * Adds a class prefix that registerModule() will use to resolve a
+ * string name to a concrete class
+ */
+ public function addPrefix($prefix)
+ {
+ $this->prefixes[] = $prefix;
+ }
+
+ /**
+ * Performs processing on modules, after being called you may
+ * use getElement() and getElements()
+ * @param HTMLPurifier_Config $config
+ */
+ public function setup($config)
+ {
+ $this->trusted = $config->get('HTML.Trusted');
+
+ // generate
+ $this->doctype = $this->doctypes->make($config);
+ $modules = $this->doctype->modules;
+
+ // take out the default modules that aren't allowed
+ $lookup = $config->get('HTML.AllowedModules');
+ $special_cases = $config->get('HTML.CoreModules');
+
+ if (is_array($lookup)) {
+ foreach ($modules as $k => $m) {
+ if (isset($special_cases[$m])) {
+ continue;
+ }
+ if (!isset($lookup[$m])) {
+ unset($modules[$k]);
+ }
+ }
+ }
+
+ // custom modules
+ if ($config->get('HTML.Proprietary')) {
+ $modules[] = 'Proprietary';
+ }
+ if ($config->get('HTML.SafeObject')) {
+ $modules[] = 'SafeObject';
+ }
+ if ($config->get('HTML.SafeEmbed')) {
+ $modules[] = 'SafeEmbed';
+ }
+ if ($config->get('HTML.SafeScripting') !== array()) {
+ $modules[] = 'SafeScripting';
+ }
+ if ($config->get('HTML.Nofollow')) {
+ $modules[] = 'Nofollow';
+ }
+ if ($config->get('HTML.TargetBlank')) {
+ $modules[] = 'TargetBlank';
+ }
+ // NB: HTML.TargetNoreferrer and HTML.TargetNoopener must be AFTER HTML.TargetBlank
+ // so that its post-attr-transform gets run afterwards.
+ if ($config->get('HTML.TargetNoreferrer')) {
+ $modules[] = 'TargetNoreferrer';
+ }
+ if ($config->get('HTML.TargetNoopener')) {
+ $modules[] = 'TargetNoopener';
+ }
+
+ // merge in custom modules
+ $modules = array_merge($modules, $this->userModules);
+
+ foreach ($modules as $module) {
+ $this->processModule($module);
+ $this->modules[$module]->setup($config);
+ }
+
+ foreach ($this->doctype->tidyModules as $module) {
+ $this->processModule($module);
+ $this->modules[$module]->setup($config);
+ }
+
+ // prepare any injectors
+ foreach ($this->modules as $module) {
+ $n = array();
+ foreach ($module->info_injector as $injector) {
+ if (!is_object($injector)) {
+ $class = "HTMLPurifier_Injector_$injector";
+ $injector = new $class;
+ }
+ $n[$injector->name] = $injector;
+ }
+ $module->info_injector = $n;
+ }
+
+ // setup lookup table based on all valid modules
+ foreach ($this->modules as $module) {
+ foreach ($module->info as $name => $def) {
+ if (!isset($this->elementLookup[$name])) {
+ $this->elementLookup[$name] = array();
+ }
+ $this->elementLookup[$name][] = $module->name;
+ }
+ }
+
+ // note the different choice
+ $this->contentSets = new HTMLPurifier_ContentSets(
+ // content set assembly deals with all possible modules,
+ // not just ones deemed to be "safe"
+ $this->modules
+ );
+ $this->attrCollections = new HTMLPurifier_AttrCollections(
+ $this->attrTypes,
+ // there is no way to directly disable a global attribute,
+ // but using AllowedAttributes or simply not including
+ // the module in your custom doctype should be sufficient
+ $this->modules
+ );
+ }
+
+ /**
+ * Takes a module and adds it to the active module collection,
+ * registering it if necessary.
+ */
+ public function processModule($module)
+ {
+ if (!isset($this->registeredModules[$module]) || is_object($module)) {
+ $this->registerModule($module);
+ }
+ $this->modules[$module] = $this->registeredModules[$module];
+ }
+
+ /**
+ * Retrieves merged element definitions.
+ * @return Array of HTMLPurifier_ElementDef
+ */
+ public function getElements()
+ {
+ $elements = array();
+ foreach ($this->modules as $module) {
+ if (!$this->trusted && !$module->safe) {
+ continue;
+ }
+ foreach ($module->info as $name => $v) {
+ if (isset($elements[$name])) {
+ continue;
+ }
+ $elements[$name] = $this->getElement($name);
+ }
+ }
+
+ // remove dud elements, this happens when an element that
+ // appeared to be safe actually wasn't
+ foreach ($elements as $n => $v) {
+ if ($v === false) {
+ unset($elements[$n]);
+ }
+ }
+
+ return $elements;
+
+ }
+
+ /**
+ * Retrieves a single merged element definition
+ * @param string $name Name of element
+ * @param bool $trusted Boolean trusted overriding parameter: set to true
+ * if you want the full version of an element
+ * @return HTMLPurifier_ElementDef Merged HTMLPurifier_ElementDef
+ * @note You may notice that modules are getting iterated over twice (once
+ * in getElements() and once here). This
+ * is because
+ */
+ public function getElement($name, $trusted = null)
+ {
+ if (!isset($this->elementLookup[$name])) {
+ return false;
+ }
+
+ // setup global state variables
+ $def = false;
+ if ($trusted === null) {
+ $trusted = $this->trusted;
+ }
+
+ // iterate through each module that has registered itself to this
+ // element
+ foreach ($this->elementLookup[$name] as $module_name) {
+ $module = $this->modules[$module_name];
+
+ // refuse to create/merge from a module that is deemed unsafe--
+ // pretend the module doesn't exist--when trusted mode is not on.
+ if (!$trusted && !$module->safe) {
+ continue;
+ }
+
+ // clone is used because, ideally speaking, the original
+ // definition should not be modified. Usually, this will
+ // make no difference, but for consistency's sake
+ $new_def = clone $module->info[$name];
+
+ if (!$def && $new_def->standalone) {
+ $def = $new_def;
+ } elseif ($def) {
+ // This will occur even if $new_def is standalone. In practice,
+ // this will usually result in a full replacement.
+ $def->mergeIn($new_def);
+ } else {
+ // :TODO:
+ // non-standalone definitions that don't have a standalone
+ // to merge into could be deferred to the end
+ // HOWEVER, it is perfectly valid for a non-standalone
+ // definition to lack a standalone definition, even
+ // after all processing: this allows us to safely
+ // specify extra attributes for elements that may not be
+ // enabled all in one place. In particular, this might
+ // be the case for trusted elements. WARNING: care must
+ // be taken that the /extra/ definitions are all safe.
+ continue;
+ }
+
+ // attribute value expansions
+ $this->attrCollections->performInclusions($def->attr);
+ $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes);
+
+ // descendants_are_inline, for ChildDef_Chameleon
+ if (is_string($def->content_model) &&
+ strpos($def->content_model, 'Inline') !== false) {
+ if ($name != 'del' && $name != 'ins') {
+ // this is for you, ins/del
+ $def->descendants_are_inline = true;
+ }
+ }
+
+ $this->contentSets->generateChildDef($def, $module);
+ }
+
+ // This can occur if there is a blank definition, but no base to
+ // mix it in with
+ if (!$def) {
+ return false;
+ }
+
+ // add information on required attributes
+ foreach ($def->attr as $attr_name => $attr_def) {
+ if ($attr_def->required) {
+ $def->required_attr[] = $attr_name;
+ }
+ }
+ return $def;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php
new file mode 100644
index 0000000000..65c902c076
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/IDAccumulator.php
@@ -0,0 +1,57 @@
+load($config->get('Attr.IDBlacklist'));
+ return $id_accumulator;
+ }
+
+ /**
+ * Add an ID to the lookup table.
+ * @param string $id ID to be added.
+ * @return bool status, true if success, false if there's a dupe
+ */
+ public function add($id)
+ {
+ if (isset($this->ids[$id])) {
+ return false;
+ }
+ return $this->ids[$id] = true;
+ }
+
+ /**
+ * Load a list of IDs into the lookup table
+ * @param $array_of_ids Array of IDs to load
+ * @note This function doesn't care about duplicates
+ */
+ public function load($array_of_ids)
+ {
+ foreach ($array_of_ids as $id) {
+ $this->ids[$id] = true;
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php
new file mode 100644
index 0000000000..116b470c37
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector.php
@@ -0,0 +1,283 @@
+processToken()
+ * documentation.
+ *
+ * @todo Allow injectors to request a re-run on their output. This
+ * would help if an operation is recursive.
+ */
+abstract class HTMLPurifier_Injector
+{
+
+ /**
+ * Advisory name of injector, this is for friendly error messages.
+ * @type string
+ */
+ public $name;
+
+ /**
+ * @type HTMLPurifier_HTMLDefinition
+ */
+ protected $htmlDefinition;
+
+ /**
+ * Reference to CurrentNesting variable in Context. This is an array
+ * list of tokens that we are currently "inside"
+ * @type array
+ */
+ protected $currentNesting;
+
+ /**
+ * Reference to current token.
+ * @type HTMLPurifier_Token
+ */
+ protected $currentToken;
+
+ /**
+ * Reference to InputZipper variable in Context.
+ * @type HTMLPurifier_Zipper
+ */
+ protected $inputZipper;
+
+ /**
+ * Array of elements and attributes this injector creates and therefore
+ * need to be allowed by the definition. Takes form of
+ * array('element' => array('attr', 'attr2'), 'element2')
+ * @type array
+ */
+ public $needed = array();
+
+ /**
+ * Number of elements to rewind backwards (relative).
+ * @type bool|int
+ */
+ protected $rewindOffset = false;
+
+ /**
+ * Rewind to a spot to re-perform processing. This is useful if you
+ * deleted a node, and now need to see if this change affected any
+ * earlier nodes. Rewinding does not affect other injectors, and can
+ * result in infinite loops if not used carefully.
+ * @param bool|int $offset
+ * @warning HTML Purifier will prevent you from fast-forwarding with this
+ * function.
+ */
+ public function rewindOffset($offset)
+ {
+ $this->rewindOffset = $offset;
+ }
+
+ /**
+ * Retrieves rewind offset, and then unsets it.
+ * @return bool|int
+ */
+ public function getRewindOffset()
+ {
+ $r = $this->rewindOffset;
+ $this->rewindOffset = false;
+ return $r;
+ }
+
+ /**
+ * Prepares the injector by giving it the config and context objects:
+ * this allows references to important variables to be made within
+ * the injector. This function also checks if the HTML environment
+ * will work with the Injector (see checkNeeded()).
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return bool|string Boolean false if success, string of missing needed element/attribute if failure
+ */
+ public function prepare($config, $context)
+ {
+ $this->htmlDefinition = $config->getHTMLDefinition();
+ // Even though this might fail, some unit tests ignore this and
+ // still test checkNeeded, so be careful. Maybe get rid of that
+ // dependency.
+ $result = $this->checkNeeded($config);
+ if ($result !== false) {
+ return $result;
+ }
+ $this->currentNesting =& $context->get('CurrentNesting');
+ $this->currentToken =& $context->get('CurrentToken');
+ $this->inputZipper =& $context->get('InputZipper');
+ return false;
+ }
+
+ /**
+ * This function checks if the HTML environment
+ * will work with the Injector: if p tags are not allowed, the
+ * Auto-Paragraphing injector should not be enabled.
+ * @param HTMLPurifier_Config $config
+ * @return bool|string Boolean false if success, string of missing needed element/attribute if failure
+ */
+ public function checkNeeded($config)
+ {
+ $def = $config->getHTMLDefinition();
+ foreach ($this->needed as $element => $attributes) {
+ if (is_int($element)) {
+ $element = $attributes;
+ }
+ if (!isset($def->info[$element])) {
+ return $element;
+ }
+ if (!is_array($attributes)) {
+ continue;
+ }
+ foreach ($attributes as $name) {
+ if (!isset($def->info[$element]->attr[$name])) {
+ return "$element.$name";
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Tests if the context node allows a certain element
+ * @param string $name Name of element to test for
+ * @return bool True if element is allowed, false if it is not
+ */
+ public function allowsElement($name)
+ {
+ if (!empty($this->currentNesting)) {
+ $parent_token = array_pop($this->currentNesting);
+ $this->currentNesting[] = $parent_token;
+ $parent = $this->htmlDefinition->info[$parent_token->name];
+ } else {
+ $parent = $this->htmlDefinition->info_parent_def;
+ }
+ if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) {
+ return false;
+ }
+ // check for exclusion
+ if (!empty($this->currentNesting)) {
+ for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) {
+ $node = $this->currentNesting[$i];
+ $def = $this->htmlDefinition->info[$node->name];
+ if (isset($def->excludes[$name])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Iterator function, which starts with the next token and continues until
+ * you reach the end of the input tokens.
+ * @warning Please prevent previous references from interfering with this
+ * functions by setting $i = null beforehand!
+ * @param int $i Current integer index variable for inputTokens
+ * @param HTMLPurifier_Token $current Current token variable.
+ * Do NOT use $token, as that variable is also a reference
+ * @return bool
+ */
+ protected function forward(&$i, &$current)
+ {
+ if ($i === null) {
+ $i = count($this->inputZipper->back) - 1;
+ } else {
+ $i--;
+ }
+ if ($i < 0) {
+ return false;
+ }
+ $current = $this->inputZipper->back[$i];
+ return true;
+ }
+
+ /**
+ * Similar to _forward, but accepts a third parameter $nesting (which
+ * should be initialized at 0) and stops when we hit the end tag
+ * for the node $this->inputIndex starts in.
+ * @param int $i Current integer index variable for inputTokens
+ * @param HTMLPurifier_Token $current Current token variable.
+ * Do NOT use $token, as that variable is also a reference
+ * @param int $nesting
+ * @return bool
+ */
+ protected function forwardUntilEndToken(&$i, &$current, &$nesting)
+ {
+ $result = $this->forward($i, $current);
+ if (!$result) {
+ return false;
+ }
+ if ($nesting === null) {
+ $nesting = 0;
+ }
+ if ($current instanceof HTMLPurifier_Token_Start) {
+ $nesting++;
+ } elseif ($current instanceof HTMLPurifier_Token_End) {
+ if ($nesting <= 0) {
+ return false;
+ }
+ $nesting--;
+ }
+ return true;
+ }
+
+ /**
+ * Iterator function, starts with the previous token and continues until
+ * you reach the beginning of input tokens.
+ * @warning Please prevent previous references from interfering with this
+ * functions by setting $i = null beforehand!
+ * @param int $i Current integer index variable for inputTokens
+ * @param HTMLPurifier_Token $current Current token variable.
+ * Do NOT use $token, as that variable is also a reference
+ * @return bool
+ */
+ protected function backward(&$i, &$current)
+ {
+ if ($i === null) {
+ $i = count($this->inputZipper->front) - 1;
+ } else {
+ $i--;
+ }
+ if ($i < 0) {
+ return false;
+ }
+ $current = $this->inputZipper->front[$i];
+ return true;
+ }
+
+ /**
+ * Handler that is called when a text token is processed
+ */
+ public function handleText(&$token)
+ {
+ }
+
+ /**
+ * Handler that is called when a start or empty token is processed
+ */
+ public function handleElement(&$token)
+ {
+ }
+
+ /**
+ * Handler that is called when an end token is processed
+ */
+ public function handleEnd(&$token)
+ {
+ $this->notifyEnd($token);
+ }
+
+ /**
+ * Notifier that is called when an end token is processed
+ * @param HTMLPurifier_Token $token Current token variable.
+ * @note This differs from handlers in that the token is read-only
+ * @deprecated
+ */
+ public function notifyEnd($token)
+ {
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php
new file mode 100644
index 0000000000..4afdd128d5
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/AutoParagraph.php
@@ -0,0 +1,356 @@
+armor['MakeWellFormed_TagClosedError'] = true;
+ return $par;
+ }
+
+ /**
+ * @param HTMLPurifier_Token_Text $token
+ */
+ public function handleText(&$token)
+ {
+ $text = $token->data;
+ // Does the current parent allow
tags?
+ if ($this->allowsElement('p')) {
+ if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) {
+ // Note that we have differing behavior when dealing with text
+ // in the anonymous root node, or a node inside the document.
+ // If the text as a double-newline, the treatment is the same;
+ // if it doesn't, see the next if-block if you're in the document.
+
+ $i = $nesting = null;
+ if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) {
+ // State 1.1: ... ^ (whitespace, then document end)
+ // ----
+ // This is a degenerate case
+ } else {
+ if (!$token->is_whitespace || $this->_isInline($current)) {
+ // State 1.2: PAR1
+ // ----
+
+ // State 1.3: PAR1\n\nPAR2
+ // ------------
+
+ // State 1.4:
PAR1\n\nPAR2 (see State 2)
+ // ------------
+ $token = array($this->_pStart());
+ $this->_splitText($text, $token);
+ } else {
+ // State 1.5: \n
+ // --
+ }
+ }
+ } else {
+ // State 2:
PAR1... (similar to 1.4)
+ // ----
+
+ // We're in an element that allows paragraph tags, but we're not
+ // sure if we're going to need them.
+ if ($this->_pLookAhead()) {
+ // State 2.1:
PAR1PAR1\n\nPAR2
+ // ----
+ // Note: This will always be the first child, since any
+ // previous inline element would have triggered this very
+ // same routine, and found the double newline. One possible
+ // exception would be a comment.
+ $token = array($this->_pStart(), $token);
+ } else {
+ // State 2.2.1:
PAR1\n\nPAR2
+ // ------------
+ $token = array();
+ $this->_splitText($text, $token);
+ // Abort!
+ } else {
+ // State 4.1: ...PAR1
+ // ----
+
+ // State 4.2: ...PAR1\n\nPAR2
+ // ------------
+ }
+ }
+
+ /**
+ * @param HTMLPurifier_Token $token
+ */
+ public function handleElement(&$token)
+ {
+ // We don't have to check if we're already in a
tag for block
+ // tokens, because the tag would have been autoclosed by MakeWellFormed.
+ if ($this->allowsElement('p')) {
+ if (!empty($this->currentNesting)) {
+ if ($this->_isInline($token)) {
+ // State 1:
...
+ // ---
+ // Check if this token is adjacent to the parent token
+ // (seek backwards until token isn't whitespace)
+ $i = null;
+ $this->backward($i, $prev);
+
+ if (!$prev instanceof HTMLPurifier_Token_Start) {
+ // Token wasn't adjacent
+ if ($prev instanceof HTMLPurifier_Token_Text &&
+ substr($prev->data, -2) === "\n\n"
+ ) {
+ // State 1.1.4:
PAR1
\n\n
+ // ---
+ // Quite frankly, this should be handled by splitText
+ $token = array($this->_pStart(), $token);
+ } else {
+ // State 1.1.1:
PAR1
+ // ---
+ // State 1.1.2:
+ // ---
+ // State 1.1.3:
PAR
+ // ---
+ }
+ } else {
+ // State 1.2.1:
+ // ---
+ // Lookahead to see if
is needed.
+ if ($this->_pLookAhead()) {
+ // State 1.3.1:
+ // -----
+ }
+ } else {
+ if ($this->_isInline($token)) {
+ // State 3.1:
+ // ---
+ // This is where the {p} tag is inserted, not reflected in
+ // inputTokens yet, however.
+ $token = array($this->_pStart(), $token);
+ } else {
+ // State 3.2:
+ // -----
+ }
+
+ $i = null;
+ if ($this->backward($i, $prev)) {
+ if (!$prev instanceof HTMLPurifier_Token_Text) {
+ // State 3.1.1: ...{p}
+ // ---
+ // State 3.2.1: ...
+ // -----
+ if (!is_array($token)) {
+ $token = array($token);
+ }
+ array_unshift($token, new HTMLPurifier_Token_Text("\n\n"));
+ } else {
+ // State 3.1.2: ...\n\n{p}
+ // ---
+ // State 3.2.2: ...\n\n
+ // -----
+ // Note: PAR cannot occur because PAR would have been
+ // wrapped in
tags.
+ }
+ }
+ }
+ } else {
+ // State 2.2:
+ // ----
+ // State 2.4:
+ // ---
+ }
+ }
+
+ /**
+ * Splits up a text in paragraph tokens and appends them
+ * to the result stream that will replace the original
+ * @param string $data String text data that will be processed
+ * into paragraphs
+ * @param HTMLPurifier_Token[] $result Reference to array of tokens that the
+ * tags will be appended onto
+ */
+ private function _splitText($data, &$result)
+ {
+ $raw_paragraphs = explode("\n\n", $data);
+ $paragraphs = array(); // without empty paragraphs
+ $needs_start = false;
+ $needs_end = false;
+
+ $c = count($raw_paragraphs);
+ if ($c == 1) {
+ // There were no double-newlines, abort quickly. In theory this
+ // should never happen.
+ $result[] = new HTMLPurifier_Token_Text($data);
+ return;
+ }
+ for ($i = 0; $i < $c; $i++) {
+ $par = $raw_paragraphs[$i];
+ if (trim($par) !== '') {
+ $paragraphs[] = $par;
+ } else {
+ if ($i == 0) {
+ // Double newline at the front
+ if (empty($result)) {
+ // The empty result indicates that the AutoParagraph
+ // injector did not add any start paragraph tokens.
+ // This means that we have been in a paragraph for
+ // a while, and the newline means we should start a new one.
+ $result[] = new HTMLPurifier_Token_End('p');
+ $result[] = new HTMLPurifier_Token_Text("\n\n");
+ // However, the start token should only be added if
+ // there is more processing to be done (i.e. there are
+ // real paragraphs in here). If there are none, the
+ // next start paragraph tag will be handled by the
+ // next call to the injector
+ $needs_start = true;
+ } else {
+ // We just started a new paragraph!
+ // Reinstate a double-newline for presentation's sake, since
+ // it was in the source code.
+ array_unshift($result, new HTMLPurifier_Token_Text("\n\n"));
+ }
+ } elseif ($i + 1 == $c) {
+ // Double newline at the end
+ // There should be a trailing
when we're finally done.
+ $needs_end = true;
+ }
+ }
+ }
+
+ // Check if this was just a giant blob of whitespace. Move this earlier,
+ // perhaps?
+ if (empty($paragraphs)) {
+ return;
+ }
+
+ // Add the start tag indicated by \n\n at the beginning of $data
+ if ($needs_start) {
+ $result[] = $this->_pStart();
+ }
+
+ // Append the paragraphs onto the result
+ foreach ($paragraphs as $par) {
+ $result[] = new HTMLPurifier_Token_Text($par);
+ $result[] = new HTMLPurifier_Token_End('p');
+ $result[] = new HTMLPurifier_Token_Text("\n\n");
+ $result[] = $this->_pStart();
+ }
+
+ // Remove trailing start token; Injector will handle this later if
+ // it was indeed needed. This prevents from needing to do a lookahead,
+ // at the cost of a lookbehind later.
+ array_pop($result);
+
+ // If there is no need for an end tag, remove all of it and let
+ // MakeWellFormed close it later.
+ if (!$needs_end) {
+ array_pop($result); // removes \n\n
+ array_pop($result); // removes
+ }
+ }
+
+ /**
+ * Returns true if passed token is inline (and, ergo, allowed in
+ * paragraph tags)
+ * @param HTMLPurifier_Token $token
+ * @return bool
+ */
+ private function _isInline($token)
+ {
+ return isset($this->htmlDefinition->info['p']->child->elements[$token->name]);
+ }
+
+ /**
+ * Looks ahead in the token list and determines whether or not we need
+ * to insert a
tag.
+ * @return bool
+ */
+ private function _pLookAhead()
+ {
+ if ($this->currentToken instanceof HTMLPurifier_Token_Start) {
+ $nesting = 1;
+ } else {
+ $nesting = 0;
+ }
+ $ok = false;
+ $i = null;
+ while ($this->forwardUntilEndToken($i, $current, $nesting)) {
+ $result = $this->_checkNeedsP($current);
+ if ($result !== null) {
+ $ok = $result;
+ break;
+ }
+ }
+ return $ok;
+ }
+
+ /**
+ * Determines if a particular token requires an earlier inline token
+ * to get a paragraph. This should be used with _forwardUntilEndToken
+ * @param HTMLPurifier_Token $current
+ * @return bool
+ */
+ private function _checkNeedsP($current)
+ {
+ if ($current instanceof HTMLPurifier_Token_Start) {
+ if (!$this->_isInline($current)) {
+ //
PAR1
+ // ----
+ // Terminate early, since we hit a block element
+ return false;
+ }
+ } elseif ($current instanceof HTMLPurifier_Token_Text) {
+ if (strpos($current->data, "\n\n") !== false) {
+ //
PAR1PAR1...
+ // ----
+ }
+ }
+ return null;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php
new file mode 100644
index 0000000000..c19b1bc271
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/DisplayLinkURI.php
@@ -0,0 +1,40 @@
+start->attr['href'])) {
+ $url = $token->start->attr['href'];
+ unset($token->start->attr['href']);
+ $token = array($token, new HTMLPurifier_Token_Text(" ($url)"));
+ } else {
+ // nothing to display
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php
new file mode 100644
index 0000000000..3b6d70f6e7
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/Linkify.php
@@ -0,0 +1,67 @@
+ array('href'));
+
+ /**
+ * @param HTMLPurifier_Token $token
+ */
+ public function handleText(&$token)
+ {
+ if (!$this->allowsElement('a')) {
+ return;
+ }
+
+ if (strpos($token->data, '://') === false) {
+ // our really quick heuristic failed, abort
+ // this may not work so well if we want to match things like
+ // "google.com", but then again, most people don't
+ return;
+ }
+
+ // there is/are URL(s). Let's split the string.
+ // We use this regex:
+ // https://gist.github.com/gruber/249502
+ // but with @cscott's backtracking fix and also
+ // the Unicode characters un-Unicodified.
+ $bits = preg_split(
+ '/\\b((?:[a-z][\\w\\-]+:(?:\\/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}\\/)(?:[^\\s()<>]|\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\))+(?:\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:\'".,<>?\x{00ab}\x{00bb}\x{201c}\x{201d}\x{2018}\x{2019}]))/iu',
+ $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ if ($bits === false) {
+ return;
+ }
+
+ $token = array();
+
+ // $i = index
+ // $c = count
+ // $l = is link
+ for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
+ if (!$l) {
+ if ($bits[$i] === '') {
+ continue;
+ }
+ $token[] = new HTMLPurifier_Token_Text($bits[$i]);
+ } else {
+ $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i]));
+ $token[] = new HTMLPurifier_Token_Text($bits[$i]);
+ $token[] = new HTMLPurifier_Token_End('a');
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php
new file mode 100644
index 0000000000..cb9046f334
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/PurifierLinkify.php
@@ -0,0 +1,71 @@
+ array('href'));
+
+ /**
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return string
+ */
+ public function prepare($config, $context)
+ {
+ $this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL');
+ return parent::prepare($config, $context);
+ }
+
+ /**
+ * @param HTMLPurifier_Token $token
+ */
+ public function handleText(&$token)
+ {
+ if (!$this->allowsElement('a')) {
+ return;
+ }
+ if (strpos($token->data, '%') === false) {
+ return;
+ }
+
+ $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
+ $token = array();
+
+ // $i = index
+ // $c = count
+ // $l = is link
+ for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) {
+ if (!$l) {
+ if ($bits[$i] === '') {
+ continue;
+ }
+ $token[] = new HTMLPurifier_Token_Text($bits[$i]);
+ } else {
+ $token[] = new HTMLPurifier_Token_Start(
+ 'a',
+ array('href' => str_replace('%s', $bits[$i], $this->docURL))
+ );
+ $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]);
+ $token[] = new HTMLPurifier_Token_End('a');
+ }
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php
new file mode 100644
index 0000000000..0ebc477c68
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveEmpty.php
@@ -0,0 +1,112 @@
+config = $config;
+ $this->context = $context;
+ $this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp');
+ $this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions');
+ $this->exclude = $config->get('AutoFormat.RemoveEmpty.Predicate');
+ foreach ($this->exclude as $key => $attrs) {
+ if (!is_array($attrs)) {
+ // HACK, see HTMLPurifier/Printer/ConfigForm.php
+ $this->exclude[$key] = explode(';', $attrs);
+ }
+ }
+ $this->attrValidator = new HTMLPurifier_AttrValidator();
+ }
+
+ /**
+ * @param HTMLPurifier_Token $token
+ */
+ public function handleElement(&$token)
+ {
+ if (!$token instanceof HTMLPurifier_Token_Start) {
+ return;
+ }
+ $next = false;
+ $deleted = 1; // the current tag
+ for ($i = count($this->inputZipper->back) - 1; $i >= 0; $i--, $deleted++) {
+ $next = $this->inputZipper->back[$i];
+ if ($next instanceof HTMLPurifier_Token_Text) {
+ if ($next->is_whitespace) {
+ continue;
+ }
+ if ($this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) {
+ $plain = str_replace("\xC2\xA0", "", $next->data);
+ $isWsOrNbsp = $plain === '' || ctype_space($plain);
+ if ($isWsOrNbsp) {
+ continue;
+ }
+ }
+ }
+ break;
+ }
+ if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) {
+ $this->attrValidator->validateToken($token, $this->config, $this->context);
+ $token->armor['ValidateAttributes'] = true;
+ if (isset($this->exclude[$token->name])) {
+ $r = true;
+ foreach ($this->exclude[$token->name] as $elem) {
+ if (!isset($token->attr[$elem])) $r = false;
+ }
+ if ($r) return;
+ }
+ if (isset($token->attr['id']) || isset($token->attr['name'])) {
+ return;
+ }
+ $token = $deleted + 1;
+ for ($b = 0, $c = count($this->inputZipper->front); $b < $c; $b++) {
+ $prev = $this->inputZipper->front[$b];
+ if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) {
+ continue;
+ }
+ break;
+ }
+ // This is safe because we removed the token that triggered this.
+ $this->rewindOffset($b+$deleted);
+ return;
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php
new file mode 100644
index 0000000000..42d5144474
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php
@@ -0,0 +1,95 @@
+markForDeletion = new SplObjectStorage();
+ }
+
+ public function prepare($config, $context)
+ {
+ $this->attrValidator = new HTMLPurifier_AttrValidator();
+ $this->config = $config;
+ $this->context = $context;
+ return parent::prepare($config, $context);
+ }
+
+ /**
+ * @param HTMLPurifier_Token $token
+ */
+ public function handleElement(&$token)
+ {
+ if ($token->name !== 'span' || !$token instanceof HTMLPurifier_Token_Start) {
+ return;
+ }
+
+ // We need to validate the attributes now since this doesn't normally
+ // happen until after MakeWellFormed. If all the attributes are removed
+ // the span needs to be removed too.
+ $this->attrValidator->validateToken($token, $this->config, $this->context);
+ $token->armor['ValidateAttributes'] = true;
+
+ if (!empty($token->attr)) {
+ return;
+ }
+
+ $nesting = 0;
+ while ($this->forwardUntilEndToken($i, $current, $nesting)) {
+ }
+
+ if ($current instanceof HTMLPurifier_Token_End && $current->name === 'span') {
+ // Mark closing span tag for deletion
+ $this->markForDeletion->attach($current);
+ // Delete open span tag
+ $token = false;
+ }
+ }
+
+ /**
+ * @param HTMLPurifier_Token $token
+ */
+ public function handleEnd(&$token)
+ {
+ if ($this->markForDeletion->contains($token)) {
+ $this->markForDeletion->detach($token);
+ $token = false;
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php
new file mode 100644
index 0000000000..317f7864dd
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Injector/SafeObject.php
@@ -0,0 +1,124 @@
+ 'never',
+ 'allowNetworking' => 'internal',
+ );
+
+ /**
+ * These are all lower-case keys.
+ * @type array
+ */
+ protected $allowedParam = array(
+ 'wmode' => true,
+ 'movie' => true,
+ 'flashvars' => true,
+ 'src' => true,
+ 'allowfullscreen' => true, // if omitted, assume to be 'false'
+ );
+
+ /**
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return void
+ */
+ public function prepare($config, $context)
+ {
+ parent::prepare($config, $context);
+ }
+
+ /**
+ * @param HTMLPurifier_Token $token
+ */
+ public function handleElement(&$token)
+ {
+ if ($token->name == 'object') {
+ $this->objectStack[] = $token;
+ $this->paramStack[] = array();
+ $new = array($token);
+ foreach ($this->addParam as $name => $value) {
+ $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value));
+ }
+ $token = $new;
+ } elseif ($token->name == 'param') {
+ $nest = count($this->currentNesting) - 1;
+ if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') {
+ $i = count($this->objectStack) - 1;
+ if (!isset($token->attr['name'])) {
+ $token = false;
+ return;
+ }
+ $n = $token->attr['name'];
+ // We need this fix because YouTube doesn't supply a data
+ // attribute, which we need if a type is specified. This is
+ // *very* Flash specific.
+ if (!isset($this->objectStack[$i]->attr['data']) &&
+ ($token->attr['name'] == 'movie' || $token->attr['name'] == 'src')
+ ) {
+ $this->objectStack[$i]->attr['data'] = $token->attr['value'];
+ }
+ // Check if the parameter is the correct value but has not
+ // already been added
+ if (!isset($this->paramStack[$i][$n]) &&
+ isset($this->addParam[$n]) &&
+ $token->attr['name'] === $this->addParam[$n]) {
+ // keep token, and add to param stack
+ $this->paramStack[$i][$n] = true;
+ } elseif (isset($this->allowedParam[strtolower($n)])) {
+ // keep token, don't do anything to it
+ // (could possibly check for duplicates here)
+ // Note: In principle, parameters should be case sensitive.
+ // But it seems they are not really; so accept any case.
+ } else {
+ $token = false;
+ }
+ } else {
+ // not directly inside an object, DENY!
+ $token = false;
+ }
+ }
+ }
+
+ public function handleEnd(&$token)
+ {
+ // This is the WRONG way of handling the object and param stacks;
+ // we should be inserting them directly on the relevant object tokens
+ // so that the global stack handling handles it.
+ if ($token->name == 'object') {
+ array_pop($this->objectStack);
+ array_pop($this->paramStack);
+ }
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language.php
new file mode 100644
index 0000000000..65277dd43c
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language.php
@@ -0,0 +1,204 @@
+config = $config;
+ $this->context = $context;
+ }
+
+ /**
+ * Loads language object with necessary info from factory cache
+ * @note This is a lazy loader
+ */
+ public function load()
+ {
+ if ($this->_loaded) {
+ return;
+ }
+ $factory = HTMLPurifier_LanguageFactory::instance();
+ $factory->loadLanguage($this->code);
+ foreach ($factory->keys as $key) {
+ $this->$key = $factory->cache[$this->code][$key];
+ }
+ $this->_loaded = true;
+ }
+
+ /**
+ * Retrieves a localised message.
+ * @param string $key string identifier of message
+ * @return string localised message
+ */
+ public function getMessage($key)
+ {
+ if (!$this->_loaded) {
+ $this->load();
+ }
+ if (!isset($this->messages[$key])) {
+ return "[$key]";
+ }
+ return $this->messages[$key];
+ }
+
+ /**
+ * Retrieves a localised error name.
+ * @param int $int error number, corresponding to PHP's error reporting
+ * @return string localised message
+ */
+ public function getErrorName($int)
+ {
+ if (!$this->_loaded) {
+ $this->load();
+ }
+ if (!isset($this->errorNames[$int])) {
+ return "[Error: $int]";
+ }
+ return $this->errorNames[$int];
+ }
+
+ /**
+ * Converts an array list into a string readable representation
+ * @param array $array
+ * @return string
+ */
+ public function listify($array)
+ {
+ $sep = $this->getMessage('Item separator');
+ $sep_last = $this->getMessage('Item separator last');
+ $ret = '';
+ for ($i = 0, $c = count($array); $i < $c; $i++) {
+ if ($i == 0) {
+ } elseif ($i + 1 < $c) {
+ $ret .= $sep;
+ } else {
+ $ret .= $sep_last;
+ }
+ $ret .= $array[$i];
+ }
+ return $ret;
+ }
+
+ /**
+ * Formats a localised message with passed parameters
+ * @param string $key string identifier of message
+ * @param array $args Parameters to substitute in
+ * @return string localised message
+ * @todo Implement conditionals? Right now, some messages make
+ * reference to line numbers, but those aren't always available
+ */
+ public function formatMessage($key, $args = array())
+ {
+ if (!$this->_loaded) {
+ $this->load();
+ }
+ if (!isset($this->messages[$key])) {
+ return "[$key]";
+ }
+ $raw = $this->messages[$key];
+ $subst = array();
+ $generator = false;
+ foreach ($args as $i => $value) {
+ if (is_object($value)) {
+ if ($value instanceof HTMLPurifier_Token) {
+ // factor this out some time
+ if (!$generator) {
+ $generator = $this->context->get('Generator');
+ }
+ if (isset($value->name)) {
+ $subst['$'.$i.'.Name'] = $value->name;
+ }
+ if (isset($value->data)) {
+ $subst['$'.$i.'.Data'] = $value->data;
+ }
+ $subst['$'.$i.'.Compact'] =
+ $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value);
+ // a more complex algorithm for compact representation
+ // could be introduced for all types of tokens. This
+ // may need to be factored out into a dedicated class
+ if (!empty($value->attr)) {
+ $stripped_token = clone $value;
+ $stripped_token->attr = array();
+ $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token);
+ }
+ $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown';
+ }
+ continue;
+ } elseif (is_array($value)) {
+ $keys = array_keys($value);
+ if (array_keys($keys) === $keys) {
+ // list
+ $subst['$'.$i] = $this->listify($value);
+ } else {
+ // associative array
+ // no $i implementation yet, sorry
+ $subst['$'.$i.'.Keys'] = $this->listify($keys);
+ $subst['$'.$i.'.Values'] = $this->listify(array_values($value));
+ }
+ continue;
+ }
+ $subst['$' . $i] = $value;
+ }
+ return strtr($raw, $subst);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en.php
new file mode 100644
index 0000000000..c7f197e1e1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Language/messages/en.php
@@ -0,0 +1,55 @@
+ 'HTML Purifier',
+// for unit testing purposes
+ 'LanguageFactoryTest: Pizza' => 'Pizza',
+ 'LanguageTest: List' => '$1',
+ 'LanguageTest: Hash' => '$1.Keys; $1.Values',
+ 'Item separator' => ', ',
+ 'Item separator last' => ' and ', // non-Harvard style
+
+ 'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.',
+ 'ErrorCollector: At line' => ' at line $line',
+ 'ErrorCollector: Incidental errors' => 'Incidental errors',
+ 'Lexer: Unclosed comment' => 'Unclosed comment',
+ 'Lexer: Unescaped lt' => 'Unescaped less-than sign (<) should be <',
+ 'Lexer: Missing gt' => 'Missing greater-than sign (>), previous less-than sign (<) should be escaped',
+ 'Lexer: Missing attribute key' => 'Attribute declaration has no key',
+ 'Lexer: Missing end quote' => 'Attribute declaration has no end quote',
+ 'Lexer: Extracted body' => 'Removed document metadata tags',
+ 'Strategy_RemoveForeignElements: Tag transform' => '<$1> element transformed into $CurrentToken.Serialized',
+ 'Strategy_RemoveForeignElements: Missing required attribute' => '$CurrentToken.Compact element missing required attribute $1',
+ 'Strategy_RemoveForeignElements: Foreign element to text' => 'Unrecognized $CurrentToken.Serialized tag converted to text',
+ 'Strategy_RemoveForeignElements: Foreign element removed' => 'Unrecognized $CurrentToken.Serialized tag removed',
+ 'Strategy_RemoveForeignElements: Comment removed' => 'Comment containing "$CurrentToken.Data" removed',
+ 'Strategy_RemoveForeignElements: Foreign meta element removed' => 'Unrecognized $CurrentToken.Serialized meta tag and all descendants removed',
+ 'Strategy_RemoveForeignElements: Token removed to end' => 'Tags and text starting from $1 element where removed to end',
+ 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' => 'Trailing hyphen(s) in comment removed',
+ 'Strategy_RemoveForeignElements: Hyphens in comment collapsed' => 'Double hyphens in comments are not allowed, and were collapsed into single hyphens',
+ 'Strategy_MakeWellFormed: Unnecessary end tag removed' => 'Unnecessary $CurrentToken.Serialized tag removed',
+ 'Strategy_MakeWellFormed: Unnecessary end tag to text' => 'Unnecessary $CurrentToken.Serialized tag converted to text',
+ 'Strategy_MakeWellFormed: Tag auto closed' => '$1.Compact started on line $1.Line auto-closed by $CurrentToken.Compact',
+ 'Strategy_MakeWellFormed: Tag carryover' => '$1.Compact started on line $1.Line auto-continued into $CurrentToken.Compact',
+ 'Strategy_MakeWellFormed: Stray end tag removed' => 'Stray $CurrentToken.Serialized tag removed',
+ 'Strategy_MakeWellFormed: Stray end tag to text' => 'Stray $CurrentToken.Serialized tag converted to text',
+ 'Strategy_MakeWellFormed: Tag closed by element end' => '$1.Compact tag started on line $1.Line closed by end of $CurrentToken.Serialized',
+ 'Strategy_MakeWellFormed: Tag closed by document end' => '$1.Compact tag started on line $1.Line closed by end of document',
+ 'Strategy_FixNesting: Node removed' => '$CurrentToken.Compact node removed',
+ 'Strategy_FixNesting: Node excluded' => '$CurrentToken.Compact node removed due to descendant exclusion by ancestor element',
+ 'Strategy_FixNesting: Node reorganized' => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model',
+ 'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed',
+ 'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys',
+ 'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed',
+);
+
+$errorNames = array(
+ E_ERROR => 'Error',
+ E_WARNING => 'Warning',
+ E_NOTICE => 'Notice'
+);
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php
new file mode 100644
index 0000000000..16a4f6932d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/LanguageFactory.php
@@ -0,0 +1,209 @@
+cache[$language_code][$key] = $value
+ * @type array
+ */
+ public $cache;
+
+ /**
+ * Valid keys in the HTMLPurifier_Language object. Designates which
+ * variables to slurp out of a message file.
+ * @type array
+ */
+ public $keys = array('fallback', 'messages', 'errorNames');
+
+ /**
+ * Instance to validate language codes.
+ * @type HTMLPurifier_AttrDef_Lang
+ *
+ */
+ protected $validator;
+
+ /**
+ * Cached copy of dirname(__FILE__), directory of current file without
+ * trailing slash.
+ * @type string
+ */
+ protected $dir;
+
+ /**
+ * Keys whose contents are a hash map and can be merged.
+ * @type array
+ */
+ protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true);
+
+ /**
+ * Keys whose contents are a list and can be merged.
+ * @value array lookup
+ */
+ protected $mergeable_keys_list = array();
+
+ /**
+ * Retrieve sole instance of the factory.
+ * @param HTMLPurifier_LanguageFactory $prototype Optional prototype to overload sole instance with,
+ * or bool true to reset to default factory.
+ * @return HTMLPurifier_LanguageFactory
+ */
+ public static function instance($prototype = null)
+ {
+ static $instance = null;
+ if ($prototype !== null) {
+ $instance = $prototype;
+ } elseif ($instance === null || $prototype == true) {
+ $instance = new HTMLPurifier_LanguageFactory();
+ $instance->setup();
+ }
+ return $instance;
+ }
+
+ /**
+ * Sets up the singleton, much like a constructor
+ * @note Prevents people from getting this outside of the singleton
+ */
+ public function setup()
+ {
+ $this->validator = new HTMLPurifier_AttrDef_Lang();
+ $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier';
+ }
+
+ /**
+ * Creates a language object, handles class fallbacks
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @param bool|string $code Code to override configuration with. Private parameter.
+ * @return HTMLPurifier_Language
+ */
+ public function create($config, $context, $code = false)
+ {
+ // validate language code
+ if ($code === false) {
+ $code = $this->validator->validate(
+ $config->get('Core.Language'),
+ $config,
+ $context
+ );
+ } else {
+ $code = $this->validator->validate($code, $config, $context);
+ }
+ if ($code === false) {
+ $code = 'en'; // malformed code becomes English
+ }
+
+ $pcode = str_replace('-', '_', $code); // make valid PHP classname
+ static $depth = 0; // recursion protection
+
+ if ($code == 'en') {
+ $lang = new HTMLPurifier_Language($config, $context);
+ } else {
+ $class = 'HTMLPurifier_Language_' . $pcode;
+ $file = $this->dir . '/Language/classes/' . $code . '.php';
+ if (file_exists($file) || class_exists($class)) {
+ $lang = new $class($config, $context);
+ } else {
+ // Go fallback
+ $raw_fallback = $this->getFallbackFor($code);
+ $fallback = $raw_fallback ? $raw_fallback : 'en';
+ $depth++;
+ $lang = $this->create($config, $context, $fallback);
+ if (!$raw_fallback) {
+ $lang->error = true;
+ }
+ $depth--;
+ }
+ }
+ $lang->code = $code;
+ return $lang;
+ }
+
+ /**
+ * Returns the fallback language for language
+ * @note Loads the original language into cache
+ * @param string $code language code
+ * @return string|bool
+ */
+ public function getFallbackFor($code)
+ {
+ $this->loadLanguage($code);
+ return $this->cache[$code]['fallback'];
+ }
+
+ /**
+ * Loads language into the cache, handles message file and fallbacks
+ * @param string $code language code
+ */
+ public function loadLanguage($code)
+ {
+ static $languages_seen = array(); // recursion guard
+
+ // abort if we've already loaded it
+ if (isset($this->cache[$code])) {
+ return;
+ }
+
+ // generate filename
+ $filename = $this->dir . '/Language/messages/' . $code . '.php';
+
+ // default fallback : may be overwritten by the ensuing include
+ $fallback = ($code != 'en') ? 'en' : false;
+
+ // load primary localisation
+ if (!file_exists($filename)) {
+ // skip the include: will rely solely on fallback
+ $filename = $this->dir . '/Language/messages/en.php';
+ $cache = array();
+ } else {
+ include $filename;
+ $cache = compact($this->keys);
+ }
+
+ // load fallback localisation
+ if (!empty($fallback)) {
+
+ // infinite recursion guard
+ if (isset($languages_seen[$code])) {
+ trigger_error(
+ 'Circular fallback reference in language ' .
+ $code,
+ E_USER_ERROR
+ );
+ $fallback = 'en';
+ }
+ $language_seen[$code] = true;
+
+ // load the fallback recursively
+ $this->loadLanguage($fallback);
+ $fallback_cache = $this->cache[$fallback];
+
+ // merge fallback with current language
+ foreach ($this->keys as $key) {
+ if (isset($cache[$key]) && isset($fallback_cache[$key])) {
+ if (isset($this->mergeable_keys_map[$key])) {
+ $cache[$key] = $cache[$key] + $fallback_cache[$key];
+ } elseif (isset($this->mergeable_keys_list[$key])) {
+ $cache[$key] = array_merge($fallback_cache[$key], $cache[$key]);
+ }
+ } else {
+ $cache[$key] = $fallback_cache[$key];
+ }
+ }
+ }
+
+ // save to cache for later retrieval
+ $this->cache[$code] = $cache;
+ return;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php
new file mode 100644
index 0000000000..b6ea12345d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php
@@ -0,0 +1,162 @@
+ true, 'ex' => true, 'px' => true, 'in' => true,
+ 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true,
+ 'ch' => true, 'rem' => true, 'vw' => true, 'vh' => true,
+ 'vmin' => true, 'vmax' => true
+ );
+
+ /**
+ * @param string $n Magnitude
+ * @param bool|string $u Unit
+ */
+ public function __construct($n = '0', $u = false)
+ {
+ $this->n = (string) $n;
+ $this->unit = $u !== false ? (string) $u : false;
+ }
+
+ /**
+ * @param string $s Unit string, like '2em' or '3.4in'
+ * @return HTMLPurifier_Length
+ * @warning Does not perform validation.
+ */
+ public static function make($s)
+ {
+ if ($s instanceof HTMLPurifier_Length) {
+ return $s;
+ }
+ $n_length = strspn($s, '1234567890.+-');
+ $n = substr($s, 0, $n_length);
+ $unit = substr($s, $n_length);
+ if ($unit === '') {
+ $unit = false;
+ }
+ return new HTMLPurifier_Length($n, $unit);
+ }
+
+ /**
+ * Validates the number and unit.
+ * @return bool
+ */
+ protected function validate()
+ {
+ // Special case:
+ if ($this->n === '+0' || $this->n === '-0') {
+ $this->n = '0';
+ }
+ if ($this->n === '0' && $this->unit === false) {
+ return true;
+ }
+ if ($this->unit === false || !ctype_lower($this->unit)) {
+ $this->unit = strtolower($this->unit);
+ }
+ if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) {
+ return false;
+ }
+ // Hack:
+ $def = new HTMLPurifier_AttrDef_CSS_Number();
+ $result = $def->validate($this->n, false, false);
+ if ($result === false) {
+ return false;
+ }
+ $this->n = $result;
+ return true;
+ }
+
+ /**
+ * Returns string representation of number.
+ * @return string
+ */
+ public function toString()
+ {
+ if (!$this->isValid()) {
+ return false;
+ }
+ return $this->n . $this->unit;
+ }
+
+ /**
+ * Retrieves string numeric magnitude.
+ * @return string
+ */
+ public function getN()
+ {
+ return $this->n;
+ }
+
+ /**
+ * Retrieves string unit.
+ * @return string
+ */
+ public function getUnit()
+ {
+ return $this->unit;
+ }
+
+ /**
+ * Returns true if this length unit is valid.
+ * @return bool
+ */
+ public function isValid()
+ {
+ if ($this->isValid === null) {
+ $this->isValid = $this->validate();
+ }
+ return $this->isValid;
+ }
+
+ /**
+ * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal.
+ * @param HTMLPurifier_Length $l
+ * @return int
+ * @warning If both values are too large or small, this calculation will
+ * not work properly
+ */
+ public function compareTo($l)
+ {
+ if ($l === false) {
+ return false;
+ }
+ if ($l->unit !== $this->unit) {
+ $converter = new HTMLPurifier_UnitConverter();
+ $l = $converter->convert($l, $this->unit);
+ if ($l === false) {
+ return false;
+ }
+ }
+ return $this->n - $l->n;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php
new file mode 100644
index 0000000000..fe87fa4ba1
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php
@@ -0,0 +1,371 @@
+get('Core.LexerImpl');
+ }
+
+ $needs_tracking =
+ $config->get('Core.MaintainLineNumbers') ||
+ $config->get('Core.CollectErrors');
+
+ $inst = null;
+ if (is_object($lexer)) {
+ $inst = $lexer;
+ } else {
+ if (is_null($lexer)) {
+ do {
+ // auto-detection algorithm
+ if ($needs_tracking) {
+ $lexer = 'DirectLex';
+ break;
+ }
+
+ if (class_exists('DOMDocument') &&
+ method_exists('DOMDocument', 'loadHTML') &&
+ !extension_loaded('domxml')
+ ) {
+ // check for DOM support, because while it's part of the
+ // core, it can be disabled compile time. Also, the PECL
+ // domxml extension overrides the default DOM, and is evil
+ // and nasty and we shan't bother to support it
+ $lexer = 'DOMLex';
+ } else {
+ $lexer = 'DirectLex';
+ }
+ } while (0);
+ } // do..while so we can break
+
+ // instantiate recognized string names
+ switch ($lexer) {
+ case 'DOMLex':
+ $inst = new HTMLPurifier_Lexer_DOMLex();
+ break;
+ case 'DirectLex':
+ $inst = new HTMLPurifier_Lexer_DirectLex();
+ break;
+ case 'PH5P':
+ $inst = new HTMLPurifier_Lexer_PH5P();
+ break;
+ default:
+ throw new HTMLPurifier_Exception(
+ "Cannot instantiate unrecognized Lexer type " .
+ htmlspecialchars($lexer)
+ );
+ }
+ }
+
+ if (!$inst) {
+ throw new HTMLPurifier_Exception('No lexer was instantiated');
+ }
+
+ // once PHP DOM implements native line numbers, or we
+ // hack out something using XSLT, remove this stipulation
+ if ($needs_tracking && !$inst->tracksLineNumbers) {
+ throw new HTMLPurifier_Exception(
+ 'Cannot use lexer that does not support line numbers with ' .
+ 'Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)'
+ );
+ }
+
+ return $inst;
+
+ }
+
+ // -- CONVENIENCE MEMBERS ---------------------------------------------
+
+ public function __construct()
+ {
+ $this->_entity_parser = new HTMLPurifier_EntityParser();
+ }
+
+ /**
+ * Most common entity to raw value conversion table for special entities.
+ * @type array
+ */
+ protected $_special_entity2str =
+ array(
+ '"' => '"',
+ '&' => '&',
+ '<' => '<',
+ '>' => '>',
+ ''' => "'",
+ ''' => "'",
+ ''' => "'"
+ );
+
+ public function parseText($string, $config) {
+ return $this->parseData($string, false, $config);
+ }
+
+ public function parseAttr($string, $config) {
+ return $this->parseData($string, true, $config);
+ }
+
+ /**
+ * Parses special entities into the proper characters.
+ *
+ * This string will translate escaped versions of the special characters
+ * into the correct ones.
+ *
+ * @param string $string String character data to be parsed.
+ * @return string Parsed character data.
+ */
+ public function parseData($string, $is_attr, $config)
+ {
+ // following functions require at least one character
+ if ($string === '') {
+ return '';
+ }
+
+ // subtracts amps that cannot possibly be escaped
+ $num_amp = substr_count($string, '&') - substr_count($string, '& ') -
+ ($string[strlen($string) - 1] === '&' ? 1 : 0);
+
+ if (!$num_amp) {
+ return $string;
+ } // abort if no entities
+ $num_esc_amp = substr_count($string, '&');
+ $string = strtr($string, $this->_special_entity2str);
+
+ // code duplication for sake of optimization, see above
+ $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') -
+ ($string[strlen($string) - 1] === '&' ? 1 : 0);
+
+ if ($num_amp_2 <= $num_esc_amp) {
+ return $string;
+ }
+
+ // hmm... now we have some uncommon entities. Use the callback.
+ if ($config->get('Core.LegacyEntityDecoder')) {
+ $string = $this->_entity_parser->substituteSpecialEntities($string);
+ } else {
+ if ($is_attr) {
+ $string = $this->_entity_parser->substituteAttrEntities($string);
+ } else {
+ $string = $this->_entity_parser->substituteTextEntities($string);
+ }
+ }
+ return $string;
+ }
+
+ /**
+ * Lexes an HTML string into tokens.
+ * @param $string String HTML.
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return HTMLPurifier_Token[] array representation of HTML.
+ */
+ public function tokenizeHTML($string, $config, $context)
+ {
+ trigger_error('Call to abstract class', E_USER_ERROR);
+ }
+
+ /**
+ * Translates CDATA sections into regular sections (through escaping).
+ * @param string $string HTML string to process.
+ * @return string HTML with CDATA sections escaped.
+ */
+ protected static function escapeCDATA($string)
+ {
+ return preg_replace_callback(
+ '//s',
+ array('HTMLPurifier_Lexer', 'CDATACallback'),
+ $string
+ );
+ }
+
+ /**
+ * Special CDATA case that is especially convoluted for #i', '', $html);
+ }
+
+ return $html;
+ }
+
+ /**
+ * Takes a string of HTML (fragment or document) and returns the content
+ * @todo Consider making protected
+ */
+ public function extractBody($html)
+ {
+ $matches = array();
+ $result = preg_match('|(.*?)]*>(.*)|is', $html, $matches);
+ if ($result) {
+ // Make sure it's not in a comment
+ $comment_start = strrpos($matches[1], '');
+ if ($comment_start === false ||
+ ($comment_end !== false && $comment_end > $comment_start)) {
+ return $matches[2];
+ }
+ }
+ return $html;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php
new file mode 100644
index 0000000000..7d579837c4
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php
@@ -0,0 +1,340 @@
+factory = new HTMLPurifier_TokenFactory();
+ }
+
+ /**
+ * @param string $html
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return HTMLPurifier_Token[]
+ */
+ public function tokenizeHTML($html, $config, $context)
+ {
+ $html = $this->normalize($html, $config, $context);
+
+ // attempt to armor stray angled brackets that cannot possibly
+ // form tags and thus are probably being used as emoticons
+ if ($config->get('Core.AggressivelyFixLt')) {
+ $char = '[^a-z!\/]';
+ $comment = "/|\z)/is";
+ $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
+ do {
+ $old = $html;
+ $html = preg_replace("/<($char)/i", '<\\1', $html);
+ } while ($html !== $old);
+ $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments
+ }
+
+ // preprocess html, essential for UTF-8
+ $html = $this->wrapHTML($html, $config, $context);
+
+ $doc = new DOMDocument();
+ $doc->encoding = 'UTF-8'; // theoretically, the above has this covered
+
+ $options = 0;
+ if ($config->get('Core.AllowParseManyTags') && defined('LIBXML_PARSEHUGE')) {
+ $options |= LIBXML_PARSEHUGE;
+ }
+ if ($config->get('Core.RemoveBlanks') && defined('LIBXML_NOBLANKS')) {
+ $options |= LIBXML_NOBLANKS;
+ }
+
+ set_error_handler(array($this, 'muteErrorHandler'));
+ // loadHTML() fails on PHP 5.3 when second parameter is given
+ if ($options) {
+ $doc->loadHTML($html, $options);
+ } else {
+ $doc->loadHTML($html);
+ }
+ restore_error_handler();
+
+ $body = $doc->getElementsByTagName('html')->item(0)-> //
+ getElementsByTagName('body')->item(0); //
+
+ $div = $body->getElementsByTagName('div')->item(0); //
+ $tokens = array();
+ $this->tokenizeDOM($div, $tokens, $config);
+ // If the div has a sibling, that means we tripped across
+ // a premature
tag. So remove the div we parsed,
+ // and then tokenize the rest of body. We can't tokenize
+ // the sibling directly as we'll lose the tags in that case.
+ if ($div->nextSibling) {
+ $body->removeChild($div);
+ $this->tokenizeDOM($body, $tokens, $config);
+ }
+ return $tokens;
+ }
+
+ /**
+ * Iterative function that tokenizes a node, putting it into an accumulator.
+ * To iterate is human, to recurse divine - L. Peter Deutsch
+ * @param DOMNode $node DOMNode to be tokenized.
+ * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens.
+ */
+ protected function tokenizeDOM($node, &$tokens, $config)
+ {
+ $level = 0;
+ $nodes = array($level => new HTMLPurifier_Queue(array($node)));
+ $closingNodes = array();
+ do {
+ while (!$nodes[$level]->isEmpty()) {
+ $node = $nodes[$level]->shift(); // FIFO
+ $collect = $level > 0 ? true : false;
+ $needEndingTag = $this->createStartNode($node, $tokens, $collect, $config);
+ if ($needEndingTag) {
+ $closingNodes[$level][] = $node;
+ }
+ if ($node->childNodes && $node->childNodes->length) {
+ $level++;
+ $nodes[$level] = new HTMLPurifier_Queue();
+ foreach ($node->childNodes as $childNode) {
+ $nodes[$level]->push($childNode);
+ }
+ }
+ }
+ $level--;
+ if ($level && isset($closingNodes[$level])) {
+ while ($node = array_pop($closingNodes[$level])) {
+ $this->createEndNode($node, $tokens);
+ }
+ }
+ } while ($level > 0);
+ }
+
+ /**
+ * Portably retrieve the tag name of a node; deals with older versions
+ * of libxml like 2.7.6
+ * @param DOMNode $node
+ */
+ protected function getTagName($node)
+ {
+ if (isset($node->tagName)) {
+ return $node->tagName;
+ } else if (isset($node->nodeName)) {
+ return $node->nodeName;
+ } else if (isset($node->localName)) {
+ return $node->localName;
+ }
+ return null;
+ }
+
+ /**
+ * Portably retrieve the data of a node; deals with older versions
+ * of libxml like 2.7.6
+ * @param DOMNode $node
+ */
+ protected function getData($node)
+ {
+ if (isset($node->data)) {
+ return $node->data;
+ } else if (isset($node->nodeValue)) {
+ return $node->nodeValue;
+ } else if (isset($node->textContent)) {
+ return $node->textContent;
+ }
+ return null;
+ }
+
+
+ /**
+ * @param DOMNode $node DOMNode to be tokenized.
+ * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens.
+ * @param bool $collect Says whether or start and close are collected, set to
+ * false at first recursion because it's the implicit DIV
+ * tag you're dealing with.
+ * @return bool if the token needs an endtoken
+ * @todo data and tagName properties don't seem to exist in DOMNode?
+ */
+ protected function createStartNode($node, &$tokens, $collect, $config)
+ {
+ // intercept non element nodes. WE MUST catch all of them,
+ // but we're not getting the character reference nodes because
+ // those should have been preprocessed
+ if ($node->nodeType === XML_TEXT_NODE) {
+ $data = $this->getData($node); // Handle variable data property
+ if ($data !== null) {
+ $tokens[] = $this->factory->createText($data);
+ }
+ return false;
+ } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) {
+ // undo libxml's special treatment of )#si',
+ array($this, 'scriptCallback'),
+ $html
+ );
+ }
+
+ $html = $this->normalize($html, $config, $context);
+
+ $cursor = 0; // our location in the text
+ $inside_tag = false; // whether or not we're parsing the inside of a tag
+ $array = array(); // result array
+
+ // This is also treated to mean maintain *column* numbers too
+ $maintain_line_numbers = $config->get('Core.MaintainLineNumbers');
+
+ if ($maintain_line_numbers === null) {
+ // automatically determine line numbering by checking
+ // if error collection is on
+ $maintain_line_numbers = $config->get('Core.CollectErrors');
+ }
+
+ if ($maintain_line_numbers) {
+ $current_line = 1;
+ $current_col = 0;
+ $length = strlen($html);
+ } else {
+ $current_line = false;
+ $current_col = false;
+ $length = false;
+ }
+ $context->register('CurrentLine', $current_line);
+ $context->register('CurrentCol', $current_col);
+ $nl = "\n";
+ // how often to manually recalculate. This will ALWAYS be right,
+ // but it's pretty wasteful. Set to 0 to turn off
+ $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval');
+
+ $e = false;
+ if ($config->get('Core.CollectErrors')) {
+ $e =& $context->get('ErrorCollector');
+ }
+
+ // for testing synchronization
+ $loops = 0;
+
+ while (++$loops) {
+ // $cursor is either at the start of a token, or inside of
+ // a tag (i.e. there was a < immediately before it), as indicated
+ // by $inside_tag
+
+ if ($maintain_line_numbers) {
+ // $rcursor, however, is always at the start of a token.
+ $rcursor = $cursor - (int)$inside_tag;
+
+ // Column number is cheap, so we calculate it every round.
+ // We're interested at the *end* of the newline string, so
+ // we need to add strlen($nl) == 1 to $nl_pos before subtracting it
+ // from our "rcursor" position.
+ $nl_pos = strrpos($html, $nl, $rcursor - $length);
+ $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
+
+ // recalculate lines
+ if ($synchronize_interval && // synchronization is on
+ $cursor > 0 && // cursor is further than zero
+ $loops % $synchronize_interval === 0) { // time to synchronize!
+ $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
+ }
+ }
+
+ $position_next_lt = strpos($html, '<', $cursor);
+ $position_next_gt = strpos($html, '>', $cursor);
+
+ // triggers on "asdf" but not "asdf "
+ // special case to set up context
+ if ($position_next_lt === $cursor) {
+ $inside_tag = true;
+ $cursor++;
+ }
+
+ if (!$inside_tag && $position_next_lt !== false) {
+ // We are not inside tag and there still is another tag to parse
+ $token = new
+ HTMLPurifier_Token_Text(
+ $this->parseText(
+ substr(
+ $html,
+ $cursor,
+ $position_next_lt - $cursor
+ ), $config
+ )
+ );
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
+ }
+ $array[] = $token;
+ $cursor = $position_next_lt + 1;
+ $inside_tag = true;
+ continue;
+ } elseif (!$inside_tag) {
+ // We are not inside tag but there are no more tags
+ // If we're already at the end, break
+ if ($cursor === strlen($html)) {
+ break;
+ }
+ // Create Text of rest of string
+ $token = new
+ HTMLPurifier_Token_Text(
+ $this->parseText(
+ substr(
+ $html,
+ $cursor
+ ), $config
+ )
+ );
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ }
+ $array[] = $token;
+ break;
+ } elseif ($inside_tag && $position_next_gt !== false) {
+ // We are in tag and it is well formed
+ // Grab the internals of the tag
+ $strlen_segment = $position_next_gt - $cursor;
+
+ if ($strlen_segment < 1) {
+ // there's nothing to process!
+ $token = new HTMLPurifier_Token_Text('<');
+ $cursor++;
+ continue;
+ }
+
+ $segment = substr($html, $cursor, $strlen_segment);
+
+ if ($segment === false) {
+ // somehow, we attempted to access beyond the end of
+ // the string, defense-in-depth, reported by Nate Abele
+ break;
+ }
+
+ // Check if it's a comment
+ if (substr($segment, 0, 3) === '!--') {
+ // re-determine segment length, looking for -->
+ $position_comment_end = strpos($html, '-->', $cursor);
+ if ($position_comment_end === false) {
+ // uh oh, we have a comment that extends to
+ // infinity. Can't be helped: set comment
+ // end position to end of string
+ if ($e) {
+ $e->send(E_WARNING, 'Lexer: Unclosed comment');
+ }
+ $position_comment_end = strlen($html);
+ $end = true;
+ } else {
+ $end = false;
+ }
+ $strlen_segment = $position_comment_end - $cursor;
+ $segment = substr($html, $cursor, $strlen_segment);
+ $token = new
+ HTMLPurifier_Token_Comment(
+ substr(
+ $segment,
+ 3,
+ $strlen_segment - 3
+ )
+ );
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
+ }
+ $array[] = $token;
+ $cursor = $end ? $position_comment_end : $position_comment_end + 3;
+ $inside_tag = false;
+ continue;
+ }
+
+ // Check if it's an end tag
+ $is_end_tag = (strpos($segment, '/') === 0);
+ if ($is_end_tag) {
+ $type = substr($segment, 1);
+ $token = new HTMLPurifier_Token_End($type);
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $inside_tag = false;
+ $cursor = $position_next_gt + 1;
+ continue;
+ }
+
+ // Check leading character is alnum, if not, we may
+ // have accidently grabbed an emoticon. Translate into
+ // text and go our merry way
+ if (!ctype_alpha($segment[0])) {
+ // XML: $segment[0] !== '_' && $segment[0] !== ':'
+ if ($e) {
+ $e->send(E_NOTICE, 'Lexer: Unescaped lt');
+ }
+ $token = new HTMLPurifier_Token_Text('<');
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $inside_tag = false;
+ continue;
+ }
+
+ // Check if it is explicitly self closing, if so, remove
+ // trailing slash. Remember, we could have a tag like , so
+ // any later token processing scripts must convert improperly
+ // classified EmptyTags from StartTags.
+ $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1);
+ if ($is_self_closing) {
+ $strlen_segment--;
+ $segment = substr($segment, 0, $strlen_segment);
+ }
+
+ // Check if there are any attributes
+ $position_first_space = strcspn($segment, $this->_whitespace);
+
+ if ($position_first_space >= $strlen_segment) {
+ if ($is_self_closing) {
+ $token = new HTMLPurifier_Token_Empty($segment);
+ } else {
+ $token = new HTMLPurifier_Token_Start($segment);
+ }
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $inside_tag = false;
+ $cursor = $position_next_gt + 1;
+ continue;
+ }
+
+ // Grab out all the data
+ $type = substr($segment, 0, $position_first_space);
+ $attribute_string =
+ trim(
+ substr(
+ $segment,
+ $position_first_space
+ )
+ );
+ if ($attribute_string) {
+ $attr = $this->parseAttributeString(
+ $attribute_string,
+ $config,
+ $context
+ );
+ } else {
+ $attr = array();
+ }
+
+ if ($is_self_closing) {
+ $token = new HTMLPurifier_Token_Empty($type, $attr);
+ } else {
+ $token = new HTMLPurifier_Token_Start($type, $attr);
+ }
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
+ }
+ $array[] = $token;
+ $cursor = $position_next_gt + 1;
+ $inside_tag = false;
+ continue;
+ } else {
+ // inside tag, but there's no ending > sign
+ if ($e) {
+ $e->send(E_WARNING, 'Lexer: Missing gt');
+ }
+ $token = new
+ HTMLPurifier_Token_Text(
+ '<' .
+ $this->parseText(
+ substr($html, $cursor), $config
+ )
+ );
+ if ($maintain_line_numbers) {
+ $token->rawPosition($current_line, $current_col);
+ }
+ // no cursor scroll? Hmm...
+ $array[] = $token;
+ break;
+ }
+ break;
+ }
+
+ $context->destroy('CurrentLine');
+ $context->destroy('CurrentCol');
+ return $array;
+ }
+
+ /**
+ * PHP 5.0.x compatible substr_count that implements offset and length
+ * @param string $haystack
+ * @param string $needle
+ * @param int $offset
+ * @param int $length
+ * @return int
+ */
+ protected function substrCount($haystack, $needle, $offset, $length)
+ {
+ static $oldVersion;
+ if ($oldVersion === null) {
+ $oldVersion = version_compare(PHP_VERSION, '5.1', '<');
+ }
+ if ($oldVersion) {
+ $haystack = substr($haystack, $offset, $length);
+ return substr_count($haystack, $needle);
+ } else {
+ return substr_count($haystack, $needle, $offset, $length);
+ }
+ }
+
+ /**
+ * Takes the inside of an HTML tag and makes an assoc array of attributes.
+ *
+ * @param string $string Inside of tag excluding name.
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array Assoc array of attributes.
+ */
+ public function parseAttributeString($string, $config, $context)
+ {
+ $string = (string)$string; // quick typecast
+
+ if ($string == '') {
+ return array();
+ } // no attributes
+
+ $e = false;
+ if ($config->get('Core.CollectErrors')) {
+ $e =& $context->get('ErrorCollector');
+ }
+
+ // let's see if we can abort as quickly as possible
+ // one equal sign, no spaces => one attribute
+ $num_equal = substr_count($string, '=');
+ $has_space = strpos($string, ' ');
+ if ($num_equal === 0 && !$has_space) {
+ // bool attribute
+ return array($string => $string);
+ } elseif ($num_equal === 1 && !$has_space) {
+ // only one attribute
+ list($key, $quoted_value) = explode('=', $string);
+ $quoted_value = trim($quoted_value);
+ if (!$key) {
+ if ($e) {
+ $e->send(E_ERROR, 'Lexer: Missing attribute key');
+ }
+ return array();
+ }
+ if (!$quoted_value) {
+ return array($key => '');
+ }
+ $first_char = @$quoted_value[0];
+ $last_char = @$quoted_value[strlen($quoted_value) - 1];
+
+ $same_quote = ($first_char == $last_char);
+ $open_quote = ($first_char == '"' || $first_char == "'");
+
+ if ($same_quote && $open_quote) {
+ // well behaved
+ $value = substr($quoted_value, 1, strlen($quoted_value) - 2);
+ } else {
+ // not well behaved
+ if ($open_quote) {
+ if ($e) {
+ $e->send(E_ERROR, 'Lexer: Missing end quote');
+ }
+ $value = substr($quoted_value, 1);
+ } else {
+ $value = $quoted_value;
+ }
+ }
+ if ($value === false) {
+ $value = '';
+ }
+ return array($key => $this->parseAttr($value, $config));
+ }
+
+ // setup loop environment
+ $array = array(); // return assoc array of attributes
+ $cursor = 0; // current position in string (moves forward)
+ $size = strlen($string); // size of the string (stays the same)
+
+ // if we have unquoted attributes, the parser expects a terminating
+ // space, so let's guarantee that there's always a terminating space.
+ $string .= ' ';
+
+ $old_cursor = -1;
+ while ($cursor < $size) {
+ if ($old_cursor >= $cursor) {
+ throw new Exception("Infinite loop detected");
+ }
+ $old_cursor = $cursor;
+
+ $cursor += ($value = strspn($string, $this->_whitespace, $cursor));
+ // grab the key
+
+ $key_begin = $cursor; //we're currently at the start of the key
+
+ // scroll past all characters that are the key (not whitespace or =)
+ $cursor += strcspn($string, $this->_whitespace . '=', $cursor);
+
+ $key_end = $cursor; // now at the end of the key
+
+ $key = substr($string, $key_begin, $key_end - $key_begin);
+
+ if (!$key) {
+ if ($e) {
+ $e->send(E_ERROR, 'Lexer: Missing attribute key');
+ }
+ $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop
+ continue; // empty key
+ }
+
+ // scroll past all whitespace
+ $cursor += strspn($string, $this->_whitespace, $cursor);
+
+ if ($cursor >= $size) {
+ $array[$key] = $key;
+ break;
+ }
+
+ // if the next character is an equal sign, we've got a regular
+ // pair, otherwise, it's a bool attribute
+ $first_char = @$string[$cursor];
+
+ if ($first_char == '=') {
+ // key="value"
+
+ $cursor++;
+ $cursor += strspn($string, $this->_whitespace, $cursor);
+
+ if ($cursor === false) {
+ $array[$key] = '';
+ break;
+ }
+
+ // we might be in front of a quote right now
+
+ $char = @$string[$cursor];
+
+ if ($char == '"' || $char == "'") {
+ // it's quoted, end bound is $char
+ $cursor++;
+ $value_begin = $cursor;
+ $cursor = strpos($string, $char, $cursor);
+ $value_end = $cursor;
+ } else {
+ // it's not quoted, end bound is whitespace
+ $value_begin = $cursor;
+ $cursor += strcspn($string, $this->_whitespace, $cursor);
+ $value_end = $cursor;
+ }
+
+ // we reached a premature end
+ if ($cursor === false) {
+ $cursor = $size;
+ $value_end = $cursor;
+ }
+
+ $value = substr($string, $value_begin, $value_end - $value_begin);
+ if ($value === false) {
+ $value = '';
+ }
+ $array[$key] = $this->parseAttr($value, $config);
+ $cursor++;
+ } else {
+ // boolattr
+ if ($key !== '') {
+ $array[$key] = $key;
+ } else {
+ // purely theoretical
+ if ($e) {
+ $e->send(E_ERROR, 'Lexer: Missing attribute key');
+ }
+ }
+ }
+ }
+ return $array;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php
new file mode 100644
index 0000000000..1564f283dc
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/PH5P.php
@@ -0,0 +1,4788 @@
+normalize($html, $config, $context);
+ $new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */);
+ try {
+ $parser = new HTML5($new_html);
+ $doc = $parser->save();
+ } catch (DOMException $e) {
+ // Uh oh, it failed. Punt to DirectLex.
+ $lexer = new HTMLPurifier_Lexer_DirectLex();
+ $context->register('PH5PError', $e); // save the error, so we can detect it
+ return $lexer->tokenizeHTML($html, $config, $context); // use original HTML
+ }
+ $tokens = array();
+ $this->tokenizeDOM(
+ $doc->getElementsByTagName('html')->item(0)-> //
+ getElementsByTagName('body')->item(0) //
+ ,
+ $tokens, $config
+ );
+ return $tokens;
+ }
+}
+
+/*
+
+Copyright 2007 Jeroen van der Meer
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+class HTML5
+{
+ private $data;
+ private $char;
+ private $EOF;
+ private $state;
+ private $tree;
+ private $token;
+ private $content_model;
+ private $escape = false;
+ private $entities = array(
+ 'AElig;',
+ 'AElig',
+ 'AMP;',
+ 'AMP',
+ 'Aacute;',
+ 'Aacute',
+ 'Acirc;',
+ 'Acirc',
+ 'Agrave;',
+ 'Agrave',
+ 'Alpha;',
+ 'Aring;',
+ 'Aring',
+ 'Atilde;',
+ 'Atilde',
+ 'Auml;',
+ 'Auml',
+ 'Beta;',
+ 'COPY;',
+ 'COPY',
+ 'Ccedil;',
+ 'Ccedil',
+ 'Chi;',
+ 'Dagger;',
+ 'Delta;',
+ 'ETH;',
+ 'ETH',
+ 'Eacute;',
+ 'Eacute',
+ 'Ecirc;',
+ 'Ecirc',
+ 'Egrave;',
+ 'Egrave',
+ 'Epsilon;',
+ 'Eta;',
+ 'Euml;',
+ 'Euml',
+ 'GT;',
+ 'GT',
+ 'Gamma;',
+ 'Iacute;',
+ 'Iacute',
+ 'Icirc;',
+ 'Icirc',
+ 'Igrave;',
+ 'Igrave',
+ 'Iota;',
+ 'Iuml;',
+ 'Iuml',
+ 'Kappa;',
+ 'LT;',
+ 'LT',
+ 'Lambda;',
+ 'Mu;',
+ 'Ntilde;',
+ 'Ntilde',
+ 'Nu;',
+ 'OElig;',
+ 'Oacute;',
+ 'Oacute',
+ 'Ocirc;',
+ 'Ocirc',
+ 'Ograve;',
+ 'Ograve',
+ 'Omega;',
+ 'Omicron;',
+ 'Oslash;',
+ 'Oslash',
+ 'Otilde;',
+ 'Otilde',
+ 'Ouml;',
+ 'Ouml',
+ 'Phi;',
+ 'Pi;',
+ 'Prime;',
+ 'Psi;',
+ 'QUOT;',
+ 'QUOT',
+ 'REG;',
+ 'REG',
+ 'Rho;',
+ 'Scaron;',
+ 'Sigma;',
+ 'THORN;',
+ 'THORN',
+ 'TRADE;',
+ 'Tau;',
+ 'Theta;',
+ 'Uacute;',
+ 'Uacute',
+ 'Ucirc;',
+ 'Ucirc',
+ 'Ugrave;',
+ 'Ugrave',
+ 'Upsilon;',
+ 'Uuml;',
+ 'Uuml',
+ 'Xi;',
+ 'Yacute;',
+ 'Yacute',
+ 'Yuml;',
+ 'Zeta;',
+ 'aacute;',
+ 'aacute',
+ 'acirc;',
+ 'acirc',
+ 'acute;',
+ 'acute',
+ 'aelig;',
+ 'aelig',
+ 'agrave;',
+ 'agrave',
+ 'alefsym;',
+ 'alpha;',
+ 'amp;',
+ 'amp',
+ 'and;',
+ 'ang;',
+ 'apos;',
+ 'aring;',
+ 'aring',
+ 'asymp;',
+ 'atilde;',
+ 'atilde',
+ 'auml;',
+ 'auml',
+ 'bdquo;',
+ 'beta;',
+ 'brvbar;',
+ 'brvbar',
+ 'bull;',
+ 'cap;',
+ 'ccedil;',
+ 'ccedil',
+ 'cedil;',
+ 'cedil',
+ 'cent;',
+ 'cent',
+ 'chi;',
+ 'circ;',
+ 'clubs;',
+ 'cong;',
+ 'copy;',
+ 'copy',
+ 'crarr;',
+ 'cup;',
+ 'curren;',
+ 'curren',
+ 'dArr;',
+ 'dagger;',
+ 'darr;',
+ 'deg;',
+ 'deg',
+ 'delta;',
+ 'diams;',
+ 'divide;',
+ 'divide',
+ 'eacute;',
+ 'eacute',
+ 'ecirc;',
+ 'ecirc',
+ 'egrave;',
+ 'egrave',
+ 'empty;',
+ 'emsp;',
+ 'ensp;',
+ 'epsilon;',
+ 'equiv;',
+ 'eta;',
+ 'eth;',
+ 'eth',
+ 'euml;',
+ 'euml',
+ 'euro;',
+ 'exist;',
+ 'fnof;',
+ 'forall;',
+ 'frac12;',
+ 'frac12',
+ 'frac14;',
+ 'frac14',
+ 'frac34;',
+ 'frac34',
+ 'frasl;',
+ 'gamma;',
+ 'ge;',
+ 'gt;',
+ 'gt',
+ 'hArr;',
+ 'harr;',
+ 'hearts;',
+ 'hellip;',
+ 'iacute;',
+ 'iacute',
+ 'icirc;',
+ 'icirc',
+ 'iexcl;',
+ 'iexcl',
+ 'igrave;',
+ 'igrave',
+ 'image;',
+ 'infin;',
+ 'int;',
+ 'iota;',
+ 'iquest;',
+ 'iquest',
+ 'isin;',
+ 'iuml;',
+ 'iuml',
+ 'kappa;',
+ 'lArr;',
+ 'lambda;',
+ 'lang;',
+ 'laquo;',
+ 'laquo',
+ 'larr;',
+ 'lceil;',
+ 'ldquo;',
+ 'le;',
+ 'lfloor;',
+ 'lowast;',
+ 'loz;',
+ 'lrm;',
+ 'lsaquo;',
+ 'lsquo;',
+ 'lt;',
+ 'lt',
+ 'macr;',
+ 'macr',
+ 'mdash;',
+ 'micro;',
+ 'micro',
+ 'middot;',
+ 'middot',
+ 'minus;',
+ 'mu;',
+ 'nabla;',
+ 'nbsp;',
+ 'nbsp',
+ 'ndash;',
+ 'ne;',
+ 'ni;',
+ 'not;',
+ 'not',
+ 'notin;',
+ 'nsub;',
+ 'ntilde;',
+ 'ntilde',
+ 'nu;',
+ 'oacute;',
+ 'oacute',
+ 'ocirc;',
+ 'ocirc',
+ 'oelig;',
+ 'ograve;',
+ 'ograve',
+ 'oline;',
+ 'omega;',
+ 'omicron;',
+ 'oplus;',
+ 'or;',
+ 'ordf;',
+ 'ordf',
+ 'ordm;',
+ 'ordm',
+ 'oslash;',
+ 'oslash',
+ 'otilde;',
+ 'otilde',
+ 'otimes;',
+ 'ouml;',
+ 'ouml',
+ 'para;',
+ 'para',
+ 'part;',
+ 'permil;',
+ 'perp;',
+ 'phi;',
+ 'pi;',
+ 'piv;',
+ 'plusmn;',
+ 'plusmn',
+ 'pound;',
+ 'pound',
+ 'prime;',
+ 'prod;',
+ 'prop;',
+ 'psi;',
+ 'quot;',
+ 'quot',
+ 'rArr;',
+ 'radic;',
+ 'rang;',
+ 'raquo;',
+ 'raquo',
+ 'rarr;',
+ 'rceil;',
+ 'rdquo;',
+ 'real;',
+ 'reg;',
+ 'reg',
+ 'rfloor;',
+ 'rho;',
+ 'rlm;',
+ 'rsaquo;',
+ 'rsquo;',
+ 'sbquo;',
+ 'scaron;',
+ 'sdot;',
+ 'sect;',
+ 'sect',
+ 'shy;',
+ 'shy',
+ 'sigma;',
+ 'sigmaf;',
+ 'sim;',
+ 'spades;',
+ 'sub;',
+ 'sube;',
+ 'sum;',
+ 'sup1;',
+ 'sup1',
+ 'sup2;',
+ 'sup2',
+ 'sup3;',
+ 'sup3',
+ 'sup;',
+ 'supe;',
+ 'szlig;',
+ 'szlig',
+ 'tau;',
+ 'there4;',
+ 'theta;',
+ 'thetasym;',
+ 'thinsp;',
+ 'thorn;',
+ 'thorn',
+ 'tilde;',
+ 'times;',
+ 'times',
+ 'trade;',
+ 'uArr;',
+ 'uacute;',
+ 'uacute',
+ 'uarr;',
+ 'ucirc;',
+ 'ucirc',
+ 'ugrave;',
+ 'ugrave',
+ 'uml;',
+ 'uml',
+ 'upsih;',
+ 'upsilon;',
+ 'uuml;',
+ 'uuml',
+ 'weierp;',
+ 'xi;',
+ 'yacute;',
+ 'yacute',
+ 'yen;',
+ 'yen',
+ 'yuml;',
+ 'yuml',
+ 'zeta;',
+ 'zwj;',
+ 'zwnj;'
+ );
+
+ const PCDATA = 0;
+ const RCDATA = 1;
+ const CDATA = 2;
+ const PLAINTEXT = 3;
+
+ const DOCTYPE = 0;
+ const STARTTAG = 1;
+ const ENDTAG = 2;
+ const COMMENT = 3;
+ const CHARACTR = 4;
+ const EOF = 5;
+
+ public function __construct($data)
+ {
+ $this->data = $data;
+ $this->char = -1;
+ $this->EOF = strlen($data);
+ $this->tree = new HTML5TreeConstructer;
+ $this->content_model = self::PCDATA;
+
+ $this->state = 'data';
+
+ while ($this->state !== null) {
+ $this->{$this->state . 'State'}();
+ }
+ }
+
+ public function save()
+ {
+ return $this->tree->save();
+ }
+
+ private function char()
+ {
+ return ($this->char < $this->EOF)
+ ? $this->data[$this->char]
+ : false;
+ }
+
+ private function character($s, $l = 0)
+ {
+ if ($s + $l < $this->EOF) {
+ if ($l === 0) {
+ return $this->data[$s];
+ } else {
+ return substr($this->data, $s, $l);
+ }
+ }
+ }
+
+ private function characters($char_class, $start)
+ {
+ return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start));
+ }
+
+ private function dataState()
+ {
+ // Consume the next input character
+ $this->char++;
+ $char = $this->char();
+
+ if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) {
+ /* U+0026 AMPERSAND (&)
+ When the content model flag is set to one of the PCDATA or RCDATA
+ states: switch to the entity data state. Otherwise: treat it as per
+ the "anything else" entry below. */
+ $this->state = 'entityData';
+
+ } elseif ($char === '-') {
+ /* If the content model flag is set to either the RCDATA state or
+ the CDATA state, and the escape flag is false, and there are at
+ least three characters before this one in the input stream, and the
+ last four characters in the input stream, including this one, are
+ U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS,
+ and U+002D HYPHEN-MINUS (""),
+ set the escape flag to false. */
+ if (($this->content_model === self::RCDATA ||
+ $this->content_model === self::CDATA) && $this->escape === true &&
+ $this->character($this->char, 3) === '-->'
+ ) {
+ $this->escape = false;
+ }
+
+ /* In any case, emit the input character as a character token.
+ Stay in the data state. */
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ )
+ );
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Emit an end-of-file token. */
+ $this->EOF();
+
+ } elseif ($this->content_model === self::PLAINTEXT) {
+ /* When the content model flag is set to the PLAINTEXT state
+ THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of
+ the text and emit it as a character token. */
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => substr($this->data, $this->char)
+ )
+ );
+
+ $this->EOF();
+
+ } else {
+ /* Anything else
+ THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that
+ otherwise would also be treated as a character token and emit it
+ as a single character token. Stay in the data state. */
+ $len = strcspn($this->data, '<&', $this->char);
+ $char = substr($this->data, $this->char, $len);
+ $this->char += $len - 1;
+
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ )
+ );
+
+ $this->state = 'data';
+ }
+ }
+
+ private function entityDataState()
+ {
+ // Attempt to consume an entity.
+ $entity = $this->entity();
+
+ // If nothing is returned, emit a U+0026 AMPERSAND character token.
+ // Otherwise, emit the character token that was returned.
+ $char = (!$entity) ? '&' : $entity;
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ )
+ );
+
+ // Finally, switch to the data state.
+ $this->state = 'data';
+ }
+
+ private function tagOpenState()
+ {
+ switch ($this->content_model) {
+ case self::RCDATA:
+ case self::CDATA:
+ /* If the next input character is a U+002F SOLIDUS (/) character,
+ consume it and switch to the close tag open state. If the next
+ input character is not a U+002F SOLIDUS (/) character, emit a
+ U+003C LESS-THAN SIGN character token and switch to the data
+ state to process the next input character. */
+ if ($this->character($this->char + 1) === '/') {
+ $this->char++;
+ $this->state = 'closeTagOpen';
+
+ } else {
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => '<'
+ )
+ );
+
+ $this->state = 'data';
+ }
+ break;
+
+ case self::PCDATA:
+ // If the content model flag is set to the PCDATA state
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->char();
+
+ if ($char === '!') {
+ /* U+0021 EXCLAMATION MARK (!)
+ Switch to the markup declaration open state. */
+ $this->state = 'markupDeclarationOpen';
+
+ } elseif ($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Switch to the close tag open state. */
+ $this->state = 'closeTagOpen';
+
+ } elseif (preg_match('/^[A-Za-z]$/', $char)) {
+ /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
+ Create a new start tag token, set its tag name to the lowercase
+ version of the input character (add 0x0020 to the character's code
+ point), then switch to the tag name state. (Don't emit the token
+ yet; further details will be filled in before it is emitted.) */
+ $this->token = array(
+ 'name' => strtolower($char),
+ 'type' => self::STARTTAG,
+ 'attr' => array()
+ );
+
+ $this->state = 'tagName';
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and a
+ U+003E GREATER-THAN SIGN character token. Switch to the data state. */
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => '<>'
+ )
+ );
+
+ $this->state = 'data';
+
+ } elseif ($char === '?') {
+ /* U+003F QUESTION MARK (?)
+ Parse error. Switch to the bogus comment state. */
+ $this->state = 'bogusComment';
+
+ } else {
+ /* Anything else
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and
+ reconsume the current input character in the data state. */
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => '<'
+ )
+ );
+
+ $this->char--;
+ $this->state = 'data';
+ }
+ break;
+ }
+ }
+
+ private function closeTagOpenState()
+ {
+ $next_node = strtolower($this->characters('A-Za-z', $this->char + 1));
+ $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName;
+
+ if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) &&
+ (!$the_same || ($the_same && (!preg_match(
+ '/[\t\n\x0b\x0c >\/]/',
+ $this->character($this->char + 1 + strlen($next_node))
+ ) || $this->EOF === $this->char)))
+ ) {
+ /* If the content model flag is set to the RCDATA or CDATA states then
+ examine the next few characters. If they do not match the tag name of
+ the last start tag token emitted (case insensitively), or if they do but
+ they are not immediately followed by one of the following characters:
+ * U+0009 CHARACTER TABULATION
+ * U+000A LINE FEED (LF)
+ * U+000B LINE TABULATION
+ * U+000C FORM FEED (FF)
+ * U+0020 SPACE
+ * U+003E GREATER-THAN SIGN (>)
+ * U+002F SOLIDUS (/)
+ * EOF
+ ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character
+ token, a U+002F SOLIDUS character token, and switch to the data state
+ to process the next input character. */
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => ''
+ )
+ );
+
+ $this->state = 'data';
+
+ } else {
+ /* Otherwise, if the content model flag is set to the PCDATA state,
+ or if the next few characters do match that tag name, consume the
+ next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if (preg_match('/^[A-Za-z]$/', $char)) {
+ /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
+ Create a new end tag token, set its tag name to the lowercase version
+ of the input character (add 0x0020 to the character's code point), then
+ switch to the tag name state. (Don't emit the token yet; further details
+ will be filled in before it is emitted.) */
+ $this->token = array(
+ 'name' => strtolower($char),
+ 'type' => self::ENDTAG
+ );
+
+ $this->state = 'tagName';
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Parse error. Switch to the data state. */
+ $this->state = 'data';
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F
+ SOLIDUS character token. Reconsume the EOF character in the data state. */
+ $this->emitToken(
+ array(
+ 'type' => self::CHARACTR,
+ 'data' => ''
+ )
+ );
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Parse error. Switch to the bogus comment state. */
+ $this->state = 'bogusComment';
+ }
+ }
+ }
+
+ private function tagNameState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } elseif ($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current tag token's tag name.
+ Stay in the tag name state. */
+ $this->token['name'] .= strtolower($char);
+ $this->state = 'tagName';
+ }
+ }
+
+ private function beforeAttributeNameState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif ($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Stay in the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Start a new attribute in the current tag token. Set that attribute's
+ name to the current input character, and its value to the empty string.
+ Switch to the attribute name state. */
+ $this->token['attr'][] = array(
+ 'name' => strtolower($char),
+ 'value' => null
+ );
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function attributeNameState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute name state. */
+ $this->state = 'afterAttributeName';
+
+ } elseif ($char === '=') {
+ /* U+003D EQUALS SIGN (=)
+ Switch to the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif ($char === '/' && $this->character($this->char + 1) !== '>') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's name.
+ Stay in the attribute name state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['name'] .= strtolower($char);
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function afterAttributeNameState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the after attribute name state. */
+ $this->state = 'afterAttributeName';
+
+ } elseif ($char === '=') {
+ /* U+003D EQUALS SIGN (=)
+ Switch to the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif ($char === '/' && $this->character($this->char + 1) !== '>') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the
+ before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Start a new attribute in the current tag token. Set that attribute's
+ name to the current input character, and its value to the empty string.
+ Switch to the attribute name state. */
+ $this->token['attr'][] = array(
+ 'name' => strtolower($char),
+ 'value' => null
+ );
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function beforeAttributeValueState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif ($char === '"') {
+ /* U+0022 QUOTATION MARK (")
+ Switch to the attribute value (double-quoted) state. */
+ $this->state = 'attributeValueDoubleQuoted';
+
+ } elseif ($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the attribute value (unquoted) state and reconsume
+ this input character. */
+ $this->char--;
+ $this->state = 'attributeValueUnquoted';
+
+ } elseif ($char === '\'') {
+ /* U+0027 APOSTROPHE (')
+ Switch to the attribute value (single-quoted) state. */
+ $this->state = 'attributeValueSingleQuoted';
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Switch to the attribute value (unquoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueUnquoted';
+ }
+ }
+
+ private function attributeValueDoubleQuotedState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if ($char === '"') {
+ /* U+0022 QUOTATION MARK (")
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState('double');
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the character
+ in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (double-quoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueDoubleQuoted';
+ }
+ }
+
+ private function attributeValueSingleQuotedState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if ($char === '\'') {
+ /* U+0022 QUOTATION MARK (')
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState('single');
+
+ } elseif ($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the character
+ in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (single-quoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueSingleQuoted';
+ }
+ }
+
+ private function attributeValueUnquotedState()
+ {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif ($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState();
+
+ } elseif ($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (unquoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueUnquoted';
+ }
+ }
+
+ private function entityInAttributeValueState()
+ {
+ // Attempt to consume an entity.
+ $entity = $this->entity();
+
+ // If nothing is returned, append a U+0026 AMPERSAND character to the
+ // current attribute's value. Otherwise, emit the character token that
+ // was returned.
+ $char = (!$entity)
+ ? '&'
+ : $entity;
+
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+ }
+
+ private function bogusCommentState()
+ {
+ /* Consume every character up to the first U+003E GREATER-THAN SIGN
+ character (>) or the end of the file (EOF), whichever comes first. Emit
+ a comment token whose data is the concatenation of all the characters
+ starting from and including the character that caused the state machine
+ to switch into the bogus comment state, up to and including the last
+ consumed character before the U+003E character, if any, or up to the
+ end of the file otherwise. (If the comment was started by the end of
+ the file (EOF), the token is empty.) */
+ $data = $this->characters('^>', $this->char);
+ $this->emitToken(
+ array(
+ 'data' => $data,
+ 'type' => self::COMMENT
+ )
+ );
+
+ $this->char += strlen($data);
+
+ /* Switch to the data state. */
+ $this->state = 'data';
+
+ /* If the end of the file was reached, reconsume the EOF character. */
+ if ($this->char === $this->EOF) {
+ $this->char = $this->EOF - 1;
+ }
+ }
+
+ private function markupDeclarationOpenState()
+ {
+ /* If the next two characters are both U+002D HYPHEN-MINUS (-)
+ characters, consume those two characters, create a comment token whose
+ data is the empty string, and switch to the comment state. */
+ if ($this->character($this->char + 1, 2) === '--') {
+ $this->char += 2;
+ $this->state = 'comment';
+ $this->token = array(
+ 'data' => null,
+ 'type' => self::COMMENT
+ );
+
+ /* Otherwise if the next seven chacacters are a case-insensitive match
+ for the word "DOCTYPE", then consume those characters and switch to the
+ DOCTYPE state. */
+ } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') {
+ $this->char += 7;
+ $this->state = 'doctype';
+
+ /* Otherwise, is is a parse error. Switch to the bogus comment state.
+ The next character that is consumed, if any, is the first character
+ that will be in the comment. */
+ } else {
+ $this->char++;
+ $this->state = 'bogusComment';
+ }
+ }
+
+ private function commentState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ /* U+002D HYPHEN-MINUS (-) */
+ if ($char === '-') {
+ /* Switch to the comment dash state */
+ $this->state = 'commentDash';
+
+ /* EOF */
+ } elseif ($this->char === $this->EOF) {
+ /* Parse error. Emit the comment token. Reconsume the EOF character
+ in the data state. */
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ /* Anything else */
+ } else {
+ /* Append the input character to the comment token's data. Stay in
+ the comment state. */
+ $this->token['data'] .= $char;
+ }
+ }
+
+ private function commentDashState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ /* U+002D HYPHEN-MINUS (-) */
+ if ($char === '-') {
+ /* Switch to the comment end state */
+ $this->state = 'commentEnd';
+
+ /* EOF */
+ } elseif ($this->char === $this->EOF) {
+ /* Parse error. Emit the comment token. Reconsume the EOF character
+ in the data state. */
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ /* Anything else */
+ } else {
+ /* Append a U+002D HYPHEN-MINUS (-) character and the input
+ character to the comment token's data. Switch to the comment state. */
+ $this->token['data'] .= '-' . $char;
+ $this->state = 'comment';
+ }
+ }
+
+ private function commentEndState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if ($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif ($char === '-') {
+ $this->token['data'] .= '-';
+
+ } elseif ($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['data'] .= '--' . $char;
+ $this->state = 'comment';
+ }
+ }
+
+ private function doctypeState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ $this->state = 'beforeDoctypeName';
+
+ } else {
+ $this->char--;
+ $this->state = 'beforeDoctypeName';
+ }
+ }
+
+ private function beforeDoctypeNameState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ // Stay in the before DOCTYPE name state.
+
+ } elseif (preg_match('/^[a-z]$/', $char)) {
+ $this->token = array(
+ 'name' => strtoupper($char),
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ );
+
+ $this->state = 'doctypeName';
+
+ } elseif ($char === '>') {
+ $this->emitToken(
+ array(
+ 'name' => null,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ )
+ );
+
+ $this->state = 'data';
+
+ } elseif ($this->char === $this->EOF) {
+ $this->emitToken(
+ array(
+ 'name' => null,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ )
+ );
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token = array(
+ 'name' => $char,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ );
+
+ $this->state = 'doctypeName';
+ }
+ }
+
+ private function doctypeNameState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ $this->state = 'AfterDoctypeName';
+
+ } elseif ($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif (preg_match('/^[a-z]$/', $char)) {
+ $this->token['name'] .= strtoupper($char);
+
+ } elseif ($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['name'] .= $char;
+ }
+
+ $this->token['error'] = ($this->token['name'] === 'HTML')
+ ? false
+ : true;
+ }
+
+ private function afterDoctypeNameState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ // Stay in the DOCTYPE name state.
+
+ } elseif ($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif ($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['error'] = true;
+ $this->state = 'bogusDoctype';
+ }
+ }
+
+ private function bogusDoctypeState()
+ {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if ($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif ($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ // Stay in the bogus DOCTYPE state.
+ }
+ }
+
+ private function entity()
+ {
+ $start = $this->char;
+
+ // This section defines how to consume an entity. This definition is
+ // used when parsing entities in text and in attributes.
+
+ // The behaviour depends on the identity of the next character (the
+ // one immediately after the U+0026 AMPERSAND character):
+
+ switch ($this->character($this->char + 1)) {
+ // U+0023 NUMBER SIGN (#)
+ case '#':
+
+ // The behaviour further depends on the character after the
+ // U+0023 NUMBER SIGN:
+ switch ($this->character($this->char + 1)) {
+ // U+0078 LATIN SMALL LETTER X
+ // U+0058 LATIN CAPITAL LETTER X
+ case 'x':
+ case 'X':
+ // Follow the steps below, but using the range of
+ // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
+ // NINE, U+0061 LATIN SMALL LETTER A through to U+0066
+ // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER
+ // A, through to U+0046 LATIN CAPITAL LETTER F (in other
+ // words, 0-9, A-F, a-f).
+ $char = 1;
+ $char_class = '0-9A-Fa-f';
+ break;
+
+ // Anything else
+ default:
+ // Follow the steps below, but using the range of
+ // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
+ // NINE (i.e. just 0-9).
+ $char = 0;
+ $char_class = '0-9';
+ break;
+ }
+
+ // Consume as many characters as match the range of characters
+ // given above.
+ $this->char++;
+ $e_name = $this->characters($char_class, $this->char + $char + 1);
+ $entity = $this->character($start, $this->char);
+ $cond = strlen($e_name) > 0;
+
+ // The rest of the parsing happens below.
+ break;
+
+ // Anything else
+ default:
+ // Consume the maximum number of characters possible, with the
+ // consumed characters case-sensitively matching one of the
+ // identifiers in the first column of the entities table.
+
+ $e_name = $this->characters('0-9A-Za-z;', $this->char + 1);
+ $len = strlen($e_name);
+
+ for ($c = 1; $c <= $len; $c++) {
+ $id = substr($e_name, 0, $c);
+ $this->char++;
+
+ if (in_array($id, $this->entities)) {
+ if ($e_name[$c - 1] !== ';') {
+ if ($c < $len && $e_name[$c] == ';') {
+ $this->char++; // consume extra semicolon
+ }
+ }
+ $entity = $id;
+ break;
+ }
+ }
+
+ $cond = isset($entity);
+ // The rest of the parsing happens below.
+ break;
+ }
+
+ if (!$cond) {
+ // If no match can be made, then this is a parse error. No
+ // characters are consumed, and nothing is returned.
+ $this->char = $start;
+ return false;
+ }
+
+ // Return a character token for the character corresponding to the
+ // entity name (as given by the second column of the entities table).
+ return html_entity_decode('&' . rtrim($entity, ';') . ';', ENT_QUOTES, 'UTF-8');
+ }
+
+ private function emitToken($token)
+ {
+ $emit = $this->tree->emitToken($token);
+
+ if (is_int($emit)) {
+ $this->content_model = $emit;
+
+ } elseif ($token['type'] === self::ENDTAG) {
+ $this->content_model = self::PCDATA;
+ }
+ }
+
+ private function EOF()
+ {
+ $this->state = null;
+ $this->tree->emitToken(
+ array(
+ 'type' => self::EOF
+ )
+ );
+ }
+}
+
+class HTML5TreeConstructer
+{
+ public $stack = array();
+
+ private $phase;
+ private $mode;
+ private $dom;
+ private $foster_parent = null;
+ private $a_formatting = array();
+
+ private $head_pointer = null;
+ private $form_pointer = null;
+
+ private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th');
+ private $formatting = array(
+ 'a',
+ 'b',
+ 'big',
+ 'em',
+ 'font',
+ 'i',
+ 'nobr',
+ 's',
+ 'small',
+ 'strike',
+ 'strong',
+ 'tt',
+ 'u'
+ );
+ private $special = array(
+ 'address',
+ 'area',
+ 'base',
+ 'basefont',
+ 'bgsound',
+ 'blockquote',
+ 'body',
+ 'br',
+ 'center',
+ 'col',
+ 'colgroup',
+ 'dd',
+ 'dir',
+ 'div',
+ 'dl',
+ 'dt',
+ 'embed',
+ 'fieldset',
+ 'form',
+ 'frame',
+ 'frameset',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'head',
+ 'hr',
+ 'iframe',
+ 'image',
+ 'img',
+ 'input',
+ 'isindex',
+ 'li',
+ 'link',
+ 'listing',
+ 'menu',
+ 'meta',
+ 'noembed',
+ 'noframes',
+ 'noscript',
+ 'ol',
+ 'optgroup',
+ 'option',
+ 'p',
+ 'param',
+ 'plaintext',
+ 'pre',
+ 'script',
+ 'select',
+ 'spacer',
+ 'style',
+ 'tbody',
+ 'textarea',
+ 'tfoot',
+ 'thead',
+ 'title',
+ 'tr',
+ 'ul',
+ 'wbr'
+ );
+
+ // The different phases.
+ const INIT_PHASE = 0;
+ const ROOT_PHASE = 1;
+ const MAIN_PHASE = 2;
+ const END_PHASE = 3;
+
+ // The different insertion modes for the main phase.
+ const BEFOR_HEAD = 0;
+ const IN_HEAD = 1;
+ const AFTER_HEAD = 2;
+ const IN_BODY = 3;
+ const IN_TABLE = 4;
+ const IN_CAPTION = 5;
+ const IN_CGROUP = 6;
+ const IN_TBODY = 7;
+ const IN_ROW = 8;
+ const IN_CELL = 9;
+ const IN_SELECT = 10;
+ const AFTER_BODY = 11;
+ const IN_FRAME = 12;
+ const AFTR_FRAME = 13;
+
+ // The different types of elements.
+ const SPECIAL = 0;
+ const SCOPING = 1;
+ const FORMATTING = 2;
+ const PHRASING = 3;
+
+ const MARKER = 0;
+
+ public function __construct()
+ {
+ $this->phase = self::INIT_PHASE;
+ $this->mode = self::BEFOR_HEAD;
+ $this->dom = new DOMDocument;
+
+ $this->dom->encoding = 'UTF-8';
+ $this->dom->preserveWhiteSpace = true;
+ $this->dom->substituteEntities = true;
+ $this->dom->strictErrorChecking = false;
+ }
+
+ // Process tag tokens
+ public function emitToken($token)
+ {
+ switch ($this->phase) {
+ case self::INIT_PHASE:
+ return $this->initPhase($token);
+ break;
+ case self::ROOT_PHASE:
+ return $this->rootElementPhase($token);
+ break;
+ case self::MAIN_PHASE:
+ return $this->mainPhase($token);
+ break;
+ case self::END_PHASE :
+ return $this->trailingEndPhase($token);
+ break;
+ }
+ }
+
+ private function initPhase($token)
+ {
+ /* Initially, the tree construction stage must handle each token
+ emitted from the tokenisation stage as follows: */
+
+ /* A DOCTYPE token that is marked as being in error
+ A comment token
+ A start tag token
+ An end tag token
+ A character token that is not one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE
+ An end-of-file token */
+ if ((isset($token['error']) && $token['error']) ||
+ $token['type'] === HTML5::COMMENT ||
+ $token['type'] === HTML5::STARTTAG ||
+ $token['type'] === HTML5::ENDTAG ||
+ $token['type'] === HTML5::EOF ||
+ ($token['type'] === HTML5::CHARACTR && isset($token['data']) &&
+ !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))
+ ) {
+ /* This specification does not define how to handle this case. In
+ particular, user agents may ignore the entirety of this specification
+ altogether for such documents, and instead invoke special parse modes
+ with a greater emphasis on backwards compatibility. */
+
+ $this->phase = self::ROOT_PHASE;
+ return $this->rootElementPhase($token);
+
+ /* A DOCTYPE token marked as being correct */
+ } elseif (isset($token['error']) && !$token['error']) {
+ /* Append a DocumentType node to the Document node, with the name
+ attribute set to the name given in the DOCTYPE token (which will be
+ "HTML"), and the other attributes specific to DocumentType objects
+ set to null, empty lists, or the empty string as appropriate. */
+ $doctype = new DOMDocumentType(null, null, 'HTML');
+
+ /* Then, switch to the root element phase of the tree construction
+ stage. */
+ $this->phase = self::ROOT_PHASE;
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif (isset($token['data']) && preg_match(
+ '/^[\t\n\x0b\x0c ]+$/',
+ $token['data']
+ )
+ ) {
+ /* Append that character to the Document node. */
+ $text = $this->dom->createTextNode($token['data']);
+ $this->dom->appendChild($text);
+ }
+ }
+
+ private function rootElementPhase($token)
+ {
+ /* After the initial phase, as each token is emitted from the tokenisation
+ stage, it must be processed as described in this section. */
+
+ /* A DOCTYPE token */
+ if ($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the Document object with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->dom->appendChild($comment);
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Append that character to the Document node. */
+ $text = $this->dom->createTextNode($token['data']);
+ $this->dom->appendChild($text);
+
+ /* A character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED
+ (FF), or U+0020 SPACE
+ A start tag token
+ An end tag token
+ An end-of-file token */
+ } elseif (($token['type'] === HTML5::CHARACTR &&
+ !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
+ $token['type'] === HTML5::STARTTAG ||
+ $token['type'] === HTML5::ENDTAG ||
+ $token['type'] === HTML5::EOF
+ ) {
+ /* Create an HTMLElement node with the tag name html, in the HTML
+ namespace. Append it to the Document object. Switch to the main
+ phase and reprocess the current token. */
+ $html = $this->dom->createElement('html');
+ $this->dom->appendChild($html);
+ $this->stack[] = $html;
+
+ $this->phase = self::MAIN_PHASE;
+ return $this->mainPhase($token);
+ }
+ }
+
+ private function mainPhase($token)
+ {
+ /* Tokens in the main phase must be handled as follows: */
+
+ /* A DOCTYPE token */
+ if ($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A start tag token with the tag name "html" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') {
+ /* If this start tag token was not the first start tag token, then
+ it is a parse error. */
+
+ /* For each attribute on the token, check to see if the attribute
+ is already present on the top element of the stack of open elements.
+ If it is not, add the attribute and its corresponding value to that
+ element. */
+ foreach ($token['attr'] as $attr) {
+ if (!$this->stack[0]->hasAttribute($attr['name'])) {
+ $this->stack[0]->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+
+ /* An end-of-file token */
+ } elseif ($token['type'] === HTML5::EOF) {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Anything else. */
+ } else {
+ /* Depends on the insertion mode: */
+ switch ($this->mode) {
+ case self::BEFOR_HEAD:
+ return $this->beforeHead($token);
+ break;
+ case self::IN_HEAD:
+ return $this->inHead($token);
+ break;
+ case self::AFTER_HEAD:
+ return $this->afterHead($token);
+ break;
+ case self::IN_BODY:
+ return $this->inBody($token);
+ break;
+ case self::IN_TABLE:
+ return $this->inTable($token);
+ break;
+ case self::IN_CAPTION:
+ return $this->inCaption($token);
+ break;
+ case self::IN_CGROUP:
+ return $this->inColumnGroup($token);
+ break;
+ case self::IN_TBODY:
+ return $this->inTableBody($token);
+ break;
+ case self::IN_ROW:
+ return $this->inRow($token);
+ break;
+ case self::IN_CELL:
+ return $this->inCell($token);
+ break;
+ case self::IN_SELECT:
+ return $this->inSelect($token);
+ break;
+ case self::AFTER_BODY:
+ return $this->afterBody($token);
+ break;
+ case self::IN_FRAME:
+ return $this->inFrameset($token);
+ break;
+ case self::AFTR_FRAME:
+ return $this->afterFrameset($token);
+ break;
+ case self::END_PHASE:
+ return $this->trailingEndPhase($token);
+ break;
+ }
+ }
+ }
+
+ private function beforeHead($token)
+ {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token with the tag name "head" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') {
+ /* Create an element for the token, append the new element to the
+ current node and push it onto the stack of open elements. */
+ $element = $this->insertElement($token);
+
+ /* Set the head element pointer to this new element node. */
+ $this->head_pointer = $element;
+
+ /* Change the insertion mode to "in head". */
+ $this->mode = self::IN_HEAD;
+
+ /* A start tag token whose tag name is one of: "base", "link", "meta",
+ "script", "style", "title". Or an end tag with the tag name "html".
+ Or a character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE. Or any other start tag token */
+ } elseif ($token['type'] === HTML5::STARTTAG ||
+ ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') ||
+ ($token['type'] === HTML5::CHARACTR && !preg_match(
+ '/^[\t\n\x0b\x0c ]$/',
+ $token['data']
+ ))
+ ) {
+ /* Act as if a start tag token with the tag name "head" and no
+ attributes had been seen, then reprocess the current token. */
+ $this->beforeHead(
+ array(
+ 'name' => 'head',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ return $this->inHead($token);
+
+ /* Any other end tag */
+ } elseif ($token['type'] === HTML5::ENDTAG) {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function inHead($token)
+ {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE.
+
+ THIS DIFFERS FROM THE SPEC: If the current node is either a title, style
+ or script element, append the character to the current node regardless
+ of its content. */
+ if (($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || (
+ $token['type'] === HTML5::CHARACTR && in_array(
+ end($this->stack)->nodeName,
+ array('title', 'style', 'script')
+ ))
+ ) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('title', 'style', 'script'))
+ ) {
+ array_pop($this->stack);
+ return HTML5::PCDATA;
+
+ /* A start tag with the tag name "title" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if ($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ } else {
+ $element = $this->insertElement($token);
+ }
+
+ /* Switch the tokeniser's content model flag to the RCDATA state. */
+ return HTML5::RCDATA;
+
+ /* A start tag with the tag name "style" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if ($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ } else {
+ $this->insertElement($token);
+ }
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+
+ /* A start tag with the tag name "script" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') {
+ /* Create an element for the token. */
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+
+ /* A start tag with the tag name "base", "link", or "meta" */
+ } elseif ($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array('base', 'link', 'meta')
+ )
+ ) {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if ($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+ array_pop($this->stack);
+
+ } else {
+ $this->insertElement($token);
+ }
+
+ /* An end tag with the tag name "head" */
+ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') {
+ /* If the current node is a head element, pop the current node off
+ the stack of open elements. */
+ if ($this->head_pointer->isSameNode(end($this->stack))) {
+ array_pop($this->stack);
+
+ /* Otherwise, this is a parse error. */
+ } else {
+ // k
+ }
+
+ /* Change the insertion mode to "after head". */
+ $this->mode = self::AFTER_HEAD;
+
+ /* A start tag with the tag name "head" or an end tag except "html". */
+ } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') ||
+ ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')
+ ) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* If the current node is a head element, act as if an end tag
+ token with the tag name "head" had been seen. */
+ if ($this->head_pointer->isSameNode(end($this->stack))) {
+ $this->inHead(
+ array(
+ 'name' => 'head',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ /* Otherwise, change the insertion mode to "after head". */
+ } else {
+ $this->mode = self::AFTER_HEAD;
+ }
+
+ /* Then, reprocess the current token. */
+ return $this->afterHead($token);
+ }
+ }
+
+ private function afterHead($token)
+ {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token with the tag name "body" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') {
+ /* Insert a body element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in body". */
+ $this->mode = self::IN_BODY;
+
+ /* A start tag token with the tag name "frameset" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') {
+ /* Insert a frameset element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in frameset". */
+ $this->mode = self::IN_FRAME;
+
+ /* A start tag token whose tag name is one of: "base", "link", "meta",
+ "script", "style", "title" */
+ } elseif ($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array('base', 'link', 'meta', 'script', 'style', 'title')
+ )
+ ) {
+ /* Parse error. Switch the insertion mode back to "in head" and
+ reprocess the token. */
+ $this->mode = self::IN_HEAD;
+ return $this->inHead($token);
+
+ /* Anything else */
+ } else {
+ /* Act as if a start tag token with the tag name "body" and no
+ attributes had been seen, and then reprocess the current token. */
+ $this->afterHead(
+ array(
+ 'name' => 'body',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ return $this->inBody($token);
+ }
+ }
+
+ private function inBody($token)
+ {
+ /* Handle the token as follows: */
+
+ switch ($token['type']) {
+ /* A character token */
+ case HTML5::CHARACTR:
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Append the token's character to the current node. */
+ $this->insertText($token['data']);
+ break;
+
+ /* A comment token */
+ case HTML5::COMMENT:
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+ break;
+
+ case HTML5::STARTTAG:
+ switch ($token['name']) {
+ /* A start tag token whose tag name is one of: "script",
+ "style" */
+ case 'script':
+ case 'style':
+ /* Process the token as if the insertion mode had been "in
+ head". */
+ return $this->inHead($token);
+ break;
+
+ /* A start tag token whose tag name is one of: "base", "link",
+ "meta", "title" */
+ case 'base':
+ case 'link':
+ case 'meta':
+ case 'title':
+ /* Parse error. Process the token as if the insertion mode
+ had been "in head". */
+ return $this->inHead($token);
+ break;
+
+ /* A start tag token with the tag name "body" */
+ case 'body':
+ /* Parse error. If the second element on the stack of open
+ elements is not a body element, or, if the stack of open
+ elements has only one node on it, then ignore the token.
+ (innerHTML case) */
+ if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') {
+ // Ignore
+
+ /* Otherwise, for each attribute on the token, check to see
+ if the attribute is already present on the body element (the
+ second element) on the stack of open elements. If it is not,
+ add the attribute and its corresponding value to that
+ element. */
+ } else {
+ foreach ($token['attr'] as $attr) {
+ if (!$this->stack[1]->hasAttribute($attr['name'])) {
+ $this->stack[1]->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+ }
+ break;
+
+ /* A start tag whose tag name is one of: "address",
+ "blockquote", "center", "dir", "div", "dl", "fieldset",
+ "listing", "menu", "ol", "p", "ul" */
+ case 'address':
+ case 'blockquote':
+ case 'center':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'listing':
+ case 'menu':
+ case 'ol':
+ case 'p':
+ case 'ul':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if ($this->elementInScope('p')) {
+ $this->emitToken(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag whose tag name is "form" */
+ case 'form':
+ /* If the form element pointer is not null, ignore the
+ token with a parse error. */
+ if ($this->form_pointer !== null) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* If the stack of open elements has a p element in
+ scope, then act as if an end tag with the tag name p
+ had been seen. */
+ if ($this->elementInScope('p')) {
+ $this->emitToken(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Insert an HTML element for the token, and set the
+ form element pointer to point to the element created. */
+ $element = $this->insertElement($token);
+ $this->form_pointer = $element;
+ }
+ break;
+
+ /* A start tag whose tag name is "li", "dd" or "dt" */
+ case 'li':
+ case 'dd':
+ case 'dt':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if ($this->elementInScope('p')) {
+ $this->emitToken(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ $stack_length = count($this->stack) - 1;
+
+ for ($n = $stack_length; 0 <= $n; $n--) {
+ /* 1. Initialise node to be the current node (the
+ bottommost node of the stack). */
+ $stop = false;
+ $node = $this->stack[$n];
+ $cat = $this->getElementCategory($node->tagName);
+
+ /* 2. If node is an li, dd or dt element, then pop all
+ the nodes from the current node up to node, including
+ node, then stop this algorithm. */
+ if ($token['name'] === $node->tagName || ($token['name'] !== 'li'
+ && ($node->tagName === 'dd' || $node->tagName === 'dt'))
+ ) {
+ for ($x = $stack_length; $x >= $n; $x--) {
+ array_pop($this->stack);
+ }
+
+ break;
+ }
+
+ /* 3. If node is not in the formatting category, and is
+ not in the phrasing category, and is not an address or
+ div element, then stop this algorithm. */
+ if ($cat !== self::FORMATTING && $cat !== self::PHRASING &&
+ $node->tagName !== 'address' && $node->tagName !== 'div'
+ ) {
+ break;
+ }
+ }
+
+ /* Finally, insert an HTML element with the same tag
+ name as the token's. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag token whose tag name is "plaintext" */
+ case 'plaintext':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if ($this->elementInScope('p')) {
+ $this->emitToken(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ return HTML5::PLAINTEXT;
+ break;
+
+ /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
+ "h5", "h6" */
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if ($this->elementInScope('p')) {
+ $this->emitToken(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* If the stack of open elements has in scope an element whose
+ tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
+ this is a parse error; pop elements from the stack until an
+ element with one of those tag names has been popped from the
+ stack. */
+ while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) {
+ array_pop($this->stack);
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag whose tag name is "a" */
+ case 'a':
+ /* If the list of active formatting elements contains
+ an element whose tag name is "a" between the end of the
+ list and the last marker on the list (or the start of
+ the list if there is no marker on the list), then this
+ is a parse error; act as if an end tag with the tag name
+ "a" had been seen, then remove that element from the list
+ of active formatting elements and the stack of open
+ elements if the end tag didn't already remove it (it
+ might not have if the element is not in table scope). */
+ $leng = count($this->a_formatting);
+
+ for ($n = $leng - 1; $n >= 0; $n--) {
+ if ($this->a_formatting[$n] === self::MARKER) {
+ break;
+
+ } elseif ($this->a_formatting[$n]->nodeName === 'a') {
+ $this->emitToken(
+ array(
+ 'name' => 'a',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ break;
+ }
+ }
+
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $el = $this->insertElement($token);
+
+ /* Add that element to the list of active formatting
+ elements. */
+ $this->a_formatting[] = $el;
+ break;
+
+ /* A start tag whose tag name is one of: "b", "big", "em", "font",
+ "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
+ case 'b':
+ case 'big':
+ case 'em':
+ case 'font':
+ case 'i':
+ case 'nobr':
+ case 's':
+ case 'small':
+ case 'strike':
+ case 'strong':
+ case 'tt':
+ case 'u':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $el = $this->insertElement($token);
+
+ /* Add that element to the list of active formatting
+ elements. */
+ $this->a_formatting[] = $el;
+ break;
+
+ /* A start tag token whose tag name is "button" */
+ case 'button':
+ /* If the stack of open elements has a button element in scope,
+ then this is a parse error; act as if an end tag with the tag
+ name "button" had been seen, then reprocess the token. (We don't
+ do that. Unnecessary.) */
+ if ($this->elementInScope('button')) {
+ $this->inBody(
+ array(
+ 'name' => 'button',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+ break;
+
+ /* A start tag token whose tag name is one of: "marquee", "object" */
+ case 'marquee':
+ case 'object':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+ break;
+
+ /* A start tag token whose tag name is "xmp" */
+ case 'xmp':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Switch the content model flag to the CDATA state. */
+ return HTML5::CDATA;
+ break;
+
+ /* A start tag whose tag name is "table" */
+ case 'table':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if ($this->elementInScope('p')) {
+ $this->emitToken(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in table". */
+ $this->mode = self::IN_TABLE;
+ break;
+
+ /* A start tag whose tag name is one of: "area", "basefont",
+ "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */
+ case 'area':
+ case 'basefont':
+ case 'bgsound':
+ case 'br':
+ case 'embed':
+ case 'img':
+ case 'param':
+ case 'spacer':
+ case 'wbr':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "hr" */
+ case 'hr':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if ($this->elementInScope('p')) {
+ $this->emitToken(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "image" */
+ case 'image':
+ /* Parse error. Change the token's tag name to "img" and
+ reprocess it. (Don't ask.) */
+ $token['name'] = 'img';
+ return $this->inBody($token);
+ break;
+
+ /* A start tag whose tag name is "input" */
+ case 'input':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an input element for the token. */
+ $element = $this->insertElement($token, false);
+
+ /* If the form element pointer is not null, then associate the
+ input element with the form element pointed to by the form
+ element pointer. */
+ $this->form_pointer !== null
+ ? $this->form_pointer->appendChild($element)
+ : end($this->stack)->appendChild($element);
+
+ /* Pop that input element off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "isindex" */
+ case 'isindex':
+ /* Parse error. */
+ // w/e
+
+ /* If the form element pointer is not null,
+ then ignore the token. */
+ if ($this->form_pointer === null) {
+ /* Act as if a start tag token with the tag name "form" had
+ been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'body',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ /* Act as if a start tag token with the tag name "hr" had
+ been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'hr',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ /* Act as if a start tag token with the tag name "p" had
+ been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ /* Act as if a start tag token with the tag name "label"
+ had been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'label',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ /* Act as if a stream of character tokens had been seen. */
+ $this->insertText(
+ 'This is a searchable index. ' .
+ 'Insert your search keywords here: '
+ );
+
+ /* Act as if a start tag token with the tag name "input"
+ had been seen, with all the attributes from the "isindex"
+ token, except with the "name" attribute set to the value
+ "isindex" (ignoring any explicit "name" attribute). */
+ $attr = $token['attr'];
+ $attr[] = array('name' => 'name', 'value' => 'isindex');
+
+ $this->inBody(
+ array(
+ 'name' => 'input',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => $attr
+ )
+ );
+
+ /* Act as if a stream of character tokens had been seen
+ (see below for what they should say). */
+ $this->insertText(
+ 'This is a searchable index. ' .
+ 'Insert your search keywords here: '
+ );
+
+ /* Act as if an end tag token with the tag name "label"
+ had been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'label',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ /* Act as if an end tag token with the tag name "p" had
+ been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ /* Act as if a start tag token with the tag name "hr" had
+ been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'hr',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ /* Act as if an end tag token with the tag name "form" had
+ been seen. */
+ $this->inBody(
+ array(
+ 'name' => 'form',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+ break;
+
+ /* A start tag whose tag name is "textarea" */
+ case 'textarea':
+ $this->insertElement($token);
+
+ /* Switch the tokeniser's content model flag to the
+ RCDATA state. */
+ return HTML5::RCDATA;
+ break;
+
+ /* A start tag whose tag name is one of: "iframe", "noembed",
+ "noframes" */
+ case 'iframe':
+ case 'noembed':
+ case 'noframes':
+ $this->insertElement($token);
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+ break;
+
+ /* A start tag whose tag name is "select" */
+ case 'select':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in select". */
+ $this->mode = self::IN_SELECT;
+ break;
+
+ /* A start or end tag whose tag name is one of: "caption", "col",
+ "colgroup", "frame", "frameset", "head", "option", "optgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr". */
+ case 'caption':
+ case 'col':
+ case 'colgroup':
+ case 'frame':
+ case 'frameset':
+ case 'head':
+ case 'option':
+ case 'optgroup':
+ case 'tbody':
+ case 'td':
+ case 'tfoot':
+ case 'th':
+ case 'thead':
+ case 'tr':
+ // Parse error. Ignore the token.
+ break;
+
+ /* A start or end tag whose tag name is one of: "event-source",
+ "section", "nav", "article", "aside", "header", "footer",
+ "datagrid", "command" */
+ case 'event-source':
+ case 'section':
+ case 'nav':
+ case 'article':
+ case 'aside':
+ case 'header':
+ case 'footer':
+ case 'datagrid':
+ case 'command':
+ // Work in progress!
+ break;
+
+ /* A start tag token not covered by the previous entries */
+ default:
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ $this->insertElement($token, true, true);
+ break;
+ }
+ break;
+
+ case HTML5::ENDTAG:
+ switch ($token['name']) {
+ /* An end tag with the tag name "body" */
+ case 'body':
+ /* If the second element in the stack of open elements is
+ not a body element, this is a parse error. Ignore the token.
+ (innerHTML case) */
+ if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') {
+ // Ignore.
+
+ /* If the current node is not the body element, then this
+ is a parse error. */
+ } elseif (end($this->stack)->nodeName !== 'body') {
+ // Parse error.
+ }
+
+ /* Change the insertion mode to "after body". */
+ $this->mode = self::AFTER_BODY;
+ break;
+
+ /* An end tag with the tag name "html" */
+ case 'html':
+ /* Act as if an end tag with tag name "body" had been seen,
+ then, if that token wasn't ignored, reprocess the current
+ token. */
+ $this->inBody(
+ array(
+ 'name' => 'body',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ return $this->afterBody($token);
+ break;
+
+ /* An end tag whose tag name is one of: "address", "blockquote",
+ "center", "dir", "div", "dl", "fieldset", "listing", "menu",
+ "ol", "pre", "ul" */
+ case 'address':
+ case 'blockquote':
+ case 'center':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'fieldset':
+ case 'listing':
+ case 'menu':
+ case 'ol':
+ case 'pre':
+ case 'ul':
+ /* If the stack of open elements has an element in scope
+ with the same tag name as that of the token, then generate
+ implied end tags. */
+ if ($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with
+ the same tag name as that of the token, then this
+ is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in
+ scope with the same tag name as that of the token,
+ then pop elements from this stack until an element
+ with that tag name has been popped from the stack. */
+ for ($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if ($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is "form" */
+ case 'form':
+ /* If the stack of open elements has an element in scope
+ with the same tag name as that of the token, then generate
+ implied end tags. */
+ if ($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ }
+
+ if (end($this->stack)->nodeName !== $token['name']) {
+ /* Now, if the current node is not an element with the
+ same tag name as that of the token, then this is a parse
+ error. */
+ // w/e
+
+ } else {
+ /* Otherwise, if the current node is an element with
+ the same tag name as that of the token pop that element
+ from the stack. */
+ array_pop($this->stack);
+ }
+
+ /* In any case, set the form element pointer to null. */
+ $this->form_pointer = null;
+ break;
+
+ /* An end tag whose tag name is "p" */
+ case 'p':
+ /* If the stack of open elements has a p element in scope,
+ then generate implied end tags, except for p elements. */
+ if ($this->elementInScope('p')) {
+ $this->generateImpliedEndTags(array('p'));
+
+ /* If the current node is not a p element, then this is
+ a parse error. */
+ // k
+
+ /* If the stack of open elements has a p element in
+ scope, then pop elements from this stack until the stack
+ no longer has a p element in scope. */
+ for ($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if ($this->elementInScope('p')) {
+ array_pop($this->stack);
+
+ } else {
+ break;
+ }
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is "dd", "dt", or "li" */
+ case 'dd':
+ case 'dt':
+ case 'li':
+ /* If the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then
+ generate implied end tags, except for elements with the
+ same tag name as the token. */
+ if ($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags(array($token['name']));
+
+ /* If the current node is not an element with the same
+ tag name as the token, then this is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then
+ pop elements from this stack until an element with that
+ tag name has been popped from the stack. */
+ for ($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if ($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
+ "h5", "h6" */
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
+
+ /* If the stack of open elements has in scope an element whose
+ tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
+ generate implied end tags. */
+ if ($this->elementInScope($elements)) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with the same
+ tag name as that of the token, then this is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has in scope an element
+ whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
+ "h6", then pop elements from the stack until an element
+ with one of those tag names has been popped from the stack. */
+ while ($this->elementInScope($elements)) {
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is one of: "a", "b", "big", "em",
+ "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
+ case 'a':
+ case 'b':
+ case 'big':
+ case 'em':
+ case 'font':
+ case 'i':
+ case 'nobr':
+ case 's':
+ case 'small':
+ case 'strike':
+ case 'strong':
+ case 'tt':
+ case 'u':
+ /* 1. Let the formatting element be the last element in
+ the list of active formatting elements that:
+ * is between the end of the list and the last scope
+ marker in the list, if any, or the start of the list
+ otherwise, and
+ * has the same tag name as the token.
+ */
+ while (true) {
+ for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
+ if ($this->a_formatting[$a] === self::MARKER) {
+ break;
+
+ } elseif ($this->a_formatting[$a]->tagName === $token['name']) {
+ $formatting_element = $this->a_formatting[$a];
+ $in_stack = in_array($formatting_element, $this->stack, true);
+ $fe_af_pos = $a;
+ break;
+ }
+ }
+
+ /* If there is no such node, or, if that node is
+ also in the stack of open elements but the element
+ is not in scope, then this is a parse error. Abort
+ these steps. The token is ignored. */
+ if (!isset($formatting_element) || ($in_stack &&
+ !$this->elementInScope($token['name']))
+ ) {
+ break;
+
+ /* Otherwise, if there is such a node, but that node
+ is not in the stack of open elements, then this is a
+ parse error; remove the element from the list, and
+ abort these steps. */
+ } elseif (isset($formatting_element) && !$in_stack) {
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+ break;
+ }
+
+ /* 2. Let the furthest block be the topmost node in the
+ stack of open elements that is lower in the stack
+ than the formatting element, and is not an element in
+ the phrasing or formatting categories. There might
+ not be one. */
+ $fe_s_pos = array_search($formatting_element, $this->stack, true);
+ $length = count($this->stack);
+
+ for ($s = $fe_s_pos + 1; $s < $length; $s++) {
+ $category = $this->getElementCategory($this->stack[$s]->nodeName);
+
+ if ($category !== self::PHRASING && $category !== self::FORMATTING) {
+ $furthest_block = $this->stack[$s];
+ }
+ }
+
+ /* 3. If there is no furthest block, then the UA must
+ skip the subsequent steps and instead just pop all
+ the nodes from the bottom of the stack of open
+ elements, from the current node up to the formatting
+ element, and remove the formatting element from the
+ list of active formatting elements. */
+ if (!isset($furthest_block)) {
+ for ($n = $length - 1; $n >= $fe_s_pos; $n--) {
+ array_pop($this->stack);
+ }
+
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+ break;
+ }
+
+ /* 4. Let the common ancestor be the element
+ immediately above the formatting element in the stack
+ of open elements. */
+ $common_ancestor = $this->stack[$fe_s_pos - 1];
+
+ /* 5. If the furthest block has a parent node, then
+ remove the furthest block from its parent node. */
+ if ($furthest_block->parentNode !== null) {
+ $furthest_block->parentNode->removeChild($furthest_block);
+ }
+
+ /* 6. Let a bookmark note the position of the
+ formatting element in the list of active formatting
+ elements relative to the elements on either side
+ of it in the list. */
+ $bookmark = $fe_af_pos;
+
+ /* 7. Let node and last node be the furthest block.
+ Follow these steps: */
+ $node = $furthest_block;
+ $last_node = $furthest_block;
+
+ while (true) {
+ for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
+ /* 7.1 Let node be the element immediately
+ prior to node in the stack of open elements. */
+ $node = $this->stack[$n];
+
+ /* 7.2 If node is not in the list of active
+ formatting elements, then remove node from
+ the stack of open elements and then go back
+ to step 1. */
+ if (!in_array($node, $this->a_formatting, true)) {
+ unset($this->stack[$n]);
+ $this->stack = array_merge($this->stack);
+
+ } else {
+ break;
+ }
+ }
+
+ /* 7.3 Otherwise, if node is the formatting
+ element, then go to the next step in the overall
+ algorithm. */
+ if ($node === $formatting_element) {
+ break;
+
+ /* 7.4 Otherwise, if last node is the furthest
+ block, then move the aforementioned bookmark to
+ be immediately after the node in the list of
+ active formatting elements. */
+ } elseif ($last_node === $furthest_block) {
+ $bookmark = array_search($node, $this->a_formatting, true) + 1;
+ }
+
+ /* 7.5 If node has any children, perform a
+ shallow clone of node, replace the entry for
+ node in the list of active formatting elements
+ with an entry for the clone, replace the entry
+ for node in the stack of open elements with an
+ entry for the clone, and let node be the clone. */
+ if ($node->hasChildNodes()) {
+ $clone = $node->cloneNode();
+ $s_pos = array_search($node, $this->stack, true);
+ $a_pos = array_search($node, $this->a_formatting, true);
+
+ $this->stack[$s_pos] = $clone;
+ $this->a_formatting[$a_pos] = $clone;
+ $node = $clone;
+ }
+
+ /* 7.6 Insert last node into node, first removing
+ it from its previous parent node if any. */
+ if ($last_node->parentNode !== null) {
+ $last_node->parentNode->removeChild($last_node);
+ }
+
+ $node->appendChild($last_node);
+
+ /* 7.7 Let last node be node. */
+ $last_node = $node;
+ }
+
+ /* 8. Insert whatever last node ended up being in
+ the previous step into the common ancestor node,
+ first removing it from its previous parent node if
+ any. */
+ if ($last_node->parentNode !== null) {
+ $last_node->parentNode->removeChild($last_node);
+ }
+
+ $common_ancestor->appendChild($last_node);
+
+ /* 9. Perform a shallow clone of the formatting
+ element. */
+ $clone = $formatting_element->cloneNode();
+
+ /* 10. Take all of the child nodes of the furthest
+ block and append them to the clone created in the
+ last step. */
+ while ($furthest_block->hasChildNodes()) {
+ $child = $furthest_block->firstChild;
+ $furthest_block->removeChild($child);
+ $clone->appendChild($child);
+ }
+
+ /* 11. Append that clone to the furthest block. */
+ $furthest_block->appendChild($clone);
+
+ /* 12. Remove the formatting element from the list
+ of active formatting elements, and insert the clone
+ into the list of active formatting elements at the
+ position of the aforementioned bookmark. */
+ $fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+
+ $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
+ $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting));
+ $this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
+
+ /* 13. Remove the formatting element from the stack
+ of open elements, and insert the clone into the stack
+ of open elements immediately after (i.e. in a more
+ deeply nested position than) the position of the
+ furthest block in that stack. */
+ $fe_s_pos = array_search($formatting_element, $this->stack, true);
+ $fb_s_pos = array_search($furthest_block, $this->stack, true);
+ unset($this->stack[$fe_s_pos]);
+
+ $s_part1 = array_slice($this->stack, 0, $fb_s_pos);
+ $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack));
+ $this->stack = array_merge($s_part1, array($clone), $s_part2);
+
+ /* 14. Jump back to step 1 in this series of steps. */
+ unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
+ }
+ break;
+
+ /* An end tag token whose tag name is one of: "button",
+ "marquee", "object" */
+ case 'button':
+ case 'marquee':
+ case 'object':
+ /* If the stack of open elements has an element in scope whose
+ tag name matches the tag name of the token, then generate implied
+ tags. */
+ if ($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with the same
+ tag name as the token, then this is a parse error. */
+ // k
+
+ /* Now, if the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then pop
+ elements from the stack until that element has been popped from
+ the stack, and clear the list of active formatting elements up
+ to the last marker. */
+ for ($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if ($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+
+ $marker = end(array_keys($this->a_formatting, self::MARKER, true));
+
+ for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
+ array_pop($this->a_formatting);
+ }
+ }
+ break;
+
+ /* Or an end tag whose tag name is one of: "area", "basefont",
+ "bgsound", "br", "embed", "hr", "iframe", "image", "img",
+ "input", "isindex", "noembed", "noframes", "param", "select",
+ "spacer", "table", "textarea", "wbr" */
+ case 'area':
+ case 'basefont':
+ case 'bgsound':
+ case 'br':
+ case 'embed':
+ case 'hr':
+ case 'iframe':
+ case 'image':
+ case 'img':
+ case 'input':
+ case 'isindex':
+ case 'noembed':
+ case 'noframes':
+ case 'param':
+ case 'select':
+ case 'spacer':
+ case 'table':
+ case 'textarea':
+ case 'wbr':
+ // Parse error. Ignore the token.
+ break;
+
+ /* An end tag token not covered by the previous entries */
+ default:
+ for ($n = count($this->stack) - 1; $n >= 0; $n--) {
+ /* Initialise node to be the current node (the bottommost
+ node of the stack). */
+ $node = end($this->stack);
+
+ /* If node has the same tag name as the end tag token,
+ then: */
+ if ($token['name'] === $node->nodeName) {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* If the tag name of the end tag token does not
+ match the tag name of the current node, this is a
+ parse error. */
+ // k
+
+ /* Pop all the nodes from the current node up to
+ node, including node, then stop this algorithm. */
+ for ($x = count($this->stack) - $n; $x >= $n; $x--) {
+ array_pop($this->stack);
+ }
+
+ } else {
+ $category = $this->getElementCategory($node);
+
+ if ($category !== self::SPECIAL && $category !== self::SCOPING) {
+ /* Otherwise, if node is in neither the formatting
+ category nor the phrasing category, then this is a
+ parse error. Stop this algorithm. The end tag token
+ is ignored. */
+ return false;
+ }
+ }
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ private function inTable($token)
+ {
+ $clear = array('html', 'table');
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Append the character to the current node. */
+ $text = $this->dom->createTextNode($token['data']);
+ end($this->stack)->appendChild($text);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ end($this->stack)->appendChild($comment);
+
+ /* A start tag whose tag name is "caption" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'caption'
+ ) {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+
+ /* Insert an HTML element for the token, then switch the
+ insertion mode to "in caption". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CAPTION;
+
+ /* A start tag whose tag name is "colgroup" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'colgroup'
+ ) {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the
+ insertion mode to "in column group". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CGROUP;
+
+ /* A start tag whose tag name is "col" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'col'
+ ) {
+ $this->inTable(
+ array(
+ 'name' => 'colgroup',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ $this->inColumnGroup($token);
+
+ /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif ($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array('tbody', 'tfoot', 'thead')
+ )
+ ) {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the insertion
+ mode to "in table body". */
+ $this->insertElement($token);
+ $this->mode = self::IN_TBODY;
+
+ /* A start tag whose tag name is one of: "td", "th", "tr" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ in_array($token['name'], array('td', 'th', 'tr'))
+ ) {
+ /* Act as if a start tag token with the tag name "tbody" had been
+ seen, then reprocess the current token. */
+ $this->inTable(
+ array(
+ 'name' => 'tbody',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ return $this->inTableBody($token);
+
+ /* A start tag whose tag name is "table" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'table'
+ ) {
+ /* Parse error. Act as if an end tag token with the tag name "table"
+ had been seen, then, if that token wasn't ignored, reprocess the
+ current token. */
+ $this->inTable(
+ array(
+ 'name' => 'table',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ return $this->mainPhase($token);
+
+ /* An end tag whose tag name is "table" */
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'table'
+ ) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if (!$this->elementInScope($token['name'], true)) {
+ return false;
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not a table element, then this
+ is a parse error. */
+ // w/e
+
+ /* Pop elements from this stack until a table element has been
+ popped from the stack. */
+ while (true) {
+ $current = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if ($current === 'table') {
+ break;
+ }
+ }
+
+ /* Reset the insertion mode appropriately. */
+ $this->resetInsertionMode();
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif ($token['type'] === HTML5::ENDTAG && in_array(
+ $token['name'],
+ array(
+ 'body',
+ 'caption',
+ 'col',
+ 'colgroup',
+ 'html',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr'
+ )
+ )
+ ) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* Parse error. Process the token as if the insertion mode was "in
+ body", with the following exception: */
+
+ /* If the current node is a table, tbody, tfoot, thead, or tr
+ element, then, whenever a node would be inserted into the current
+ node, it must instead be inserted into the foster parent element. */
+ if (in_array(
+ end($this->stack)->nodeName,
+ array('table', 'tbody', 'tfoot', 'thead', 'tr')
+ )
+ ) {
+ /* The foster parent element is the parent element of the last
+ table element in the stack of open elements, if there is a
+ table element and it has such a parent element. If there is no
+ table element in the stack of open elements (innerHTML case),
+ then the foster parent element is the first element in the
+ stack of open elements (the html element). Otherwise, if there
+ is a table element in the stack of open elements, but the last
+ table element in the stack of open elements has no parent, or
+ its parent node is not an element, then the foster parent
+ element is the element before the last table element in the
+ stack of open elements. */
+ for ($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if ($this->stack[$n]->nodeName === 'table') {
+ $table = $this->stack[$n];
+ break;
+ }
+ }
+
+ if (isset($table) && $table->parentNode !== null) {
+ $this->foster_parent = $table->parentNode;
+
+ } elseif (!isset($table)) {
+ $this->foster_parent = $this->stack[0];
+
+ } elseif (isset($table) && ($table->parentNode === null ||
+ $table->parentNode->nodeType !== XML_ELEMENT_NODE)
+ ) {
+ $this->foster_parent = $this->stack[$n - 1];
+ }
+ }
+
+ $this->inBody($token);
+ }
+ }
+
+ private function inCaption($token)
+ {
+ /* An end tag whose tag name is "caption" */
+ if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if (!$this->elementInScope($token['name'], true)) {
+ // Ignore
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not a caption element, then this
+ is a parse error. */
+ // w/e
+
+ /* Pop elements from this stack until a caption element has
+ been popped from the stack. */
+ while (true) {
+ $node = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if ($node === 'caption') {
+ break;
+ }
+ }
+
+ /* Clear the list of active formatting elements up to the last
+ marker. */
+ $this->clearTheActiveFormattingElementsUpToTheLastMarker();
+
+ /* Switch the insertion mode to "in table". */
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
+ name is "table" */
+ } elseif (($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array(
+ 'caption',
+ 'col',
+ 'colgroup',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr'
+ )
+ )) || ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'table')
+ ) {
+ /* Parse error. Act as if an end tag with the tag name "caption"
+ had been seen, then, if that token wasn't ignored, reprocess the
+ current token. */
+ $this->inCaption(
+ array(
+ 'name' => 'caption',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ return $this->inTable($token);
+
+ /* An end tag whose tag name is one of: "body", "col", "colgroup",
+ "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif ($token['type'] === HTML5::ENDTAG && in_array(
+ $token['name'],
+ array(
+ 'body',
+ 'col',
+ 'colgroup',
+ 'html',
+ 'tbody',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr'
+ )
+ )
+ ) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in body". */
+ $this->inBody($token);
+ }
+ }
+
+ private function inColumnGroup($token)
+ {
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Append the character to the current node. */
+ $text = $this->dom->createTextNode($token['data']);
+ end($this->stack)->appendChild($text);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ end($this->stack)->appendChild($comment);
+
+ /* A start tag whose tag name is "col" */
+ } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') {
+ /* Insert a col element for the token. Immediately pop the current
+ node off the stack of open elements. */
+ $this->insertElement($token);
+ array_pop($this->stack);
+
+ /* An end tag whose tag name is "colgroup" */
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'colgroup'
+ ) {
+ /* If the current node is the root html element, then this is a
+ parse error, ignore the token. (innerHTML case) */
+ if (end($this->stack)->nodeName === 'html') {
+ // Ignore
+
+ /* Otherwise, pop the current node (which will be a colgroup
+ element) from the stack of open elements. Switch the insertion
+ mode to "in table". */
+ } else {
+ array_pop($this->stack);
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* An end tag whose tag name is "col" */
+ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Act as if an end tag with the tag name "colgroup" had been seen,
+ and then, if that token wasn't ignored, reprocess the current token. */
+ $this->inColumnGroup(
+ array(
+ 'name' => 'colgroup',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ return $this->inTable($token);
+ }
+ }
+
+ private function inTableBody($token)
+ {
+ $clear = array('tbody', 'tfoot', 'thead', 'html');
+
+ /* A start tag whose tag name is "tr" */
+ if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert a tr element for the token, then switch the insertion
+ mode to "in row". */
+ $this->insertElement($token);
+ $this->mode = self::IN_ROW;
+
+ /* A start tag whose tag name is one of: "th", "td" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ ($token['name'] === 'th' || $token['name'] === 'td')
+ ) {
+ /* Parse error. Act as if a start tag with the tag name "tr" had
+ been seen, then reprocess the current token. */
+ $this->inTableBody(
+ array(
+ 'name' => 'tr',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ )
+ );
+
+ return $this->inRow($token);
+
+ /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('tbody', 'tfoot', 'thead'))
+ ) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. */
+ if (!$this->elementInScope($token['name'], true)) {
+ // Ignore
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Pop the current node from the stack of open elements. Switch
+ the insertion mode to "in table". */
+ array_pop($this->stack);
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
+ } elseif (($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead')
+ )) ||
+ ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')
+ ) {
+ /* If the stack of open elements does not have a tbody, thead, or
+ tfoot element in table scope, this is a parse error. Ignore the
+ token. (innerHTML case) */
+ if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Act as if an end tag with the same tag name as the current
+ node ("tbody", "tfoot", or "thead") had been seen, then
+ reprocess the current token. */
+ $this->inTableBody(
+ array(
+ 'name' => end($this->stack)->nodeName,
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ return $this->mainPhase($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "td", "th", "tr" */
+ } elseif ($token['type'] === HTML5::ENDTAG && in_array(
+ $token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr')
+ )
+ ) {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in table". */
+ $this->inTable($token);
+ }
+ }
+
+ private function inRow($token)
+ {
+ $clear = array('tr', 'html');
+
+ /* A start tag whose tag name is one of: "th", "td" */
+ if ($token['type'] === HTML5::STARTTAG &&
+ ($token['name'] === 'th' || $token['name'] === 'td')
+ ) {
+ /* Clear the stack back to a table row context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the insertion
+ mode to "in cell". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CELL;
+
+ /* Insert a marker at the end of the list of active formatting
+ elements. */
+ $this->a_formatting[] = self::MARKER;
+
+ /* An end tag whose tag name is "tr" */
+ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if (!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table row context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Pop the current node (which will be a tr element) from the
+ stack of open elements. Switch the insertion mode to "in table
+ body". */
+ array_pop($this->stack);
+ $this->mode = self::IN_TBODY;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
+ } elseif ($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr')
+ )
+ ) {
+ /* Act as if an end tag with the tag name "tr" had been seen, then,
+ if that token wasn't ignored, reprocess the current token. */
+ $this->inRow(
+ array(
+ 'name' => 'tr',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ return $this->inCell($token);
+
+ /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('tbody', 'tfoot', 'thead'))
+ ) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. */
+ if (!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Otherwise, act as if an end tag with the tag name "tr" had
+ been seen, then reprocess the current token. */
+ $this->inRow(
+ array(
+ 'name' => 'tr',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ return $this->inCell($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "td", "th" */
+ } elseif ($token['type'] === HTML5::ENDTAG && in_array(
+ $token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr')
+ )
+ ) {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in table". */
+ $this->inTable($token);
+ }
+ }
+
+ private function inCell($token)
+ {
+ /* An end tag whose tag name is one of: "td", "th" */
+ if ($token['type'] === HTML5::ENDTAG &&
+ ($token['name'] === 'td' || $token['name'] === 'th')
+ ) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as that of the token, then this is a
+ parse error and the token must be ignored. */
+ if (!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags, except for elements with the same
+ tag name as the token. */
+ $this->generateImpliedEndTags(array($token['name']));
+
+ /* Now, if the current node is not an element with the same tag
+ name as the token, then this is a parse error. */
+ // k
+
+ /* Pop elements from this stack until an element with the same
+ tag name as the token has been popped from the stack. */
+ while (true) {
+ $node = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if ($node === $token['name']) {
+ break;
+ }
+ }
+
+ /* Clear the list of active formatting elements up to the last
+ marker. */
+ $this->clearTheActiveFormattingElementsUpToTheLastMarker();
+
+ /* Switch the insertion mode to "in row". (The current node
+ will be a tr element at this point.) */
+ $this->mode = self::IN_ROW;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif ($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array(
+ 'caption',
+ 'col',
+ 'colgroup',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr'
+ )
+ )
+ ) {
+ /* If the stack of open elements does not have a td or th element
+ in table scope, then this is a parse error; ignore the token.
+ (innerHTML case) */
+ if (!$this->elementInScope(array('td', 'th'), true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif ($token['type'] === HTML5::STARTTAG && in_array(
+ $token['name'],
+ array(
+ 'caption',
+ 'col',
+ 'colgroup',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr'
+ )
+ )
+ ) {
+ /* If the stack of open elements does not have a td or th element
+ in table scope, then this is a parse error; ignore the token.
+ (innerHTML case) */
+ if (!$this->elementInScope(array('td', 'th'), true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html" */
+ } elseif ($token['type'] === HTML5::ENDTAG && in_array(
+ $token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html')
+ )
+ ) {
+ /* Parse error. Ignore the token. */
+
+ /* An end tag whose tag name is one of: "table", "tbody", "tfoot",
+ "thead", "tr" */
+ } elseif ($token['type'] === HTML5::ENDTAG && in_array(
+ $token['name'],
+ array('table', 'tbody', 'tfoot', 'thead', 'tr')
+ )
+ ) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as that of the token (which can only
+ happen for "tbody", "tfoot" and "thead", or, in the innerHTML case),
+ then this is a parse error and the token must be ignored. */
+ if (!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in body". */
+ $this->inBody($token);
+ }
+ }
+
+ private function inSelect($token)
+ {
+ /* Handle the token as follows: */
+
+ /* A character token */
+ if ($token['type'] === HTML5::CHARACTR) {
+ /* Append the token's character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token whose tag name is "option" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'option'
+ ) {
+ /* If the current node is an option element, act as if an end tag
+ with the tag name "option" had been seen. */
+ if (end($this->stack)->nodeName === 'option') {
+ $this->inSelect(
+ array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* A start tag token whose tag name is "optgroup" */
+ } elseif ($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'optgroup'
+ ) {
+ /* If the current node is an option element, act as if an end tag
+ with the tag name "option" had been seen. */
+ if (end($this->stack)->nodeName === 'option') {
+ $this->inSelect(
+ array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* If the current node is an optgroup element, act as if an end tag
+ with the tag name "optgroup" had been seen. */
+ if (end($this->stack)->nodeName === 'optgroup') {
+ $this->inSelect(
+ array(
+ 'name' => 'optgroup',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* An end tag token whose tag name is "optgroup" */
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'optgroup'
+ ) {
+ /* First, if the current node is an option element, and the node
+ immediately before it in the stack of open elements is an optgroup
+ element, then act as if an end tag with the tag name "option" had
+ been seen. */
+ $elements_in_stack = count($this->stack);
+
+ if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' &&
+ $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup'
+ ) {
+ $this->inSelect(
+ array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+ }
+
+ /* If the current node is an optgroup element, then pop that node
+ from the stack of open elements. Otherwise, this is a parse error,
+ ignore the token. */
+ if ($this->stack[$elements_in_stack - 1] === 'optgroup') {
+ array_pop($this->stack);
+ }
+
+ /* An end tag token whose tag name is "option" */
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'option'
+ ) {
+ /* If the current node is an option element, then pop that node
+ from the stack of open elements. Otherwise, this is a parse error,
+ ignore the token. */
+ if (end($this->stack)->nodeName === 'option') {
+ array_pop($this->stack);
+ }
+
+ /* An end tag whose tag name is "select" */
+ } elseif ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'select'
+ ) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if (!$this->elementInScope($token['name'], true)) {
+ // w/e
+
+ /* Otherwise: */
+ } else {
+ /* Pop elements from the stack of open elements until a select
+ element has been popped from the stack. */
+ while (true) {
+ $current = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if ($current === 'select') {
+ break;
+ }
+ }
+
+ /* Reset the insertion mode appropriately. */
+ $this->resetInsertionMode();
+ }
+
+ /* A start tag whose tag name is "select" */
+ } elseif ($token['name'] === 'select' &&
+ $token['type'] === HTML5::STARTTAG
+ ) {
+ /* Parse error. Act as if the token had been an end tag with the
+ tag name "select" instead. */
+ $this->inSelect(
+ array(
+ 'name' => 'select',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ /* An end tag whose tag name is one of: "caption", "table", "tbody",
+ "tfoot", "thead", "tr", "td", "th" */
+ } elseif (in_array(
+ $token['name'],
+ array(
+ 'caption',
+ 'table',
+ 'tbody',
+ 'tfoot',
+ 'thead',
+ 'tr',
+ 'td',
+ 'th'
+ )
+ ) && $token['type'] === HTML5::ENDTAG
+ ) {
+ /* Parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in table scope with
+ the same tag name as that of the token, then act as if an end tag
+ with the tag name "select" had been seen, and reprocess the token.
+ Otherwise, ignore the token. */
+ if ($this->elementInScope($token['name'], true)) {
+ $this->inSelect(
+ array(
+ 'name' => 'select',
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ $this->mainPhase($token);
+ }
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function afterBody($token)
+ {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Process the token as it would be processed if the insertion mode
+ was "in body". */
+ $this->inBody($token);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the first element in the stack of open
+ elements (the html element), with the data attribute set to the
+ data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->stack[0]->appendChild($comment);
+
+ /* An end tag with the tag name "html" */
+ } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') {
+ /* If the parser was originally created in order to handle the
+ setting of an element's innerHTML attribute, this is a parse error;
+ ignore the token. (The element will be an html element in this
+ case.) (innerHTML case) */
+
+ /* Otherwise, switch to the trailing end phase. */
+ $this->phase = self::END_PHASE;
+
+ /* Anything else */
+ } else {
+ /* Parse error. Set the insertion mode to "in body" and reprocess
+ the token. */
+ $this->mode = self::IN_BODY;
+ return $this->inBody($token);
+ }
+ }
+
+ private function inFrameset($token)
+ {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
+ if ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag with the tag name "frameset" */
+ } elseif ($token['name'] === 'frameset' &&
+ $token['type'] === HTML5::STARTTAG
+ ) {
+ $this->insertElement($token);
+
+ /* An end tag with the tag name "frameset" */
+ } elseif ($token['name'] === 'frameset' &&
+ $token['type'] === HTML5::ENDTAG
+ ) {
+ /* If the current node is the root html element, then this is a
+ parse error; ignore the token. (innerHTML case) */
+ if (end($this->stack)->nodeName === 'html') {
+ // Ignore
+
+ } else {
+ /* Otherwise, pop the current node from the stack of open
+ elements. */
+ array_pop($this->stack);
+
+ /* If the parser was not originally created in order to handle
+ the setting of an element's innerHTML attribute (innerHTML case),
+ and the current node is no longer a frameset element, then change
+ the insertion mode to "after frameset". */
+ $this->mode = self::AFTR_FRAME;
+ }
+
+ /* A start tag with the tag name "frame" */
+ } elseif ($token['name'] === 'frame' &&
+ $token['type'] === HTML5::STARTTAG
+ ) {
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+
+ /* A start tag with the tag name "noframes" */
+ } elseif ($token['name'] === 'noframes' &&
+ $token['type'] === HTML5::STARTTAG
+ ) {
+ /* Process the token as if the insertion mode had been "in body". */
+ $this->inBody($token);
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function afterFrameset($token)
+ {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
+ if ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* An end tag with the tag name "html" */
+ } elseif ($token['name'] === 'html' &&
+ $token['type'] === HTML5::ENDTAG
+ ) {
+ /* Switch to the trailing end phase. */
+ $this->phase = self::END_PHASE;
+
+ /* A start tag with the tag name "noframes" */
+ } elseif ($token['name'] === 'noframes' &&
+ $token['type'] === HTML5::STARTTAG
+ ) {
+ /* Process the token as if the insertion mode had been "in body". */
+ $this->inBody($token);
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function trailingEndPhase($token)
+ {
+ /* After the main phase, as each token is emitted from the tokenisation
+ stage, it must be processed as described in this section. */
+
+ /* A DOCTYPE token */
+ if ($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A comment token */
+ } elseif ($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the Document object with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->dom->appendChild($comment);
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif ($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])
+ ) {
+ /* Process the token as it would be processed in the main phase. */
+ $this->mainPhase($token);
+
+ /* A character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE. Or a start tag token. Or an end tag token. */
+ } elseif (($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
+ $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG
+ ) {
+ /* Parse error. Switch back to the main phase and reprocess the
+ token. */
+ $this->phase = self::MAIN_PHASE;
+ return $this->mainPhase($token);
+
+ /* An end-of-file token */
+ } elseif ($token['type'] === HTML5::EOF) {
+ /* OMG DONE!! */
+ }
+ }
+
+ private function insertElement($token, $append = true, $check = false)
+ {
+ // Proprietary workaround for libxml2's limitations with tag names
+ if ($check) {
+ // Slightly modified HTML5 tag-name modification,
+ // removing anything that's not an ASCII letter, digit, or hyphen
+ $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']);
+ // Remove leading hyphens and numbers
+ $token['name'] = ltrim($token['name'], '-0..9');
+ // In theory, this should ever be needed, but just in case
+ if ($token['name'] === '') {
+ $token['name'] = 'span';
+ } // arbitrary generic choice
+ }
+
+ $el = $this->dom->createElement($token['name']);
+
+ foreach ($token['attr'] as $attr) {
+ if (!$el->hasAttribute($attr['name'])) {
+ $el->setAttribute($attr['name'], (string)$attr['value']);
+ }
+ }
+
+ $this->appendToRealParent($el);
+ $this->stack[] = $el;
+
+ return $el;
+ }
+
+ private function insertText($data)
+ {
+ $text = $this->dom->createTextNode($data);
+ $this->appendToRealParent($text);
+ }
+
+ private function insertComment($data)
+ {
+ $comment = $this->dom->createComment($data);
+ $this->appendToRealParent($comment);
+ }
+
+ private function appendToRealParent($node)
+ {
+ if ($this->foster_parent === null) {
+ end($this->stack)->appendChild($node);
+
+ } elseif ($this->foster_parent !== null) {
+ /* If the foster parent element is the parent element of the
+ last table element in the stack of open elements, then the new
+ node must be inserted immediately before the last table element
+ in the stack of open elements in the foster parent element;
+ otherwise, the new node must be appended to the foster parent
+ element. */
+ for ($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if ($this->stack[$n]->nodeName === 'table' &&
+ $this->stack[$n]->parentNode !== null
+ ) {
+ $table = $this->stack[$n];
+ break;
+ }
+ }
+
+ if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) {
+ $this->foster_parent->insertBefore($node, $table);
+ } else {
+ $this->foster_parent->appendChild($node);
+ }
+
+ $this->foster_parent = null;
+ }
+ }
+
+ private function elementInScope($el, $table = false)
+ {
+ if (is_array($el)) {
+ foreach ($el as $element) {
+ if ($this->elementInScope($element, $table)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ $leng = count($this->stack);
+
+ for ($n = 0; $n < $leng; $n++) {
+ /* 1. Initialise node to be the current node (the bottommost node of
+ the stack). */
+ $node = $this->stack[$leng - 1 - $n];
+
+ if ($node->tagName === $el) {
+ /* 2. If node is the target node, terminate in a match state. */
+ return true;
+
+ } elseif ($node->tagName === 'table') {
+ /* 3. Otherwise, if node is a table element, terminate in a failure
+ state. */
+ return false;
+
+ } elseif ($table === true && in_array(
+ $node->tagName,
+ array(
+ 'caption',
+ 'td',
+ 'th',
+ 'button',
+ 'marquee',
+ 'object'
+ )
+ )
+ ) {
+ /* 4. Otherwise, if the algorithm is the "has an element in scope"
+ variant (rather than the "has an element in table scope" variant),
+ and node is one of the following, terminate in a failure state. */
+ return false;
+
+ } elseif ($node === $node->ownerDocument->documentElement) {
+ /* 5. Otherwise, if node is an html element (root element), terminate
+ in a failure state. (This can only happen if the node is the topmost
+ node of the stack of open elements, and prevents the next step from
+ being invoked if there are no more elements in the stack.) */
+ return false;
+ }
+
+ /* Otherwise, set node to the previous entry in the stack of open
+ elements and return to step 2. (This will never fail, since the loop
+ will always terminate in the previous step if the top of the stack
+ is reached.) */
+ }
+ }
+
+ private function reconstructActiveFormattingElements()
+ {
+ /* 1. If there are no entries in the list of active formatting elements,
+ then there is nothing to reconstruct; stop this algorithm. */
+ $formatting_elements = count($this->a_formatting);
+
+ if ($formatting_elements === 0) {
+ return false;
+ }
+
+ /* 3. Let entry be the last (most recently added) element in the list
+ of active formatting elements. */
+ $entry = end($this->a_formatting);
+
+ /* 2. If the last (most recently added) entry in the list of active
+ formatting elements is a marker, or if it is an element that is in the
+ stack of open elements, then there is nothing to reconstruct; stop this
+ algorithm. */
+ if ($entry === self::MARKER || in_array($entry, $this->stack, true)) {
+ return false;
+ }
+
+ for ($a = $formatting_elements - 1; $a >= 0; true) {
+ /* 4. If there are no entries before entry in the list of active
+ formatting elements, then jump to step 8. */
+ if ($a === 0) {
+ $step_seven = false;
+ break;
+ }
+
+ /* 5. Let entry be the entry one earlier than entry in the list of
+ active formatting elements. */
+ $a--;
+ $entry = $this->a_formatting[$a];
+
+ /* 6. If entry is neither a marker nor an element that is also in
+ thetack of open elements, go to step 4. */
+ if ($entry === self::MARKER || in_array($entry, $this->stack, true)) {
+ break;
+ }
+ }
+
+ while (true) {
+ /* 7. Let entry be the element one later than entry in the list of
+ active formatting elements. */
+ if (isset($step_seven) && $step_seven === true) {
+ $a++;
+ $entry = $this->a_formatting[$a];
+ }
+
+ /* 8. Perform a shallow clone of the element entry to obtain clone. */
+ $clone = $entry->cloneNode();
+
+ /* 9. Append clone to the current node and push it onto the stack
+ of open elements so that it is the new current node. */
+ end($this->stack)->appendChild($clone);
+ $this->stack[] = $clone;
+
+ /* 10. Replace the entry for entry in the list with an entry for
+ clone. */
+ $this->a_formatting[$a] = $clone;
+
+ /* 11. If the entry for clone in the list of active formatting
+ elements is not the last entry in the list, return to step 7. */
+ if (end($this->a_formatting) !== $clone) {
+ $step_seven = true;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private function clearTheActiveFormattingElementsUpToTheLastMarker()
+ {
+ /* When the steps below require the UA to clear the list of active
+ formatting elements up to the last marker, the UA must perform the
+ following steps: */
+
+ while (true) {
+ /* 1. Let entry be the last (most recently added) entry in the list
+ of active formatting elements. */
+ $entry = end($this->a_formatting);
+
+ /* 2. Remove entry from the list of active formatting elements. */
+ array_pop($this->a_formatting);
+
+ /* 3. If entry was a marker, then stop the algorithm at this point.
+ The list has been cleared up to the last marker. */
+ if ($entry === self::MARKER) {
+ break;
+ }
+ }
+ }
+
+ private function generateImpliedEndTags($exclude = array())
+ {
+ /* When the steps below require the UA to generate implied end tags,
+ then, if the current node is a dd element, a dt element, an li element,
+ a p element, a td element, a th element, or a tr element, the UA must
+ act as if an end tag with the respective tag name had been seen and
+ then generate implied end tags again. */
+ $node = end($this->stack);
+ $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
+
+ while (in_array(end($this->stack)->nodeName, $elements)) {
+ array_pop($this->stack);
+ }
+ }
+
+ private function getElementCategory($node)
+ {
+ $name = $node->tagName;
+ if (in_array($name, $this->special)) {
+ return self::SPECIAL;
+ } elseif (in_array($name, $this->scoping)) {
+ return self::SCOPING;
+ } elseif (in_array($name, $this->formatting)) {
+ return self::FORMATTING;
+ } else {
+ return self::PHRASING;
+ }
+ }
+
+ private function clearStackToTableContext($elements)
+ {
+ /* When the steps above require the UA to clear the stack back to a
+ table context, it means that the UA must, while the current node is not
+ a table element or an html element, pop elements from the stack of open
+ elements. If this causes any elements to be popped from the stack, then
+ this is a parse error. */
+ while (true) {
+ $node = end($this->stack)->nodeName;
+
+ if (in_array($node, $elements)) {
+ break;
+ } else {
+ array_pop($this->stack);
+ }
+ }
+ }
+
+ private function resetInsertionMode()
+ {
+ /* 1. Let last be false. */
+ $last = false;
+ $leng = count($this->stack);
+
+ for ($n = $leng - 1; $n >= 0; $n--) {
+ /* 2. Let node be the last node in the stack of open elements. */
+ $node = $this->stack[$n];
+
+ /* 3. If node is the first node in the stack of open elements, then
+ set last to true. If the element whose innerHTML attribute is being
+ set is neither a td element nor a th element, then set node to the
+ element whose innerHTML attribute is being set. (innerHTML case) */
+ if ($this->stack[0]->isSameNode($node)) {
+ $last = true;
+ }
+
+ /* 4. If node is a select element, then switch the insertion mode to
+ "in select" and abort these steps. (innerHTML case) */
+ if ($node->nodeName === 'select') {
+ $this->mode = self::IN_SELECT;
+ break;
+
+ /* 5. If node is a td or th element, then switch the insertion mode
+ to "in cell" and abort these steps. */
+ } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') {
+ $this->mode = self::IN_CELL;
+ break;
+
+ /* 6. If node is a tr element, then switch the insertion mode to
+ "in row" and abort these steps. */
+ } elseif ($node->nodeName === 'tr') {
+ $this->mode = self::IN_ROW;
+ break;
+
+ /* 7. If node is a tbody, thead, or tfoot element, then switch the
+ insertion mode to "in table body" and abort these steps. */
+ } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) {
+ $this->mode = self::IN_TBODY;
+ break;
+
+ /* 8. If node is a caption element, then switch the insertion mode
+ to "in caption" and abort these steps. */
+ } elseif ($node->nodeName === 'caption') {
+ $this->mode = self::IN_CAPTION;
+ break;
+
+ /* 9. If node is a colgroup element, then switch the insertion mode
+ to "in column group" and abort these steps. (innerHTML case) */
+ } elseif ($node->nodeName === 'colgroup') {
+ $this->mode = self::IN_CGROUP;
+ break;
+
+ /* 10. If node is a table element, then switch the insertion mode
+ to "in table" and abort these steps. */
+ } elseif ($node->nodeName === 'table') {
+ $this->mode = self::IN_TABLE;
+ break;
+
+ /* 11. If node is a head element, then switch the insertion mode
+ to "in body" ("in body"! not "in head"!) and abort these steps.
+ (innerHTML case) */
+ } elseif ($node->nodeName === 'head') {
+ $this->mode = self::IN_BODY;
+ break;
+
+ /* 12. If node is a body element, then switch the insertion mode to
+ "in body" and abort these steps. */
+ } elseif ($node->nodeName === 'body') {
+ $this->mode = self::IN_BODY;
+ break;
+
+ /* 13. If node is a frameset element, then switch the insertion
+ mode to "in frameset" and abort these steps. (innerHTML case) */
+ } elseif ($node->nodeName === 'frameset') {
+ $this->mode = self::IN_FRAME;
+ break;
+
+ /* 14. If node is an html element, then: if the head element
+ pointer is null, switch the insertion mode to "before head",
+ otherwise, switch the insertion mode to "after head". In either
+ case, abort these steps. (innerHTML case) */
+ } elseif ($node->nodeName === 'html') {
+ $this->mode = ($this->head_pointer === null)
+ ? self::BEFOR_HEAD
+ : self::AFTER_HEAD;
+
+ break;
+
+ /* 15. If last is true, then set the insertion mode to "in body"
+ and abort these steps. (innerHTML case) */
+ } elseif ($last) {
+ $this->mode = self::IN_BODY;
+ break;
+ }
+ }
+ }
+
+ private function closeCell()
+ {
+ /* If the stack of open elements has a td or th element in table scope,
+ then act as if an end tag token with that tag name had been seen. */
+ foreach (array('td', 'th') as $cell) {
+ if ($this->elementInScope($cell, true)) {
+ $this->inCell(
+ array(
+ 'name' => $cell,
+ 'type' => HTML5::ENDTAG
+ )
+ );
+
+ break;
+ }
+ }
+ }
+
+ public function save()
+ {
+ return $this->dom;
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php
new file mode 100644
index 0000000000..3995fec9fe
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node.php
@@ -0,0 +1,49 @@
+data = $data;
+ $this->line = $line;
+ $this->col = $col;
+ }
+
+ public function toTokenPair() {
+ return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null);
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php
new file mode 100644
index 0000000000..6cbf56dada
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Element.php
@@ -0,0 +1,59 @@
+ form or the form, i.e.
+ * is it a pair of start/end tokens or an empty token.
+ * @bool
+ */
+ public $empty = false;
+
+ public $endCol = null, $endLine = null, $endArmor = array();
+
+ public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) {
+ $this->name = $name;
+ $this->attr = $attr;
+ $this->line = $line;
+ $this->col = $col;
+ $this->armor = $armor;
+ }
+
+ public function toTokenPair() {
+ // XXX inefficiency here, normalization is not necessary
+ if ($this->empty) {
+ return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null);
+ } else {
+ $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor);
+ $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor);
+ //$end->start = $start;
+ return array($start, $end);
+ }
+ }
+}
+
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php
new file mode 100644
index 0000000000..aec9166473
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Node/Text.php
@@ -0,0 +1,54 @@
+data = $data;
+ $this->is_whitespace = $is_whitespace;
+ $this->line = $line;
+ $this->col = $col;
+ }
+
+ public function toTokenPair() {
+ return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php
new file mode 100644
index 0000000000..18c8bbb00a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PercentEncoder.php
@@ -0,0 +1,111 @@
+preserve[$i] = true;
+ }
+ for ($i = 65; $i <= 90; $i++) { // upper-case
+ $this->preserve[$i] = true;
+ }
+ for ($i = 97; $i <= 122; $i++) { // lower-case
+ $this->preserve[$i] = true;
+ }
+ $this->preserve[45] = true; // Dash -
+ $this->preserve[46] = true; // Period .
+ $this->preserve[95] = true; // Underscore _
+ $this->preserve[126]= true; // Tilde ~
+
+ // extra letters not to escape
+ if ($preserve !== false) {
+ for ($i = 0, $c = strlen($preserve); $i < $c; $i++) {
+ $this->preserve[ord($preserve[$i])] = true;
+ }
+ }
+ }
+
+ /**
+ * Our replacement for urlencode, it encodes all non-reserved characters,
+ * as well as any extra characters that were instructed to be preserved.
+ * @note
+ * Assumes that the string has already been normalized, making any
+ * and all percent escape sequences valid. Percents will not be
+ * re-escaped, regardless of their status in $preserve
+ * @param string $string String to be encoded
+ * @return string Encoded string.
+ */
+ public function encode($string)
+ {
+ $ret = '';
+ for ($i = 0, $c = strlen($string); $i < $c; $i++) {
+ if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])])) {
+ $ret .= '%' . sprintf('%02X', $int);
+ } else {
+ $ret .= $string[$i];
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Fix up percent-encoding by decoding unreserved characters and normalizing.
+ * @warning This function is affected by $preserve, even though the
+ * usual desired behavior is for this not to preserve those
+ * characters. Be careful when reusing instances of PercentEncoder!
+ * @param string $string String to normalize
+ * @return string
+ */
+ public function normalize($string)
+ {
+ if ($string == '') {
+ return '';
+ }
+ $parts = explode('%', $string);
+ $ret = array_shift($parts);
+ foreach ($parts as $part) {
+ $length = strlen($part);
+ if ($length < 2) {
+ $ret .= '%25' . $part;
+ continue;
+ }
+ $encoding = substr($part, 0, 2);
+ $text = substr($part, 2);
+ if (!ctype_xdigit($encoding)) {
+ $ret .= '%25' . $part;
+ continue;
+ }
+ $int = hexdec($encoding);
+ if (isset($this->preserve[$int])) {
+ $ret .= chr($int) . $text;
+ continue;
+ }
+ $encoding = strtoupper($encoding);
+ $ret .= '%' . $encoding . $text;
+ }
+ return $ret;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php
new file mode 100644
index 0000000000..549e4cea1e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer.php
@@ -0,0 +1,218 @@
+getAll();
+ $context = new HTMLPurifier_Context();
+ $this->generator = new HTMLPurifier_Generator($config, $context);
+ }
+
+ /**
+ * Main function that renders object or aspect of that object
+ * @note Parameters vary depending on printer
+ */
+ // function render() {}
+
+ /**
+ * Returns a start tag
+ * @param string $tag Tag name
+ * @param array $attr Attribute array
+ * @return string
+ */
+ protected function start($tag, $attr = array())
+ {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_Start($tag, $attr ? $attr : array())
+ );
+ }
+
+ /**
+ * Returns an end tag
+ * @param string $tag Tag name
+ * @return string
+ */
+ protected function end($tag)
+ {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_End($tag)
+ );
+ }
+
+ /**
+ * Prints a complete element with content inside
+ * @param string $tag Tag name
+ * @param string $contents Element contents
+ * @param array $attr Tag attributes
+ * @param bool $escape whether or not to escape contents
+ * @return string
+ */
+ protected function element($tag, $contents, $attr = array(), $escape = true)
+ {
+ return $this->start($tag, $attr) .
+ ($escape ? $this->escape($contents) : $contents) .
+ $this->end($tag);
+ }
+
+ /**
+ * @param string $tag
+ * @param array $attr
+ * @return string
+ */
+ protected function elementEmpty($tag, $attr = array())
+ {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_Empty($tag, $attr)
+ );
+ }
+
+ /**
+ * @param string $text
+ * @return string
+ */
+ protected function text($text)
+ {
+ return $this->generator->generateFromToken(
+ new HTMLPurifier_Token_Text($text)
+ );
+ }
+
+ /**
+ * Prints a simple key/value row in a table.
+ * @param string $name Key
+ * @param mixed $value Value
+ * @return string
+ */
+ protected function row($name, $value)
+ {
+ if (is_bool($value)) {
+ $value = $value ? 'On' : 'Off';
+ }
+ return
+ $this->start('tr') . "\n" .
+ $this->element('th', $name) . "\n" .
+ $this->element('td', $value) . "\n" .
+ $this->end('tr');
+ }
+
+ /**
+ * Escapes a string for HTML output.
+ * @param string $string String to escape
+ * @return string
+ */
+ protected function escape($string)
+ {
+ $string = HTMLPurifier_Encoder::cleanUTF8($string);
+ $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8');
+ return $string;
+ }
+
+ /**
+ * Takes a list of strings and turns them into a single list
+ * @param string[] $array List of strings
+ * @param bool $polite Bool whether or not to add an end before the last
+ * @return string
+ */
+ protected function listify($array, $polite = false)
+ {
+ if (empty($array)) {
+ return 'None';
+ }
+ $ret = '';
+ $i = count($array);
+ foreach ($array as $value) {
+ $i--;
+ $ret .= $value;
+ if ($i > 0 && !($polite && $i == 1)) {
+ $ret .= ', ';
+ }
+ if ($polite && $i == 1) {
+ $ret .= 'and ';
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Retrieves the class of an object without prefixes, as well as metadata
+ * @param object $obj Object to determine class of
+ * @param string $sec_prefix Further prefix to remove
+ * @return string
+ */
+ protected function getClass($obj, $sec_prefix = '')
+ {
+ static $five = null;
+ if ($five === null) {
+ $five = version_compare(PHP_VERSION, '5', '>=');
+ }
+ $prefix = 'HTMLPurifier_' . $sec_prefix;
+ if (!$five) {
+ $prefix = strtolower($prefix);
+ }
+ $class = str_replace($prefix, '', get_class($obj));
+ $lclass = strtolower($class);
+ $class .= '(';
+ switch ($lclass) {
+ case 'enum':
+ $values = array();
+ foreach ($obj->valid_values as $value => $bool) {
+ $values[] = $value;
+ }
+ $class .= implode(', ', $values);
+ break;
+ case 'css_composite':
+ $values = array();
+ foreach ($obj->defs as $def) {
+ $values[] = $this->getClass($def, $sec_prefix);
+ }
+ $class .= implode(', ', $values);
+ break;
+ case 'css_multiple':
+ $class .= $this->getClass($obj->single, $sec_prefix) . ', ';
+ $class .= $obj->max;
+ break;
+ case 'css_denyelementdecorator':
+ $class .= $this->getClass($obj->def, $sec_prefix) . ', ';
+ $class .= $obj->element;
+ break;
+ case 'css_importantdecorator':
+ $class .= $this->getClass($obj->def, $sec_prefix);
+ if ($obj->allow) {
+ $class .= ', !important';
+ }
+ break;
+ }
+ $class .= ')';
+ return $class;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php
new file mode 100644
index 0000000000..29505fe12d
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/CSSDefinition.php
@@ -0,0 +1,44 @@
+def = $config->getCSSDefinition();
+ $ret = '';
+
+ $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer'));
+ $ret .= $this->start('table');
+
+ $ret .= $this->element('caption', 'Properties ($info)');
+
+ $ret .= $this->start('thead');
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Property', array('class' => 'heavy'));
+ $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;'));
+ $ret .= $this->end('tr');
+ $ret .= $this->end('thead');
+
+ ksort($this->def->info);
+ foreach ($this->def->info as $property => $obj) {
+ $name = $this->getClass($obj, 'AttrDef_');
+ $ret .= $this->row($property, $name);
+ }
+
+ $ret .= $this->end('table');
+ $ret .= $this->end('div');
+
+ return $ret;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css
new file mode 100644
index 0000000000..3ff1a88aa4
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.css
@@ -0,0 +1,10 @@
+
+.hp-config {}
+
+.hp-config tbody th {text-align:right; padding-right:0.5em;}
+.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;}
+.hp-config .namespace th {text-align:center;}
+.hp-config .verbose {display:none;}
+.hp-config .controls {text-align:center;}
+
+/* vim: et sw=4 sts=4 */
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js
new file mode 100644
index 0000000000..cba00c9b80
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.js
@@ -0,0 +1,5 @@
+function toggleWriteability(id_of_patient, checked) {
+ document.getElementById(id_of_patient).disabled = checked;
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php
new file mode 100644
index 0000000000..4c3ce1758e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/ConfigForm.php
@@ -0,0 +1,456 @@
+docURL = $doc_url;
+ $this->name = $name;
+ $this->compress = $compress;
+ // initialize sub-printers
+ $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default();
+ $this->fields[HTMLPurifier_VarParser::C_BOOL] = new HTMLPurifier_Printer_ConfigForm_bool();
+ }
+
+ /**
+ * Sets default column and row size for textareas in sub-printers
+ * @param $cols Integer columns of textarea, null to use default
+ * @param $rows Integer rows of textarea, null to use default
+ */
+ public function setTextareaDimensions($cols = null, $rows = null)
+ {
+ if ($cols) {
+ $this->fields['default']->cols = $cols;
+ }
+ if ($rows) {
+ $this->fields['default']->rows = $rows;
+ }
+ }
+
+ /**
+ * Retrieves styling, in case it is not accessible by webserver
+ */
+ public static function getCSS()
+ {
+ return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css');
+ }
+
+ /**
+ * Retrieves JavaScript, in case it is not accessible by webserver
+ */
+ public static function getJavaScript()
+ {
+ return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js');
+ }
+
+ /**
+ * Returns HTML output for a configuration form
+ * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array
+ * where [0] has an HTML namespace and [1] is being rendered.
+ * @param array|bool $allowed Optional namespace(s) and directives to restrict form to.
+ * @param bool $render_controls
+ * @return string
+ */
+ public function render($config, $allowed = true, $render_controls = true)
+ {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+
+ $this->config = $config;
+ $this->genConfig = $gen_config;
+ $this->prepareGenerator($gen_config);
+
+ $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def);
+ $all = array();
+ foreach ($allowed as $key) {
+ list($ns, $directive) = $key;
+ $all[$ns][$directive] = $config->get($ns . '.' . $directive);
+ }
+
+ $ret = '';
+ $ret .= $this->start('table', array('class' => 'hp-config'));
+ $ret .= $this->start('thead');
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive'));
+ $ret .= $this->element('th', 'Value', array('class' => 'hp-value'));
+ $ret .= $this->end('tr');
+ $ret .= $this->end('thead');
+ foreach ($all as $ns => $directives) {
+ $ret .= $this->renderNamespace($ns, $directives);
+ }
+ if ($render_controls) {
+ $ret .= $this->start('tbody');
+ $ret .= $this->start('tr');
+ $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls'));
+ $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit'));
+ $ret .= '[Reset]';
+ $ret .= $this->end('td');
+ $ret .= $this->end('tr');
+ $ret .= $this->end('tbody');
+ }
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders a single namespace
+ * @param $ns String namespace name
+ * @param array $directives array of directives to values
+ * @return string
+ */
+ protected function renderNamespace($ns, $directives)
+ {
+ $ret = '';
+ $ret .= $this->start('tbody', array('class' => 'namespace'));
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', $ns, array('colspan' => 2));
+ $ret .= $this->end('tr');
+ $ret .= $this->end('tbody');
+ $ret .= $this->start('tbody');
+ foreach ($directives as $directive => $value) {
+ $ret .= $this->start('tr');
+ $ret .= $this->start('th');
+ if ($this->docURL) {
+ $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL);
+ $ret .= $this->start('a', array('href' => $url));
+ }
+ $attr = array('for' => "{$this->name}:$ns.$directive");
+
+ // crop directive name if it's too long
+ if (!$this->compress || (strlen($directive) < $this->compress)) {
+ $directive_disp = $directive;
+ } else {
+ $directive_disp = substr($directive, 0, $this->compress - 2) . '...';
+ $attr['title'] = $directive;
+ }
+
+ $ret .= $this->element(
+ 'label',
+ $directive_disp,
+ // component printers must create an element with this id
+ $attr
+ );
+ if ($this->docURL) {
+ $ret .= $this->end('a');
+ }
+ $ret .= $this->end('th');
+
+ $ret .= $this->start('td');
+ $def = $this->config->def->info["$ns.$directive"];
+ if (is_int($def)) {
+ $allow_null = $def < 0;
+ $type = abs($def);
+ } else {
+ $type = $def->type;
+ $allow_null = isset($def->allow_null);
+ }
+ if (!isset($this->fields[$type])) {
+ $type = 0;
+ } // default
+ $type_obj = $this->fields[$type];
+ if ($allow_null) {
+ $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj);
+ }
+ $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config));
+ $ret .= $this->end('td');
+ $ret .= $this->end('tr');
+ }
+ $ret .= $this->end('tbody');
+ return $ret;
+ }
+
+}
+
+/**
+ * Printer decorator for directives that accept null
+ */
+class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer
+{
+ /**
+ * Printer being decorated
+ * @type HTMLPurifier_Printer
+ */
+ protected $obj;
+
+ /**
+ * @param HTMLPurifier_Printer $obj Printer to decorate
+ */
+ public function __construct($obj)
+ {
+ parent::__construct();
+ $this->obj = $obj;
+ }
+
+ /**
+ * @param string $ns
+ * @param string $directive
+ * @param string $value
+ * @param string $name
+ * @param HTMLPurifier_Config|array $config
+ * @return string
+ */
+ public function render($ns, $directive, $value, $name, $config)
+ {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
+
+ $ret = '';
+ $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive"));
+ $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
+ $ret .= $this->text(' Null/Disabled');
+ $ret .= $this->end('label');
+ $attr = array(
+ 'type' => 'checkbox',
+ 'value' => '1',
+ 'class' => 'null-toggle',
+ 'name' => "$name" . "[Null_$ns.$directive]",
+ 'id' => "$name:Null_$ns.$directive",
+ 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!!
+ );
+ if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) {
+ // modify inline javascript slightly
+ $attr['onclick'] =
+ "toggleWriteability('$name:Yes_$ns.$directive',checked);" .
+ "toggleWriteability('$name:No_$ns.$directive',checked)";
+ }
+ if ($value === null) {
+ $attr['checked'] = 'checked';
+ }
+ $ret .= $this->elementEmpty('input', $attr);
+ $ret .= $this->text(' or ');
+ $ret .= $this->elementEmpty('br');
+ $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config));
+ return $ret;
+ }
+}
+
+/**
+ * Swiss-army knife configuration form field printer
+ */
+class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer
+{
+ /**
+ * @type int
+ */
+ public $cols = 18;
+
+ /**
+ * @type int
+ */
+ public $rows = 5;
+
+ /**
+ * @param string $ns
+ * @param string $directive
+ * @param string $value
+ * @param string $name
+ * @param HTMLPurifier_Config|array $config
+ * @return string
+ */
+ public function render($ns, $directive, $value, $name, $config)
+ {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
+ // this should probably be split up a little
+ $ret = '';
+ $def = $config->def->info["$ns.$directive"];
+ if (is_int($def)) {
+ $type = abs($def);
+ } else {
+ $type = $def->type;
+ }
+ if (is_array($value)) {
+ switch ($type) {
+ case HTMLPurifier_VarParser::LOOKUP:
+ $array = $value;
+ $value = array();
+ foreach ($array as $val => $b) {
+ $value[] = $val;
+ }
+ //TODO does this need a break?
+ case HTMLPurifier_VarParser::ALIST:
+ $value = implode(PHP_EOL, $value);
+ break;
+ case HTMLPurifier_VarParser::HASH:
+ $nvalue = '';
+ foreach ($value as $i => $v) {
+ if (is_array($v)) {
+ // HACK
+ $v = implode(";", $v);
+ }
+ $nvalue .= "$i:$v" . PHP_EOL;
+ }
+ $value = $nvalue;
+ break;
+ default:
+ $value = '';
+ }
+ }
+ if ($type === HTMLPurifier_VarParser::C_MIXED) {
+ return 'Not supported';
+ $value = serialize($value);
+ }
+ $attr = array(
+ 'name' => "$name" . "[$ns.$directive]",
+ 'id' => "$name:$ns.$directive"
+ );
+ if ($value === null) {
+ $attr['disabled'] = 'disabled';
+ }
+ if (isset($def->allowed)) {
+ $ret .= $this->start('select', $attr);
+ foreach ($def->allowed as $val => $b) {
+ $attr = array();
+ if ($value == $val) {
+ $attr['selected'] = 'selected';
+ }
+ $ret .= $this->element('option', $val, $attr);
+ }
+ $ret .= $this->end('select');
+ } elseif ($type === HTMLPurifier_VarParser::TEXT ||
+ $type === HTMLPurifier_VarParser::ITEXT ||
+ $type === HTMLPurifier_VarParser::ALIST ||
+ $type === HTMLPurifier_VarParser::HASH ||
+ $type === HTMLPurifier_VarParser::LOOKUP) {
+ $attr['cols'] = $this->cols;
+ $attr['rows'] = $this->rows;
+ $ret .= $this->start('textarea', $attr);
+ $ret .= $this->text($value);
+ $ret .= $this->end('textarea');
+ } else {
+ $attr['value'] = $value;
+ $attr['type'] = 'text';
+ $ret .= $this->elementEmpty('input', $attr);
+ }
+ return $ret;
+ }
+}
+
+/**
+ * Bool form field printer
+ */
+class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer
+{
+ /**
+ * @param string $ns
+ * @param string $directive
+ * @param string $value
+ * @param string $name
+ * @param HTMLPurifier_Config|array $config
+ * @return string
+ */
+ public function render($ns, $directive, $value, $name, $config)
+ {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
+ $ret = '';
+ $ret .= $this->start('div', array('id' => "$name:$ns.$directive"));
+
+ $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive"));
+ $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
+ $ret .= $this->text(' Yes');
+ $ret .= $this->end('label');
+
+ $attr = array(
+ 'type' => 'radio',
+ 'name' => "$name" . "[$ns.$directive]",
+ 'id' => "$name:Yes_$ns.$directive",
+ 'value' => '1'
+ );
+ if ($value === true) {
+ $attr['checked'] = 'checked';
+ }
+ if ($value === null) {
+ $attr['disabled'] = 'disabled';
+ }
+ $ret .= $this->elementEmpty('input', $attr);
+
+ $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive"));
+ $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
+ $ret .= $this->text(' No');
+ $ret .= $this->end('label');
+
+ $attr = array(
+ 'type' => 'radio',
+ 'name' => "$name" . "[$ns.$directive]",
+ 'id' => "$name:No_$ns.$directive",
+ 'value' => '0'
+ );
+ if ($value === false) {
+ $attr['checked'] = 'checked';
+ }
+ if ($value === null) {
+ $attr['disabled'] = 'disabled';
+ }
+ $ret .= $this->elementEmpty('input', $attr);
+
+ $ret .= $this->end('div');
+
+ return $ret;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php
new file mode 100644
index 0000000000..ae8639176e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Printer/HTMLDefinition.php
@@ -0,0 +1,324 @@
+config =& $config;
+
+ $this->def = $config->getHTMLDefinition();
+
+ $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer'));
+
+ $ret .= $this->renderDoctype();
+ $ret .= $this->renderEnvironment();
+ $ret .= $this->renderContentSets();
+ $ret .= $this->renderInfo();
+
+ $ret .= $this->end('div');
+
+ return $ret;
+ }
+
+ /**
+ * Renders the Doctype table
+ * @return string
+ */
+ protected function renderDoctype()
+ {
+ $doctype = $this->def->doctype;
+ $ret = '';
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Doctype');
+ $ret .= $this->row('Name', $doctype->name);
+ $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No');
+ $ret .= $this->row('Default Modules', implode(', ', $doctype->modules));
+ $ret .= $this->row('Default Tidy Modules', implode(', ', $doctype->tidyModules));
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+
+ /**
+ * Renders environment table, which is miscellaneous info
+ * @return string
+ */
+ protected function renderEnvironment()
+ {
+ $def = $this->def;
+
+ $ret = '';
+
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Environment');
+
+ $ret .= $this->row('Parent of fragment', $def->info_parent);
+ $ret .= $this->renderChildren($def->info_parent_def->child);
+ $ret .= $this->row('Block wrap name', $def->info_block_wrapper);
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Global attributes');
+ $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0);
+ $ret .= $this->end('tr');
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Tag transforms');
+ $list = array();
+ foreach ($def->info_tag_transform as $old => $new) {
+ $new = $this->getClass($new, 'TagTransform_');
+ $list[] = "<$old> with $new";
+ }
+ $ret .= $this->element('td', $this->listify($list));
+ $ret .= $this->end('tr');
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Pre-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre));
+ $ret .= $this->end('tr');
+
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Post-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post));
+ $ret .= $this->end('tr');
+
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders the Content Sets table
+ * @return string
+ */
+ protected function renderContentSets()
+ {
+ $ret = '';
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Content Sets');
+ foreach ($this->def->info_content_sets as $name => $lookup) {
+ $ret .= $this->heavyHeader($name);
+ $ret .= $this->start('tr');
+ $ret .= $this->element('td', $this->listifyTagLookup($lookup));
+ $ret .= $this->end('tr');
+ }
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders the Elements ($info) table
+ * @return string
+ */
+ protected function renderInfo()
+ {
+ $ret = '';
+ $ret .= $this->start('table');
+ $ret .= $this->element('caption', 'Elements ($info)');
+ ksort($this->def->info);
+ $ret .= $this->heavyHeader('Allowed tags', 2);
+ $ret .= $this->start('tr');
+ $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2));
+ $ret .= $this->end('tr');
+ foreach ($this->def->info as $name => $def) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2));
+ $ret .= $this->end('tr');
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Inline content');
+ $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No');
+ $ret .= $this->end('tr');
+ if (!empty($def->excludes)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Excludes');
+ $ret .= $this->element('td', $this->listifyTagLookup($def->excludes));
+ $ret .= $this->end('tr');
+ }
+ if (!empty($def->attr_transform_pre)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Pre-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre));
+ $ret .= $this->end('tr');
+ }
+ if (!empty($def->attr_transform_post)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Post-AttrTransform');
+ $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post));
+ $ret .= $this->end('tr');
+ }
+ if (!empty($def->auto_close)) {
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Auto closed by');
+ $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close));
+ $ret .= $this->end('tr');
+ }
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', 'Allowed attributes');
+ $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0);
+ $ret .= $this->end('tr');
+
+ if (!empty($def->required_attr)) {
+ $ret .= $this->row('Required attributes', $this->listify($def->required_attr));
+ }
+
+ $ret .= $this->renderChildren($def->child);
+ }
+ $ret .= $this->end('table');
+ return $ret;
+ }
+
+ /**
+ * Renders a row describing the allowed children of an element
+ * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element
+ * @return string
+ */
+ protected function renderChildren($def)
+ {
+ $context = new HTMLPurifier_Context();
+ $ret = '';
+ $ret .= $this->start('tr');
+ $elements = array();
+ $attr = array();
+ if (isset($def->elements)) {
+ if ($def->type == 'strictblockquote') {
+ $def->validateChildren(array(), $this->config, $context);
+ }
+ $elements = $def->elements;
+ }
+ if ($def->type == 'chameleon') {
+ $attr['rowspan'] = 2;
+ } elseif ($def->type == 'empty') {
+ $elements = array();
+ } elseif ($def->type == 'table') {
+ $elements = array_flip(
+ array(
+ 'col',
+ 'caption',
+ 'colgroup',
+ 'thead',
+ 'tfoot',
+ 'tbody',
+ 'tr'
+ )
+ );
+ }
+ $ret .= $this->element('th', 'Allowed children', $attr);
+
+ if ($def->type == 'chameleon') {
+
+ $ret .= $this->element(
+ 'td',
+ 'Block: ' .
+ $this->escape($this->listifyTagLookup($def->block->elements)),
+ null,
+ 0
+ );
+ $ret .= $this->end('tr');
+ $ret .= $this->start('tr');
+ $ret .= $this->element(
+ 'td',
+ 'Inline: ' .
+ $this->escape($this->listifyTagLookup($def->inline->elements)),
+ null,
+ 0
+ );
+
+ } elseif ($def->type == 'custom') {
+
+ $ret .= $this->element(
+ 'td',
+ '' . ucfirst($def->type) . ': ' .
+ $def->dtd_regex
+ );
+
+ } else {
+ $ret .= $this->element(
+ 'td',
+ '' . ucfirst($def->type) . ': ' .
+ $this->escape($this->listifyTagLookup($elements)),
+ null,
+ 0
+ );
+ }
+ $ret .= $this->end('tr');
+ return $ret;
+ }
+
+ /**
+ * Listifies a tag lookup table.
+ * @param array $array Tag lookup array in form of array('tagname' => true)
+ * @return string
+ */
+ protected function listifyTagLookup($array)
+ {
+ ksort($array);
+ $list = array();
+ foreach ($array as $name => $discard) {
+ if ($name !== '#PCDATA' && !isset($this->def->info[$name])) {
+ continue;
+ }
+ $list[] = $name;
+ }
+ return $this->listify($list);
+ }
+
+ /**
+ * Listifies a list of objects by retrieving class names and internal state
+ * @param array $array List of objects
+ * @return string
+ * @todo Also add information about internal state
+ */
+ protected function listifyObjectList($array)
+ {
+ ksort($array);
+ $list = array();
+ foreach ($array as $obj) {
+ $list[] = $this->getClass($obj, 'AttrTransform_');
+ }
+ return $this->listify($list);
+ }
+
+ /**
+ * Listifies a hash of attributes to AttrDef classes
+ * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef)
+ * @return string
+ */
+ protected function listifyAttr($array)
+ {
+ ksort($array);
+ $list = array();
+ foreach ($array as $name => $obj) {
+ if ($obj === false) {
+ continue;
+ }
+ $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . '';
+ }
+ return $this->listify($list);
+ }
+
+ /**
+ * Creates a heavy header row
+ * @param string $text
+ * @param int $num
+ * @return string
+ */
+ protected function heavyHeader($text, $num = 1)
+ {
+ $ret = '';
+ $ret .= $this->start('tr');
+ $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy'));
+ $ret .= $this->end('tr');
+ return $ret;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php
new file mode 100644
index 0000000000..189348fd9e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyList.php
@@ -0,0 +1,122 @@
+parent = $parent;
+ }
+
+ /**
+ * Recursively retrieves the value for a key
+ * @param string $name
+ * @throws HTMLPurifier_Exception
+ */
+ public function get($name)
+ {
+ if ($this->has($name)) {
+ return $this->data[$name];
+ }
+ // possible performance bottleneck, convert to iterative if necessary
+ if ($this->parent) {
+ return $this->parent->get($name);
+ }
+ throw new HTMLPurifier_Exception("Key '$name' not found");
+ }
+
+ /**
+ * Sets the value of a key, for this plist
+ * @param string $name
+ * @param mixed $value
+ */
+ public function set($name, $value)
+ {
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * Returns true if a given key exists
+ * @param string $name
+ * @return bool
+ */
+ public function has($name)
+ {
+ return array_key_exists($name, $this->data);
+ }
+
+ /**
+ * Resets a value to the value of it's parent, usually the default. If
+ * no value is specified, the entire plist is reset.
+ * @param string $name
+ */
+ public function reset($name = null)
+ {
+ if ($name == null) {
+ $this->data = array();
+ } else {
+ unset($this->data[$name]);
+ }
+ }
+
+ /**
+ * Squashes this property list and all of its property lists into a single
+ * array, and returns the array. This value is cached by default.
+ * @param bool $force If true, ignores the cache and regenerates the array.
+ * @return array
+ */
+ public function squash($force = false)
+ {
+ if ($this->cache !== null && !$force) {
+ return $this->cache;
+ }
+ if ($this->parent) {
+ return $this->cache = array_merge($this->parent->squash($force), $this->data);
+ } else {
+ return $this->cache = $this->data;
+ }
+ }
+
+ /**
+ * Returns the parent plist.
+ * @return HTMLPurifier_PropertyList
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * Sets the parent plist.
+ * @param HTMLPurifier_PropertyList $plist Parent plist
+ */
+ public function setParent($plist)
+ {
+ $this->parent = $plist;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php
new file mode 100644
index 0000000000..f68fc8c30f
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/PropertyListIterator.php
@@ -0,0 +1,43 @@
+l = strlen($filter);
+ $this->filter = $filter;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function accept()
+ {
+ $key = $this->getInnerIterator()->key();
+ if (strncmp($key, $this->filter, $this->l) !== 0) {
+ return false;
+ }
+ return true;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php
new file mode 100644
index 0000000000..f58db9042a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Queue.php
@@ -0,0 +1,56 @@
+input = $input;
+ $this->output = array();
+ }
+
+ /**
+ * Shifts an element off the front of the queue.
+ */
+ public function shift() {
+ if (empty($this->output)) {
+ $this->output = array_reverse($this->input);
+ $this->input = array();
+ }
+ if (empty($this->output)) {
+ return NULL;
+ }
+ return array_pop($this->output);
+ }
+
+ /**
+ * Pushes an element onto the front of the queue.
+ */
+ public function push($x) {
+ array_push($this->input, $x);
+ }
+
+ /**
+ * Checks if it's empty.
+ */
+ public function isEmpty() {
+ return empty($this->input) && empty($this->output);
+ }
+}
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php
new file mode 100644
index 0000000000..e1ff3b72df
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy.php
@@ -0,0 +1,26 @@
+strategies as $strategy) {
+ $tokens = $strategy->execute($tokens, $config, $context);
+ }
+ return $tokens;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php
new file mode 100644
index 0000000000..4414c17d6e
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/Core.php
@@ -0,0 +1,17 @@
+strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements();
+ $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed();
+ $this->strategies[] = new HTMLPurifier_Strategy_FixNesting();
+ $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php
new file mode 100644
index 0000000000..6fa673db9a
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/FixNesting.php
@@ -0,0 +1,181 @@
+getHTMLDefinition();
+
+ $excludes_enabled = !$config->get('Core.DisableExcludes');
+
+ // setup the context variable 'IsInline', for chameleon processing
+ // is 'false' when we are not inline, 'true' when it must always
+ // be inline, and an integer when it is inline for a certain
+ // branch of the document tree
+ $is_inline = $definition->info_parent_def->descendants_are_inline;
+ $context->register('IsInline', $is_inline);
+
+ // setup error collector
+ $e =& $context->get('ErrorCollector', true);
+
+ //####################################################################//
+ // Loop initialization
+
+ // stack that contains all elements that are excluded
+ // it is organized by parent elements, similar to $stack,
+ // but it is only populated when an element with exclusions is
+ // processed, i.e. there won't be empty exclusions.
+ $exclude_stack = array($definition->info_parent_def->excludes);
+
+ // variable that contains the start token while we are processing
+ // nodes. This enables error reporting to do its job
+ $node = $top_node;
+ // dummy token
+ list($token, $d) = $node->toTokenPair();
+ $context->register('CurrentNode', $node);
+ $context->register('CurrentToken', $token);
+
+ //####################################################################//
+ // Loop
+
+ // We need to implement a post-order traversal iteratively, to
+ // avoid running into stack space limits. This is pretty tricky
+ // to reason about, so we just manually stack-ify the recursive
+ // variant:
+ //
+ // function f($node) {
+ // foreach ($node->children as $child) {
+ // f($child);
+ // }
+ // validate($node);
+ // }
+ //
+ // Thus, we will represent a stack frame as array($node,
+ // $is_inline, stack of children)
+ // e.g. array_reverse($node->children) - already processed
+ // children.
+
+ $parent_def = $definition->info_parent_def;
+ $stack = array(
+ array($top_node,
+ $parent_def->descendants_are_inline,
+ $parent_def->excludes, // exclusions
+ 0)
+ );
+
+ while (!empty($stack)) {
+ list($node, $is_inline, $excludes, $ix) = array_pop($stack);
+ // recursive call
+ $go = false;
+ $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name];
+ while (isset($node->children[$ix])) {
+ $child = $node->children[$ix++];
+ if ($child instanceof HTMLPurifier_Node_Element) {
+ $go = true;
+ $stack[] = array($node, $is_inline, $excludes, $ix);
+ $stack[] = array($child,
+ // ToDo: I don't think it matters if it's def or
+ // child_def, but double check this...
+ $is_inline || $def->descendants_are_inline,
+ empty($def->excludes) ? $excludes
+ : array_merge($excludes, $def->excludes),
+ 0);
+ break;
+ }
+ };
+ if ($go) continue;
+ list($token, $d) = $node->toTokenPair();
+ // base case
+ if ($excludes_enabled && isset($excludes[$node->name])) {
+ $node->dead = true;
+ if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded');
+ } else {
+ // XXX I suppose it would be slightly more efficient to
+ // avoid the allocation here and have children
+ // strategies handle it
+ $children = array();
+ foreach ($node->children as $child) {
+ if (!$child->dead) $children[] = $child;
+ }
+ $result = $def->child->validateChildren($children, $config, $context);
+ if ($result === true) {
+ // nop
+ $node->children = $children;
+ } elseif ($result === false) {
+ $node->dead = true;
+ if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed');
+ } else {
+ $node->children = $result;
+ if ($e) {
+ // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators
+ if (empty($result) && !empty($children)) {
+ $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed');
+ } else if ($result != $children) {
+ $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized');
+ }
+ }
+ }
+ }
+ }
+
+ //####################################################################//
+ // Post-processing
+
+ // remove context variables
+ $context->destroy('IsInline');
+ $context->destroy('CurrentNode');
+ $context->destroy('CurrentToken');
+
+ //####################################################################//
+ // Return
+
+ return HTMLPurifier_Arborize::flatten($node, $config, $context);
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php
new file mode 100644
index 0000000000..a6eb09e453
--- /dev/null
+++ b/lib/vendor/ezyang/htmlpurifier/library/HTMLPurifier/Strategy/MakeWellFormed.php
@@ -0,0 +1,659 @@
+getHTMLDefinition();
+
+ // local variables
+ $generator = new HTMLPurifier_Generator($config, $context);
+ $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
+ // used for autoclose early abortion
+ $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config);
+ $e = $context->get('ErrorCollector', true);
+ $i = false; // injector index
+ list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens);
+ if ($token === NULL) {
+ return array();
+ }
+ $reprocess = false; // whether or not to reprocess the same token
+ $stack = array();
+
+ // member variables
+ $this->stack =& $stack;
+ $this->tokens =& $tokens;
+ $this->token =& $token;
+ $this->zipper =& $zipper;
+ $this->config = $config;
+ $this->context = $context;
+
+ // context variables
+ $context->register('CurrentNesting', $stack);
+ $context->register('InputZipper', $zipper);
+ $context->register('CurrentToken', $token);
+
+ // -- begin INJECTOR --
+
+ $this->injectors = array();
+
+ $injectors = $config->getBatch('AutoFormat');
+ $def_injectors = $definition->info_injector;
+ $custom_injectors = $injectors['Custom'];
+ unset($injectors['Custom']); // special case
+ foreach ($injectors as $injector => $b) {
+ // XXX: Fix with a legitimate lookup table of enabled filters
+ if (strpos($injector, '.') !== false) {
+ continue;
+ }
+ $injector = "HTMLPurifier_Injector_$injector";
+ if (!$b) {
+ continue;
+ }
+ $this->injectors[] = new $injector;
+ }
+ foreach ($def_injectors as $injector) {
+ // assumed to be objects
+ $this->injectors[] = $injector;
+ }
+ foreach ($custom_injectors as $injector) {
+ if (!$injector) {
+ continue;
+ }
+ if (is_string($injector)) {
+ $injector = "HTMLPurifier_Injector_$injector";
+ $injector = new $injector;
+ }
+ $this->injectors[] = $injector;
+ }
+
+ // give the injectors references to the definition and context
+ // variables for performance reasons
+ foreach ($this->injectors as $ix => $injector) {
+ $error = $injector->prepare($config, $context);
+ if (!$error) {
+ continue;
+ }
+ array_splice($this->injectors, $ix, 1); // rm the injector
+ trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
+ }
+
+ // -- end INJECTOR --
+
+ // a note on reprocessing:
+ // In order to reduce code duplication, whenever some code needs
+ // to make HTML changes in order to make things "correct", the
+ // new HTML gets sent through the purifier, regardless of its
+ // status. This means that if we add a start token, because it
+ // was totally necessary, we don't have to update nesting; we just
+ // punt ($reprocess = true; continue;) and it does that for us.
+
+ // isset is in loop because $tokens size changes during loop exec
+ for (;;
+ // only increment if we don't need to reprocess
+ $reprocess ? $reprocess = false : $token = $zipper->next($token)) {
+
+ // check for a rewind
+ if (is_int($i)) {
+ // possibility: disable rewinding if the current token has a
+ // rewind set on it already. This would offer protection from
+ // infinite loop, but might hinder some advanced rewinding.
+ $rewind_offset = $this->injectors[$i]->getRewindOffset();
+ if (is_int($rewind_offset)) {
+ for ($j = 0; $j < $rewind_offset; $j++) {
+ if (empty($zipper->front)) break;
+ $token = $zipper->prev($token);
+ // indicate that other injectors should not process this token,
+ // but we need to reprocess it. See Note [Injector skips]
+ unset($token->skip[$i]);
+ $token->rewind = $i;
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ array_pop($this->stack);
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ $this->stack[] = $token->start;
+ }
+ }
+ }
+ $i = false;
+ }
+
+ // handle case of document end
+ if ($token === NULL) {
+ // kill processing if stack is empty
+ if (empty($this->stack)) {
+ break;
+ }
+
+ // peek
+ $top_nesting = array_pop($this->stack);
+ $this->stack[] = $top_nesting;
+
+ // send error [TagClosedSuppress]
+ if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
+ }
+
+ // append, don't splice, since this is the end
+ $token = new HTMLPurifier_Token_End($top_nesting->name);
+
+ // punt!
+ $reprocess = true;
+ continue;
+ }
+
+ //echo ' '; printZipper($zipper, $token);//printTokens($this->stack);
+ //flush();
+
+ // quick-check: if it's not a tag, no need to process
+ if (empty($token->is_tag)) {
+ if ($token instanceof HTMLPurifier_Token_Text) {
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) {
+ // See Note [Injector skips]
+ continue;
+ }
+ if ($token->rewind !== null && $token->rewind !== $i) {
+ continue;
+ }
+ // XXX fuckup
+ $r = $token;
+ $injector->handleText($r);
+ $token = $this->processToken($r, $i);
+ $reprocess = true;
+ break;
+ }
+ }
+ // another possibility is a comment
+ continue;
+ }
+
+ if (isset($definition->info[$token->name])) {
+ $type = $definition->info[$token->name]->child->type;
+ } else {
+ $type = false; // Type is unknown, treat accordingly
+ }
+
+ // quick tag checks: anything that's *not* an end tag
+ $ok = false;
+ if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
+ // claims to be a start tag but is empty
+ $token = new HTMLPurifier_Token_Empty(
+ $token->name,
+ $token->attr,
+ $token->line,
+ $token->col,
+ $token->armor
+ );
+ $ok = true;
+ } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
+ // claims to be empty but really is a start tag
+ // NB: this assignment is required
+ $old_token = $token;
+ $token = new HTMLPurifier_Token_End($token->name);
+ $token = $this->insertBefore(
+ new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor)
+ );
+ // punt (since we had to modify the input stream in a non-trivial way)
+ $reprocess = true;
+ continue;
+ } elseif ($token instanceof HTMLPurifier_Token_Empty) {
+ // real empty token
+ $ok = true;
+ } elseif ($token instanceof HTMLPurifier_Token_Start) {
+ // start tag
+
+ // ...unless they also have to close their parent
+ if (!empty($this->stack)) {
+
+ // Performance note: you might think that it's rather
+ // inefficient, recalculating the autoclose information
+ // for every tag that a token closes (since when we
+ // do an autoclose, we push a new token into the
+ // stream and then /process/ that, before
+ // re-processing this token.) But this is
+ // necessary, because an injector can make an
+ // arbitrary transformations to the autoclosing
+ // tokens we introduce, so things may have changed
+ // in the meantime. Also, doing the inefficient thing is
+ // "easy" to reason about (for certain perverse definitions
+ // of "easy")
+
+ $parent = array_pop($this->stack);
+ $this->stack[] = $parent;
+
+ $parent_def = null;
+ $parent_elements = null;
+ $autoclose = false;
+ if (isset($definition->info[$parent->name])) {
+ $parent_def = $definition->info[$parent->name];
+ $parent_elements = $parent_def->child->getAllowedElements($config);
+ $autoclose = !isset($parent_elements[$token->name]);
+ }
+
+ if ($autoclose && $definition->info[$token->name]->wrap) {
+ // Check if an element can be wrapped by another
+ // element to make it valid in a context (for
+ // example,
needs a
in between)
+ $wrapname = $definition->info[$token->name]->wrap;
+ $wrapdef = $definition->info[$wrapname];
+ $elements = $wrapdef->child->getAllowedElements($config);
+ if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) {
+ $newtoken = new HTMLPurifier_Token_Start($wrapname);
+ $token = $this->insertBefore($newtoken);
+ $reprocess = true;
+ continue;
+ }
+ }
+
+ $carryover = false;
+ if ($autoclose && $parent_def->formatting) {
+ $carryover = true;
+ }
+
+ if ($autoclose) {
+ // check if this autoclose is doomed to fail
+ // (this rechecks $parent, which his harmless)
+ $autoclose_ok = isset($global_parent_allowed_elements[$token->name]);
+ if (!$autoclose_ok) {
+ foreach ($this->stack as $ancestor) {
+ $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config);
+ if (isset($elements[$token->name])) {
+ $autoclose_ok = true;
+ break;
+ }
+ if ($definition->info[$token->name]->wrap) {
+ $wrapname = $definition->info[$token->name]->wrap;
+ $wrapdef = $definition->info[$wrapname];
+ $wrap_elements = $wrapdef->child->getAllowedElements($config);
+ if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) {
+ $autoclose_ok = true;
+ break;
+ }
+ }
+ }
+ }
+ if ($autoclose_ok) {
+ // errors need to be updated
+ $new_token = new HTMLPurifier_Token_End($parent->name);
+ $new_token->start = $parent;
+ // [TagClosedSuppress]
+ if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
+ if (!$carryover) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
+ } else {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
+ }
+ }
+ if ($carryover) {
+ $element = clone $parent;
+ // [TagClosedAuto]
+ $element->armor['MakeWellFormed_TagClosedError'] = true;
+ $element->carryover = true;
+ $token = $this->processToken(array($new_token, $token, $element));
+ } else {
+ $token = $this->insertBefore($new_token);
+ }
+ } else {
+ $token = $this->remove();
+ }
+ $reprocess = true;
+ continue;
+ }
+
+ }
+ $ok = true;
+ }
+
+ if ($ok) {
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) {
+ // See Note [Injector skips]
+ continue;
+ }
+ if ($token->rewind !== null && $token->rewind !== $i) {
+ continue;
+ }
+ $r = $token;
+ $injector->handleElement($r);
+ $token = $this->processToken($r, $i);
+ $reprocess = true;
+ break;
+ }
+ if (!$reprocess) {
+ // ah, nothing interesting happened; do normal processing
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ $this->stack[] = $token;
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ throw new HTMLPurifier_Exception(
+ 'Improper handling of end tag in start code; possible error in MakeWellFormed'
+ );
+ }
+ }
+ continue;
+ }
+
+ // sanity check: we should be dealing with a closing tag
+ if (!$token instanceof HTMLPurifier_Token_End) {
+ throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
+ }
+
+ // make sure that we have something open
+ if (empty($this->stack)) {
+ if ($escape_invalid_tags) {
+ if ($e) {
+ $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
+ }
+ $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
+ } else {
+ if ($e) {
+ $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
+ }
+ $token = $this->remove();
+ }
+ $reprocess = true;
+ continue;
+ }
+
+ // first, check for the simplest case: everything closes neatly.
+ // Eventually, everything passes through here; if there are problems
+ // we modify the input stream accordingly and then punt, so that
+ // the tokens get processed again.
+ $current_parent = array_pop($this->stack);
+ if ($current_parent->name == $token->name) {
+ $token->start = $current_parent;
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) {
+ // See Note [Injector skips]
+ continue;
+ }
+ if ($token->rewind !== null && $token->rewind !== $i) {
+ continue;
+ }
+ $r = $token;
+ $injector->handleEnd($r);
+ $token = $this->processToken($r, $i);
+ $this->stack[] = $current_parent;
+ $reprocess = true;
+ break;
+ }
+ continue;
+ }
+
+ // okay, so we're trying to close the wrong tag
+
+ // undo the pop previous pop
+ $this->stack[] = $current_parent;
+
+ // scroll back the entire nest, trying to find our tag.
+ // (feature could be to specify how far you'd like to go)
+ $size = count($this->stack);
+ // -2 because -1 is the last element, but we already checked that
+ $skipped_tags = false;
+ for ($j = $size - 2; $j >= 0; $j--) {
+ if ($this->stack[$j]->name == $token->name) {
+ $skipped_tags = array_slice($this->stack, $j);
+ break;
+ }
+ }
+
+ // we didn't find the tag, so remove
+ if ($skipped_tags === false) {
+ if ($escape_invalid_tags) {
+ if ($e) {
+ $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
+ }
+ $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
+ } else {
+ if ($e) {
+ $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
+ }
+ $token = $this->remove();
+ }
+ $reprocess = true;
+ continue;
+ }
+
+ // do errors, in REVERSE $j order: a,b,c with
This page demonstrate the FineDiff class (as in “fine granularity diff”) I wrote – starting from scratch – to generate a lossless (won't eat your line breaks), compact opcodes string listing the sequence of atomic actions (copy/delete/insert) necessary to transform one string into another (thereafter referred as the “From” and “To” string). The “To” string can be rebuilt by running the opcodes string on the “From” string. The FineDiff class allows to specify the granularity, and up to character-level granularity is possible, in order to generate the smallest diff possible (at the potential cost of increased CPU cycles.)
+
Typical usage:
+
+include 'finediff.php';
+$opcodes = FineDiff::getDiffOpcodes($from_text, $to_text /* , default granularity is set to character */);
+// store opcodes for later use...
+
Later, $to_text can be re-created from $from_text using $opcodes as follow:
Rendered Diff: Show Deletions only All Insertions only
+
+
+
+
+
+
+
+
Notes
+
The PHP-based engine of Text_Diff is forced, in order to meaningfully compare results with PHP-based FineDiff. Text_Diff is naturally geared toward line-level granularity, and to compute diff for a higher granularity (sequences, words, characters), line break characters (\n, \r) are replaced in order to avoid having Text_Diff from eating our line breaks — so extra steps are required.
+
FineDiff is natively better equipped to generate diff at granularity higher than line levels. An example of this is that using the above built-in sample text, for word and character-level granularity, FineDiff roughly executes in 25 ms and 30 ms, respectively, while Text_Diff roughly executes in 75 ms and 6.5 seconds, respectively (on my development computer, a run of the mill Intel i5 core desktop computer).
+ - hello world
+text_content:
+ - hello world
\ No newline at end of file
diff --git a/lib/vendor/overtrue/pinyin/.github/FUNDING.yml b/lib/vendor/overtrue/pinyin/.github/FUNDING.yml
new file mode 100644
index 0000000000..a3be7fa855
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/.github/FUNDING.yml
@@ -0,0 +1,9 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: overtrue
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+custom: # Replace with a single custom sponsorship URL
diff --git a/lib/vendor/overtrue/pinyin/LICENSE b/lib/vendor/overtrue/pinyin/LICENSE
new file mode 100755
index 0000000000..3975dd5bbf
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 安正超
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/lib/vendor/overtrue/pinyin/README.md b/lib/vendor/overtrue/pinyin/README.md
new file mode 100755
index 0000000000..61ce9c19c2
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/README.md
@@ -0,0 +1,130 @@
+
+ *
+ * @param Closure $callback
+ */
+ public function map(Closure $callback);
+
+ /**
+ * Load surname dict.
+ *
+ * @param Closure $callback
+ */
+ public function mapSurname(Closure $callback);
+}
diff --git a/lib/vendor/overtrue/pinyin/src/FileDictLoader.php b/lib/vendor/overtrue/pinyin/src/FileDictLoader.php
new file mode 100644
index 0000000000..e9895f9b3b
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/src/FileDictLoader.php
@@ -0,0 +1,76 @@
+
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Overtrue\Pinyin;
+
+use Closure;
+
+/**
+ * Dict File loader.
+ */
+class FileDictLoader implements DictLoaderInterface
+{
+ /**
+ * Words segment name.
+ *
+ * @var string
+ */
+ protected $segmentName = 'words_%s';
+
+ /**
+ * Dict path.
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Constructor.
+ *
+ * @param string $path
+ */
+ public function __construct($path)
+ {
+ $this->path = $path;
+ }
+
+ /**
+ * Load dict.
+ *
+ * @param Closure $callback
+ */
+ public function map(Closure $callback)
+ {
+ for ($i = 0; $i < 100; ++$i) {
+ $segment = $this->path.'/'.sprintf($this->segmentName, $i);
+
+ if (file_exists($segment)) {
+ $dictionary = (array) include $segment;
+ $callback($dictionary);
+ }
+ }
+ }
+
+ /**
+ * Load surname dict.
+ *
+ * @param Closure $callback
+ */
+ public function mapSurname(Closure $callback)
+ {
+ $surnames = $this->path.'/surnames';
+
+ if (file_exists($surnames)) {
+ $dictionary = (array) include $surnames;
+ $callback($dictionary);
+ }
+ }
+}
diff --git a/lib/vendor/overtrue/pinyin/src/GeneratorFileDictLoader.php b/lib/vendor/overtrue/pinyin/src/GeneratorFileDictLoader.php
new file mode 100644
index 0000000000..e7da6421a7
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/src/GeneratorFileDictLoader.php
@@ -0,0 +1,145 @@
+
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Overtrue\Pinyin;
+
+use Closure;
+use SplFileObject;
+use Generator;
+
+/**
+ * Generator syntax(yield) Dict File loader.
+ */
+class GeneratorFileDictLoader implements DictLoaderInterface
+{
+ /**
+ * Data directory.
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Words segment name.
+ *
+ * @var string
+ */
+ protected $segmentName = 'words_%s';
+
+ /**
+ * SplFileObjects.
+ *
+ * @var array
+ */
+ protected static $handles = [];
+
+ /**
+ * surnames.
+ *
+ * @var SplFileObject
+ */
+ protected static $surnamesHandle;
+
+ /**
+ * Constructor.
+ *
+ * @param string $path
+ */
+ public function __construct($path)
+ {
+ $this->path = $path;
+
+ for ($i = 0; $i < 100; ++$i) {
+ $segment = $this->path.'/'.sprintf($this->segmentName, $i);
+
+ if (file_exists($segment) && is_file($segment)) {
+ array_push(static::$handles, $this->openFile($segment));
+ }
+ }
+ }
+
+ /**
+ * Construct a new file object.
+ *
+ * @param string $filename file path
+ * @param string $mode file open mode
+ *
+ * @return SplFileObject
+ */
+ protected function openFile($filename, $mode = 'r')
+ {
+ return new SplFileObject($filename, $mode);
+ }
+
+ /**
+ * get Generator syntax.
+ *
+ * @param array $handles SplFileObjects
+ *
+ * @return Generator
+ */
+ protected function getGenerator(array $handles)
+ {
+ foreach ($handles as $handle) {
+ $handle->seek(0);
+ while (false === $handle->eof()) {
+ $string = str_replace(['\'', ' ', PHP_EOL, ','], '', $handle->fgets());
+
+ if (false === strpos($string, '=>')) {
+ continue;
+ }
+
+ list($string, $pinyin) = explode('=>', $string);
+
+ yield $string => $pinyin;
+ }
+ }
+ }
+
+ /**
+ * Traverse the stream.
+ *
+ * @param Generator $generator
+ * @param Closure $callback
+ *
+ * @author Seven Du
+ */
+ protected function traversing(Generator $generator, Closure $callback)
+ {
+ foreach ($generator as $string => $pinyin) {
+ $callback([$string => $pinyin]);
+ }
+ }
+
+ /**
+ * Load dict.
+ *
+ * @param Closure $callback
+ */
+ public function map(Closure $callback)
+ {
+ $this->traversing($this->getGenerator(static::$handles), $callback);
+ }
+
+ /**
+ * Load surname dict.
+ *
+ * @param Closure $callback
+ */
+ public function mapSurname(Closure $callback)
+ {
+ if (!static::$surnamesHandle instanceof SplFileObject) {
+ static::$surnamesHandle = $this->openFile($this->path.'/surnames');
+ }
+
+ $this->traversing($this->getGenerator([static::$surnamesHandle]), $callback);
+ }
+}
diff --git a/lib/vendor/overtrue/pinyin/src/MemoryFileDictLoader.php b/lib/vendor/overtrue/pinyin/src/MemoryFileDictLoader.php
new file mode 100644
index 0000000000..e540fb8518
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/src/MemoryFileDictLoader.php
@@ -0,0 +1,96 @@
+
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Overtrue\Pinyin;
+
+use Closure;
+
+/**
+ * Memory Dict File loader.
+ */
+class MemoryFileDictLoader implements DictLoaderInterface
+{
+ /**
+ * Data directory.
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Words segment name.
+ *
+ * @var string
+ */
+ protected $segmentName = 'words_%s';
+
+ /**
+ * Segment files.
+ *
+ * @var array
+ */
+ protected $segments = array();
+
+ /**
+ * Surname cache.
+ *
+ * @var array
+ */
+ protected $surnames = array();
+
+ /**
+ * Constructor.
+ *
+ * @param string $path
+ */
+ public function __construct($path)
+ {
+ $this->path = $path;
+
+ for ($i = 0; $i < 100; ++$i) {
+ $segment = $path.'/'.sprintf($this->segmentName, $i);
+
+ if (file_exists($segment)) {
+ $this->segments[] = (array) include $segment;
+ }
+ }
+ }
+
+ /**
+ * Load dict.
+ *
+ * @param Closure $callback
+ */
+ public function map(Closure $callback)
+ {
+ foreach ($this->segments as $dictionary) {
+ $callback($dictionary);
+ }
+ }
+
+ /**
+ * Load surname dict.
+ *
+ * @param Closure $callback
+ */
+ public function mapSurname(Closure $callback)
+ {
+ if (empty($this->surnames)) {
+ $surnames = $this->path.'/surnames';
+
+ if (file_exists($surnames)) {
+ $this->surnames = (array) include $surnames;
+ }
+ }
+
+ $callback($this->surnames);
+ }
+}
diff --git a/lib/vendor/overtrue/pinyin/src/Pinyin.php b/lib/vendor/overtrue/pinyin/src/Pinyin.php
new file mode 100644
index 0000000000..8f6f2aa1a7
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/src/Pinyin.php
@@ -0,0 +1,356 @@
+
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Overtrue\Pinyin;
+
+use InvalidArgumentException;
+
+/*
+ * Chinese to pinyin translator.
+ *
+ * @author overtrue
+ * @copyright 2015 overtrue
+ *
+ * @link https://github.com/overtrue/pinyin
+ * @link http://overtrue.me
+ */
+
+/**
+ * Class Pinyin.
+ *
+ * @author overtrue
+ */
+class Pinyin
+{
+ /**
+ * Dict loader.
+ *
+ * @var \Overtrue\Pinyin\DictLoaderInterface
+ */
+ protected $loader;
+
+ /**
+ * Punctuations map.
+ *
+ * @var array
+ */
+ protected $punctuations = array(
+ ',' => ',',
+ '。' => '.',
+ '!' => '!',
+ '?' => '?',
+ ':' => ':',
+ '“' => '"',
+ '”' => '"',
+ '‘' => "'",
+ '’' => "'",
+ '_' => '_',
+ );
+
+ /**
+ * Constructor.
+ *
+ * @param string $loaderName
+ */
+ public function __construct($loaderName = null)
+ {
+ $this->loader = $loaderName ?: 'Overtrue\\Pinyin\\FileDictLoader';
+ }
+
+ /**
+ * Convert string to pinyin.
+ *
+ * @param string $string
+ * @param int $option
+ *
+ * @return array
+ */
+ public function convert($string, $option = PINYIN_DEFAULT)
+ {
+ $pinyin = $this->romanize($string, $option);
+
+ return $this->splitWords($pinyin, $option);
+ }
+
+ /**
+ * Convert string (person name) to pinyin.
+ *
+ * @param string $stringName
+ * @param int $option
+ *
+ * @return array
+ */
+ public function name($stringName, $option = PINYIN_NAME)
+ {
+ $option = $option | PINYIN_NAME;
+
+ $pinyin = $this->romanize($stringName, $option);
+
+ return $this->splitWords($pinyin, $option);
+ }
+
+ /**
+ * Return a pinyin permalink from string.
+ *
+ * @param string $string
+ * @param string $delimiter
+ * @param int $option
+ *
+ * @return string
+ */
+ public function permalink($string, $delimiter = '-', $option = PINYIN_DEFAULT)
+ {
+ if (\is_int($delimiter)) {
+ list($option, $delimiter) = array($delimiter, '-');
+ }
+
+ if (!in_array($delimiter, array('_', '-', '.', ''), true)) {
+ throw new InvalidArgumentException("Delimiter must be one of: '_', '-', '', '.'.");
+ }
+
+ return implode($delimiter, $this->convert($string, $option | \PINYIN_KEEP_NUMBER | \PINYIN_KEEP_ENGLISH));
+ }
+
+ /**
+ * Return first letters.
+ *
+ * @param string $string
+ * @param string $delimiter
+ * @param int $option
+ *
+ * @return string
+ */
+ public function abbr($string, $delimiter = '', $option = PINYIN_DEFAULT)
+ {
+ if (\is_int($delimiter)) {
+ list($option, $delimiter) = array($delimiter, '');
+ }
+
+ return implode($delimiter, array_map(function ($pinyin) {
+ return \is_numeric($pinyin) ? $pinyin : mb_substr($pinyin, 0, 1);
+ }, $this->convert($string, $option)));
+ }
+
+ /**
+ * Chinese phrase to pinyin.
+ *
+ * @param string $string
+ * @param string $delimiter
+ * @param int $option
+ *
+ * @return string
+ */
+ public function phrase($string, $delimiter = ' ', $option = PINYIN_DEFAULT)
+ {
+ if (\is_int($delimiter)) {
+ list($option, $delimiter) = array($delimiter, ' ');
+ }
+
+ return implode($delimiter, $this->convert($string, $option));
+ }
+
+ /**
+ * Chinese to pinyin sentence.
+ *
+ * @param string $string
+ * @param string $delimiter
+ * @param int $option
+ *
+ * @return string
+ */
+ public function sentence($string, $delimiter = ' ', $option = \PINYIN_NO_TONE)
+ {
+ if (\is_int($delimiter)) {
+ list($option, $delimiter) = array($delimiter, ' ');
+ }
+
+ return implode($delimiter, $this->convert($string, $option | \PINYIN_KEEP_PUNCTUATION | \PINYIN_KEEP_ENGLISH | \PINYIN_KEEP_NUMBER));
+ }
+
+ /**
+ * Loader setter.
+ *
+ * @param \Overtrue\Pinyin\DictLoaderInterface $loader
+ *
+ * @return $this
+ */
+ public function setLoader(DictLoaderInterface $loader)
+ {
+ $this->loader = $loader;
+
+ return $this;
+ }
+
+ /**
+ * Return dict loader,.
+ *
+ * @return \Overtrue\Pinyin\DictLoaderInterface
+ */
+ public function getLoader()
+ {
+ if (!($this->loader instanceof DictLoaderInterface)) {
+ $dataDir = dirname(__DIR__).'/data/';
+
+ $loaderName = $this->loader;
+ $this->loader = new $loaderName($dataDir);
+ }
+
+ return $this->loader;
+ }
+
+ /**
+ * Convert Chinese to pinyin.
+ *
+ * @param string $string
+ * @param int $option
+ *
+ * @return string
+ */
+ protected function romanize($string, $option = \PINYIN_DEFAULT)
+ {
+ $string = $this->prepare($string, $option);
+
+ $dictLoader = $this->getLoader();
+
+ if ($this->hasOption($option, \PINYIN_NAME)) {
+ $string = $this->convertSurname($string, $dictLoader);
+ }
+
+ $dictLoader->map(function ($dictionary) use (&$string) {
+ $string = strtr($string, $dictionary);
+ });
+
+ return $string;
+ }
+
+ /**
+ * Convert Chinese Surname to pinyin.
+ *
+ * @param string $string
+ * @param \Overtrue\Pinyin\DictLoaderInterface $dictLoader
+ *
+ * @return string
+ */
+ protected function convertSurname($string, $dictLoader)
+ {
+ $dictLoader->mapSurname(function ($dictionary) use (&$string) {
+ foreach ($dictionary as $surname => $pinyin) {
+ if (0 === strpos($string, $surname)) {
+ $string = $pinyin.mb_substr($string, mb_strlen($surname, 'UTF-8'), mb_strlen($string, 'UTF-8') - 1, 'UTF-8');
+
+ break;
+ }
+ }
+ });
+
+ return $string;
+ }
+
+ /**
+ * Split pinyin string to words.
+ *
+ * @param string $pinyin
+ * @param string $option
+ *
+ * @return array
+ */
+ protected function splitWords($pinyin, $option)
+ {
+ $split = array_filter(preg_split('/\s+/i', $pinyin));
+
+ if (!$this->hasOption($option, PINYIN_TONE)) {
+ foreach ($split as $index => $pinyin) {
+ $split[$index] = $this->formatTone($pinyin, $option);
+ }
+ }
+
+ return array_values($split);
+ }
+
+ /**
+ * @param int $option
+ * @param int $check
+ *
+ * @return bool
+ */
+ public function hasOption($option, $check)
+ {
+ return ($option & $check) === $check;
+ }
+
+ /**
+ * Pre-process.
+ *
+ * @param string $string
+ * @param int $option
+ *
+ * @return string
+ */
+ protected function prepare($string, $option = \PINYIN_DEFAULT)
+ {
+ $string = preg_replace_callback('~[a-z0-9_-]+~i', function ($matches) {
+ return "\t".$matches[0];
+ }, $string);
+
+ $regex = array('\p{Han}', '\p{Z}', '\p{M}', "\t");
+
+ if ($this->hasOption($option, \PINYIN_KEEP_NUMBER)) {
+ \array_push($regex, '\p{N}');
+ }
+
+ if ($this->hasOption($option, \PINYIN_KEEP_ENGLISH)) {
+ \array_push($regex, 'a-zA-Z');
+ }
+
+ if ($this->hasOption($option, \PINYIN_KEEP_PUNCTUATION)) {
+ $punctuations = array_merge($this->punctuations, array("\t" => ' ', ' ' => ' '));
+ $string = trim(str_replace(array_keys($punctuations), $punctuations, $string));
+
+ \array_push($regex, preg_quote(implode(array_merge(array_keys($this->punctuations), $this->punctuations)), '~'));
+ }
+
+ return preg_replace(\sprintf('~[^%s]~u', implode($regex)), '', $string);
+ }
+
+ /**
+ * Format.
+ *
+ * @param string $pinyin
+ * @param int $option
+ *
+ * @return string
+ */
+ protected function formatTone($pinyin, $option = \PINYIN_NO_TONE)
+ {
+ $replacements = array(
+ 'üē' => array('ue', 1), 'üé' => array('ue', 2), 'üě' => array('ue', 3), 'üè' => array('ue', 4),
+ 'ā' => array('a', 1), 'ē' => array('e', 1), 'ī' => array('i', 1), 'ō' => array('o', 1), 'ū' => array('u', 1), 'ǖ' => array('yu', 1),
+ 'á' => array('a', 2), 'é' => array('e', 2), 'í' => array('i', 2), 'ó' => array('o', 2), 'ú' => array('u', 2), 'ǘ' => array('yu', 2),
+ 'ǎ' => array('a', 3), 'ě' => array('e', 3), 'ǐ' => array('i', 3), 'ǒ' => array('o', 3), 'ǔ' => array('u', 3), 'ǚ' => array('yu', 3),
+ 'à' => array('a', 4), 'è' => array('e', 4), 'ì' => array('i', 4), 'ò' => array('o', 4), 'ù' => array('u', 4), 'ǜ' => array('yu', 4),
+ );
+
+ foreach ($replacements as $unicode => $replacement) {
+ if (false !== strpos($pinyin, $unicode)) {
+ $umlaut = $replacement[0];
+
+ // https://zh.wikipedia.org/wiki/%C3%9C
+ if ($this->hasOption($option, \PINYIN_UMLAUT_V) && 'yu' == $umlaut) {
+ $umlaut = 'v';
+ }
+
+ $pinyin = str_replace($unicode, $umlaut, $pinyin).($this->hasOption($option, PINYIN_ASCII_TONE) ? $replacement[1] : '');
+ }
+ }
+
+ return $pinyin;
+ }
+}
diff --git a/lib/vendor/overtrue/pinyin/src/const.php b/lib/vendor/overtrue/pinyin/src/const.php
new file mode 100644
index 0000000000..240ecbcc25
--- /dev/null
+++ b/lib/vendor/overtrue/pinyin/src/const.php
@@ -0,0 +1,20 @@
+
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+define('PINYIN_DEFAULT', 4096);
+define('PINYIN_TONE', 2);
+define('PINYIN_NO_TONE', 4);
+define('PINYIN_ASCII_TONE', 8);
+define('PINYIN_NAME', 16);
+define('PINYIN_KEEP_NUMBER', 32);
+define('PINYIN_KEEP_ENGLISH', 64);
+define('PINYIN_UMLAUT_V', 128);
+define('PINYIN_KEEP_PUNCTUATION', 256);
diff --git a/lib/vendor/paragonie/constant_time_encoding/LICENSE.txt b/lib/vendor/paragonie/constant_time_encoding/LICENSE.txt
new file mode 100644
index 0000000000..91acaca673
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/LICENSE.txt
@@ -0,0 +1,48 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 - 2022 Paragon Initiative Enterprises
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+------------------------------------------------------------------------------
+This library was based on the work of Steve "Sc00bz" Thomas.
+------------------------------------------------------------------------------
+
+The MIT License (MIT)
+
+Copyright (c) 2014 Steve Thomas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/lib/vendor/paragonie/constant_time_encoding/README.md b/lib/vendor/paragonie/constant_time_encoding/README.md
new file mode 100644
index 0000000000..d2572e91ce
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/README.md
@@ -0,0 +1,84 @@
+# Constant-Time Encoding
+
+[](https://github.com/paragonie/constant_time_encoding/actions)
+[](https://packagist.org/packages/paragonie/constant_time_encoding)
+[](https://packagist.org/packages/paragonie/constant_time_encoding)
+[](https://packagist.org/packages/paragonie/constant_time_encoding)
+[](https://packagist.org/packages/paragonie/constant_time_encoding)
+
+Based on the [constant-time base64 implementation made by Steve "Sc00bz" Thomas](https://github.com/Sc00bz/ConstTimeEncoding),
+this library aims to offer character encoding functions that do not leak
+information about what you are encoding/decoding via processor cache
+misses. Further reading on [cache-timing attacks](http://blog.ircmaxell.com/2014/11/its-all-about-time.html).
+
+Our fork offers the following enhancements:
+
+* `mbstring.func_overload` resistance
+* Unit tests
+* Composer- and Packagist-ready
+* Base16 encoding
+* Base32 encoding
+* Uses `pack()` and `unpack()` instead of `chr()` and `ord()`
+
+## PHP Version Requirements
+
+Version 2 of this library should work on **PHP 7** or newer. For PHP 5
+support, see [the v1.x branch](https://github.com/paragonie/constant_time_encoding/tree/v1.x).
+
+If you are adding this as a dependency to a project intended to work on both PHP 5 and PHP 7, please set the required version to `^1|^2` instead of just `^1` or `^2`.
+
+## How to Install
+
+```sh
+composer require paragonie/constant_time_encoding
+```
+
+## How to Use
+
+```php
+use ParagonIE\ConstantTime\Encoding;
+
+// possibly (if applicable):
+// require 'vendor/autoload.php';
+
+$data = random_bytes(32);
+echo Encoding::base64Encode($data), "\n";
+echo Encoding::base32EncodeUpper($data), "\n";
+echo Encoding::base32Encode($data), "\n";
+echo Encoding::hexEncode($data), "\n";
+echo Encoding::hexEncodeUpper($data), "\n";
+```
+
+Example output:
+
+```
+1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE=
+2VMKKPSHSWVCVZJ6E7SONRY3ZXCNG3GE6ZZFU7TGJSX7KUKFNLAQ====
+2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq====
+d558a53e4795aa2ae53e27e4e6c71bcdc4d36cc4f6725a7e664caff551456ac1
+D558A53E4795AA2AE53E27E4E6C71BDCC4D36CC4F6725A7E664CAFF551456AC1
+```
+
+If you only need a particular variant, you can just reference the
+required class like so:
+
+```php
+use ParagonIE\ConstantTime\Base64;
+use ParagonIE\ConstantTime\Base32;
+
+$data = random_bytes(32);
+echo Base64::encode($data), "\n";
+echo Base32::encode($data), "\n";
+```
+
+Example output:
+
+```
+1VilPkeVqirlPifk5scbzcTTbMT2clp+Zkyv9VFFasE=
+2vmkkpshswvcvzj6e7sonry3zxcng3ge6zzfu7tgjsx7kukfnlaq====
+```
+
+## Support Contracts
+
+If your company uses this library in their products or services, you may be
+interested in [purchasing a support contract from Paragon Initiative Enterprises](https://paragonie.com/enterprise).
diff --git a/lib/vendor/paragonie/constant_time_encoding/composer.json b/lib/vendor/paragonie/constant_time_encoding/composer.json
new file mode 100644
index 0000000000..2fe9717adb
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/composer.json
@@ -0,0 +1,56 @@
+{
+ "name": "paragonie/constant_time_encoding",
+ "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+ "keywords": [
+ "base64",
+ "encoding",
+ "rfc4648",
+ "base32",
+ "base16",
+ "hex",
+ "bin2hex",
+ "hex2bin",
+ "base64_encode",
+ "base64_decode",
+ "base32_encode",
+ "base32_decode"
+ ],
+ "license": "MIT",
+ "type": "library",
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com",
+ "role": "Maintainer"
+ },
+ {
+ "name": "Steve 'Sc00bz' Thomas",
+ "email": "steve@tobtu.com",
+ "homepage": "https://www.tobtu.com",
+ "role": "Original Developer"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+ "email": "info@paragonie.com",
+ "source": "https://github.com/paragonie/constant_time_encoding"
+ },
+ "require": {
+ "php": "^7|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6|^7|^8|^9",
+ "vimeo/psalm": "^1|^2|^3|^4"
+ },
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\Tests\\": "tests/"
+ }
+ }
+}
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/Base32.php b/lib/vendor/paragonie/constant_time_encoding/src/Base32.php
new file mode 100644
index 0000000000..48d00b9911
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/Base32.php
@@ -0,0 +1,541 @@
+ 96 && $src < 123) $ret += $src - 97 + 1; // -64
+ $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96);
+
+ // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
+ $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 5-bit integers
+ * into 8-bit integers.
+ *
+ * Uppercase variant.
+ *
+ * @param int $src
+ * @return int
+ */
+ protected static function decode5BitsUpper(int $src): int
+ {
+ $ret = -1;
+
+ // if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64
+ $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
+
+ // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
+ $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 5-bit integers.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode5Bits(int $src): string
+ {
+ $diff = 0x61;
+
+ // if ($src > 25) $ret -= 72;
+ $diff -= ((25 - $src) >> 8) & 73;
+
+ return \pack('C', $src + $diff);
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 5-bit integers.
+ *
+ * Uppercase variant.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode5BitsUpper(int $src): string
+ {
+ $diff = 0x41;
+
+ // if ($src > 25) $ret -= 40;
+ $diff -= ((25 - $src) >> 8) & 41;
+
+ return \pack('C', $src + $diff);
+ }
+
+ /**
+ * @param string $encodedString
+ * @param bool $upper
+ * @return string
+ */
+ public static function decodeNoPadding(
+ #[\SensitiveParameter]
+ string $encodedString,
+ bool $upper = false
+ ): string {
+ $srcLen = Binary::safeStrlen($encodedString);
+ if ($srcLen === 0) {
+ return '';
+ }
+ if (($srcLen & 7) === 0) {
+ for ($j = 0; $j < 7 && $j < $srcLen; ++$j) {
+ if ($encodedString[$srcLen - $j - 1] === '=') {
+ throw new InvalidArgumentException(
+ "decodeNoPadding() doesn't tolerate padding"
+ );
+ }
+ }
+ }
+ return static::doDecode(
+ $encodedString,
+ $upper,
+ true
+ );
+ }
+
+ /**
+ * Base32 decoding
+ *
+ * @param string $src
+ * @param bool $upper
+ * @param bool $strictPadding
+ * @return string
+ *
+ * @throws TypeError
+ */
+ protected static function doDecode(
+ #[\SensitiveParameter]
+ string $src,
+ bool $upper = false,
+ bool $strictPadding = false
+ ): string {
+ // We do this to reduce code duplication:
+ $method = $upper
+ ? 'decode5BitsUpper'
+ : 'decode5Bits';
+
+ // Remove padding
+ $srcLen = Binary::safeStrlen($src);
+ if ($srcLen === 0) {
+ return '';
+ }
+ if ($strictPadding) {
+ if (($srcLen & 7) === 0) {
+ for ($j = 0; $j < 7; ++$j) {
+ if ($src[$srcLen - 1] === '=') {
+ $srcLen--;
+ } else {
+ break;
+ }
+ }
+ }
+ if (($srcLen & 7) === 1) {
+ throw new RangeException(
+ 'Incorrect padding'
+ );
+ }
+ } else {
+ $src = \rtrim($src, '=');
+ $srcLen = Binary::safeStrlen($src);
+ }
+
+ $err = 0;
+ $dest = '';
+ // Main loop (no padding):
+ for ($i = 0; $i + 8 <= $srcLen; $i += 8) {
+ /** @var array $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 8));
+ /** @var int $c0 */
+ $c0 = static::$method($chunk[1]);
+ /** @var int $c1 */
+ $c1 = static::$method($chunk[2]);
+ /** @var int $c2 */
+ $c2 = static::$method($chunk[3]);
+ /** @var int $c3 */
+ $c3 = static::$method($chunk[4]);
+ /** @var int $c4 */
+ $c4 = static::$method($chunk[5]);
+ /** @var int $c5 */
+ $c5 = static::$method($chunk[6]);
+ /** @var int $c6 */
+ $c6 = static::$method($chunk[7]);
+ /** @var int $c7 */
+ $c7 = static::$method($chunk[8]);
+
+ $dest .= \pack(
+ 'CCCCC',
+ (($c0 << 3) | ($c1 >> 2) ) & 0xff,
+ (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
+ (($c3 << 4) | ($c4 >> 1) ) & 0xff,
+ (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff,
+ (($c6 << 5) | ($c7 ) ) & 0xff
+ );
+ $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8;
+ }
+ // The last chunk, which may have padding:
+ if ($i < $srcLen) {
+ /** @var array $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
+ /** @var int $c0 */
+ $c0 = static::$method($chunk[1]);
+
+ if ($i + 6 < $srcLen) {
+ /** @var int $c1 */
+ $c1 = static::$method($chunk[2]);
+ /** @var int $c2 */
+ $c2 = static::$method($chunk[3]);
+ /** @var int $c3 */
+ $c3 = static::$method($chunk[4]);
+ /** @var int $c4 */
+ $c4 = static::$method($chunk[5]);
+ /** @var int $c5 */
+ $c5 = static::$method($chunk[6]);
+ /** @var int $c6 */
+ $c6 = static::$method($chunk[7]);
+
+ $dest .= \pack(
+ 'CCCC',
+ (($c0 << 3) | ($c1 >> 2) ) & 0xff,
+ (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
+ (($c3 << 4) | ($c4 >> 1) ) & 0xff,
+ (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff
+ );
+ $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8;
+ if ($strictPadding) {
+ $err |= ($c6 << 5) & 0xff;
+ }
+ } elseif ($i + 5 < $srcLen) {
+ /** @var int $c1 */
+ $c1 = static::$method($chunk[2]);
+ /** @var int $c2 */
+ $c2 = static::$method($chunk[3]);
+ /** @var int $c3 */
+ $c3 = static::$method($chunk[4]);
+ /** @var int $c4 */
+ $c4 = static::$method($chunk[5]);
+ /** @var int $c5 */
+ $c5 = static::$method($chunk[6]);
+
+ $dest .= \pack(
+ 'CCCC',
+ (($c0 << 3) | ($c1 >> 2) ) & 0xff,
+ (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
+ (($c3 << 4) | ($c4 >> 1) ) & 0xff,
+ (($c4 << 7) | ($c5 << 2) ) & 0xff
+ );
+ $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8;
+ } elseif ($i + 4 < $srcLen) {
+ /** @var int $c1 */
+ $c1 = static::$method($chunk[2]);
+ /** @var int $c2 */
+ $c2 = static::$method($chunk[3]);
+ /** @var int $c3 */
+ $c3 = static::$method($chunk[4]);
+ /** @var int $c4 */
+ $c4 = static::$method($chunk[5]);
+
+ $dest .= \pack(
+ 'CCC',
+ (($c0 << 3) | ($c1 >> 2) ) & 0xff,
+ (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
+ (($c3 << 4) | ($c4 >> 1) ) & 0xff
+ );
+ $err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8;
+ if ($strictPadding) {
+ $err |= ($c4 << 7) & 0xff;
+ }
+ } elseif ($i + 3 < $srcLen) {
+ /** @var int $c1 */
+ $c1 = static::$method($chunk[2]);
+ /** @var int $c2 */
+ $c2 = static::$method($chunk[3]);
+ /** @var int $c3 */
+ $c3 = static::$method($chunk[4]);
+
+ $dest .= \pack(
+ 'CC',
+ (($c0 << 3) | ($c1 >> 2) ) & 0xff,
+ (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff
+ );
+ $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
+ if ($strictPadding) {
+ $err |= ($c3 << 4) & 0xff;
+ }
+ } elseif ($i + 2 < $srcLen) {
+ /** @var int $c1 */
+ $c1 = static::$method($chunk[2]);
+ /** @var int $c2 */
+ $c2 = static::$method($chunk[3]);
+
+ $dest .= \pack(
+ 'CC',
+ (($c0 << 3) | ($c1 >> 2) ) & 0xff,
+ (($c1 << 6) | ($c2 << 1) ) & 0xff
+ );
+ $err |= ($c0 | $c1 | $c2) >> 8;
+ if ($strictPadding) {
+ $err |= ($c2 << 6) & 0xff;
+ }
+ } elseif ($i + 1 < $srcLen) {
+ /** @var int $c1 */
+ $c1 = static::$method($chunk[2]);
+
+ $dest .= \pack(
+ 'C',
+ (($c0 << 3) | ($c1 >> 2) ) & 0xff
+ );
+ $err |= ($c0 | $c1) >> 8;
+ if ($strictPadding) {
+ $err |= ($c1 << 6) & 0xff;
+ }
+ } else {
+ $dest .= \pack(
+ 'C',
+ (($c0 << 3) ) & 0xff
+ );
+ $err |= ($c0) >> 8;
+ }
+ }
+ $check = ($err === 0);
+ if (!$check) {
+ throw new RangeException(
+ 'Base32::doDecode() only expects characters in the correct base32 alphabet'
+ );
+ }
+ return $dest;
+ }
+
+ /**
+ * Base32 Encoding
+ *
+ * @param string $src
+ * @param bool $upper
+ * @param bool $pad
+ * @return string
+ * @throws TypeError
+ */
+ protected static function doEncode(
+ #[\SensitiveParameter]
+ string $src,
+ bool $upper = false,
+ $pad = true
+ ): string {
+ // We do this to reduce code duplication:
+ $method = $upper
+ ? 'encode5BitsUpper'
+ : 'encode5Bits';
+
+ $dest = '';
+ $srcLen = Binary::safeStrlen($src);
+
+ // Main loop (no padding):
+ for ($i = 0; $i + 5 <= $srcLen; $i += 5) {
+ /** @var array $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5));
+ $b0 = $chunk[1];
+ $b1 = $chunk[2];
+ $b2 = $chunk[3];
+ $b3 = $chunk[4];
+ $b4 = $chunk[5];
+ $dest .=
+ static::$method( ($b0 >> 3) & 31) .
+ static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
+ static::$method((($b1 >> 1) ) & 31) .
+ static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
+ static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
+ static::$method((($b3 >> 2) ) & 31) .
+ static::$method((($b3 << 3) | ($b4 >> 5)) & 31) .
+ static::$method( $b4 & 31);
+ }
+ // The last chunk, which may have padding:
+ if ($i < $srcLen) {
+ /** @var array $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
+ $b0 = $chunk[1];
+ if ($i + 3 < $srcLen) {
+ $b1 = $chunk[2];
+ $b2 = $chunk[3];
+ $b3 = $chunk[4];
+ $dest .=
+ static::$method( ($b0 >> 3) & 31) .
+ static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
+ static::$method((($b1 >> 1) ) & 31) .
+ static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
+ static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
+ static::$method((($b3 >> 2) ) & 31) .
+ static::$method((($b3 << 3) ) & 31);
+ if ($pad) {
+ $dest .= '=';
+ }
+ } elseif ($i + 2 < $srcLen) {
+ $b1 = $chunk[2];
+ $b2 = $chunk[3];
+ $dest .=
+ static::$method( ($b0 >> 3) & 31) .
+ static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
+ static::$method((($b1 >> 1) ) & 31) .
+ static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
+ static::$method((($b2 << 1) ) & 31);
+ if ($pad) {
+ $dest .= '===';
+ }
+ } elseif ($i + 1 < $srcLen) {
+ $b1 = $chunk[2];
+ $dest .=
+ static::$method( ($b0 >> 3) & 31) .
+ static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
+ static::$method((($b1 >> 1) ) & 31) .
+ static::$method((($b1 << 4) ) & 31);
+ if ($pad) {
+ $dest .= '====';
+ }
+ } else {
+ $dest .=
+ static::$method( ($b0 >> 3) & 31) .
+ static::$method( ($b0 << 2) & 31);
+ if ($pad) {
+ $dest .= '======';
+ }
+ }
+ }
+ return $dest;
+ }
+}
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/Base32Hex.php b/lib/vendor/paragonie/constant_time_encoding/src/Base32Hex.php
new file mode 100644
index 0000000000..b868dd0483
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/Base32Hex.php
@@ -0,0 +1,111 @@
+ 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
+ $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
+
+ // if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86
+ $ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86);
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 5-bit integers
+ * into 8-bit integers.
+ *
+ * @param int $src
+ * @return int
+ */
+ protected static function decode5BitsUpper(int $src): int
+ {
+ $ret = -1;
+
+ // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
+ $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
+
+ // if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54
+ $ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54);
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 5-bit integers.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode5Bits(int $src): string
+ {
+ $src += 0x30;
+
+ // if ($src > 0x39) $src += 0x61 - 0x3a; // 39
+ $src += ((0x39 - $src) >> 8) & 39;
+
+ return \pack('C', $src);
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 5-bit integers.
+ *
+ * Uppercase variant.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode5BitsUpper(int $src): string
+ {
+ $src += 0x30;
+
+ // if ($src > 0x39) $src += 0x41 - 0x3a; // 7
+ $src += ((0x39 - $src) >> 8) & 7;
+
+ return \pack('C', $src);
+ }
+}
\ No newline at end of file
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/Base64.php b/lib/vendor/paragonie/constant_time_encoding/src/Base64.php
new file mode 100644
index 0000000000..2e3ecc8595
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/Base64.php
@@ -0,0 +1,319 @@
+ $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3));
+ $b0 = $chunk[1];
+ $b1 = $chunk[2];
+ $b2 = $chunk[3];
+
+ $dest .=
+ static::encode6Bits( $b0 >> 2 ) .
+ static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
+ static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
+ static::encode6Bits( $b2 & 63);
+ }
+ // The last chunk, which may have padding:
+ if ($i < $srcLen) {
+ /** @var array $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
+ $b0 = $chunk[1];
+ if ($i + 1 < $srcLen) {
+ $b1 = $chunk[2];
+ $dest .=
+ static::encode6Bits($b0 >> 2) .
+ static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
+ static::encode6Bits(($b1 << 2) & 63);
+ if ($pad) {
+ $dest .= '=';
+ }
+ } else {
+ $dest .=
+ static::encode6Bits( $b0 >> 2) .
+ static::encode6Bits(($b0 << 4) & 63);
+ if ($pad) {
+ $dest .= '==';
+ }
+ }
+ }
+ return $dest;
+ }
+
+ /**
+ * decode from base64 into binary
+ *
+ * Base64 character set "./[A-Z][a-z][0-9]"
+ *
+ * @param string $encodedString
+ * @param bool $strictPadding
+ * @return string
+ *
+ * @throws RangeException
+ * @throws TypeError
+ */
+ public static function decode(
+ #[\SensitiveParameter]
+ string $encodedString,
+ bool $strictPadding = false
+ ): string {
+ // Remove padding
+ $srcLen = Binary::safeStrlen($encodedString);
+ if ($srcLen === 0) {
+ return '';
+ }
+
+ if ($strictPadding) {
+ if (($srcLen & 3) === 0) {
+ if ($encodedString[$srcLen - 1] === '=') {
+ $srcLen--;
+ if ($encodedString[$srcLen - 1] === '=') {
+ $srcLen--;
+ }
+ }
+ }
+ if (($srcLen & 3) === 1) {
+ throw new RangeException(
+ 'Incorrect padding'
+ );
+ }
+ if ($encodedString[$srcLen - 1] === '=') {
+ throw new RangeException(
+ 'Incorrect padding'
+ );
+ }
+ } else {
+ $encodedString = \rtrim($encodedString, '=');
+ $srcLen = Binary::safeStrlen($encodedString);
+ }
+
+ $err = 0;
+ $dest = '';
+ // Main loop (no padding):
+ for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
+ /** @var array $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4));
+ $c0 = static::decode6Bits($chunk[1]);
+ $c1 = static::decode6Bits($chunk[2]);
+ $c2 = static::decode6Bits($chunk[3]);
+ $c3 = static::decode6Bits($chunk[4]);
+
+ $dest .= \pack(
+ 'CCC',
+ ((($c0 << 2) | ($c1 >> 4)) & 0xff),
+ ((($c1 << 4) | ($c2 >> 2)) & 0xff),
+ ((($c2 << 6) | $c3 ) & 0xff)
+ );
+ $err |= ($c0 | $c1 | $c2 | $c3) >> 8;
+ }
+ // The last chunk, which may have padding:
+ if ($i < $srcLen) {
+ /** @var array $chunk */
+ $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i));
+ $c0 = static::decode6Bits($chunk[1]);
+
+ if ($i + 2 < $srcLen) {
+ $c1 = static::decode6Bits($chunk[2]);
+ $c2 = static::decode6Bits($chunk[3]);
+ $dest .= \pack(
+ 'CC',
+ ((($c0 << 2) | ($c1 >> 4)) & 0xff),
+ ((($c1 << 4) | ($c2 >> 2)) & 0xff)
+ );
+ $err |= ($c0 | $c1 | $c2) >> 8;
+ if ($strictPadding) {
+ $err |= ($c2 << 6) & 0xff;
+ }
+ } elseif ($i + 1 < $srcLen) {
+ $c1 = static::decode6Bits($chunk[2]);
+ $dest .= \pack(
+ 'C',
+ ((($c0 << 2) | ($c1 >> 4)) & 0xff)
+ );
+ $err |= ($c0 | $c1) >> 8;
+ if ($strictPadding) {
+ $err |= ($c1 << 4) & 0xff;
+ }
+ } elseif ($strictPadding) {
+ $err |= 1;
+ }
+ }
+ $check = ($err === 0);
+ if (!$check) {
+ throw new RangeException(
+ 'Base64::decode() only expects characters in the correct base64 alphabet'
+ );
+ }
+ return $dest;
+ }
+
+ /**
+ * @param string $encodedString
+ * @return string
+ */
+ public static function decodeNoPadding(
+ #[\SensitiveParameter]
+ string $encodedString
+ ): string {
+ $srcLen = Binary::safeStrlen($encodedString);
+ if ($srcLen === 0) {
+ return '';
+ }
+ if (($srcLen & 3) === 0) {
+ // If $strLen is not zero, and it is divisible by 4, then it's at least 4.
+ if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') {
+ throw new InvalidArgumentException(
+ "decodeNoPadding() doesn't tolerate padding"
+ );
+ }
+ }
+ return static::decode(
+ $encodedString,
+ true
+ );
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 6-bit integers
+ * into 8-bit integers.
+ *
+ * Base64 character set:
+ * [A-Z] [a-z] [0-9] + /
+ * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
+ *
+ * @param int $src
+ * @return int
+ */
+ protected static function decode6Bits(int $src): int
+ {
+ $ret = -1;
+
+ // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
+ $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
+
+ // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
+ $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
+
+ // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
+ $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
+
+ // if ($src == 0x2b) $ret += 62 + 1;
+ $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63;
+
+ // if ($src == 0x2f) ret += 63 + 1;
+ $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64;
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 6-bit integers.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode6Bits(int $src): string
+ {
+ $diff = 0x41;
+
+ // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
+ $diff += ((25 - $src) >> 8) & 6;
+
+ // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
+ $diff -= ((51 - $src) >> 8) & 75;
+
+ // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15
+ $diff -= ((61 - $src) >> 8) & 15;
+
+ // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3
+ $diff += ((62 - $src) >> 8) & 3;
+
+ return \pack('C', $src + $diff);
+ }
+}
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php b/lib/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php
new file mode 100644
index 0000000000..5e98a8f793
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/Base64DotSlash.php
@@ -0,0 +1,88 @@
+ 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45
+ $ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45);
+
+ // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62
+ $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62);
+
+ // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68
+ $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68);
+
+ // if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7
+ $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7);
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 6-bit integers.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode6Bits(int $src): string
+ {
+ $src += 0x2e;
+
+ // if ($src > 0x2f) $src += 0x41 - 0x30; // 17
+ $src += ((0x2f - $src) >> 8) & 17;
+
+ // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
+ $src += ((0x5a - $src) >> 8) & 6;
+
+ // if ($src > 0x7a) $src += 0x30 - 0x7b; // -75
+ $src -= ((0x7a - $src) >> 8) & 75;
+
+ return \pack('C', $src);
+ }
+}
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php b/lib/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php
new file mode 100644
index 0000000000..9780b14bb0
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php
@@ -0,0 +1,82 @@
+ 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45
+ $ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45);
+
+ // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52
+ $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52);
+
+ // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58
+ $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58);
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 6-bit integers.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode6Bits(int $src): string
+ {
+ $src += 0x2e;
+
+ // if ($src > 0x39) $src += 0x41 - 0x3a; // 7
+ $src += ((0x39 - $src) >> 8) & 7;
+
+ // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
+ $src += ((0x5a - $src) >> 8) & 6;
+
+ return \pack('C', $src);
+ }
+}
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php b/lib/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php
new file mode 100644
index 0000000000..8192c63d58
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/Base64UrlSafe.php
@@ -0,0 +1,95 @@
+ 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
+ $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
+
+ // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
+ $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
+
+ // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
+ $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
+
+ // if ($src == 0x2c) $ret += 62 + 1;
+ $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63;
+
+ // if ($src == 0x5f) ret += 63 + 1;
+ $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64;
+
+ return $ret;
+ }
+
+ /**
+ * Uses bitwise operators instead of table-lookups to turn 8-bit integers
+ * into 6-bit integers.
+ *
+ * @param int $src
+ * @return string
+ */
+ protected static function encode6Bits(int $src): string
+ {
+ $diff = 0x41;
+
+ // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
+ $diff += ((25 - $src) >> 8) & 6;
+
+ // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
+ $diff -= ((51 - $src) >> 8) & 75;
+
+ // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13
+ $diff -= ((61 - $src) >> 8) & 13;
+
+ // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3
+ $diff += ((62 - $src) >> 8) & 49;
+
+ return \pack('C', $src + $diff);
+ }
+}
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/Binary.php b/lib/vendor/paragonie/constant_time_encoding/src/Binary.php
new file mode 100644
index 0000000000..5368e4bba7
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/Binary.php
@@ -0,0 +1,93 @@
+ $chunk */
+ $chunk = \unpack('C', $binString[$i]);
+ $c = $chunk[1] & 0xf;
+ $b = $chunk[1] >> 4;
+
+ $hex .= \pack(
+ 'CC',
+ (87 + $b + ((($b - 10) >> 8) & ~38)),
+ (87 + $c + ((($c - 10) >> 8) & ~38))
+ );
+ }
+ return $hex;
+ }
+
+ /**
+ * Convert a binary string into a hexadecimal string without cache-timing
+ * leaks, returning uppercase letters (as per RFC 4648)
+ *
+ * @param string $binString (raw binary)
+ * @return string
+ * @throws TypeError
+ */
+ public static function encodeUpper(
+ #[\SensitiveParameter]
+ string $binString
+ ): string {
+ $hex = '';
+ $len = Binary::safeStrlen($binString);
+
+ for ($i = 0; $i < $len; ++$i) {
+ /** @var array $chunk */
+ $chunk = \unpack('C', $binString[$i]);
+ $c = $chunk[1] & 0xf;
+ $b = $chunk[1] >> 4;
+
+ $hex .= \pack(
+ 'CC',
+ (55 + $b + ((($b - 10) >> 8) & ~6)),
+ (55 + $c + ((($c - 10) >> 8) & ~6))
+ );
+ }
+ return $hex;
+ }
+
+ /**
+ * Convert a hexadecimal string into a binary string without cache-timing
+ * leaks
+ *
+ * @param string $encodedString
+ * @param bool $strictPadding
+ * @return string (raw binary)
+ * @throws RangeException
+ */
+ public static function decode(
+ #[\SensitiveParameter]
+ string $encodedString,
+ bool $strictPadding = false
+ ): string {
+ $hex_pos = 0;
+ $bin = '';
+ $c_acc = 0;
+ $hex_len = Binary::safeStrlen($encodedString);
+ $state = 0;
+ if (($hex_len & 1) !== 0) {
+ if ($strictPadding) {
+ throw new RangeException(
+ 'Expected an even number of hexadecimal characters'
+ );
+ } else {
+ $encodedString = '0' . $encodedString;
+ ++$hex_len;
+ }
+ }
+
+ /** @var array $chunk */
+ $chunk = \unpack('C*', $encodedString);
+ while ($hex_pos < $hex_len) {
+ ++$hex_pos;
+ $c = $chunk[$hex_pos];
+ $c_num = $c ^ 48;
+ $c_num0 = ($c_num - 10) >> 8;
+ $c_alpha = ($c & ~32) - 55;
+ $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
+
+ if (($c_num0 | $c_alpha0) === 0) {
+ throw new RangeException(
+ 'Expected hexadecimal character'
+ );
+ }
+ $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
+ if ($state === 0) {
+ $c_acc = $c_val * 16;
+ } else {
+ $bin .= \pack('C', $c_acc | $c_val);
+ }
+ $state ^= 1;
+ }
+ return $bin;
+ }
+}
diff --git a/lib/vendor/paragonie/constant_time_encoding/src/RFC4648.php b/lib/vendor/paragonie/constant_time_encoding/src/RFC4648.php
new file mode 100644
index 0000000000..7cd2e9909d
--- /dev/null
+++ b/lib/vendor/paragonie/constant_time_encoding/src/RFC4648.php
@@ -0,0 +1,206 @@
+ "Zm9v"
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base64Encode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base64::encode($str);
+ }
+
+ /**
+ * RFC 4648 Base64 decoding
+ *
+ * "Zm9v" -> "foo"
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base64Decode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base64::decode($str, true);
+ }
+
+ /**
+ * RFC 4648 Base64 (URL Safe) encoding
+ *
+ * "foo" -> "Zm9v"
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base64UrlSafeEncode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base64UrlSafe::encode($str);
+ }
+
+ /**
+ * RFC 4648 Base64 (URL Safe) decoding
+ *
+ * "Zm9v" -> "foo"
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base64UrlSafeDecode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base64UrlSafe::decode($str, true);
+ }
+
+ /**
+ * RFC 4648 Base32 encoding
+ *
+ * "foo" -> "MZXW6==="
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base32Encode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base32::encodeUpper($str);
+ }
+
+ /**
+ * RFC 4648 Base32 encoding
+ *
+ * "MZXW6===" -> "foo"
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base32Decode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base32::decodeUpper($str, true);
+ }
+
+ /**
+ * RFC 4648 Base32-Hex encoding
+ *
+ * "foo" -> "CPNMU==="
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base32HexEncode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base32::encodeUpper($str);
+ }
+
+ /**
+ * RFC 4648 Base32-Hex decoding
+ *
+ * "CPNMU===" -> "foo"
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base32HexDecode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Base32::decodeUpper($str, true);
+ }
+
+ /**
+ * RFC 4648 Base16 decoding
+ *
+ * "foo" -> "666F6F"
+ *
+ * @param string $str
+ * @return string
+ *
+ * @throws TypeError
+ */
+ public static function base16Encode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Hex::encodeUpper($str);
+ }
+
+ /**
+ * RFC 4648 Base16 decoding
+ *
+ * "666F6F" -> "foo"
+ *
+ * @param string $str
+ * @return string
+ */
+ public static function base16Decode(
+ #[\SensitiveParameter]
+ string $str
+ ): string {
+ return Hex::decode($str, true);
+ }
+}
diff --git a/lib/vendor/paragonie/random_compat/LICENSE b/lib/vendor/paragonie/random_compat/LICENSE
new file mode 100644
index 0000000000..45c7017dfb
--- /dev/null
+++ b/lib/vendor/paragonie/random_compat/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Paragon Initiative Enterprises
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/lib/vendor/paragonie/random_compat/build-phar.sh b/lib/vendor/paragonie/random_compat/build-phar.sh
new file mode 100755
index 0000000000..b4a5ba31cc
--- /dev/null
+++ b/lib/vendor/paragonie/random_compat/build-phar.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+basedir=$( dirname $( readlink -f ${BASH_SOURCE[0]} ) )
+
+php -dphar.readonly=0 "$basedir/other/build_phar.php" $*
\ No newline at end of file
diff --git a/lib/vendor/paragonie/random_compat/composer.json b/lib/vendor/paragonie/random_compat/composer.json
new file mode 100644
index 0000000000..f2b9c4e510
--- /dev/null
+++ b/lib/vendor/paragonie/random_compat/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "paragonie/random_compat",
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "random",
+ "polyfill",
+ "pseudorandom"
+ ],
+ "license": "MIT",
+ "type": "library",
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/paragonie/random_compat/issues",
+ "email": "info@paragonie.com",
+ "source": "https://github.com/paragonie/random_compat"
+ },
+ "require": {
+ "php": ">= 7"
+ },
+ "require-dev": {
+ "vimeo/psalm": "^1",
+ "phpunit/phpunit": "4.*|5.*"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ }
+}
diff --git a/lib/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey b/lib/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey
new file mode 100644
index 0000000000..eb50ebfcd6
--- /dev/null
+++ b/lib/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey
@@ -0,0 +1,5 @@
+-----BEGIN PUBLIC KEY-----
+MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEEd+wCqJDrx5B4OldM0dQE0ZMX+lx1ZWm
+pui0SUqD4G29L3NGsz9UhJ/0HjBdbnkhIK5xviT0X5vtjacF6ajgcCArbTB+ds+p
++h7Q084NuSuIpNb6YPfoUFgC/CL9kAoc
+-----END PUBLIC KEY-----
diff --git a/lib/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc b/lib/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc
new file mode 100644
index 0000000000..6a1d7f3006
--- /dev/null
+++ b/lib/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc
@@ -0,0 +1,11 @@
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v2.0.22 (MingW32)
+
+iQEcBAABAgAGBQJWtW1hAAoJEGuXocKCZATaJf0H+wbZGgskK1dcRTsuVJl9IWip
+QwGw/qIKI280SD6/ckoUMxKDCJiFuPR14zmqnS36k7N5UNPnpdTJTS8T11jttSpg
+1LCmgpbEIpgaTah+cELDqFCav99fS+bEiAL5lWDAHBTE/XPjGVCqeehyPYref4IW
+NDBIEsvnHPHPLsn6X5jq4+Yj5oUixgxaMPiR+bcO4Sh+RzOVB6i2D0upWfRXBFXA
+NNnsg9/zjvoC7ZW73y9uSH+dPJTt/Vgfeiv52/v41XliyzbUyLalf02GNPY+9goV
+JHG1ulEEBJOCiUD9cE1PUIJwHA/HqyhHIvV350YoEFiHl8iSwm7SiZu5kPjaq74=
+=B6+8
+-----END PGP SIGNATURE-----
diff --git a/lib/vendor/paragonie/random_compat/lib/random.php b/lib/vendor/paragonie/random_compat/lib/random.php
new file mode 100644
index 0000000000..c7731a56ff
--- /dev/null
+++ b/lib/vendor/paragonie/random_compat/lib/random.php
@@ -0,0 +1,32 @@
+buildFromDirectory(dirname(__DIR__).'/lib');
+rename(
+ dirname(__DIR__).'/lib/index.php',
+ dirname(__DIR__).'/lib/random.php'
+);
+
+/**
+ * If we pass an (optional) path to a private key as a second argument, we will
+ * sign the Phar with OpenSSL.
+ *
+ * If you leave this out, it will produce an unsigned .phar!
+ */
+if ($argc > 1) {
+ if (!@is_readable($argv[1])) {
+ echo 'Could not read the private key file:', $argv[1], "\n";
+ exit(255);
+ }
+ $pkeyFile = file_get_contents($argv[1]);
+
+ $private = openssl_get_privatekey($pkeyFile);
+ if ($private !== false) {
+ $pkey = '';
+ openssl_pkey_export($private, $pkey);
+ $phar->setSignatureAlgorithm(Phar::OPENSSL, $pkey);
+
+ /**
+ * Save the corresponding public key to the file
+ */
+ if (!@is_readable($dist.'/random_compat.phar.pubkey')) {
+ $details = openssl_pkey_get_details($private);
+ file_put_contents(
+ $dist.'/random_compat.phar.pubkey',
+ $details['key']
+ );
+ }
+ } else {
+ echo 'An error occurred reading the private key from OpenSSL.', "\n";
+ exit(255);
+ }
+}
diff --git a/lib/vendor/paragonie/random_compat/psalm-autoload.php b/lib/vendor/paragonie/random_compat/psalm-autoload.php
new file mode 100644
index 0000000000..d71d1b818c
--- /dev/null
+++ b/lib/vendor/paragonie/random_compat/psalm-autoload.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/vendor/pclzip/pclzip/composer.json b/lib/vendor/pclzip/pclzip/composer.json
new file mode 100644
index 0000000000..a4df7fb6e3
--- /dev/null
+++ b/lib/vendor/pclzip/pclzip/composer.json
@@ -0,0 +1,16 @@
+{
+ "name": "pclzip/pclzip",
+ "type": "library",
+ "description": "A PHP library that offers compression and extraction functions for Zip formatted archives",
+ "keywords": ["php", "zip"],
+ "homepage": "http://www.phpconcept.net/pclzip",
+ "license": ["LGPL-2.1"],
+ "authors": [
+ {
+ "name": "Vincent Blavet"
+ }
+ ],
+ "autoload": {
+ "classmap": ["pclzip.lib.php"]
+ }
+}
diff --git a/lib/vendor/pclzip/pclzip/gnu-lgpl.txt b/lib/vendor/pclzip/pclzip/gnu-lgpl.txt
new file mode 100644
index 0000000000..583509c7ef
--- /dev/null
+++ b/lib/vendor/pclzip/pclzip/gnu-lgpl.txt
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/lib/vendor/pclzip/pclzip/pclzip.lib.php b/lib/vendor/pclzip/pclzip/pclzip.lib.php
new file mode 100644
index 0000000000..33be0d7869
--- /dev/null
+++ b/lib/vendor/pclzip/pclzip/pclzip.lib.php
@@ -0,0 +1,5414 @@
+zipname = $p_zipname;
+ $this->zip_fd = 0;
+ $this->magic_quotes_status = -1;
+
+ // ----- Return
+ return;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // create($p_filelist, $p_add_dir="", $p_remove_dir="")
+ // create($p_filelist, $p_option, $p_option_value, ...)
+ // Description :
+ // This method supports two different synopsis. The first one is historical.
+ // This method creates a Zip Archive. The Zip file is created in the
+ // filesystem. The files and directories indicated in $p_filelist
+ // are added in the archive. See the parameters description for the
+ // supported format of $p_filelist.
+ // When a directory is in the list, the directory and its content is added
+ // in the archive.
+ // In this synopsis, the function takes an optional variable list of
+ // options. See bellow the supported options.
+ // Parameters :
+ // $p_filelist : An array containing file or directory names, or
+ // a string containing one filename or one directory name, or
+ // a string containing a list of filenames and/or directory
+ // names separated by spaces.
+ // $p_add_dir : A path to add before the real path of the archived file,
+ // in order to have it memorized in the archive.
+ // $p_remove_dir : A path to remove from the real path of the file to archive,
+ // in order to have a shorter path memorized in the archive.
+ // When $p_add_dir and $p_remove_dir are set, $p_remove_dir
+ // is removed first, before $p_add_dir is added.
+ // Options :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_OPT_COMMENT :
+ // PCLZIP_CB_PRE_ADD :
+ // PCLZIP_CB_POST_ADD :
+ // Return Values :
+ // 0 on failure,
+ // The list of the added files, with a status of the add action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function create($p_filelist)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Set default values
+ $v_options = array();
+ $v_options[PCLZIP_OPT_NO_COMPRESSION] = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Look for arguments
+ if ($v_size > 1) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Remove from the options list the first argument
+ array_shift($v_arg_list);
+ $v_size--;
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_ADD => 'optional',
+ PCLZIP_CB_POST_ADD => 'optional',
+ PCLZIP_OPT_NO_COMPRESSION => 'optional',
+ PCLZIP_OPT_COMMENT => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ //, PCLZIP_OPT_CRYPT => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_options[PCLZIP_OPT_ADD_PATH] = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ return 0;
+ }
+ }
+ }
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Init
+ $v_string_list = array();
+ $v_att_list = array();
+ $v_filedescr_list = array();
+ $p_result_list = array();
+
+ // ----- Look if the $p_filelist is really an array
+ if (is_array($p_filelist)) {
+
+ // ----- Look if the first element is also an array
+ // This will mean that this is a file description entry
+ if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
+ $v_att_list = $p_filelist;
+
+ // ----- The list is a list of string names
+ } else {
+ $v_string_list = $p_filelist;
+ }
+
+ // ----- Look if the $p_filelist is a string
+ } elseif (is_string($p_filelist)) {
+ // ----- Create a list from the string
+ $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
+
+ // ----- Invalid variable type for $p_filelist
+ } else {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_filelist");
+
+ return 0;
+ }
+
+ // ----- Reformat the string list
+ if (sizeof($v_string_list) != 0) {
+ foreach ($v_string_list as $v_string) {
+ if ($v_string != '') {
+ $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
+ } else {
+ }
+ }
+ }
+
+ // ----- For each file in the list check the attributes
+ $v_supported_attributes = array(
+ PCLZIP_ATT_FILE_NAME => 'mandatory',
+ PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional',
+ PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional',
+ PCLZIP_ATT_FILE_MTIME => 'optional',
+ PCLZIP_ATT_FILE_CONTENT => 'optional',
+ PCLZIP_ATT_FILE_COMMENT => 'optional'
+ );
+ foreach ($v_att_list as $v_entry) {
+ $v_result = $this->privFileDescrParseAtt($v_entry, $v_filedescr_list[], $v_options, $v_supported_attributes);
+ if ($v_result != 1) {
+ return 0;
+ }
+ }
+
+ // ----- Expand the filelist (expand directories)
+ $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Call the create fct
+ $v_result = $this->privCreate($v_filedescr_list, $p_result_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Return
+ return $p_result_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // add($p_filelist, $p_add_dir="", $p_remove_dir="")
+ // add($p_filelist, $p_option, $p_option_value, ...)
+ // Description :
+ // This method supports two synopsis. The first one is historical.
+ // This methods add the list of files in an existing archive.
+ // If a file with the same name already exists, it is added at the end of the
+ // archive, the first one is still present.
+ // If the archive does not exist, it is created.
+ // Parameters :
+ // $p_filelist : An array containing file or directory names, or
+ // a string containing one filename or one directory name, or
+ // a string containing a list of filenames and/or directory
+ // names separated by spaces.
+ // $p_add_dir : A path to add before the real path of the archived file,
+ // in order to have it memorized in the archive.
+ // $p_remove_dir : A path to remove from the real path of the file to archive,
+ // in order to have a shorter path memorized in the archive.
+ // When $p_add_dir and $p_remove_dir are set, $p_remove_dir
+ // is removed first, before $p_add_dir is added.
+ // Options :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_OPT_COMMENT :
+ // PCLZIP_OPT_ADD_COMMENT :
+ // PCLZIP_OPT_PREPEND_COMMENT :
+ // PCLZIP_CB_PRE_ADD :
+ // PCLZIP_CB_POST_ADD :
+ // Return Values :
+ // 0 on failure,
+ // The list of the added files, with a status of the add action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function add($p_filelist)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Set default values
+ $v_options = array();
+ $v_options[PCLZIP_OPT_NO_COMPRESSION] = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Look for arguments
+ if ($v_size > 1) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Remove form the options list the first argument
+ array_shift($v_arg_list);
+ $v_size--;
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_ADD => 'optional',
+ PCLZIP_CB_POST_ADD => 'optional',
+ PCLZIP_OPT_NO_COMPRESSION => 'optional',
+ PCLZIP_OPT_COMMENT => 'optional',
+ PCLZIP_OPT_ADD_COMMENT => 'optional',
+ PCLZIP_OPT_PREPEND_COMMENT => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ //, PCLZIP_OPT_CRYPT => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_options[PCLZIP_OPT_ADD_PATH] = $v_add_path = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_options[PCLZIP_OPT_REMOVE_PATH] = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ // ----- Return
+ return 0;
+ }
+ }
+ }
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Init
+ $v_string_list = array();
+ $v_att_list = array();
+ $v_filedescr_list = array();
+ $p_result_list = array();
+
+ // ----- Look if the $p_filelist is really an array
+ if (is_array($p_filelist)) {
+
+ // ----- Look if the first element is also an array
+ // This will mean that this is a file description entry
+ if (isset($p_filelist[0]) && is_array($p_filelist[0])) {
+ $v_att_list = $p_filelist;
+
+ // ----- The list is a list of string names
+ } else {
+ $v_string_list = $p_filelist;
+ }
+
+ // ----- Look if the $p_filelist is a string
+ } elseif (is_string($p_filelist)) {
+ // ----- Create a list from the string
+ $v_string_list = explode(PCLZIP_SEPARATOR, $p_filelist);
+
+ // ----- Invalid variable type for $p_filelist
+ } else {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type '" . gettype($p_filelist) . "' for p_filelist");
+
+ return 0;
+ }
+
+ // ----- Reformat the string list
+ if (sizeof($v_string_list) != 0) {
+ foreach ($v_string_list as $v_string) {
+ $v_att_list[][PCLZIP_ATT_FILE_NAME] = $v_string;
+ }
+ }
+
+ // ----- For each file in the list check the attributes
+ $v_supported_attributes = array(
+ PCLZIP_ATT_FILE_NAME => 'mandatory',
+ PCLZIP_ATT_FILE_NEW_SHORT_NAME => 'optional',
+ PCLZIP_ATT_FILE_NEW_FULL_NAME => 'optional',
+ PCLZIP_ATT_FILE_MTIME => 'optional',
+ PCLZIP_ATT_FILE_CONTENT => 'optional',
+ PCLZIP_ATT_FILE_COMMENT => 'optional'
+ );
+ foreach ($v_att_list as $v_entry) {
+ $v_result = $this->privFileDescrParseAtt($v_entry, $v_filedescr_list[], $v_options, $v_supported_attributes);
+ if ($v_result != 1) {
+ return 0;
+ }
+ }
+
+ // ----- Expand the filelist (expand directories)
+ $v_result = $this->privFileDescrExpand($v_filedescr_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Call the create fct
+ $v_result = $this->privAdd($v_filedescr_list, $p_result_list, $v_options);
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Return
+ return $p_result_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : listContent()
+ // Description :
+ // This public method, gives the list of the files and directories, with their
+ // properties.
+ // The properties of each entries in the list are (used also in other functions) :
+ // filename : Name of the file. For a create or add action it is the filename
+ // given by the user. For an extract function it is the filename
+ // of the extracted file.
+ // stored_filename : Name of the file / directory stored in the archive.
+ // size : Size of the stored file.
+ // compressed_size : Size of the file's data compressed in the archive
+ // (without the headers overhead)
+ // mtime : Last known modification date of the file (UNIX timestamp)
+ // comment : Comment associated with the file
+ // folder : true | false
+ // index : index of the file in the archive
+ // status : status of the action (depending of the action) :
+ // Values are :
+ // ok : OK !
+ // filtered : the file / dir is not extracted (filtered by user)
+ // already_a_directory : the file can not be extracted because a
+ // directory with the same name already exists
+ // write_protected : the file can not be extracted because a file
+ // with the same name already exists and is
+ // write protected
+ // newer_exist : the file was not extracted because a newer file exists
+ // path_creation_fail : the file is not extracted because the folder
+ // does not exist and can not be created
+ // write_error : the file was not extracted because there was a
+ // error while writing the file
+ // read_error : the file was not extracted because there was a error
+ // while reading the file
+ // invalid_header : the file was not extracted because of an archive
+ // format error (bad file header)
+ // Note that each time a method can continue operating when there
+ // is an action error on a file, the error is only logged in the file status.
+ // Return Values :
+ // 0 on an unrecoverable failure,
+ // The list of the files in the archive.
+ // --------------------------------------------------------------------------------
+ public function listContent()
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Call the extracting fct
+ $p_list = array();
+ if (($v_result = $this->privList($p_list)) != 1) {
+ unset($p_list);
+
+ return (0);
+ }
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // extract($p_path="./", $p_remove_path="")
+ // extract([$p_option, $p_option_value, ...])
+ // Description :
+ // This method supports two synopsis. The first one is historical.
+ // This method extract all the files / directories from the archive to the
+ // folder indicated in $p_path.
+ // If you want to ignore the 'root' part of path of the memorized files
+ // you can indicate this in the optional $p_remove_path parameter.
+ // By default, if a newer file with the same name already exists, the
+ // file is not extracted.
+ //
+ // If both PCLZIP_OPT_PATH and PCLZIP_OPT_ADD_PATH aoptions
+ // are used, the path indicated in PCLZIP_OPT_ADD_PATH is append
+ // at the end of the path value of PCLZIP_OPT_PATH.
+ // Parameters :
+ // $p_path : Path where the files and directories are to be extracted
+ // $p_remove_path : First part ('root' part) of the memorized path
+ // (if any similar) to remove while extracting.
+ // Options :
+ // PCLZIP_OPT_PATH :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_CB_PRE_EXTRACT :
+ // PCLZIP_CB_POST_EXTRACT :
+ // Return Values :
+ // 0 or a negative value on failure,
+ // The list of the extracted files, with a status of the action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function extract()
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Set default values
+ $v_options = array();
+ // $v_path = "./";
+ $v_path = '';
+ $v_remove_path = "";
+ $v_remove_all_path = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Default values for option
+ $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+
+ // ----- Look for arguments
+ if ($v_size > 0) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_EXTRACT => 'optional',
+ PCLZIP_CB_POST_EXTRACT => 'optional',
+ PCLZIP_OPT_SET_CHMOD => 'optional',
+ PCLZIP_OPT_BY_NAME => 'optional',
+ PCLZIP_OPT_BY_EREG => 'optional',
+ PCLZIP_OPT_BY_PREG => 'optional',
+ PCLZIP_OPT_BY_INDEX => 'optional',
+ PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
+ PCLZIP_OPT_EXTRACT_IN_OUTPUT => 'optional',
+ PCLZIP_OPT_REPLACE_NEWER => 'optional',
+ PCLZIP_OPT_STOP_ON_ERROR => 'optional',
+ PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Set the arguments
+ if (isset($v_options[PCLZIP_OPT_PATH])) {
+ $v_path = $v_options[PCLZIP_OPT_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
+ $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+ $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
+ // ----- Check for '/' in last path char
+ if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
+ $v_path .= '/';
+ }
+ $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_path = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_remove_path = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ // ----- Return
+ return 0;
+ }
+ }
+ }
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Trace
+
+ // ----- Call the extracting fct
+ $p_list = array();
+ $v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options);
+ if ($v_result < 1) {
+ unset($p_list);
+
+ return (0);
+ }
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // extractByIndex($p_index, $p_path="./", $p_remove_path="")
+ // extractByIndex($p_index, [$p_option, $p_option_value, ...])
+ // Description :
+ // This method supports two synopsis. The first one is historical.
+ // This method is doing a partial extract of the archive.
+ // The extracted files or folders are identified by their index in the
+ // archive (from 0 to n).
+ // Note that if the index identify a folder, only the folder entry is
+ // extracted, not all the files included in the archive.
+ // Parameters :
+ // $p_index : A single index (integer) or a string of indexes of files to
+ // extract. The form of the string is "0,4-6,8-12" with only numbers
+ // and '-' for range or ',' to separate ranges. No spaces or ';'
+ // are allowed.
+ // $p_path : Path where the files and directories are to be extracted
+ // $p_remove_path : First part ('root' part) of the memorized path
+ // (if any similar) to remove while extracting.
+ // Options :
+ // PCLZIP_OPT_PATH :
+ // PCLZIP_OPT_ADD_PATH :
+ // PCLZIP_OPT_REMOVE_PATH :
+ // PCLZIP_OPT_REMOVE_ALL_PATH :
+ // PCLZIP_OPT_EXTRACT_AS_STRING : The files are extracted as strings and
+ // not as files.
+ // The resulting content is in a new field 'content' in the file
+ // structure.
+ // This option must be used alone (any other options are ignored).
+ // PCLZIP_CB_PRE_EXTRACT :
+ // PCLZIP_CB_POST_EXTRACT :
+ // Return Values :
+ // 0 on failure,
+ // The list of the extracted files, with a status of the action.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ //function extractByIndex($p_index, options...)
+ public function extractByIndex($p_index)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Set default values
+ $v_options = array();
+ // $v_path = "./";
+ $v_path = '';
+ $v_remove_path = "";
+ $v_remove_all_path = false;
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Default values for option
+ $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+
+ // ----- Look for arguments
+ if ($v_size > 1) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Remove form the options list the first argument
+ array_shift($v_arg_list);
+ $v_size--;
+
+ // ----- Look for first arg
+ if ((is_integer($v_arg_list[0])) && ($v_arg_list[0] > 77000)) {
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_PATH => 'optional',
+ PCLZIP_OPT_REMOVE_ALL_PATH => 'optional',
+ PCLZIP_OPT_EXTRACT_AS_STRING => 'optional',
+ PCLZIP_OPT_ADD_PATH => 'optional',
+ PCLZIP_CB_PRE_EXTRACT => 'optional',
+ PCLZIP_CB_POST_EXTRACT => 'optional',
+ PCLZIP_OPT_SET_CHMOD => 'optional',
+ PCLZIP_OPT_REPLACE_NEWER => 'optional',
+ PCLZIP_OPT_STOP_ON_ERROR => 'optional',
+ PCLZIP_OPT_EXTRACT_DIR_RESTRICTION => 'optional',
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD => 'optional',
+ PCLZIP_OPT_TEMP_FILE_ON => 'optional',
+ PCLZIP_OPT_TEMP_FILE_OFF => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+
+ // ----- Set the arguments
+ if (isset($v_options[PCLZIP_OPT_PATH])) {
+ $v_path = $v_options[PCLZIP_OPT_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_PATH])) {
+ $v_remove_path = $v_options[PCLZIP_OPT_REMOVE_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+ $v_remove_all_path = $v_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+ }
+ if (isset($v_options[PCLZIP_OPT_ADD_PATH])) {
+ // ----- Check for '/' in last path char
+ if ((strlen($v_path) > 0) && (substr($v_path, -1) != '/')) {
+ $v_path .= '/';
+ }
+ $v_path .= $v_options[PCLZIP_OPT_ADD_PATH];
+ }
+ if (!isset($v_options[PCLZIP_OPT_EXTRACT_AS_STRING])) {
+ $v_options[PCLZIP_OPT_EXTRACT_AS_STRING] = false;
+ } else {
+ }
+
+ // ----- Look for 2 args
+ // Here we need to support the first historic synopsis of the
+ // method.
+ } else {
+
+ // ----- Get the first argument
+ $v_path = $v_arg_list[0];
+
+ // ----- Look for the optional second argument
+ if ($v_size == 2) {
+ $v_remove_path = $v_arg_list[1];
+ } elseif ($v_size > 2) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid number / type of arguments");
+
+ // ----- Return
+ return 0;
+ }
+ }
+ }
+
+ // ----- Trace
+
+ // ----- Trick
+ // Here I want to reuse extractByRule(), so I need to parse the $p_index
+ // with privParseOptions()
+ $v_arg_trick = array(
+ PCLZIP_OPT_BY_INDEX,
+ $p_index
+ );
+ $v_options_trick = array();
+ $v_result = $this->privParseOptions($v_arg_trick, sizeof($v_arg_trick), $v_options_trick, array(
+ PCLZIP_OPT_BY_INDEX => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+ $v_options[PCLZIP_OPT_BY_INDEX] = $v_options_trick[PCLZIP_OPT_BY_INDEX];
+
+ // ----- Look for default option values
+ $this->privOptionDefaultThreshold($v_options);
+
+ // ----- Call the extracting fct
+ if (($v_result = $this->privExtractByRule($p_list, $v_path, $v_remove_path, $v_remove_all_path, $v_options)) < 1) {
+ return (0);
+ }
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function :
+ // delete([$p_option, $p_option_value, ...])
+ // Description :
+ // This method removes files from the archive.
+ // If no parameters are given, then all the archive is emptied.
+ // Parameters :
+ // None or optional arguments.
+ // Options :
+ // PCLZIP_OPT_BY_INDEX :
+ // PCLZIP_OPT_BY_NAME :
+ // PCLZIP_OPT_BY_EREG :
+ // PCLZIP_OPT_BY_PREG :
+ // Return Values :
+ // 0 on failure,
+ // The list of the files which are still present in the archive.
+ // (see PclZip::listContent() for list entry format)
+ // --------------------------------------------------------------------------------
+ public function delete()
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Set default values
+ $v_options = array();
+
+ // ----- Look for variable options arguments
+ $v_size = func_num_args();
+
+ // ----- Look for arguments
+ if ($v_size > 0) {
+ // ----- Get the arguments
+ $v_arg_list = func_get_args();
+
+ // ----- Parse the options
+ $v_result = $this->privParseOptions($v_arg_list, $v_size, $v_options, array(
+ PCLZIP_OPT_BY_NAME => 'optional',
+ PCLZIP_OPT_BY_EREG => 'optional',
+ PCLZIP_OPT_BY_PREG => 'optional',
+ PCLZIP_OPT_BY_INDEX => 'optional'
+ ));
+ if ($v_result != 1) {
+ return 0;
+ }
+ }
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Call the delete fct
+ $v_list = array();
+ if (($v_result = $this->privDeleteByRule($v_list, $v_options)) != 1) {
+ $this->privSwapBackMagicQuotes();
+ unset($v_list);
+
+ return (0);
+ }
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : deleteByIndex()
+ // Description :
+ // ***** Deprecated *****
+ // delete(PCLZIP_OPT_BY_INDEX, $p_index) should be prefered.
+ // --------------------------------------------------------------------------------
+ public function deleteByIndex($p_index)
+ {
+
+ $p_list = $this->delete(PCLZIP_OPT_BY_INDEX, $p_index);
+
+ // ----- Return
+ return $p_list;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : properties()
+ // Description :
+ // This method gives the properties of the archive.
+ // The properties are :
+ // nb : Number of files in the archive
+ // comment : Comment associated with the archive file
+ // status : not_exist, ok
+ // Parameters :
+ // None
+ // Return Values :
+ // 0 on failure,
+ // An array with the archive properties.
+ // --------------------------------------------------------------------------------
+ public function properties()
+ {
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ $this->privSwapBackMagicQuotes();
+
+ return (0);
+ }
+
+ // ----- Default properties
+ $v_prop = array();
+ $v_prop['comment'] = '';
+ $v_prop['nb'] = 0;
+ $v_prop['status'] = 'not_exist';
+
+ // ----- Look if file exists
+ if (@is_file($this->zipname)) {
+ // ----- Open the zip file
+ if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) {
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in binary read mode');
+
+ // ----- Return
+ return 0;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return 0;
+ }
+
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ // ----- Set the user attributes
+ $v_prop['comment'] = $v_central_dir['comment'];
+ $v_prop['nb'] = $v_central_dir['entries'];
+ $v_prop['status'] = 'ok';
+ }
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_prop;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : duplicate()
+ // Description :
+ // This method creates an archive by copying the content of an other one. If
+ // the archive already exist, it is replaced by the new one without any warning.
+ // Parameters :
+ // $p_archive : The filename of a valid archive, or
+ // a valid PclZip object.
+ // Return Values :
+ // 1 on success.
+ // 0 or a negative value on error (error code).
+ // --------------------------------------------------------------------------------
+ public function duplicate($p_archive)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Look if the $p_archive is a PclZip object
+ if ((is_object($p_archive)) && (get_class($p_archive) == 'pclzip')) {
+
+ // ----- Duplicate the archive
+ $v_result = $this->privDuplicate($p_archive->zipname);
+
+ // ----- Look if the $p_archive is a string (so a filename)
+ } elseif (is_string($p_archive)) {
+
+ // ----- Check that $p_archive is a valid zip file
+ // TBC : Should also check the archive format
+ if (!is_file($p_archive)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "No file with filename '" . $p_archive . "'");
+ $v_result = PCLZIP_ERR_MISSING_FILE;
+ } else {
+ // ----- Duplicate the archive
+ $v_result = $this->privDuplicate($p_archive);
+ }
+
+ // ----- Invalid variable
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
+ $v_result = PCLZIP_ERR_INVALID_PARAMETER;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : merge()
+ // Description :
+ // This method merge the $p_archive_to_add archive at the end of the current
+ // one ($this).
+ // If the archive ($this) does not exist, the merge becomes a duplicate.
+ // If the $p_archive_to_add archive does not exist, the merge is a success.
+ // Parameters :
+ // $p_archive_to_add : It can be directly the filename of a valid zip archive,
+ // or a PclZip object archive.
+ // Return Values :
+ // 1 on success,
+ // 0 or negative values on error (see below).
+ // --------------------------------------------------------------------------------
+ public function merge($p_archive_to_add)
+ {
+ $v_result = 1;
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Check archive
+ if (!$this->privCheckFormat()) {
+ return (0);
+ }
+
+ // ----- Look if the $p_archive_to_add is a PclZip object
+ if ((is_object($p_archive_to_add)) && (get_class($p_archive_to_add) == 'pclzip')) {
+
+ // ----- Merge the archive
+ $v_result = $this->privMerge($p_archive_to_add);
+
+ // ----- Look if the $p_archive_to_add is a string (so a filename)
+ } elseif (is_string($p_archive_to_add)) {
+
+ // ----- Create a temporary archive
+ $v_object_archive = new PclZip($p_archive_to_add);
+
+ // ----- Merge the archive
+ $v_result = $this->privMerge($v_object_archive);
+
+ // ----- Invalid variable
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid variable type p_archive_to_add");
+ $v_result = PCLZIP_ERR_INVALID_PARAMETER;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : errorCode()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function errorCode()
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ return (PclErrorCode());
+ } else {
+ return ($this->error_code);
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : errorName()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function errorName($p_with_code = false)
+ {
+ $v_name = array(
+ PCLZIP_ERR_NO_ERROR => 'PCLZIP_ERR_NO_ERROR',
+ PCLZIP_ERR_WRITE_OPEN_FAIL => 'PCLZIP_ERR_WRITE_OPEN_FAIL',
+ PCLZIP_ERR_READ_OPEN_FAIL => 'PCLZIP_ERR_READ_OPEN_FAIL',
+ PCLZIP_ERR_INVALID_PARAMETER => 'PCLZIP_ERR_INVALID_PARAMETER',
+ PCLZIP_ERR_MISSING_FILE => 'PCLZIP_ERR_MISSING_FILE',
+ PCLZIP_ERR_FILENAME_TOO_LONG => 'PCLZIP_ERR_FILENAME_TOO_LONG',
+ PCLZIP_ERR_INVALID_ZIP => 'PCLZIP_ERR_INVALID_ZIP',
+ PCLZIP_ERR_BAD_EXTRACTED_FILE => 'PCLZIP_ERR_BAD_EXTRACTED_FILE',
+ PCLZIP_ERR_DIR_CREATE_FAIL => 'PCLZIP_ERR_DIR_CREATE_FAIL',
+ PCLZIP_ERR_BAD_EXTENSION => 'PCLZIP_ERR_BAD_EXTENSION',
+ PCLZIP_ERR_BAD_FORMAT => 'PCLZIP_ERR_BAD_FORMAT',
+ PCLZIP_ERR_DELETE_FILE_FAIL => 'PCLZIP_ERR_DELETE_FILE_FAIL',
+ PCLZIP_ERR_RENAME_FILE_FAIL => 'PCLZIP_ERR_RENAME_FILE_FAIL',
+ PCLZIP_ERR_BAD_CHECKSUM => 'PCLZIP_ERR_BAD_CHECKSUM',
+ PCLZIP_ERR_INVALID_ARCHIVE_ZIP => 'PCLZIP_ERR_INVALID_ARCHIVE_ZIP',
+ PCLZIP_ERR_MISSING_OPTION_VALUE => 'PCLZIP_ERR_MISSING_OPTION_VALUE',
+ PCLZIP_ERR_INVALID_OPTION_VALUE => 'PCLZIP_ERR_INVALID_OPTION_VALUE',
+ PCLZIP_ERR_UNSUPPORTED_COMPRESSION => 'PCLZIP_ERR_UNSUPPORTED_COMPRESSION',
+ PCLZIP_ERR_UNSUPPORTED_ENCRYPTION => 'PCLZIP_ERR_UNSUPPORTED_ENCRYPTION',
+ PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE => 'PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE',
+ PCLZIP_ERR_DIRECTORY_RESTRICTION => 'PCLZIP_ERR_DIRECTORY_RESTRICTION'
+ );
+
+ if (isset($v_name[$this->error_code])) {
+ $v_value = $v_name[$this->error_code];
+ } else {
+ $v_value = 'NoName';
+ }
+
+ if ($p_with_code) {
+ return ($v_value . ' (' . $this->error_code . ')');
+ } else {
+ return ($v_value);
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : errorInfo()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function errorInfo($p_full = false)
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ return (PclErrorString());
+ } else {
+ if ($p_full) {
+ return ($this->errorName(true) . " : " . $this->error_string);
+ } else {
+ return ($this->error_string . " [code " . $this->error_code . "]");
+ }
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // ***** UNDER THIS LINE ARE DEFINED PRIVATE INTERNAL FUNCTIONS *****
+ // ***** *****
+ // ***** THESES FUNCTIONS MUST NOT BE USED DIRECTLY *****
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCheckFormat()
+ // Description :
+ // This method check that the archive exists and is a valid zip archive.
+ // Several level of check exists. (futur)
+ // Parameters :
+ // $p_level : Level of check. Default 0.
+ // 0 : Check the first bytes (magic codes) (default value))
+ // 1 : 0 + Check the central directory (futur)
+ // 2 : 1 + Check each file header (futur)
+ // Return Values :
+ // true on success,
+ // false on error, the error code is set.
+ // --------------------------------------------------------------------------------
+ public function privCheckFormat($p_level = 0)
+ {
+ $v_result = true;
+
+ // ----- Reset the file system cache
+ clearstatcache();
+
+ // ----- Reset the error handler
+ $this->privErrorReset();
+
+ // ----- Look if the file exits
+ if (!is_file($this->zipname)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "Missing archive file '" . $this->zipname . "'");
+
+ return (false);
+ }
+
+ // ----- Check that the file is readeable
+ if (!is_readable($this->zipname)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to read archive '" . $this->zipname . "'");
+
+ return (false);
+ }
+
+ // ----- Check the magic code
+ // TBC
+
+ // ----- Check the central header
+ // TBC
+
+ // ----- Check each file header
+ // TBC
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privParseOptions()
+ // Description :
+ // This internal methods reads the variable list of arguments ($p_options_list,
+ // $p_size) and generate an array with the options and values ($v_result_list).
+ // $v_requested_options contains the options that can be present and those that
+ // must be present.
+ // $v_requested_options is an array, with the option value as key, and 'optional',
+ // or 'mandatory' as value.
+ // Parameters :
+ // See above.
+ // Return Values :
+ // 1 on success.
+ // 0 on failure.
+ // --------------------------------------------------------------------------------
+ public function privParseOptions(&$p_options_list, $p_size, &$v_result_list, $v_requested_options = false)
+ {
+ $v_result = 1;
+
+ // ----- Read the options
+ $i = 0;
+ while ($i < $p_size) {
+
+ // ----- Check if the option is supported
+ if (!isset($v_requested_options[$p_options_list[$i]])) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid optional parameter '" . $p_options_list[$i] . "' for this method");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for next option
+ switch ($p_options_list[$i]) {
+ // ----- Look for options that request a path value
+ case PCLZIP_OPT_PATH:
+ case PCLZIP_OPT_REMOVE_PATH:
+ case PCLZIP_OPT_ADD_PATH:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i + 1], false);
+ $i++;
+ break;
+
+ case PCLZIP_OPT_TEMP_FILE_THRESHOLD:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Check the value
+ $v_value = $p_options_list[$i + 1];
+ if ((!is_integer($v_value)) || ($v_value < 0)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Integer expected for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value (and convert it in bytes)
+ $v_result_list[$p_options_list[$i]] = $v_value * 1048576;
+ $i++;
+ break;
+
+ case PCLZIP_OPT_TEMP_FILE_ON:
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_OFF])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_OFF'");
+
+ return PclZip::errorCode();
+ }
+
+ $v_result_list[$p_options_list[$i]] = true;
+ break;
+
+ case PCLZIP_OPT_TEMP_FILE_OFF:
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_ON])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_ON'");
+
+ return PclZip::errorCode();
+ }
+ // ----- Check for incompatible options
+ if (isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Option '" . PclZipUtilOptionText($p_options_list[$i]) . "' can not be used with option 'PCLZIP_OPT_TEMP_FILE_THRESHOLD'");
+
+ return PclZip::errorCode();
+ }
+
+ $v_result_list[$p_options_list[$i]] = true;
+ break;
+
+ case PCLZIP_OPT_EXTRACT_DIR_RESTRICTION:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1]) && ($p_options_list[$i + 1] != '')) {
+ $v_result_list[$p_options_list[$i]] = PclZipUtilTranslateWinPath($p_options_list[$i + 1], false);
+ $i++;
+ } else {
+ }
+ break;
+
+ // ----- Look for options that request an array of string for value
+ case PCLZIP_OPT_BY_NAME:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]][0] = $p_options_list[$i + 1];
+ } elseif (is_array($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $i++;
+ break;
+
+ // ----- Look for options that request an EREG or PREG expression
+ case PCLZIP_OPT_BY_EREG:
+ $p_options_list[$i] = PCLZIP_OPT_BY_PREG;
+ // ereg() is deprecated starting with PHP 5.3. Move PCLZIP_OPT_BY_EREG
+ // to PCLZIP_OPT_BY_PREG
+ case PCLZIP_OPT_BY_PREG:
+ //case PCLZIP_OPT_CRYPT :
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $i++;
+ break;
+
+ // ----- Look for options that takes a string
+ case PCLZIP_OPT_COMMENT:
+ case PCLZIP_OPT_ADD_COMMENT:
+ case PCLZIP_OPT_PREPEND_COMMENT:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ if (is_string($p_options_list[$i + 1])) {
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Wrong parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $i++;
+ break;
+
+ // ----- Look for options that request an array of index
+ case PCLZIP_OPT_BY_INDEX:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_work_list = array();
+ if (is_string($p_options_list[$i + 1])) {
+
+ // ----- Remove spaces
+ $p_options_list[$i + 1] = strtr($p_options_list[$i + 1], ' ', '');
+
+ // ----- Parse items
+ $v_work_list = explode(",", $p_options_list[$i + 1]);
+ } elseif (is_integer($p_options_list[$i + 1])) {
+ $v_work_list[0] = $p_options_list[$i + 1] . '-' . $p_options_list[$i + 1];
+ } elseif (is_array($p_options_list[$i + 1])) {
+ $v_work_list = $p_options_list[$i + 1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Value must be integer, string or array for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Reduce the index list
+ // each index item in the list must be a couple with a start and
+ // an end value : [0,3], [5-5], [8-10], ...
+ // ----- Check the format of each item
+ $v_sort_flag = false;
+ $v_sort_value = 0;
+ for ($j = 0; $j < sizeof($v_work_list); $j++) {
+ // ----- Explode the item
+ $v_item_list = explode("-", $v_work_list[$j]);
+ $v_size_item_list = sizeof($v_item_list);
+
+ // ----- TBC : Here we might check that each item is a
+ // real integer ...
+
+ // ----- Look for single value
+ if ($v_size_item_list == 1) {
+ // ----- Set the option value
+ $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
+ $v_result_list[$p_options_list[$i]][$j]['end'] = $v_item_list[0];
+ } elseif ($v_size_item_list == 2) {
+ // ----- Set the option value
+ $v_result_list[$p_options_list[$i]][$j]['start'] = $v_item_list[0];
+ $v_result_list[$p_options_list[$i]][$j]['end'] = $v_item_list[1];
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Too many values in index range for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for list sort
+ if ($v_result_list[$p_options_list[$i]][$j]['start'] < $v_sort_value) {
+ $v_sort_flag = true;
+
+ // ----- TBC : An automatic sort should be writen ...
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Invalid order of index range for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ $v_sort_value = $v_result_list[$p_options_list[$i]][$j]['start'];
+ }
+
+ // ----- Sort the items
+ if ($v_sort_flag) {
+ // TBC : To Be Completed
+ }
+
+ // ----- Next option
+ $i++;
+ break;
+
+ // ----- Look for options that request no value
+ case PCLZIP_OPT_REMOVE_ALL_PATH:
+ case PCLZIP_OPT_EXTRACT_AS_STRING:
+ case PCLZIP_OPT_NO_COMPRESSION:
+ case PCLZIP_OPT_EXTRACT_IN_OUTPUT:
+ case PCLZIP_OPT_REPLACE_NEWER:
+ case PCLZIP_OPT_STOP_ON_ERROR:
+ $v_result_list[$p_options_list[$i]] = true;
+ break;
+
+ // ----- Look for options that request an octal value
+ case PCLZIP_OPT_SET_CHMOD:
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_result_list[$p_options_list[$i]] = $p_options_list[$i + 1];
+ $i++;
+ break;
+
+ // ----- Look for options that request a call-back
+ case PCLZIP_CB_PRE_EXTRACT:
+ case PCLZIP_CB_POST_EXTRACT:
+ case PCLZIP_CB_PRE_ADD:
+ case PCLZIP_CB_POST_ADD:
+ /* for futur use
+ case PCLZIP_CB_PRE_DELETE :
+ case PCLZIP_CB_POST_DELETE :
+ case PCLZIP_CB_PRE_LIST :
+ case PCLZIP_CB_POST_LIST :
+ */
+ // ----- Check the number of parameters
+ if (($i + 1) >= $p_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_OPTION_VALUE, "Missing parameter value for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Get the value
+ $v_function_name = $p_options_list[$i + 1];
+
+ // ----- Check that the value is a valid existing function
+ if (!function_exists($v_function_name)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_OPTION_VALUE, "Function '" . $v_function_name . "()' is not an existing function for option '" . PclZipUtilOptionText($p_options_list[$i]) . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Set the attribute
+ $v_result_list[$p_options_list[$i]] = $v_function_name;
+ $i++;
+ break;
+
+ default:
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Unknown parameter '" . $p_options_list[$i] . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Next options
+ $i++;
+ }
+
+ // ----- Look for mandatory options
+ if ($v_requested_options !== false) {
+ for ($key = reset($v_requested_options); $key = key($v_requested_options); $key = next($v_requested_options)) {
+ // ----- Look for mandatory option
+ if ($v_requested_options[$key] == 'mandatory') {
+ // ----- Look if present
+ if (!isset($v_result_list[$key])) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter " . PclZipUtilOptionText($key) . "(" . $key . ")");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ }
+ }
+ }
+
+ // ----- Look for default values
+ if (!isset($v_result_list[PCLZIP_OPT_TEMP_FILE_THRESHOLD])) {
+
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privOptionDefaultThreshold()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privOptionDefaultThreshold(&$p_options)
+ {
+ $v_result = 1;
+
+ if (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) || isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) {
+ return $v_result;
+ }
+
+ // ----- Get 'memory_limit' configuration value
+ $v_memory_limit = ini_get('memory_limit');
+ $v_memory_limit = trim($v_memory_limit);
+ $last = strtolower(substr($v_memory_limit, -1));
+
+ if ($last == 'g') {
+ //$v_memory_limit = $v_memory_limit*1024*1024*1024;
+ $v_memory_limit = $v_memory_limit * 1073741824;
+ }
+ if ($last == 'm') {
+ //$v_memory_limit = $v_memory_limit*1024*1024;
+ $v_memory_limit = $v_memory_limit * 1048576;
+ }
+ if ($last == 'k') {
+ $v_memory_limit = $v_memory_limit * 1024;
+ }
+
+ $p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] = floor($v_memory_limit * PCLZIP_TEMPORARY_FILE_RATIO);
+
+ // ----- Sanity check : No threshold if value lower than 1M
+ if ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] < 1048576) {
+ unset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privFileDescrParseAtt()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // 1 on success.
+ // 0 on failure.
+ // --------------------------------------------------------------------------------
+ public function privFileDescrParseAtt(&$p_file_list, &$p_filedescr, $v_options, $v_requested_options = false)
+ {
+ $v_result = 1;
+
+ // ----- For each file in the list check the attributes
+ foreach ($p_file_list as $v_key => $v_value) {
+
+ // ----- Check if the option is supported
+ if (!isset($v_requested_options[$v_key])) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file attribute '" . $v_key . "' for this file");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for attribute
+ switch ($v_key) {
+ case PCLZIP_ATT_FILE_NAME:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['filename'] = PclZipUtilPathReduction($v_value);
+
+ if ($p_filedescr['filename'] == '') {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ break;
+
+ case PCLZIP_ATT_FILE_NEW_SHORT_NAME:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['new_short_name'] = PclZipUtilPathReduction($v_value);
+
+ if ($p_filedescr['new_short_name'] == '') {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty short filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+ break;
+
+ case PCLZIP_ATT_FILE_NEW_FULL_NAME:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['new_full_name'] = PclZipUtilPathReduction($v_value);
+
+ if ($p_filedescr['new_full_name'] == '') {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid empty full filename for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+ break;
+
+ // ----- Look for options that takes a string
+ case PCLZIP_ATT_FILE_COMMENT:
+ if (!is_string($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". String expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['comment'] = $v_value;
+ break;
+
+ case PCLZIP_ATT_FILE_MTIME:
+ if (!is_integer($v_value)) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE, "Invalid type " . gettype($v_value) . ". Integer expected for attribute '" . PclZipUtilOptionText($v_key) . "'");
+
+ return PclZip::errorCode();
+ }
+
+ $p_filedescr['mtime'] = $v_value;
+ break;
+
+ case PCLZIP_ATT_FILE_CONTENT:
+ $p_filedescr['content'] = $v_value;
+ break;
+
+ default:
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Unknown parameter '" . $v_key . "'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for mandatory options
+ if ($v_requested_options !== false) {
+ for ($key = reset($v_requested_options); $key = key($v_requested_options); $key = next($v_requested_options)) {
+ // ----- Look for mandatory option
+ if ($v_requested_options[$key] == 'mandatory') {
+ // ----- Look if present
+ if (!isset($p_file_list[$key])) {
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Missing mandatory parameter " . PclZipUtilOptionText($key) . "(" . $key . ")");
+
+ return PclZip::errorCode();
+ }
+ }
+ }
+ }
+
+ // end foreach
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privFileDescrExpand()
+ // Description :
+ // This method look for each item of the list to see if its a file, a folder
+ // or a string to be added as file. For any other type of files (link, other)
+ // just ignore the item.
+ // Then prepare the information that will be stored for that file.
+ // When its a folder, expand the folder with all the files that are in that
+ // folder (recursively).
+ // Parameters :
+ // Return Values :
+ // 1 on success.
+ // 0 on failure.
+ // --------------------------------------------------------------------------------
+ public function privFileDescrExpand(&$p_filedescr_list, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Create a result list
+ $v_result_list = array();
+
+ // ----- Look each entry
+ for ($i = 0; $i < sizeof($p_filedescr_list); $i++) {
+
+ // ----- Get filedescr
+ $v_descr = $p_filedescr_list[$i];
+
+ // ----- Reduce the filename
+ $v_descr['filename'] = PclZipUtilTranslateWinPath($v_descr['filename'], false);
+ $v_descr['filename'] = PclZipUtilPathReduction($v_descr['filename']);
+
+ // ----- Look for real file or folder
+ if (file_exists($v_descr['filename'])) {
+ if (@is_file($v_descr['filename'])) {
+ $v_descr['type'] = 'file';
+ } elseif (@is_dir($v_descr['filename'])) {
+ $v_descr['type'] = 'folder';
+ } elseif (@is_link($v_descr['filename'])) {
+ // skip
+ continue;
+ } else {
+ // skip
+ continue;
+ }
+
+ // ----- Look for string added as file
+ } elseif (isset($v_descr['content'])) {
+ $v_descr['type'] = 'virtual_file';
+
+ // ----- Missing file
+ } else {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '" . $v_descr['filename'] . "' does not exist");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Calculate the stored filename
+ $this->privCalculateStoredFilename($v_descr, $p_options);
+
+ // ----- Add the descriptor in result list
+ $v_result_list[sizeof($v_result_list)] = $v_descr;
+
+ // ----- Look for folder
+ if ($v_descr['type'] == 'folder') {
+ // ----- List of items in folder
+ $v_dirlist_descr = array();
+ $v_dirlist_nb = 0;
+ if ($v_folder_handler = @opendir($v_descr['filename'])) {
+ while (($v_item_handler = @readdir($v_folder_handler)) !== false) {
+
+ // ----- Skip '.' and '..'
+ if (($v_item_handler == '.') || ($v_item_handler == '..')) {
+ continue;
+ }
+
+ // ----- Compose the full filename
+ $v_dirlist_descr[$v_dirlist_nb]['filename'] = $v_descr['filename'] . '/' . $v_item_handler;
+
+ // ----- Look for different stored filename
+ // Because the name of the folder was changed, the name of the
+ // files/sub-folders also change
+ if (($v_descr['stored_filename'] != $v_descr['filename']) && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) {
+ if ($v_descr['stored_filename'] != '') {
+ $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_descr['stored_filename'] . '/' . $v_item_handler;
+ } else {
+ $v_dirlist_descr[$v_dirlist_nb]['new_full_name'] = $v_item_handler;
+ }
+ }
+
+ $v_dirlist_nb++;
+ }
+
+ @closedir($v_folder_handler);
+ } else {
+ // TBC : unable to open folder in read mode
+ }
+
+ // ----- Expand each element of the list
+ if ($v_dirlist_nb != 0) {
+ // ----- Expand
+ if (($v_result = $this->privFileDescrExpand($v_dirlist_descr, $p_options)) != 1) {
+ return $v_result;
+ }
+
+ // ----- Concat the resulting list
+ $v_result_list = array_merge($v_result_list, $v_dirlist_descr);
+ } else {
+ }
+
+ // ----- Free local array
+ unset($v_dirlist_descr);
+ }
+ }
+
+ // ----- Get the result list
+ $p_filedescr_list = $v_result_list;
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCreate()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privCreate($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_list_detail = array();
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Open the file in write mode
+ if (($v_result = $this->privOpenFd('wb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Add the list of files
+ $v_result = $this->privAddList($p_filedescr_list, $p_result_list, $p_options);
+
+ // ----- Close
+ $this->privCloseFd();
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAdd()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAdd($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_list_detail = array();
+
+ // ----- Look if the archive exists or is empty
+ if ((!is_file($this->zipname)) || (filesize($this->zipname) == 0)) {
+
+ // ----- Do a create
+ $v_result = $this->privCreate($p_filedescr_list, $p_result_list, $p_options);
+
+ // ----- Return
+ return $v_result;
+ }
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($this->zip_fd);
+
+ // ----- Creates a temporay file
+ $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.tmp';
+
+ // ----- Open the temporary file in write mode
+ if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_zip_temp_name . '\' in binary write mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Copy the files from the archive to the temporary file
+ // TBC : Here I should better append the file and go back to erase the central dir
+ $v_size = $v_central_dir['offset'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($this->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Swap the file descriptor
+ // Here is a trick : I swap the temporary fd with the zip fd, in order to use
+ // the following methods on the temporary fil and not the real archive
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Add the files
+ $v_header_list = array();
+ if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) {
+ fclose($v_zip_temp_fd);
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($this->zip_fd);
+
+ // ----- Copy the block of file headers from the old archive
+ $v_size = $v_central_dir['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($v_zip_temp_fd, $v_read_size);
+ @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Create the Central Dir files header
+ for ($i = 0, $v_count = 0; $i < sizeof($v_header_list); $i++) {
+ // ----- Create the file header
+ if ($v_header_list[$i]['status'] == 'ok') {
+ if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+ fclose($v_zip_temp_fd);
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ $v_count++;
+ }
+
+ // ----- Transform the header to a 'usable' info
+ $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+ }
+
+ // ----- Zip file comment
+ $v_comment = $v_central_dir['comment'];
+ if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+ }
+ if (isset($p_options[PCLZIP_OPT_ADD_COMMENT])) {
+ $v_comment = $v_comment . $p_options[PCLZIP_OPT_ADD_COMMENT];
+ }
+ if (isset($p_options[PCLZIP_OPT_PREPEND_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_PREPEND_COMMENT] . $v_comment;
+ }
+
+ // ----- Calculate the size of the central header
+ $v_size = @ftell($this->zip_fd) - $v_offset;
+
+ // ----- Create the central dir footer
+ if (($v_result = $this->privWriteCentralHeader($v_count + $v_central_dir['entries'], $v_size, $v_offset, $v_comment)) != 1) {
+ // ----- Reset the file list
+ unset($v_header_list);
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Swap back the file descriptor
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Close
+ $this->privCloseFd();
+
+ // ----- Close the temporary file
+ @fclose($v_zip_temp_fd);
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Delete the zip file
+ // TBC : I should test the result ...
+ @unlink($this->zipname);
+
+ // ----- Rename the temporary file
+ // TBC : I should test the result ...
+ //@rename($v_zip_temp_name, $this->zipname);
+ PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privOpenFd()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privOpenFd($p_mode)
+ {
+ $v_result = 1;
+
+ // ----- Look if already open
+ if ($this->zip_fd != 0) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Zip file \'' . $this->zipname . '\' already open');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Open the zip file
+ if (($this->zip_fd = @fopen($this->zipname, $p_mode)) == 0) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in ' . $p_mode . ' mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCloseFd()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privCloseFd()
+ {
+ $v_result = 1;
+
+ if ($this->zip_fd != 0) {
+ @fclose($this->zip_fd);
+ }
+ $this->zip_fd = 0;
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddList()
+ // Description :
+ // $p_add_dir and $p_remove_dir will give the ability to memorize a path which is
+ // different from the real path of the file. This is usefull if you want to have PclTar
+ // running in any directory, and memorize relative path from an other directory.
+ // Parameters :
+ // $p_list : An array containing the file or directory names to add in the tar
+ // $p_result_list : list of added files with their properties (specially the status field)
+ // $p_add_dir : Path to add in the filename path archived
+ // $p_remove_dir : Path to remove in the filename path archived
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ // function privAddList($p_list, &$p_result_list, $p_add_dir, $p_remove_dir, $p_remove_all_dir, &$p_options)
+ public function privAddList($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Add the files
+ $v_header_list = array();
+ if (($v_result = $this->privAddFileList($p_filedescr_list, $v_header_list, $p_options)) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($this->zip_fd);
+
+ // ----- Create the Central Dir files header
+ for ($i = 0, $v_count = 0; $i < sizeof($v_header_list); $i++) {
+ // ----- Create the file header
+ if ($v_header_list[$i]['status'] == 'ok') {
+ if (($v_result = $this->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+ $v_count++;
+ }
+
+ // ----- Transform the header to a 'usable' info
+ $this->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+ }
+
+ // ----- Zip file comment
+ $v_comment = '';
+ if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+ }
+
+ // ----- Calculate the size of the central header
+ $v_size = @ftell($this->zip_fd) - $v_offset;
+
+ // ----- Create the central dir footer
+ if (($v_result = $this->privWriteCentralHeader($v_count, $v_size, $v_offset, $v_comment)) != 1) {
+ // ----- Reset the file list
+ unset($v_header_list);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddFileList()
+ // Description :
+ // Parameters :
+ // $p_filedescr_list : An array containing the file description
+ // or directory names to add in the zip
+ // $p_result_list : list of added files with their properties (specially the status field)
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAddFileList($p_filedescr_list, &$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_header = array();
+
+ // ----- Recuperate the current number of elt in list
+ $v_nb = sizeof($p_result_list);
+
+ // ----- Loop on the files
+ for ($j = 0; ($j < sizeof($p_filedescr_list)) && ($v_result == 1); $j++) {
+ // ----- Format the filename
+ $p_filedescr_list[$j]['filename'] = PclZipUtilTranslateWinPath($p_filedescr_list[$j]['filename'], false);
+
+ // ----- Skip empty file names
+ // TBC : Can this be possible ? not checked in DescrParseAtt ?
+ if ($p_filedescr_list[$j]['filename'] == "") {
+ continue;
+ }
+
+ // ----- Check the filename
+ if (($p_filedescr_list[$j]['type'] != 'virtual_file') && (!file_exists($p_filedescr_list[$j]['filename']))) {
+ PclZip::privErrorLog(PCLZIP_ERR_MISSING_FILE, "File '" . $p_filedescr_list[$j]['filename'] . "' does not exist");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look if it is a file or a dir with no all path remove option
+ // or a dir with all its path removed
+ // if ( (is_file($p_filedescr_list[$j]['filename']))
+ // || ( is_dir($p_filedescr_list[$j]['filename'])
+ if (($p_filedescr_list[$j]['type'] == 'file') || ($p_filedescr_list[$j]['type'] == 'virtual_file') || (($p_filedescr_list[$j]['type'] == 'folder') && (!isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH]) || !$p_options[PCLZIP_OPT_REMOVE_ALL_PATH]))) {
+
+ // ----- Add the file
+ $v_result = $this->privAddFile($p_filedescr_list[$j], $v_header, $p_options);
+ if ($v_result != 1) {
+ return $v_result;
+ }
+
+ // ----- Store the file infos
+ $p_result_list[$v_nb++] = $v_header;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAddFile($p_filedescr, &$p_header, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Working variable
+ $p_filename = $p_filedescr['filename'];
+
+ // TBC : Already done in the fileAtt check ... ?
+ if ($p_filename == "") {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_PARAMETER, "Invalid file list parameter (invalid or empty list)");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for a stored different filename
+ /* TBC : Removed
+ if (isset($p_filedescr['stored_filename'])) {
+ $v_stored_filename = $p_filedescr['stored_filename'];
+ } else {
+ $v_stored_filename = $p_filedescr['stored_filename'];
+ }
+ */
+
+ // ----- Set the file properties
+ clearstatcache();
+ $p_header['version'] = 20;
+ $p_header['version_extracted'] = 10;
+ $p_header['flag'] = 0;
+ $p_header['compression'] = 0;
+ $p_header['crc'] = 0;
+ $p_header['compressed_size'] = 0;
+ $p_header['filename_len'] = strlen($p_filename);
+ $p_header['extra_len'] = 0;
+ $p_header['disk'] = 0;
+ $p_header['internal'] = 0;
+ $p_header['offset'] = 0;
+ $p_header['filename'] = $p_filename;
+ // TBC : Removed $p_header['stored_filename'] = $v_stored_filename;
+ $p_header['stored_filename'] = $p_filedescr['stored_filename'];
+ $p_header['extra'] = '';
+ $p_header['status'] = 'ok';
+ $p_header['index'] = -1;
+
+ // ----- Look for regular file
+ if ($p_filedescr['type'] == 'file') {
+ $p_header['external'] = 0x00000000;
+ $p_header['size'] = filesize($p_filename);
+
+ // ----- Look for regular folder
+ } elseif ($p_filedescr['type'] == 'folder') {
+ $p_header['external'] = 0x00000010;
+ $p_header['mtime'] = filemtime($p_filename);
+ $p_header['size'] = filesize($p_filename);
+
+ // ----- Look for virtual file
+ } elseif ($p_filedescr['type'] == 'virtual_file') {
+ $p_header['external'] = 0x00000000;
+ $p_header['size'] = strlen($p_filedescr['content']);
+ }
+
+ // ----- Look for filetime
+ if (isset($p_filedescr['mtime'])) {
+ $p_header['mtime'] = $p_filedescr['mtime'];
+ } elseif ($p_filedescr['type'] == 'virtual_file') {
+ $p_header['mtime'] = time();
+ } else {
+ $p_header['mtime'] = filemtime($p_filename);
+ }
+
+ // ------ Look for file comment
+ if (isset($p_filedescr['comment'])) {
+ $p_header['comment_len'] = strlen($p_filedescr['comment']);
+ $p_header['comment'] = $p_filedescr['comment'];
+ } else {
+ $p_header['comment_len'] = 0;
+ $p_header['comment'] = '';
+ }
+
+ // ----- Look for pre-add callback
+ if (isset($p_options[PCLZIP_CB_PRE_ADD])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_header, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_ADD].'(PCLZIP_CB_PRE_ADD, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_ADD](PCLZIP_CB_PRE_ADD, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_header['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ if ($p_header['stored_filename'] != $v_local_header['stored_filename']) {
+ $p_header['stored_filename'] = PclZipUtilPathReduction($v_local_header['stored_filename']);
+ }
+ }
+
+ // ----- Look for empty stored filename
+ if ($p_header['stored_filename'] == "") {
+ $p_header['status'] = "filtered";
+ }
+
+ // ----- Check the path length
+ if (strlen($p_header['stored_filename']) > 0xFF) {
+ $p_header['status'] = 'filename_too_long';
+ }
+
+ // ----- Look if no error, or file not skipped
+ if ($p_header['status'] == 'ok') {
+
+ // ----- Look for a file
+ if ($p_filedescr['type'] == 'file') {
+ // ----- Look for using temporary file to zip
+ if ((!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_header['size'])))) {
+ $v_result = $this->privAddFileUsingTempFile($p_filedescr, $p_header, $p_options);
+ if ($v_result < PCLZIP_ERR_NO_ERROR) {
+ return $v_result;
+ }
+
+ // ----- Use "in memory" zip algo
+ } else {
+
+ // ----- Open the source file
+ if (($v_file = @fopen($p_filename, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file content
+ $v_content = @fread($v_file, $p_header['size']);
+
+ // ----- Close the file
+ @fclose($v_file);
+
+ // ----- Calculate the CRC
+ $p_header['crc'] = @crc32($v_content);
+
+ // ----- Look for no compression
+ if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
+ // ----- Set header parameters
+ $p_header['compressed_size'] = $p_header['size'];
+ $p_header['compression'] = 0;
+
+ // ----- Look for normal compression
+ } else {
+ // ----- Compress the content
+ $v_content = @gzdeflate($v_content);
+
+ // ----- Set header parameters
+ $p_header['compressed_size'] = strlen($v_content);
+ $p_header['compression'] = 8;
+ }
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ @fclose($v_file);
+
+ return $v_result;
+ }
+
+ // ----- Write the compressed (or not) content
+ @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);
+
+ }
+
+ // ----- Look for a virtual file (a file from string)
+ } elseif ($p_filedescr['type'] == 'virtual_file') {
+
+ $v_content = $p_filedescr['content'];
+
+ // ----- Calculate the CRC
+ $p_header['crc'] = @crc32($v_content);
+
+ // ----- Look for no compression
+ if ($p_options[PCLZIP_OPT_NO_COMPRESSION]) {
+ // ----- Set header parameters
+ $p_header['compressed_size'] = $p_header['size'];
+ $p_header['compression'] = 0;
+
+ // ----- Look for normal compression
+ } else {
+ // ----- Compress the content
+ $v_content = @gzdeflate($v_content);
+
+ // ----- Set header parameters
+ $p_header['compressed_size'] = strlen($v_content);
+ $p_header['compression'] = 8;
+ }
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ @fclose($v_file);
+
+ return $v_result;
+ }
+
+ // ----- Write the compressed (or not) content
+ @fwrite($this->zip_fd, $v_content, $p_header['compressed_size']);
+
+ // ----- Look for a directory
+ } elseif ($p_filedescr['type'] == 'folder') {
+ // ----- Look for directory last '/'
+ if (@substr($p_header['stored_filename'], -1) != '/') {
+ $p_header['stored_filename'] .= '/';
+ }
+
+ // ----- Set the file properties
+ $p_header['size'] = 0;
+ //$p_header['external'] = 0x41FF0010; // Value for a folder : to be checked
+ $p_header['external'] = 0x00000010; // Value for a folder : to be checked
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ return $v_result;
+ }
+ }
+ }
+
+ // ----- Look for post-add callback
+ if (isset($p_options[PCLZIP_CB_POST_ADD])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_header, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_ADD].'(PCLZIP_CB_POST_ADD, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_ADD](PCLZIP_CB_POST_ADD, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Ignored
+ $v_result = 1;
+ }
+
+ // ----- Update the informations
+ // Nothing can be modified
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privAddFileUsingTempFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privAddFileUsingTempFile($p_filedescr, &$p_header, &$p_options)
+ {
+ $v_result = PCLZIP_ERR_NO_ERROR;
+
+ // ----- Working variable
+ $p_filename = $p_filedescr['filename'];
+
+ // ----- Open the source file
+ if (($v_file = @fopen($p_filename, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, "Unable to open file '$p_filename' in binary read mode");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Creates a compressed temporary file
+ $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.gz';
+ if (($v_file_compressed = @gzopen($v_gzip_temp_name, "wb")) == 0) {
+ fclose($v_file);
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary write mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = filesize($p_filename);
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($v_file, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @gzputs($v_file_compressed, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Close the file
+ @fclose($v_file);
+ @gzclose($v_file_compressed);
+
+ // ----- Check the minimum file size
+ if (filesize($v_gzip_temp_name) < 18) {
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'gzip temporary file \'' . $v_gzip_temp_name . '\' has invalid filesize - should be minimum 18 bytes');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the compressed attributes
+ if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the gzip file header
+ $v_binary_data = @fread($v_file_compressed, 10);
+ $v_data_header = unpack('a1id1/a1id2/a1cm/a1flag/Vmtime/a1xfl/a1os', $v_binary_data);
+
+ // ----- Check some parameters
+ $v_data_header['os'] = bin2hex($v_data_header['os']);
+
+ // ----- Read the gzip file footer
+ @fseek($v_file_compressed, filesize($v_gzip_temp_name) - 8);
+ $v_binary_data = @fread($v_file_compressed, 8);
+ $v_data_footer = unpack('Vcrc/Vcompressed_size', $v_binary_data);
+
+ // ----- Set the attributes
+ $p_header['compression'] = ord($v_data_header['cm']);
+ //$p_header['mtime'] = $v_data_header['mtime'];
+ $p_header['crc'] = $v_data_footer['crc'];
+ $p_header['compressed_size'] = filesize($v_gzip_temp_name) - 18;
+
+ // ----- Close the file
+ @fclose($v_file_compressed);
+
+ // ----- Call the header generation
+ if (($v_result = $this->privWriteFileHeader($p_header)) != 1) {
+ return $v_result;
+ }
+
+ // ----- Add the compressed data
+ if (($v_file_compressed = @fopen($v_gzip_temp_name, "rb")) == 0) {
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ fseek($v_file_compressed, 10);
+ $v_size = $p_header['compressed_size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($v_file_compressed, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Close the file
+ @fclose($v_file_compressed);
+
+ // ----- Unlink the temporary file
+ @unlink($v_gzip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCalculateStoredFilename()
+ // Description :
+ // Based on file descriptor properties and global options, this method
+ // calculate the filename that will be stored in the archive.
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privCalculateStoredFilename(&$p_filedescr, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Working variables
+ $p_filename = $p_filedescr['filename'];
+ if (isset($p_options[PCLZIP_OPT_ADD_PATH])) {
+ $p_add_dir = $p_options[PCLZIP_OPT_ADD_PATH];
+ } else {
+ $p_add_dir = '';
+ }
+ if (isset($p_options[PCLZIP_OPT_REMOVE_PATH])) {
+ $p_remove_dir = $p_options[PCLZIP_OPT_REMOVE_PATH];
+ } else {
+ $p_remove_dir = '';
+ }
+ if (isset($p_options[PCLZIP_OPT_REMOVE_ALL_PATH])) {
+ $p_remove_all_dir = $p_options[PCLZIP_OPT_REMOVE_ALL_PATH];
+ } else {
+ $p_remove_all_dir = 0;
+ }
+
+ // ----- Look for full name change
+ if (isset($p_filedescr['new_full_name'])) {
+ // ----- Remove drive letter if any
+ $v_stored_filename = PclZipUtilTranslateWinPath($p_filedescr['new_full_name']);
+
+ // ----- Look for path and/or short name change
+ } else {
+
+ // ----- Look for short name change
+ // Its when we cahnge just the filename but not the path
+ if (isset($p_filedescr['new_short_name'])) {
+ $v_path_info = pathinfo($p_filename);
+ $v_dir = '';
+ if ($v_path_info['dirname'] != '') {
+ $v_dir = $v_path_info['dirname'] . '/';
+ }
+ $v_stored_filename = $v_dir . $p_filedescr['new_short_name'];
+ } else {
+ // ----- Calculate the stored filename
+ $v_stored_filename = $p_filename;
+ }
+
+ // ----- Look for all path to remove
+ if ($p_remove_all_dir) {
+ $v_stored_filename = basename($p_filename);
+
+ // ----- Look for partial path remove
+ } elseif ($p_remove_dir != "") {
+ if (substr($p_remove_dir, -1) != '/') {
+ $p_remove_dir .= "/";
+ }
+
+ if ((substr($p_filename, 0, 2) == "./") || (substr($p_remove_dir, 0, 2) == "./")) {
+
+ if ((substr($p_filename, 0, 2) == "./") && (substr($p_remove_dir, 0, 2) != "./")) {
+ $p_remove_dir = "./" . $p_remove_dir;
+ }
+ if ((substr($p_filename, 0, 2) != "./") && (substr($p_remove_dir, 0, 2) == "./")) {
+ $p_remove_dir = substr($p_remove_dir, 2);
+ }
+ }
+
+ $v_compare = PclZipUtilPathInclusion($p_remove_dir, $v_stored_filename);
+ if ($v_compare > 0) {
+ if ($v_compare == 2) {
+ $v_stored_filename = "";
+ } else {
+ $v_stored_filename = substr($v_stored_filename, strlen($p_remove_dir));
+ }
+ }
+ }
+
+ // ----- Remove drive letter if any
+ $v_stored_filename = PclZipUtilTranslateWinPath($v_stored_filename);
+
+ // ----- Look for path to add
+ if ($p_add_dir != "") {
+ if (substr($p_add_dir, -1) == "/") {
+ $v_stored_filename = $p_add_dir . $v_stored_filename;
+ } else {
+ $v_stored_filename = $p_add_dir . "/" . $v_stored_filename;
+ }
+ }
+ }
+
+ // ----- Filename (reduce the path of stored name)
+ $v_stored_filename = PclZipUtilPathReduction($v_stored_filename);
+ $p_filedescr['stored_filename'] = $v_stored_filename;
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privWriteFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privWriteFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // ----- Store the offset position of the file
+ $p_header['offset'] = ftell($this->zip_fd);
+
+ // ----- Transform UNIX mtime to DOS format mdate/mtime
+ $v_date = getdate($p_header['mtime']);
+ $v_mtime = ($v_date['hours'] << 11) + ($v_date['minutes'] << 5) + $v_date['seconds'] / 2;
+ $v_mdate = (($v_date['year'] - 1980) << 9) + ($v_date['mon'] << 5) + $v_date['mday'];
+
+ // ----- Packed data
+ $v_binary_data = pack("VvvvvvVVVvv", 0x04034b50, $p_header['version_extracted'], $p_header['flag'], $p_header['compression'], $v_mtime, $v_mdate, $p_header['crc'], $p_header['compressed_size'], $p_header['size'], strlen($p_header['stored_filename']), $p_header['extra_len']);
+
+ // ----- Write the first 148 bytes of the header in the archive
+ fputs($this->zip_fd, $v_binary_data, 30);
+
+ // ----- Write the variable fields
+ if (strlen($p_header['stored_filename']) != 0) {
+ fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
+ }
+ if ($p_header['extra_len'] != 0) {
+ fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privWriteCentralFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privWriteCentralFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // TBC
+ //for (reset($p_header); $key = key($p_header); next($p_header)) {
+ //}
+
+ // ----- Transform UNIX mtime to DOS format mdate/mtime
+ $v_date = getdate($p_header['mtime']);
+ $v_mtime = ($v_date['hours'] << 11) + ($v_date['minutes'] << 5) + $v_date['seconds'] / 2;
+ $v_mdate = (($v_date['year'] - 1980) << 9) + ($v_date['mon'] << 5) + $v_date['mday'];
+
+ // ----- Packed data
+ $v_binary_data = pack("VvvvvvvVVVvvvvvVV", 0x02014b50, $p_header['version'], $p_header['version_extracted'], $p_header['flag'], $p_header['compression'], $v_mtime, $v_mdate, $p_header['crc'], $p_header['compressed_size'], $p_header['size'], strlen($p_header['stored_filename']), $p_header['extra_len'], $p_header['comment_len'], $p_header['disk'], $p_header['internal'], $p_header['external'], $p_header['offset']);
+
+ // ----- Write the 42 bytes of the header in the zip file
+ fputs($this->zip_fd, $v_binary_data, 46);
+
+ // ----- Write the variable fields
+ if (strlen($p_header['stored_filename']) != 0) {
+ fputs($this->zip_fd, $p_header['stored_filename'], strlen($p_header['stored_filename']));
+ }
+ if ($p_header['extra_len'] != 0) {
+ fputs($this->zip_fd, $p_header['extra'], $p_header['extra_len']);
+ }
+ if ($p_header['comment_len'] != 0) {
+ fputs($this->zip_fd, $p_header['comment'], $p_header['comment_len']);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privWriteCentralHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privWriteCentralHeader($p_nb_entries, $p_size, $p_offset, $p_comment)
+ {
+ $v_result = 1;
+
+ // ----- Packed data
+ $v_binary_data = pack("VvvvvVVv", 0x06054b50, 0, 0, $p_nb_entries, $p_nb_entries, $p_size, $p_offset, strlen($p_comment));
+
+ // ----- Write the 22 bytes of the header in the zip file
+ fputs($this->zip_fd, $v_binary_data, 22);
+
+ // ----- Write the variable fields
+ if (strlen($p_comment) != 0) {
+ fputs($this->zip_fd, $p_comment, strlen($p_comment));
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privList()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privList(&$p_list)
+ {
+ $v_result = 1;
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Open the zip file
+ if (($this->zip_fd = @fopen($this->zipname, 'rb')) == 0) {
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive \'' . $this->zipname . '\' in binary read mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of Central Dir
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_central_dir['offset'])) {
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read each entry
+ for ($i = 0; $i < $v_central_dir['entries']; $i++) {
+ // ----- Read the file header
+ if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+ $v_header['index'] = $i;
+
+ // ----- Get the only interesting attributes
+ $this->privConvertHeader2FileInfo($v_header, $p_list[$i]);
+ unset($v_header);
+ }
+
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ // ----- Magic quotes trick
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privConvertHeader2FileInfo()
+ // Description :
+ // This function takes the file informations from the central directory
+ // entries and extract the interesting parameters that will be given back.
+ // The resulting file infos are set in the array $p_info
+ // $p_info['filename'] : Filename with full path. Given by user (add),
+ // extracted in the filesystem (extract).
+ // $p_info['stored_filename'] : Stored filename in the archive.
+ // $p_info['size'] = Size of the file.
+ // $p_info['compressed_size'] = Compressed size of the file.
+ // $p_info['mtime'] = Last modification date of the file.
+ // $p_info['comment'] = Comment associated with the file.
+ // $p_info['folder'] = true/false : indicates if the entry is a folder or not.
+ // $p_info['status'] = status of the action on the file.
+ // $p_info['crc'] = CRC of the file content.
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privConvertHeader2FileInfo($p_header, &$p_info)
+ {
+ $v_result = 1;
+
+ // ----- Get the interesting attributes
+ $v_temp_path = PclZipUtilPathReduction($p_header['filename']);
+ $p_info['filename'] = $v_temp_path;
+ $v_temp_path = PclZipUtilPathReduction($p_header['stored_filename']);
+ $p_info['stored_filename'] = $v_temp_path;
+ $p_info['size'] = $p_header['size'];
+ $p_info['compressed_size'] = $p_header['compressed_size'];
+ $p_info['mtime'] = $p_header['mtime'];
+ $p_info['comment'] = $p_header['comment'];
+ $p_info['folder'] = (($p_header['external'] & 0x00000010) == 0x00000010);
+ $p_info['index'] = $p_header['index'];
+ $p_info['status'] = $p_header['status'];
+ $p_info['crc'] = $p_header['crc'];
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractByRule()
+ // Description :
+ // Extract a file or directory depending of rules (by index, by name, ...)
+ // Parameters :
+ // $p_file_list : An array where will be placed the properties of each
+ // extracted file
+ // $p_path : Path to add while writing the extracted files
+ // $p_remove_path : Path to remove (from the file memorized path) while writing the
+ // extracted files. If the path does not match the file path,
+ // the file is extracted with its memorized path.
+ // $p_remove_path does not apply to 'list' mode.
+ // $p_path and $p_remove_path are commulative.
+ // Return Values :
+ // 1 on success,0 or less on error (see error code list)
+ // --------------------------------------------------------------------------------
+ public function privExtractByRule(&$p_file_list, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Magic quotes trick
+ $this->privDisableMagicQuotes();
+
+ // ----- Check the path
+ if (($p_path == "") || ((substr($p_path, 0, 1) != "/") && (substr($p_path, 0, 3) != "../") && (substr($p_path, 1, 2) != ":/"))) {
+ $p_path = "./" . $p_path;
+ }
+
+ // ----- Reduce the path last (and duplicated) '/'
+ if (($p_path != "./") && ($p_path != "/")) {
+ // ----- Look for the path end '/'
+ while (substr($p_path, -1) == "/") {
+ $p_path = substr($p_path, 0, strlen($p_path) - 1);
+ }
+ }
+
+ // ----- Look for path to remove format (should end by /)
+ if (($p_remove_path != "") && (substr($p_remove_path, -1) != '/')) {
+ $p_remove_path .= '/';
+ }
+ $p_remove_path_size = strlen($p_remove_path);
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Start at beginning of Central Dir
+ $v_pos_entry = $v_central_dir['offset'];
+
+ // ----- Read each entry
+ $j_start = 0;
+ for ($i = 0, $v_nb_extracted = 0; $i < $v_central_dir['entries']; $i++) {
+
+ // ----- Read next Central dir entry
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_pos_entry)) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file header
+ $v_header = array();
+ if (($v_result = $this->privReadCentralFileHeader($v_header)) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Store the index
+ $v_header['index'] = $i;
+
+ // ----- Store the file position
+ $v_pos_entry = ftell($this->zip_fd);
+
+ // ----- Look for the specific extract rules
+ $v_extract = false;
+
+ // ----- Look for extract by name rule
+ if ((isset($p_options[PCLZIP_OPT_BY_NAME])) && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {
+
+ // ----- Look if the filename is in the list
+ for ($j = 0; ($j < sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_extract); $j++) {
+
+ // ----- Look for a directory
+ if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {
+
+ // ----- Look if the directory is in the filename path
+ if ((strlen($v_header['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) && (substr($v_header['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+ $v_extract = true;
+ }
+
+ // ----- Look for a filename
+ } elseif ($v_header['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
+ $v_extract = true;
+ }
+ }
+ // ----- Look for extract by ereg rule
+ // ereg() is deprecated with PHP 5.3
+ /*
+ elseif ( (isset($p_options[PCLZIP_OPT_BY_EREG]))
+ && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {
+
+ if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header['stored_filename'])) {
+ $v_extract = true;
+ }
+ }
+ */
+
+ // ----- Look for extract by preg rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_PREG])) && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {
+
+ if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header['stored_filename'])) {
+ $v_extract = true;
+ }
+
+ // ----- Look for extract by index rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_INDEX])) && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {
+
+ // ----- Look if the index is in the list
+ for ($j = $j_start; ($j < sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_extract); $j++) {
+
+ if (($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i <= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
+ $v_extract = true;
+ }
+ if ($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
+ $j_start = $j + 1;
+ }
+
+ if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start'] > $i) {
+ break;
+ }
+ }
+
+ // ----- Look for no rule, which means extract all the archive
+ } else {
+ $v_extract = true;
+ }
+
+ // ----- Check compression method
+ if (($v_extract) && (($v_header['compression'] != 8) && ($v_header['compression'] != 0))) {
+ $v_header['status'] = 'unsupported_compression';
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ $this->privSwapBackMagicQuotes();
+
+ PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_COMPRESSION, "Filename '" . $v_header['stored_filename'] . "' is " . "compressed by an unsupported compression " . "method (" . $v_header['compression'] . ") ");
+
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Check encrypted files
+ if (($v_extract) && (($v_header['flag'] & 1) == 1)) {
+ $v_header['status'] = 'unsupported_encryption';
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ $this->privSwapBackMagicQuotes();
+
+ PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, "Unsupported encryption for " . " filename '" . $v_header['stored_filename'] . "'");
+
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Look for real extraction
+ if (($v_extract) && ($v_header['status'] != 'ok')) {
+ $v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++]);
+ if ($v_result != 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ $v_extract = false;
+ }
+
+ // ----- Look for real extraction
+ if ($v_extract) {
+
+ // ----- Go to the file position
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_header['offset'])) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for extraction as string
+ if ($p_options[PCLZIP_OPT_EXTRACT_AS_STRING]) {
+
+ $v_string = '';
+
+ // ----- Extracting the file
+ $v_result1 = $this->privExtractFileAsString($v_header, $v_string, $p_options);
+ if ($v_result1 < 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result1;
+ }
+
+ // ----- Get the only interesting attributes
+ if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Set the file content
+ $p_file_list[$v_nb_extracted]['content'] = $v_string;
+
+ // ----- Next extracted file
+ $v_nb_extracted++;
+
+ // ----- Look for user callback abort
+ if ($v_result1 == 2) {
+ break;
+ }
+
+ // ----- Look for extraction in standard output
+ } elseif ((isset($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) && ($p_options[PCLZIP_OPT_EXTRACT_IN_OUTPUT])) {
+ // ----- Extracting the file in standard output
+ $v_result1 = $this->privExtractFileInOutput($v_header, $p_options);
+ if ($v_result1 < 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result1;
+ }
+
+ // ----- Get the only interesting attributes
+ if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Look for user callback abort
+ if ($v_result1 == 2) {
+ break;
+ }
+
+ // ----- Look for normal extraction
+ } else {
+ // ----- Extracting the file
+ $v_result1 = $this->privExtractFile($v_header, $p_path, $p_remove_path, $p_remove_all_path, $p_options);
+ if ($v_result1 < 1) {
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result1;
+ }
+
+ // ----- Get the only interesting attributes
+ if (($v_result = $this->privConvertHeader2FileInfo($v_header, $p_file_list[$v_nb_extracted++])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ return $v_result;
+ }
+
+ // ----- Look for user callback abort
+ if ($v_result1 == 2) {
+ break;
+ }
+ }
+ }
+ }
+
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $this->privSwapBackMagicQuotes();
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ //
+ // 1 : ... ?
+ // PCLZIP_ERR_USER_ABORTED(2) : User ask for extraction stop in callback
+ // --------------------------------------------------------------------------------
+ public function privExtractFile(&$p_entry, $p_path, $p_remove_path, $p_remove_all_path, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Read the file header
+ if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Check that the file header is coherent with $p_entry info
+ if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+ // TBC
+ }
+
+ // ----- Look for all path to remove
+ if ($p_remove_all_path == true) {
+ // ----- Look for folder entry that not need to be extracted
+ if (($p_entry['external'] & 0x00000010) == 0x00000010) {
+
+ $p_entry['status'] = "filtered";
+
+ return $v_result;
+ }
+
+ // ----- Get the basename of the path
+ $p_entry['filename'] = basename($p_entry['filename']);
+
+ // ----- Look for path to remove
+ } elseif ($p_remove_path != "") {
+ if (PclZipUtilPathInclusion($p_remove_path, $p_entry['filename']) == 2) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "filtered";
+
+ // ----- Return
+ return $v_result;
+ }
+
+ $p_remove_path_size = strlen($p_remove_path);
+ if (substr($p_entry['filename'], 0, $p_remove_path_size) == $p_remove_path) {
+
+ // ----- Remove the path
+ $p_entry['filename'] = substr($p_entry['filename'], $p_remove_path_size);
+
+ }
+ }
+
+ // ----- Add the path
+ if ($p_path != '') {
+ $p_entry['filename'] = $p_path . "/" . $p_entry['filename'];
+ }
+
+ // ----- Check a base_dir_restriction
+ if (isset($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION])) {
+ $v_inclusion = PclZipUtilPathInclusion($p_options[PCLZIP_OPT_EXTRACT_DIR_RESTRICTION], $p_entry['filename']);
+ if ($v_inclusion == 0) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_DIRECTORY_RESTRICTION, "Filename '" . $p_entry['filename'] . "' is " . "outside PCLZIP_OPT_EXTRACT_DIR_RESTRICTION");
+
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Look for pre-extract callback
+ if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_entry['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ // ----- This status is internal and will be changed in 'skipped'
+ $p_entry['status'] = "aborted";
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ $p_entry['filename'] = $v_local_header['filename'];
+ }
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Look for specific actions while the file exist
+ if (file_exists($p_entry['filename'])) {
+
+ // ----- Look if file is a directory
+ if (is_dir($p_entry['filename'])) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "already_a_directory";
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ // For historical reason first PclZip implementation does not stop
+ // when this kind of error occurs.
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_ALREADY_A_DIRECTORY, "Filename '" . $p_entry['filename'] . "' is " . "already used by an existing directory");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look if file is write protected
+ } elseif (!is_writeable($p_entry['filename'])) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "write_protected";
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ // For historical reason first PclZip implementation does not stop
+ // when this kind of error occurs.
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, "Filename '" . $p_entry['filename'] . "' exists " . "and is write protected");
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look if the extracted file is older
+ } elseif (filemtime($p_entry['filename']) > $p_entry['mtime']) {
+ // ----- Change the file status
+ if ((isset($p_options[PCLZIP_OPT_REPLACE_NEWER])) && ($p_options[PCLZIP_OPT_REPLACE_NEWER] === true)) {
+ } else {
+ $p_entry['status'] = "newer_exist";
+
+ // ----- Look for PCLZIP_OPT_STOP_ON_ERROR
+ // For historical reason first PclZip implementation does not stop
+ // when this kind of error occurs.
+ if ((isset($p_options[PCLZIP_OPT_STOP_ON_ERROR])) && ($p_options[PCLZIP_OPT_STOP_ON_ERROR] === true)) {
+
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, "Newer version of '" . $p_entry['filename'] . "' exists " . "and option PCLZIP_OPT_REPLACE_NEWER is not selected");
+
+ return PclZip::errorCode();
+ }
+ }
+ } else {
+ }
+
+ // ----- Check the directory availability and create it if necessary
+ } else {
+ if ((($p_entry['external'] & 0x00000010) == 0x00000010) || (substr($p_entry['filename'], -1) == '/')) {
+ $v_dir_to_check = $p_entry['filename'];
+ } elseif (!strstr($p_entry['filename'], "/")) {
+ $v_dir_to_check = "";
+ } else {
+ $v_dir_to_check = dirname($p_entry['filename']);
+ }
+
+ if (($v_result = $this->privDirCheck($v_dir_to_check, (($p_entry['external'] & 0x00000010) == 0x00000010))) != 1) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "path_creation_fail";
+
+ // ----- Return
+ //return $v_result;
+ $v_result = 1;
+ }
+ }
+ }
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Do the extraction (if not a folder)
+ if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+ // ----- Look for not compressed file
+ if ($p_entry['compression'] == 0) {
+
+ // ----- Opening destination file
+ if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "write_error";
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = $p_entry['compressed_size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($this->zip_fd, $v_read_size);
+ /* Try to speed up the code
+ $v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($v_dest_file, $v_binary_data, $v_read_size);
+ */
+ @fwrite($v_dest_file, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Closing the destination file
+ fclose($v_dest_file);
+
+ // ----- Change the file mtime
+ touch($p_entry['filename'], $p_entry['mtime']);
+
+ } else {
+ // ----- TBC
+ // Need to be finished
+ if (($p_entry['flag'] & 1) == 1) {
+ PclZip::privErrorLog(PCLZIP_ERR_UNSUPPORTED_ENCRYPTION, 'File \'' . $p_entry['filename'] . '\' is encrypted. Encrypted files are not supported.');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Look for using temporary file to unzip
+ if ((!isset($p_options[PCLZIP_OPT_TEMP_FILE_OFF])) && (isset($p_options[PCLZIP_OPT_TEMP_FILE_ON]) || (isset($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD]) && ($p_options[PCLZIP_OPT_TEMP_FILE_THRESHOLD] <= $p_entry['size'])))) {
+ $v_result = $this->privExtractFileUsingTempFile($p_entry, $p_options);
+ if ($v_result < PCLZIP_ERR_NO_ERROR) {
+ return $v_result;
+ }
+
+ // ----- Look for extract in memory
+ } else {
+
+ // ----- Read the compressed file in a buffer (one shot)
+ $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Decompress the file
+ $v_file_content = @gzinflate($v_buffer);
+ unset($v_buffer);
+ if ($v_file_content === false) {
+
+ // ----- Change the file status
+ // TBC
+ $p_entry['status'] = "error";
+
+ return $v_result;
+ }
+
+ // ----- Opening destination file
+ if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+
+ // ----- Change the file status
+ $p_entry['status'] = "write_error";
+
+ return $v_result;
+ }
+
+ // ----- Write the uncompressed data
+ @fwrite($v_dest_file, $v_file_content, $p_entry['size']);
+ unset($v_file_content);
+
+ // ----- Closing the destination file
+ @fclose($v_dest_file);
+
+ }
+
+ // ----- Change the file mtime
+ @touch($p_entry['filename'], $p_entry['mtime']);
+ }
+
+ // ----- Look for chmod option
+ if (isset($p_options[PCLZIP_OPT_SET_CHMOD])) {
+
+ // ----- Change the mode of the file
+ @chmod($p_entry['filename'], $p_options[PCLZIP_OPT_SET_CHMOD]);
+ }
+
+ }
+ }
+
+ // ----- Change abort status
+ if ($p_entry['status'] == "aborted") {
+ $p_entry['status'] = "skipped";
+
+ // ----- Look for post-extract callback
+ } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFileUsingTempFile()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privExtractFileUsingTempFile(&$p_entry, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Creates a temporary file
+ $v_gzip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.gz';
+ if (($v_dest_file = @fopen($v_gzip_temp_name, "wb")) == 0) {
+ fclose($v_file);
+ PclZip::privErrorLog(PCLZIP_ERR_WRITE_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary write mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Write gz file format header
+ $v_binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($p_entry['compression']), Chr(0x00), time(), Chr(0x00), Chr(3));
+ @fwrite($v_dest_file, $v_binary_data, 10);
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = $p_entry['compressed_size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($this->zip_fd, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($v_dest_file, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Write gz file format footer
+ $v_binary_data = pack('VV', $p_entry['crc'], $p_entry['size']);
+ @fwrite($v_dest_file, $v_binary_data, 8);
+
+ // ----- Close the temporary file
+ @fclose($v_dest_file);
+
+ // ----- Opening destination file
+ if (($v_dest_file = @fopen($p_entry['filename'], 'wb')) == 0) {
+ $p_entry['status'] = "write_error";
+
+ return $v_result;
+ }
+
+ // ----- Open the temporary gz file
+ if (($v_src_file = @gzopen($v_gzip_temp_name, 'rb')) == 0) {
+ @fclose($v_dest_file);
+ $p_entry['status'] = "read_error";
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_gzip_temp_name . '\' in binary read mode');
+
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file by PCLZIP_READ_BLOCK_SIZE octets blocks
+ $v_size = $p_entry['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @gzread($v_src_file, $v_read_size);
+ //$v_binary_data = pack('a'.$v_read_size, $v_buffer);
+ @fwrite($v_dest_file, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+ @fclose($v_dest_file);
+ @gzclose($v_src_file);
+
+ // ----- Delete the temporary file
+ @unlink($v_gzip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFileInOutput()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privExtractFileInOutput(&$p_entry, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Read the file header
+ if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+ return $v_result;
+ }
+
+ // ----- Check that the file header is coherent with $p_entry info
+ if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+ // TBC
+ }
+
+ // ----- Look for pre-extract callback
+ if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_entry['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ // ----- This status is internal and will be changed in 'skipped'
+ $p_entry['status'] = "aborted";
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ $p_entry['filename'] = $v_local_header['filename'];
+ }
+
+ // ----- Trace
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Do the extraction (if not a folder)
+ if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+ // ----- Look for not compressed file
+ if ($p_entry['compressed_size'] == $p_entry['size']) {
+
+ // ----- Read the file in a buffer (one shot)
+ $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Send the file to the output
+ echo $v_buffer;
+ unset($v_buffer);
+ } else {
+
+ // ----- Read the compressed file in a buffer (one shot)
+ $v_buffer = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Decompress the file
+ $v_file_content = gzinflate($v_buffer);
+ unset($v_buffer);
+
+ // ----- Send the file to the output
+ echo $v_file_content;
+ unset($v_file_content);
+ }
+ }
+ }
+
+ // ----- Change abort status
+ if ($p_entry['status'] == "aborted") {
+ $p_entry['status'] = "skipped";
+
+ // ----- Look for post-extract callback
+ } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+ }
+
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privExtractFileAsString()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privExtractFileAsString(&$p_entry, &$p_string, &$p_options)
+ {
+ $v_result = 1;
+
+ // ----- Read the file header
+ $v_header = array();
+ if (($v_result = $this->privReadFileHeader($v_header)) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Check that the file header is coherent with $p_entry info
+ if ($this->privCheckFileHeaders($v_header, $p_entry) != 1) {
+ // TBC
+ }
+
+ // ----- Look for pre-extract callback
+ if (isset($p_options[PCLZIP_CB_PRE_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_PRE_EXTRACT].'(PCLZIP_CB_PRE_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_PRE_EXTRACT](PCLZIP_CB_PRE_EXTRACT, $v_local_header);
+ if ($v_result == 0) {
+ // ----- Change the file status
+ $p_entry['status'] = "skipped";
+ $v_result = 1;
+ }
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ // ----- This status is internal and will be changed in 'skipped'
+ $p_entry['status'] = "aborted";
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+
+ // ----- Update the informations
+ // Only some fields can be modified
+ $p_entry['filename'] = $v_local_header['filename'];
+ }
+
+ // ----- Look if extraction should be done
+ if ($p_entry['status'] == 'ok') {
+
+ // ----- Do the extraction (if not a folder)
+ if (!(($p_entry['external'] & 0x00000010) == 0x00000010)) {
+ // ----- Look for not compressed file
+ // if ($p_entry['compressed_size'] == $p_entry['size'])
+ if ($p_entry['compression'] == 0) {
+
+ // ----- Reading the file
+ $p_string = @fread($this->zip_fd, $p_entry['compressed_size']);
+ } else {
+
+ // ----- Reading the file
+ $v_data = @fread($this->zip_fd, $p_entry['compressed_size']);
+
+ // ----- Decompress the file
+ if (($p_string = @gzinflate($v_data)) === false) {
+ // TBC
+ }
+ }
+
+ // ----- Trace
+ } else {
+ // TBC : error : can not extract a folder in a string
+ }
+
+ }
+
+ // ----- Change abort status
+ if ($p_entry['status'] == "aborted") {
+ $p_entry['status'] = "skipped";
+
+ // ----- Look for post-extract callback
+ } elseif (isset($p_options[PCLZIP_CB_POST_EXTRACT])) {
+
+ // ----- Generate a local information
+ $v_local_header = array();
+ $this->privConvertHeader2FileInfo($p_entry, $v_local_header);
+
+ // ----- Swap the content to header
+ $v_local_header['content'] = $p_string;
+ $p_string = '';
+
+ // ----- Call the callback
+ // Here I do not use call_user_func() because I need to send a reference to the
+ // header.
+ // eval('$v_result = '.$p_options[PCLZIP_CB_POST_EXTRACT].'(PCLZIP_CB_POST_EXTRACT, $v_local_header);');
+ $v_result = $p_options[PCLZIP_CB_POST_EXTRACT](PCLZIP_CB_POST_EXTRACT, $v_local_header);
+
+ // ----- Swap back the content to header
+ $p_string = $v_local_header['content'];
+ unset($v_local_header['content']);
+
+ // ----- Look for abort result
+ if ($v_result == 2) {
+ $v_result = PCLZIP_ERR_USER_ABORTED;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privReadFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privReadFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // ----- Read the 4 bytes signature
+ $v_binary_data = @fread($this->zip_fd, 4);
+ $v_data = unpack('Vid', $v_binary_data);
+
+ // ----- Check signature
+ if ($v_data['id'] != 0x04034b50) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the first 42 bytes of the header
+ $v_binary_data = fread($this->zip_fd, 26);
+
+ // ----- Look for invalid block size
+ if (strlen($v_binary_data) != 26) {
+ $p_header['filename'] = "";
+ $p_header['status'] = "invalid_header";
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : " . strlen($v_binary_data));
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the values
+ $v_data = unpack('vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $v_binary_data);
+
+ // ----- Get filename
+ $p_header['filename'] = fread($this->zip_fd, $v_data['filename_len']);
+
+ // ----- Get extra_fields
+ if ($v_data['extra_len'] != 0) {
+ $p_header['extra'] = fread($this->zip_fd, $v_data['extra_len']);
+ } else {
+ $p_header['extra'] = '';
+ }
+
+ // ----- Extract properties
+ $p_header['version_extracted'] = $v_data['version'];
+ $p_header['compression'] = $v_data['compression'];
+ $p_header['size'] = $v_data['size'];
+ $p_header['compressed_size'] = $v_data['compressed_size'];
+ $p_header['crc'] = $v_data['crc'];
+ $p_header['flag'] = $v_data['flag'];
+ $p_header['filename_len'] = $v_data['filename_len'];
+
+ // ----- Recuperate date in UNIX format
+ $p_header['mdate'] = $v_data['mdate'];
+ $p_header['mtime'] = $v_data['mtime'];
+ if ($p_header['mdate'] && $p_header['mtime']) {
+ // ----- Extract time
+ $v_hour = ($p_header['mtime'] & 0xF800) >> 11;
+ $v_minute = ($p_header['mtime'] & 0x07E0) >> 5;
+ $v_seconde = ($p_header['mtime'] & 0x001F) * 2;
+
+ // ----- Extract date
+ $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
+ $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
+ $v_day = $p_header['mdate'] & 0x001F;
+
+ // ----- Get UNIX date format
+ $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);
+
+ } else {
+ $p_header['mtime'] = time();
+ }
+
+ // TBC
+ //for (reset($v_data); $key = key($v_data); next($v_data)) {
+ //}
+
+ // ----- Set the stored filename
+ $p_header['stored_filename'] = $p_header['filename'];
+
+ // ----- Set the status field
+ $p_header['status'] = "ok";
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privReadCentralFileHeader()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privReadCentralFileHeader(&$p_header)
+ {
+ $v_result = 1;
+
+ // ----- Read the 4 bytes signature
+ $v_binary_data = @fread($this->zip_fd, 4);
+ $v_data = unpack('Vid', $v_binary_data);
+
+ // ----- Check signature
+ if ($v_data['id'] != 0x02014b50) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Invalid archive structure');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the first 42 bytes of the header
+ $v_binary_data = fread($this->zip_fd, 42);
+
+ // ----- Look for invalid block size
+ if (strlen($v_binary_data) != 42) {
+ $p_header['filename'] = "";
+ $p_header['status'] = "invalid_header";
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid block size : " . strlen($v_binary_data));
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the values
+ $p_header = unpack('vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $v_binary_data);
+
+ // ----- Get filename
+ if ($p_header['filename_len'] != 0) {
+ $p_header['filename'] = fread($this->zip_fd, $p_header['filename_len']);
+ } else {
+ $p_header['filename'] = '';
+ }
+
+ // ----- Get extra
+ if ($p_header['extra_len'] != 0) {
+ $p_header['extra'] = fread($this->zip_fd, $p_header['extra_len']);
+ } else {
+ $p_header['extra'] = '';
+ }
+
+ // ----- Get comment
+ if ($p_header['comment_len'] != 0) {
+ $p_header['comment'] = fread($this->zip_fd, $p_header['comment_len']);
+ } else {
+ $p_header['comment'] = '';
+ }
+
+ // ----- Extract properties
+
+ // ----- Recuperate date in UNIX format
+ //if ($p_header['mdate'] && $p_header['mtime'])
+ // TBC : bug : this was ignoring time with 0/0/0
+ if (1) {
+ // ----- Extract time
+ $v_hour = ($p_header['mtime'] & 0xF800) >> 11;
+ $v_minute = ($p_header['mtime'] & 0x07E0) >> 5;
+ $v_seconde = ($p_header['mtime'] & 0x001F) * 2;
+
+ // ----- Extract date
+ $v_year = (($p_header['mdate'] & 0xFE00) >> 9) + 1980;
+ $v_month = ($p_header['mdate'] & 0x01E0) >> 5;
+ $v_day = $p_header['mdate'] & 0x001F;
+
+ // ----- Get UNIX date format
+ $p_header['mtime'] = @mktime($v_hour, $v_minute, $v_seconde, $v_month, $v_day, $v_year);
+
+ } else {
+ $p_header['mtime'] = time();
+ }
+
+ // ----- Set the stored filename
+ $p_header['stored_filename'] = $p_header['filename'];
+
+ // ----- Set default status to ok
+ $p_header['status'] = 'ok';
+
+ // ----- Look if it is a directory
+ if (substr($p_header['filename'], -1) == '/') {
+ //$p_header['external'] = 0x41FF0010;
+ $p_header['external'] = 0x00000010;
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privCheckFileHeaders()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // 1 on success,
+ // 0 on error;
+ // --------------------------------------------------------------------------------
+ public function privCheckFileHeaders(&$p_local_header, &$p_central_header)
+ {
+ $v_result = 1;
+
+ // ----- Check the static values
+ // TBC
+ if ($p_local_header['filename'] != $p_central_header['filename']) {
+ }
+ if ($p_local_header['version_extracted'] != $p_central_header['version_extracted']) {
+ }
+ if ($p_local_header['flag'] != $p_central_header['flag']) {
+ }
+ if ($p_local_header['compression'] != $p_central_header['compression']) {
+ }
+ if ($p_local_header['mtime'] != $p_central_header['mtime']) {
+ }
+ if ($p_local_header['filename_len'] != $p_central_header['filename_len']) {
+ }
+
+ // ----- Look for flag bit 3
+ if (($p_local_header['flag'] & 8) == 8) {
+ $p_local_header['size'] = $p_central_header['size'];
+ $p_local_header['compressed_size'] = $p_central_header['compressed_size'];
+ $p_local_header['crc'] = $p_central_header['crc'];
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privReadEndCentralDir()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privReadEndCentralDir(&$p_central_dir)
+ {
+ $v_result = 1;
+
+ // ----- Go to the end of the zip file
+ $v_size = filesize($this->zipname);
+ @fseek($this->zip_fd, $v_size);
+ if (@ftell($this->zip_fd) != $v_size) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to go to the end of the archive \'' . $this->zipname . '\'');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- First try : look if this is an archive with no commentaries (most of the time)
+ // in this case the end of central dir is at 22 bytes of the file end
+ $v_found = 0;
+ if ($v_size > 26) {
+ @fseek($this->zip_fd, $v_size - 22);
+ if (($v_pos = @ftell($this->zip_fd)) != ($v_size - 22)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \'' . $this->zipname . '\'');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read for bytes
+ $v_binary_data = @fread($this->zip_fd, 4);
+ $v_data = @unpack('Vid', $v_binary_data);
+
+ // ----- Check signature
+ if ($v_data['id'] == 0x06054b50) {
+ $v_found = 1;
+ }
+
+ $v_pos = ftell($this->zip_fd);
+ }
+
+ // ----- Go back to the maximum possible size of the Central Dir End Record
+ if (!$v_found) {
+ $v_maximum_size = 65557; // 0xFFFF + 22;
+ if ($v_maximum_size > $v_size) {
+ $v_maximum_size = $v_size;
+ }
+ @fseek($this->zip_fd, $v_size - $v_maximum_size);
+ if (@ftell($this->zip_fd) != ($v_size - $v_maximum_size)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'Unable to seek back to the middle of the archive \'' . $this->zipname . '\'');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read byte per byte in order to find the signature
+ $v_pos = ftell($this->zip_fd);
+ $v_bytes = 0x00000000;
+ while ($v_pos < $v_size) {
+ // ----- Read a byte
+ $v_byte = @fread($this->zip_fd, 1);
+
+ // ----- Add the byte
+ //$v_bytes = ($v_bytes << 8) | Ord($v_byte);
+ // Note we mask the old value down such that once shifted we can never end up with more than a 32bit number
+ // Otherwise on systems where we have 64bit integers the check below for the magic number will fail.
+ $v_bytes = (($v_bytes & 0xFFFFFF) << 8) | Ord($v_byte);
+
+ // ----- Compare the bytes
+ if ($v_bytes == 0x504b0506) {
+ $v_pos++;
+ break;
+ }
+
+ $v_pos++;
+ }
+
+ // ----- Look if not found end of central dir
+ if ($v_pos == $v_size) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Unable to find End of Central Dir Record signature");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Read the first 18 bytes of the header
+ $v_binary_data = fread($this->zip_fd, 18);
+
+ // ----- Look for invalid block size
+ if (strlen($v_binary_data) != 18) {
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, "Invalid End of Central Dir Record size : " . strlen($v_binary_data));
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Extract the values
+ $v_data = unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', $v_binary_data);
+
+ // ----- Check the global size
+ if (($v_pos + $v_data['comment_size'] + 18) != $v_size) {
+
+ // ----- Removed in release 2.2 see readme file
+ // The check of the file size is a little too strict.
+ // Some bugs where found when a zip is encrypted/decrypted with 'crypt'.
+ // While decrypted, zip has training 0 bytes
+ if (0) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_BAD_FORMAT, 'The central dir is not at the end of the archive.' . ' Some trailing bytes exists after the archive.');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+ }
+
+ // ----- Get comment
+ if ($v_data['comment_size'] != 0) {
+ $p_central_dir['comment'] = fread($this->zip_fd, $v_data['comment_size']);
+ } else {
+ $p_central_dir['comment'] = '';
+ }
+
+ $p_central_dir['entries'] = $v_data['entries'];
+ $p_central_dir['disk_entries'] = $v_data['disk_entries'];
+ $p_central_dir['offset'] = $v_data['offset'];
+ $p_central_dir['size'] = $v_data['size'];
+ $p_central_dir['disk'] = $v_data['disk'];
+ $p_central_dir['disk_start'] = $v_data['disk_start'];
+
+ // TBC
+ //for (reset($p_central_dir); $key = key($p_central_dir); next($p_central_dir)) {
+ //}
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDeleteByRule()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privDeleteByRule(&$p_result_list, &$p_options)
+ {
+ $v_result = 1;
+ $v_list_detail = array();
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($this->zip_fd);
+
+ // ----- Scan all the files
+ // ----- Start at beginning of Central Dir
+ $v_pos_entry = $v_central_dir['offset'];
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_pos_entry)) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read each entry
+ $v_header_list = array();
+ $j_start = 0;
+ for ($i = 0, $v_nb_extracted = 0; $i < $v_central_dir['entries']; $i++) {
+
+ // ----- Read the file header
+ $v_header_list[$v_nb_extracted] = array();
+ if (($v_result = $this->privReadCentralFileHeader($v_header_list[$v_nb_extracted])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Store the index
+ $v_header_list[$v_nb_extracted]['index'] = $i;
+
+ // ----- Look for the specific extract rules
+ $v_found = false;
+
+ // ----- Look for extract by name rule
+ if ((isset($p_options[PCLZIP_OPT_BY_NAME])) && ($p_options[PCLZIP_OPT_BY_NAME] != 0)) {
+
+ // ----- Look if the filename is in the list
+ for ($j = 0; ($j < sizeof($p_options[PCLZIP_OPT_BY_NAME])) && (!$v_found); $j++) {
+
+ // ----- Look for a directory
+ if (substr($p_options[PCLZIP_OPT_BY_NAME][$j], -1) == "/") {
+
+ // ----- Look if the directory is in the filename path
+ if ((strlen($v_header_list[$v_nb_extracted]['stored_filename']) > strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) && (substr($v_header_list[$v_nb_extracted]['stored_filename'], 0, strlen($p_options[PCLZIP_OPT_BY_NAME][$j])) == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+ $v_found = true;
+ } elseif ((($v_header_list[$v_nb_extracted]['external'] & 0x00000010) == 0x00000010) /* Indicates a folder */ && ($v_header_list[$v_nb_extracted]['stored_filename'] . '/' == $p_options[PCLZIP_OPT_BY_NAME][$j])) {
+ $v_found = true;
+ }
+
+ // ----- Look for a filename
+ } elseif ($v_header_list[$v_nb_extracted]['stored_filename'] == $p_options[PCLZIP_OPT_BY_NAME][$j]) {
+ $v_found = true;
+ }
+ }
+
+ // ----- Look for extract by ereg rule
+ // ereg() is deprecated with PHP 5.3
+ /*
+ elseif ( (isset($p_options[PCLZIP_OPT_BY_EREG]))
+ && ($p_options[PCLZIP_OPT_BY_EREG] != "")) {
+
+ if (ereg($p_options[PCLZIP_OPT_BY_EREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
+ $v_found = true;
+ }
+ }
+ */
+
+ // ----- Look for extract by preg rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_PREG])) && ($p_options[PCLZIP_OPT_BY_PREG] != "")) {
+
+ if (preg_match($p_options[PCLZIP_OPT_BY_PREG], $v_header_list[$v_nb_extracted]['stored_filename'])) {
+ $v_found = true;
+ }
+
+ // ----- Look for extract by index rule
+ } elseif ((isset($p_options[PCLZIP_OPT_BY_INDEX])) && ($p_options[PCLZIP_OPT_BY_INDEX] != 0)) {
+
+ // ----- Look if the index is in the list
+ for ($j = $j_start; ($j < sizeof($p_options[PCLZIP_OPT_BY_INDEX])) && (!$v_found); $j++) {
+
+ if (($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['start']) && ($i <= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end'])) {
+ $v_found = true;
+ }
+ if ($i >= $p_options[PCLZIP_OPT_BY_INDEX][$j]['end']) {
+ $j_start = $j + 1;
+ }
+
+ if ($p_options[PCLZIP_OPT_BY_INDEX][$j]['start'] > $i) {
+ break;
+ }
+ }
+ } else {
+ $v_found = true;
+ }
+
+ // ----- Look for deletion
+ if ($v_found) {
+ unset($v_header_list[$v_nb_extracted]);
+ } else {
+ $v_nb_extracted++;
+ }
+ }
+
+ // ----- Look if something need to be deleted
+ if ($v_nb_extracted > 0) {
+
+ // ----- Creates a temporay file
+ $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.tmp';
+
+ // ----- Creates a temporary zip archive
+ $v_temp_zip = new PclZip($v_zip_temp_name);
+
+ // ----- Open the temporary zip file in write mode
+ if (($v_result = $v_temp_zip->privOpenFd('wb')) != 1) {
+ $this->privCloseFd();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Look which file need to be kept
+ for ($i = 0; $i < sizeof($v_header_list); $i++) {
+
+ // ----- Calculate the position of the header
+ @rewind($this->zip_fd);
+ if (@fseek($this->zip_fd, $v_header_list[$i]['offset'])) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_INVALID_ARCHIVE_ZIP, 'Invalid archive size');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Read the file header
+ $v_local_header = array();
+ if (($v_result = $this->privReadFileHeader($v_local_header)) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Check that local file header is same as central file header
+ if ($this->privCheckFileHeaders($v_local_header, $v_header_list[$i]) != 1) {
+ // TBC
+ }
+ unset($v_local_header);
+
+ // ----- Write the file header
+ if (($v_result = $v_temp_zip->privWriteFileHeader($v_header_list[$i])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read/write the data block
+ if (($v_result = PclZipUtilCopyBlock($this->zip_fd, $v_temp_zip->zip_fd, $v_header_list[$i]['compressed_size'])) != 1) {
+ // ----- Close the zip file
+ $this->privCloseFd();
+ $v_temp_zip->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($v_temp_zip->zip_fd);
+
+ // ----- Re-Create the Central Dir files header
+ for ($i = 0; $i < sizeof($v_header_list); $i++) {
+ // ----- Create the file header
+ if (($v_result = $v_temp_zip->privWriteCentralFileHeader($v_header_list[$i])) != 1) {
+ $v_temp_zip->privCloseFd();
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Transform the header to a 'usable' info
+ $v_temp_zip->privConvertHeader2FileInfo($v_header_list[$i], $p_result_list[$i]);
+ }
+
+ // ----- Zip file comment
+ $v_comment = '';
+ if (isset($p_options[PCLZIP_OPT_COMMENT])) {
+ $v_comment = $p_options[PCLZIP_OPT_COMMENT];
+ }
+
+ // ----- Calculate the size of the central header
+ $v_size = @ftell($v_temp_zip->zip_fd) - $v_offset;
+
+ // ----- Create the central dir footer
+ if (($v_result = $v_temp_zip->privWriteCentralHeader(sizeof($v_header_list), $v_size, $v_offset, $v_comment)) != 1) {
+ // ----- Reset the file list
+ unset($v_header_list);
+ $v_temp_zip->privCloseFd();
+ $this->privCloseFd();
+ @unlink($v_zip_temp_name);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Close
+ $v_temp_zip->privCloseFd();
+ $this->privCloseFd();
+
+ // ----- Delete the zip file
+ // TBC : I should test the result ...
+ @unlink($this->zipname);
+
+ // ----- Rename the temporary file
+ // TBC : I should test the result ...
+ //@rename($v_zip_temp_name, $this->zipname);
+ PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+ // ----- Destroy the temporary archive
+ unset($v_temp_zip);
+
+ // ----- Remove every files : reset the file
+ } elseif ($v_central_dir['entries'] != 0) {
+ $this->privCloseFd();
+
+ if (($v_result = $this->privOpenFd('wb')) != 1) {
+ return $v_result;
+ }
+
+ if (($v_result = $this->privWriteCentralHeader(0, 0, 0, '')) != 1) {
+ return $v_result;
+ }
+
+ $this->privCloseFd();
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDirCheck()
+ // Description :
+ // Check if a directory exists, if not it creates it and all the parents directory
+ // which may be useful.
+ // Parameters :
+ // $p_dir : Directory path to check.
+ // Return Values :
+ // 1 : OK
+ // -1 : Unable to create directory
+ // --------------------------------------------------------------------------------
+ public function privDirCheck($p_dir, $p_is_dir = false)
+ {
+ $v_result = 1;
+
+ // ----- Remove the final '/'
+ if (($p_is_dir) && (substr($p_dir, -1) == '/')) {
+ $p_dir = substr($p_dir, 0, strlen($p_dir) - 1);
+ }
+
+ // ----- Check the directory availability
+ if ((is_dir($p_dir)) || ($p_dir == "")) {
+ return 1;
+ }
+
+ // ----- Extract parent directory
+ $p_parent_dir = dirname($p_dir);
+
+ // ----- Just a check
+ if ($p_parent_dir != $p_dir) {
+ // ----- Look for parent directory
+ if ($p_parent_dir != "") {
+ if (($v_result = $this->privDirCheck($p_parent_dir)) != 1) {
+ return $v_result;
+ }
+ }
+ }
+
+ // ----- Create the directory
+ if (!@mkdir($p_dir, 0777)) {
+ // ----- Error log
+ PclZip::privErrorLog(PCLZIP_ERR_DIR_CREATE_FAIL, "Unable to create directory '$p_dir'");
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privMerge()
+ // Description :
+ // If $p_archive_to_add does not exist, the function exit with a success result.
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privMerge(&$p_archive_to_add)
+ {
+ $v_result = 1;
+
+ // ----- Look if the archive_to_add exists
+ if (!is_file($p_archive_to_add->zipname)) {
+
+ // ----- Nothing to merge, so merge is a success
+ $v_result = 1;
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Look if the archive exists
+ if (!is_file($this->zipname)) {
+
+ // ----- Do a duplicate
+ $v_result = $this->privDuplicate($p_archive_to_add->zipname);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('rb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir = array();
+ if (($v_result = $this->privReadEndCentralDir($v_central_dir)) != 1) {
+ $this->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($this->zip_fd);
+
+ // ----- Open the archive_to_add file
+ if (($v_result = $p_archive_to_add->privOpenFd('rb')) != 1) {
+ $this->privCloseFd();
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Read the central directory informations
+ $v_central_dir_to_add = array();
+ if (($v_result = $p_archive_to_add->privReadEndCentralDir($v_central_dir_to_add)) != 1) {
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+
+ return $v_result;
+ }
+
+ // ----- Go to beginning of File
+ @rewind($p_archive_to_add->zip_fd);
+
+ // ----- Creates a temporay file
+ $v_zip_temp_name = PCLZIP_TEMPORARY_DIR . uniqid('pclzip-') . '.tmp';
+
+ // ----- Open the temporary file in write mode
+ if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0) {
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \'' . $v_zip_temp_name . '\' in binary write mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Copy the files from the archive to the temporary file
+ // TBC : Here I should better append the file and go back to erase the central dir
+ $v_size = $v_central_dir['offset'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($this->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Copy the files from the archive_to_add into the temporary file
+ $v_size = $v_central_dir_to_add['offset'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($p_archive_to_add->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Store the offset of the central dir
+ $v_offset = @ftell($v_zip_temp_fd);
+
+ // ----- Copy the block of file headers from the old archive
+ $v_size = $v_central_dir['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($this->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Copy the block of file headers from the archive_to_add
+ $v_size = $v_central_dir_to_add['size'];
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($p_archive_to_add->zip_fd, $v_read_size);
+ @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Merge the file comments
+ $v_comment = $v_central_dir['comment'] . ' ' . $v_central_dir_to_add['comment'];
+
+ // ----- Calculate the size of the (new) central header
+ $v_size = @ftell($v_zip_temp_fd) - $v_offset;
+
+ // ----- Swap the file descriptor
+ // Here is a trick : I swap the temporary fd with the zip fd, in order to use
+ // the following methods on the temporary fil and not the real archive fd
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Create the central dir footer
+ if (($v_result = $this->privWriteCentralHeader($v_central_dir['entries'] + $v_central_dir_to_add['entries'], $v_size, $v_offset, $v_comment)) != 1) {
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+ @fclose($v_zip_temp_fd);
+ $this->zip_fd = null;
+
+ // ----- Reset the file list
+ unset($v_header_list);
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Swap back the file descriptor
+ $v_swap = $this->zip_fd;
+ $this->zip_fd = $v_zip_temp_fd;
+ $v_zip_temp_fd = $v_swap;
+
+ // ----- Close
+ $this->privCloseFd();
+ $p_archive_to_add->privCloseFd();
+
+ // ----- Close the temporary file
+ @fclose($v_zip_temp_fd);
+
+ // ----- Delete the zip file
+ // TBC : I should test the result ...
+ @unlink($this->zipname);
+
+ // ----- Rename the temporary file
+ // TBC : I should test the result ...
+ //@rename($v_zip_temp_name, $this->zipname);
+ PclZipUtilRename($v_zip_temp_name, $this->zipname);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDuplicate()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privDuplicate($p_archive_filename)
+ {
+ $v_result = 1;
+
+ // ----- Look if the $p_archive_filename exists
+ if (!is_file($p_archive_filename)) {
+
+ // ----- Nothing to duplicate, so duplicate is a success.
+ $v_result = 1;
+
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Open the zip file
+ if (($v_result = $this->privOpenFd('wb')) != 1) {
+ // ----- Return
+ return $v_result;
+ }
+
+ // ----- Open the temporary file in write mode
+ if (($v_zip_temp_fd = @fopen($p_archive_filename, 'rb')) == 0) {
+ $this->privCloseFd();
+
+ PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open archive file \'' . $p_archive_filename . '\' in binary write mode');
+
+ // ----- Return
+ return PclZip::errorCode();
+ }
+
+ // ----- Copy the files from the archive to the temporary file
+ // TBC : Here I should better append the file and go back to erase the central dir
+ $v_size = filesize($p_archive_filename);
+ while ($v_size != 0) {
+ $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = fread($v_zip_temp_fd, $v_read_size);
+ @fwrite($this->zip_fd, $v_buffer, $v_read_size);
+ $v_size -= $v_read_size;
+ }
+
+ // ----- Close
+ $this->privCloseFd();
+
+ // ----- Close the temporary file
+ @fclose($v_zip_temp_fd);
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privErrorLog()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privErrorLog($p_error_code = 0, $p_error_string = '')
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ PclError($p_error_code, $p_error_string);
+ } else {
+ $this->error_code = $p_error_code;
+ $this->error_string = $p_error_string;
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privErrorReset()
+ // Description :
+ // Parameters :
+ // --------------------------------------------------------------------------------
+ public function privErrorReset()
+ {
+ if (PCLZIP_ERROR_EXTERNAL == 1) {
+ PclErrorReset();
+ } else {
+ $this->error_code = 0;
+ $this->error_string = '';
+ }
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privDisableMagicQuotes()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privDisableMagicQuotes()
+ {
+ $v_result = 1;
+
+ // ----- Look if function exists
+ if ((!function_exists("get_magic_quotes_runtime")) || (!function_exists("set_magic_quotes_runtime"))) {
+ return $v_result;
+ }
+
+ // ----- Look if already done
+ if ($this->magic_quotes_status != -1) {
+ return $v_result;
+ }
+
+ // ----- Get and memorize the magic_quote value
+ $this->magic_quotes_status = @get_magic_quotes_runtime();
+
+ // ----- Disable magic_quotes
+ if ($this->magic_quotes_status == 1) {
+ @set_magic_quotes_runtime(0);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+
+ // --------------------------------------------------------------------------------
+ // Function : privSwapBackMagicQuotes()
+ // Description :
+ // Parameters :
+ // Return Values :
+ // --------------------------------------------------------------------------------
+ public function privSwapBackMagicQuotes()
+ {
+ $v_result = 1;
+
+ // ----- Look if function exists
+ if ((!function_exists("get_magic_quotes_runtime")) || (!function_exists("set_magic_quotes_runtime"))) {
+ return $v_result;
+ }
+
+ // ----- Look if something to do
+ if ($this->magic_quotes_status != -1) {
+ return $v_result;
+ }
+
+ // ----- Swap back magic_quotes
+ if ($this->magic_quotes_status == 1) {
+ @set_magic_quotes_runtime($this->magic_quotes_status);
+ }
+
+ // ----- Return
+ return $v_result;
+ }
+ // --------------------------------------------------------------------------------
+}
+
+// End of class
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilPathReduction()
+// Description :
+// Parameters :
+// Return Values :
+// --------------------------------------------------------------------------------
+function PclZipUtilPathReduction($p_dir)
+{
+ $v_result = "";
+
+ // ----- Look for not empty path
+ if ($p_dir != "") {
+ // ----- Explode path by directory names
+ $v_list = explode("/", $p_dir);
+
+ // ----- Study directories from last to first
+ $v_skip = 0;
+ for ($i = sizeof($v_list) - 1; $i >= 0; $i--) {
+ // ----- Look for current path
+ if ($v_list[$i] == ".") {
+ // ----- Ignore this directory
+ // Should be the first $i=0, but no check is done
+ } elseif ($v_list[$i] == "..") {
+ $v_skip++;
+ } elseif ($v_list[$i] == "") {
+ // ----- First '/' i.e. root slash
+ if ($i == 0) {
+ $v_result = "/" . $v_result;
+ if ($v_skip > 0) {
+ // ----- It is an invalid path, so the path is not modified
+ // TBC
+ $v_result = $p_dir;
+ $v_skip = 0;
+ }
+
+ // ----- Last '/' i.e. indicates a directory
+ } elseif ($i == (sizeof($v_list) - 1)) {
+ $v_result = $v_list[$i];
+
+ // ----- Double '/' inside the path
+ } else {
+ // ----- Ignore only the double '//' in path,
+ // but not the first and last '/'
+ }
+ } else {
+ // ----- Look for item to skip
+ if ($v_skip > 0) {
+ $v_skip--;
+ } else {
+ $v_result = $v_list[$i] . ($i != (sizeof($v_list) - 1) ? "/" . $v_result : "");
+ }
+ }
+ }
+
+ // ----- Look for skip
+ if ($v_skip > 0) {
+ while ($v_skip > 0) {
+ $v_result = '../' . $v_result;
+ $v_skip--;
+ }
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilPathInclusion()
+// Description :
+// This function indicates if the path $p_path is under the $p_dir tree. Or,
+// said in an other way, if the file or sub-dir $p_path is inside the dir
+// $p_dir.
+// The function indicates also if the path is exactly the same as the dir.
+// This function supports path with duplicated '/' like '//', but does not
+// support '.' or '..' statements.
+// Parameters :
+// Return Values :
+// 0 if $p_path is not inside directory $p_dir
+// 1 if $p_path is inside directory $p_dir
+// 2 if $p_path is exactly the same as $p_dir
+// --------------------------------------------------------------------------------
+function PclZipUtilPathInclusion($p_dir, $p_path)
+{
+ $v_result = 1;
+
+ // ----- Look for path beginning by ./
+ if (($p_dir == '.') || ((strlen($p_dir) >= 2) && (substr($p_dir, 0, 2) == './'))) {
+ $p_dir = PclZipUtilTranslateWinPath(getcwd(), false) . '/' . substr($p_dir, 1);
+ }
+ if (($p_path == '.') || ((strlen($p_path) >= 2) && (substr($p_path, 0, 2) == './'))) {
+ $p_path = PclZipUtilTranslateWinPath(getcwd(), false) . '/' . substr($p_path, 1);
+ }
+
+ // ----- Explode dir and path by directory separator
+ $v_list_dir = explode("/", $p_dir);
+ $v_list_dir_size = sizeof($v_list_dir);
+ $v_list_path = explode("/", $p_path);
+ $v_list_path_size = sizeof($v_list_path);
+
+ // ----- Study directories paths
+ $i = 0;
+ $j = 0;
+ while (($i < $v_list_dir_size) && ($j < $v_list_path_size) && ($v_result)) {
+
+ // ----- Look for empty dir (path reduction)
+ if ($v_list_dir[$i] == '') {
+ $i++;
+ continue;
+ }
+ if ($v_list_path[$j] == '') {
+ $j++;
+ continue;
+ }
+
+ // ----- Compare the items
+ if (($v_list_dir[$i] != $v_list_path[$j]) && ($v_list_dir[$i] != '') && ($v_list_path[$j] != '')) {
+ $v_result = 0;
+ }
+
+ // ----- Next items
+ $i++;
+ $j++;
+ }
+
+ // ----- Look if everything seems to be the same
+ if ($v_result) {
+ // ----- Skip all the empty items
+ while (($j < $v_list_path_size) && ($v_list_path[$j] == '')) {
+ $j++;
+ }
+ while (($i < $v_list_dir_size) && ($v_list_dir[$i] == '')) {
+ $i++;
+ }
+
+ if (($i >= $v_list_dir_size) && ($j >= $v_list_path_size)) {
+ // ----- There are exactly the same
+ $v_result = 2;
+ } elseif ($i < $v_list_dir_size) {
+ // ----- The path is shorter than the dir
+ $v_result = 0;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilCopyBlock()
+// Description :
+// Parameters :
+// $p_mode : read/write compression mode
+// 0 : src & dest normal
+// 1 : src gzip, dest normal
+// 2 : src normal, dest gzip
+// 3 : src & dest gzip
+// Return Values :
+// --------------------------------------------------------------------------------
+function PclZipUtilCopyBlock($p_src, $p_dest, $p_size, $p_mode = 0)
+{
+ $v_result = 1;
+
+ if ($p_mode == 0) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($p_src, $v_read_size);
+ @fwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ } elseif ($p_mode == 1) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @gzread($p_src, $v_read_size);
+ @fwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ } elseif ($p_mode == 2) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @fread($p_src, $v_read_size);
+ @gzwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ } elseif ($p_mode == 3) {
+ while ($p_size != 0) {
+ $v_read_size = ($p_size < PCLZIP_READ_BLOCK_SIZE ? $p_size : PCLZIP_READ_BLOCK_SIZE);
+ $v_buffer = @gzread($p_src, $v_read_size);
+ @gzwrite($p_dest, $v_buffer, $v_read_size);
+ $p_size -= $v_read_size;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilRename()
+// Description :
+// This function tries to do a simple rename() function. If it fails, it
+// tries to copy the $p_src file in a new $p_dest file and then unlink the
+// first one.
+// Parameters :
+// $p_src : Old filename
+// $p_dest : New filename
+// Return Values :
+// 1 on success, 0 on failure.
+// --------------------------------------------------------------------------------
+function PclZipUtilRename($p_src, $p_dest)
+{
+ $v_result = 1;
+
+ // ----- Try to rename the files
+ if (!@rename($p_src, $p_dest)) {
+
+ // ----- Try to copy & unlink the src
+ if (!@copy($p_src, $p_dest)) {
+ $v_result = 0;
+ } elseif (!@unlink($p_src)) {
+ $v_result = 0;
+ }
+ }
+
+ // ----- Return
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilOptionText()
+// Description :
+// Translate option value in text. Mainly for debug purpose.
+// Parameters :
+// $p_option : the option value.
+// Return Values :
+// The option text value.
+// --------------------------------------------------------------------------------
+function PclZipUtilOptionText($p_option)
+{
+
+ $v_list = get_defined_constants();
+ for (reset($v_list); $v_key = key($v_list); next($v_list)) {
+ $v_prefix = substr($v_key, 0, 10);
+ if ((($v_prefix == 'PCLZIP_OPT') || ($v_prefix == 'PCLZIP_CB_') || ($v_prefix == 'PCLZIP_ATT')) && ($v_list[$v_key] == $p_option)) {
+ return $v_key;
+ }
+ }
+
+ $v_result = 'Unknown';
+
+ return $v_result;
+}
+// --------------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------------
+// Function : PclZipUtilTranslateWinPath()
+// Description :
+// Translate windows path by replacing '\' by '/' and optionally removing
+// drive letter.
+// Parameters :
+// $p_path : path to translate.
+// $p_remove_disk_letter : true | false
+// Return Values :
+// The path translated.
+// --------------------------------------------------------------------------------
+function PclZipUtilTranslateWinPath($p_path, $p_remove_disk_letter = true)
+{
+ if (stristr(php_uname(), 'windows')) {
+ // ----- Look for potential disk letter
+ if (($p_remove_disk_letter) && (($v_position = strpos($p_path, ':')) != false)) {
+ $p_path = substr($p_path, $v_position + 1);
+ }
+ // ----- Change potential windows directory separator
+ if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) {
+ $p_path = strtr($p_path, '\\', '/');
+ }
+ }
+
+ return $p_path;
+}
+// --------------------------------------------------------------------------------
diff --git a/lib/vendor/pclzip/pclzip/readme.txt b/lib/vendor/pclzip/pclzip/readme.txt
new file mode 100644
index 0000000000..115c22edf1
--- /dev/null
+++ b/lib/vendor/pclzip/pclzip/readme.txt
@@ -0,0 +1,421 @@
+// --------------------------------------------------------------------------------
+// PclZip 2.8.2 - readme.txt
+// --------------------------------------------------------------------------------
+// License GNU/LGPL - August 2009
+// Vincent Blavet - vincent@phpconcept.net
+// http://www.phpconcept.net
+// --------------------------------------------------------------------------------
+// $Id: readme.txt,v 1.60 2009/09/30 20:35:21 vblavet Exp $
+// --------------------------------------------------------------------------------
+
+
+
+0 - Sommaire
+============
+ 1 - Introduction
+ 2 - What's new
+ 3 - Corrected bugs
+ 4 - Known bugs or limitations
+ 5 - License
+ 6 - Warning
+ 7 - Documentation
+ 8 - Author
+ 9 - Contribute
+
+1 - Introduction
+================
+
+ PclZip is a library that allow you to manage a Zip archive.
+
+ Full documentation about PclZip can be found here : http://www.phpconcept.net/pclzip
+
+2 - What's new
+==============
+
+ Version 2.8.2 :
+ - PCLZIP_CB_PRE_EXTRACT and PCLZIP_CB_POST_EXTRACT are now supported with
+ extraction as a string (PCLZIP_OPT_EXTRACT_AS_STRING). The string
+ can also be modified in the post-extract call back.
+ **Bugs correction :
+ - PCLZIP_OPT_REMOVE_ALL_PATH was not working correctly
+ - Remove use of eval() and do direct call to callback functions
+ - Correct support of 64bits systems (Thanks to WordPress team)
+
+ Version 2.8.1 :
+ - Move option PCLZIP_OPT_BY_EREG to PCLZIP_OPT_BY_PREG because ereg() is
+ deprecated in PHP 5.3. When using option PCLZIP_OPT_BY_EREG, PclZip will
+ automatically replace it by PCLZIP_OPT_BY_PREG.
+
+ Version 2.8 :
+ - Improve extraction of zip archive for large files by using temporary files
+ This feature is working like the one defined in r2.7.
+ Options are renamed : PCLZIP_OPT_TEMP_FILE_ON, PCLZIP_OPT_TEMP_FILE_OFF,
+ PCLZIP_OPT_TEMP_FILE_THRESHOLD
+ - Add a ratio constant PCLZIP_TEMPORARY_FILE_RATIO to configure the auto
+ sense of temporary file use.
+ - Bug correction : Reduce filepath in returned file list to remove ennoying
+ './/' preambule in file path.
+
+ Version 2.7 :
+ - Improve creation of zip archive for large files :
+ PclZip will now autosense the configured memory and use temporary files
+ when large file is suspected.
+ This feature can also ne triggered by manual options in create() and add()
+ methods. 'PCLZIP_OPT_ADD_TEMP_FILE_ON' force the use of temporary files,
+ 'PCLZIP_OPT_ADD_TEMP_FILE_OFF' disable the autosense technic,
+ 'PCLZIP_OPT_ADD_TEMP_FILE_THRESHOLD' allow for configuration of a size
+ threshold to use temporary files.
+ Using "temporary files" rather than "memory" might take more time, but
+ might give the ability to zip very large files :
+ Tested on my win laptop with a 88Mo file :
+ Zip "in-memory" : 18sec (max_execution_time=30, memory_limit=180Mo)
+ Zip "tmporary-files" : 23sec (max_execution_time=30, memory_limit=30Mo)
+ - Replace use of mktime() by time() to limit the E_STRICT error messages.
+ - Bug correction : When adding files with full windows path (drive letter)
+ PclZip is now working. Before, if the drive letter is not the default
+ path, PclZip was not able to add the file.
+
+ Version 2.6 :
+ - Code optimisation
+ - New attributes PCLZIP_ATT_FILE_COMMENT gives the ability to
+ add a comment for a specific file. (Don't really know if this is usefull)
+ - New attribute PCLZIP_ATT_FILE_CONTENT gives the ability to add a string
+ as a file.
+ - New attribute PCLZIP_ATT_FILE_MTIME modify the timestamp associated with
+ a file.
+ - Correct a bug. Files archived with a timestamp with 0h0m0s were extracted
+ with current time
+ - Add CRC value in the informations returned back for each file after an
+ action.
+ - Add missing closedir() statement.
+ - When adding a folder, and removing the path of this folder, files were
+ incorrectly added with a '/' at the beginning. Which means files are
+ related to root in unix systems. Corrected.
+ - Add conditional if before constant definition. This will allow users
+ to redefine constants without changing the file, and then improve
+ upgrade of pclzip code for new versions.
+
+ Version 2.5 :
+ - Introduce the ability to add file/folder with individual properties (file descriptor).
+ This gives for example the ability to change the filename of a zipped file.
+ . Able to add files individually
+ . Able to change full name
+ . Able to change short name
+ . Compatible with global options
+ - New attributes : PCLZIP_ATT_FILE_NAME, PCLZIP_ATT_FILE_NEW_SHORT_NAME, PCLZIP_ATT_FILE_NEW_FULL_NAME
+ - New error code : PCLZIP_ERR_INVALID_ATTRIBUTE_VALUE
+ - Add a security control feature. PclZip can extract any file in any folder
+ of a system. People may use this to upload a zip file and try to override
+ a system file. The PCLZIP_OPT_EXTRACT_DIR_RESTRICTION will give the
+ ability to forgive any directory transversal behavior.
+ - New PCLZIP_OPT_EXTRACT_DIR_RESTRICTION : check extraction path
+ - New error code : PCLZIP_ERR_DIRECTORY_RESTRICTION
+ - Modification in PclZipUtilPathInclusion() : dir and path beginning with ./ will be prepend
+ by current path (getcwd())
+
+ Version 2.4 :
+ - Code improvment : try to speed up the code by removing unusefull call to pack()
+ - Correct bug in delete() : delete() should be called with no argument. This was not
+ the case in 2.3. This is corrected in 2.4.
+ - Correct a bug in path_inclusion function. When the path has several '../../', the
+ result was bad.
+ - Add a check for magic_quotes_runtime configuration. If enabled, PclZip will
+ disable it while working and det it back to its original value.
+ This resolve a lots of bad formated archive errors.
+ - Bug correction : PclZip now correctly unzip file in some specific situation,
+ when compressed content has same size as uncompressed content.
+ - Bug correction : When selecting option 'PCLZIP_OPT_REMOVE_ALL_PATH',
+ directories are not any more created.
+ - Code improvment : correct unclosed opendir(), better handling of . and .. in
+ loops.
+
+
+ Version 2.3 :
+ - Correct a bug with PHP5 : affecting the value 0xFE49FFE0 to a variable does not
+ give the same result in PHP4 and PHP5 ....
+
+ Version 2.2 :
+ - Try development of PCLZIP_OPT_CRYPT .....
+ However this becomes to a stop. To crypt/decrypt I need to multiply 2 long integers,
+ the result (greater than a long) is not supported by PHP. Even the use of bcmath
+ functions does not help. I did not find yet a solution ...;
+ - Add missing '/' at end of directory entries
+ - Check is a file is encrypted or not. Returns status 'unsupported_encryption' and/or
+ error code PCLZIP_ERR_UNSUPPORTED_ENCRYPTION.
+ - Corrected : Bad "version need to extract" field in local file header
+ - Add private method privCheckFileHeaders() in order to check local and central
+ file headers. PclZip is now supporting purpose bit flag bit 3. Purpose bit flag bit 3 gives
+ the ability to have a local file header without size, compressed size and crc filled.
+ - Add a generic status 'error' for file status
+ - Add control of compression type. PclZip only support deflate compression method.
+ Before v2.2, PclZip does not check the compression method used in an archive while
+ extracting. With v2.2 PclZip returns a new error status for a file using an unsupported
+ compression method. New status is "unsupported_compression". New error code is
+ PCLZIP_ERR_UNSUPPORTED_COMPRESSION.
+ - Add optional attribute PCLZIP_OPT_STOP_ON_ERROR. This will stop the extract of files
+ when errors like 'a folder with same name exists' or 'a newer file exists' or
+ 'a write protected file' exists, rather than set a status for the concerning file
+ and resume the extract of the zip.
+ - Add optional attribute PCLZIP_OPT_REPLACE_NEWER. This will force, during an extract' the
+ replacement of the file, even if a newer version of the file exists.
+ Note that today if a file with the same name already exists but is older it will be
+ replaced by the extracted one.
+ - Improve PclZipUtilOption()
+ - Support of zip archive with trailing bytes. Before 2.2, PclZip checks that the central
+ directory structure is the last data in the archive. Crypt encryption/decryption of
+ zip archive put trailing 0 bytes after decryption. PclZip is now supporting this.
+
+ Version 2.1 :
+ - Add the ability to abort the extraction by using a user callback function.
+ The user can now return the value '2' in its callback which indicates to stop the
+ extraction. For a pre call-back extract is stopped before the extration of the current
+ file. For a post call back, the extraction is stopped after.
+ - Add the ability to extract a file (or several files) directly in the standard output.
+ This is done by the new parameter PCLZIP_OPT_EXTRACT_IN_OUTPUT with method extract().
+ - Add support for parameters PCLZIP_OPT_COMMENT, PCLZIP_OPT_ADD_COMMENT,
+ PCLZIP_OPT_PREPEND_COMMENT. This will create, replace, add, or prepend comments
+ in the zip archive.
+ - When merging two archives, the comments are not any more lost, but merged, with a
+ blank space separator.
+ - Corrected bug : Files are not deleted when all files are asked to be deleted.
+ - Corrected bug : Folders with name '0' made PclZip to abort the create or add feature.
+
+
+ Version 2.0 :
+ ***** Warning : Some new features may break the backward compatibility for your scripts.
+ Please carefully read the readme file.
+ - Add the ability to delete by Index, name and regular expression. This feature is
+ performed by the method delete(), which uses the optional parameters
+ PCLZIP_OPT_BY_INDEX, PCLZIP_OPT_BY_NAME, PCLZIP_OPT_BY_EREG or PCLZIP_OPT_BY_PREG.
+ - Add the ability to extract by regular expression. To extract by regexp you must use the method
+ extract(), with the option PCLZIP_OPT_BY_EREG or PCLZIP_OPT_BY_PREG
+ (depending if you want to use ereg() or preg_match() syntax) followed by the
+ regular expression pattern.
+ - Add the ability to extract by index, directly with the extract() method. This is a
+ code improvment of the extractByIndex() method.
+ - Add the ability to extract by name. To extract by name you must use the method
+ extract(), with the option PCLZIP_OPT_BY_NAME followed by the filename to
+ extract or an array of filenames to extract. To extract all a folder, use the folder
+ name rather than the filename with a '/' at the end.
+ - Add the ability to add files without compression. This is done with a new attribute
+ which is PCLZIP_OPT_NO_COMPRESSION.
+ - Add the attribute PCLZIP_OPT_EXTRACT_AS_STRING, which allow to extract a file directly
+ in a string without using any file (or temporary file).
+ - Add constant PCLZIP_SEPARATOR for static configuration of filename separators in a single string.
+ The default separator is now a comma (,) and not any more a blank space.
+ THIS BREAK THE BACKWARD COMPATIBILITY : Please check if this may have an impact with
+ your script.
+ - Improve algorythm performance by removing the use of temporary files when adding or
+ extracting files in an archive.
+ - Add (correct) detection of empty filename zipping. This can occurs when the removed
+ path is the same
+ as a zipped dir. The dir is not zipped (['status'] = filtered), only its content.
+ - Add better support for windows paths (thanks for help from manus@manusfreedom.com).
+ - Corrected bug : When the archive file already exists with size=0, the add() method
+ fails. Corrected in 2.0.
+ - Remove the use of OS_WINDOWS constant. Use php_uname() function rather.
+ - Control the order of index ranges in extract by index feature.
+ - Change the internal management of folders (better handling of internal flag).
+
+
+ Version 1.3 :
+ - Removing the double include check. This is now done by include_once() and require_once()
+ PHP directives.
+ - Changing the error handling mecanism : Remove the use of an external error library.
+ The former PclError...() functions are replaced by internal equivalent methods.
+ By changing the environment variable PCLZIP_ERROR_EXTERNAL you can still use the former library.
+ Introducing the use of constants for error codes rather than integer values. This will help
+ in futur improvment.
+ Introduction of error handling functions like errorCode(), errorName() and errorInfo().
+ - Remove the deprecated use of calling function with arguments passed by reference.
+ - Add the calling of extract(), extractByIndex(), create() and add() functions
+ with variable options rather than fixed arguments.
+ - Add the ability to remove all the file path while extracting or adding,
+ without any need to specify the path to remove.
+ This is available for extract(), extractByIndex(), create() and add() functionS by using
+ the new variable options parameters :
+ - PCLZIP_OPT_REMOVE_ALL_PATH : by indicating this option while calling the fct.
+ - Ability to change the mode of a file after the extraction (chmod()).
+ This is available for extract() and extractByIndex() functionS by using
+ the new variable options parameters.
+ - PCLZIP_OPT_SET_CHMOD : by setting the value of this option.
+ - Ability to definition call-back options. These call-back will be called during the adding,
+ or the extracting of file (extract(), extractByIndex(), create() and add() functions) :
+ - PCLZIP_CB_PRE_EXTRACT : will be called before each extraction of a file. The user
+ can trigerred the change the filename of the extracted file. The user can triggered the
+ skip of the extraction. This is adding a 'skipped' status in the file list result value.
+ - PCLZIP_CB_POST_EXTRACT : will be called after each extraction of a file.
+ Nothing can be triggered from that point.
+ - PCLZIP_CB_PRE_ADD : will be called before each add of a file. The user
+ can trigerred the change the stored filename of the added file. The user can triggered the
+ skip of the add. This is adding a 'skipped' status in the file list result value.
+ - PCLZIP_CB_POST_ADD : will be called after each add of a file.
+ Nothing can be triggered from that point.
+ - Two status are added in the file list returned as function result : skipped & filename_too_long
+ 'skipped' is used when a call-back function ask for skipping the file.
+ 'filename_too_long' is used while adding a file with a too long filename to archive (the file is
+ not added)
+ - Adding the function PclZipUtilPathInclusion(), that check the inclusion of a path into
+ a directory.
+ - Add a check of the presence of the archive file before some actions (like list, ...)
+ - Add the initialisation of field "index" in header array. This means that by
+ default index will be -1 when not explicitly set by the methods.
+
+ Version 1.2 :
+ - Adding a duplicate function.
+ - Adding a merge function. The merge function is a "quick merge" function,
+ it just append the content of an archive at the end of the first one. There
+ is no check for duplicate files or more recent files.
+ - Improve the search of the central directory end.
+
+ Version 1.1.2 :
+
+ - Changing the license of PclZip. PclZip is now released under the GNU / LGPL license
+ (see License section).
+ - Adding the optional support of a static temporary directory. You will need to configure
+ the constant PCLZIP_TEMPORARY_DIR if you want to use this feature.
+ - Improving the rename() function. In some cases rename() does not work (different
+ Filesystems), so it will be replaced by a copy() + unlink() functions.
+
+ Version 1.1.1 :
+
+ - Maintenance release, no new feature.
+
+ Version 1.1 :
+
+ - New method Add() : adding files in the archive
+ - New method ExtractByIndex() : partial extract of the archive, files are identified by
+ their index in the archive
+ - New method DeleteByIndex() : delete some files/folder entries from the archive,
+ files are identified by their index in the archive.
+ - Adding a test of the zlib extension presence. If not present abort the script.
+
+ Version 1.0.1 :
+
+ - No new feature
+
+
+3 - Corrected bugs
+==================
+
+ Corrected in Version 2.0 :
+ - Corrected : During an extraction, if a call-back fucntion is used and try to skip
+ a file, all the extraction process is stopped.
+
+ Corrected in Version 1.3 :
+ - Corrected : Support of static synopsis for method extract() is broken.
+ - Corrected : invalid size of archive content field (0xFF) should be (0xFFFF).
+ - Corrected : When an extract is done with a remove_path parameter, the entry for
+ the directory with exactly the same path is not skipped/filtered.
+ - Corrected : extractByIndex() and deleteByIndex() were not managing index in the
+ right way. For example indexes '1,3-5,11' will only extract files 1 and 11. This
+ is due to a sort of the index resulting table that puts 11 before 3-5 (sort on
+ string and not interger). The sort is temporarilly removed, this means that
+ you must provide a sorted list of index ranges.
+
+ Corrected in Version 1.2 :
+
+ - Nothing.
+
+ Corrected in Version 1.1.2 :
+
+ - Corrected : Winzip is unable to delete or add new files in a PclZip created archives.
+
+ Corrected in Version 1.1.1 :
+
+ - Corrected : When archived file is not compressed (0% compression), the
+ extract method fails.
+
+ Corrected in Version 1.1 :
+
+ - Corrected : Adding a complete tree of folder may result in a bad archive
+ creation.
+
+ Corrected in Version 1.0.1 :
+
+ - Corrected : Error while compressing files greater than PCLZIP_READ_BLOCK_SIZE (default=1024).
+
+
+4 - Known bugs or limitations
+=============================
+
+ Please publish bugs reports in SourceForge :
+ http://sourceforge.net/tracker/?group_id=40254&atid=427564
+
+ In Version 2.x :
+ - PclZip does only support file uncompressed or compressed with deflate (compression method 8)
+ - PclZip does not support password protected zip archive
+ - Some concern were seen when changing mtime of a file while archiving.
+ Seems to be linked to Daylight Saving Time (PclTest_changing_mtime).
+
+ In Version 1.2 :
+
+ - merge() methods does not check for duplicate files or last date of modifications.
+
+ In Version 1.1 :
+
+ - Limitation : Using 'extract' fields in the file header in the zip archive is not supported.
+ - WinZip is unable to delete a single file in a PclZip created archive. It is also unable to
+ add a file in a PclZip created archive. (Corrected in v.1.2)
+
+ In Version 1.0.1 :
+
+ - Adding a complete tree of folder may result in a bad archive
+ creation. (Corrected in V.1.1).
+ - Path given to methods must be in the unix format (/) and not the Windows format (\).
+ Workaround : Use only / directory separators.
+ - PclZip is using temporary files that are sometime the name of the file with a .tmp or .gz
+ added suffix. Files with these names may already exist and may be overwritten.
+ Workaround : none.
+ - PclZip does not check if the zlib extension is present. If it is absent, the zip
+ file is not created and the lib abort without warning.
+ Workaround : enable the zlib extension on the php install
+
+ In Version 1.0 :
+
+ - Error while compressing files greater than PCLZIP_READ_BLOCK_SIZE (default=1024).
+ (Corrected in v.1.0.1)
+ - Limitation : Multi-disk zip archive are not supported.
+
+
+5 - License
+===========
+
+ Since version 1.1.2, PclZip Library is released under GNU/LGPL license.
+ This library is free, so you can use it at no cost.
+
+ HOWEVER, if you release a script, an application, a library or any kind of
+ code using PclZip library (or a part of it), YOU MUST :
+ - Indicate in the documentation (or a readme file), that your work
+ uses PclZip Library, and make a reference to the author and the web site
+ http://www.phpconcept.net
+ - Gives the ability to the final user to update the PclZip libary.
+
+ I will also appreciate that you send me a mail (vincent@phpconcept.net), just to
+ be aware that someone is using PclZip.
+
+ For more information about GNU/LGPL license : http://www.gnu.org
+
+6 - Warning
+=================
+
+ This library and the associated files are non commercial, non professional work.
+ It should not have unexpected results. However if any damage is caused by this software
+ the author can not be responsible.
+ The use of this software is at the risk of the user.
+
+7 - Documentation
+=================
+ PclZip User Manuel is available in English on PhpConcept : http://www.phpconcept.net/pclzip/man/en/index.php
+ A Russian translation was done by Feskov Kuzma : http://php.russofile.ru/ru/authors/unsort/zip/
+
+8 - Author
+==========
+
+ This software was written by Vincent Blavet (vincent@phpconcept.net) on its leasure time.
+
+9 - Contribute
+==============
+ If you want to contribute to the development of PclZip, please contact vincent@phpconcept.net.
+ If you can help in financing PhpConcept hosting service, please go to
+ http://www.phpconcept.net/soutien.php
diff --git a/lib/vendor/phpmailer/phpmailer/COMMITMENT b/lib/vendor/phpmailer/phpmailer/COMMITMENT
new file mode 100644
index 0000000000..a687e0ddb6
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/COMMITMENT
@@ -0,0 +1,46 @@
+GPL Cooperation Commitment
+Version 1.0
+
+Before filing or continuing to prosecute any legal proceeding or claim
+(other than a Defensive Action) arising from termination of a Covered
+License, we commit to extend to the person or entity ('you') accused
+of violating the Covered License the following provisions regarding
+cure and reinstatement, taken from GPL version 3. As used here, the
+term 'this License' refers to the specific Covered License being
+enforced.
+
+ However, if you cease all violation of this License, then your
+ license from a particular copyright holder is reinstated (a)
+ provisionally, unless and until the copyright holder explicitly
+ and finally terminates your license, and (b) permanently, if the
+ copyright holder fails to notify you of the violation by some
+ reasonable means prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+ reinstated permanently if the copyright holder notifies you of the
+ violation by some reasonable means, this is the first time you
+ have received notice of violation of this License (for any work)
+ from that copyright holder, and you cure the violation prior to 30
+ days after your receipt of the notice.
+
+We intend this Commitment to be irrevocable, and binding and
+enforceable against us and assignees of or successors to our
+copyrights.
+
+Definitions
+
+'Covered License' means the GNU General Public License, version 2
+(GPLv2), the GNU Lesser General Public License, version 2.1
+(LGPLv2.1), or the GNU Library General Public License, version 2
+(LGPLv2), all as published by the Free Software Foundation.
+
+'Defensive Action' means a legal proceeding or claim that We bring
+against you in response to a prior proceeding or claim initiated by
+you or your affiliate.
+
+'We' means each contributor to this repository as of the date of
+inclusion of this file, including subsidiaries of a corporate
+contributor.
+
+This work is available under a Creative Commons Attribution-ShareAlike
+4.0 International license (https://creativecommons.org/licenses/by-sa/4.0/).
diff --git a/lib/vendor/phpmailer/phpmailer/LICENSE b/lib/vendor/phpmailer/phpmailer/LICENSE
new file mode 100644
index 0000000000..f166cc57b2
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/LICENSE
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ , 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
\ No newline at end of file
diff --git a/lib/vendor/phpmailer/phpmailer/README.md b/lib/vendor/phpmailer/phpmailer/README.md
new file mode 100644
index 0000000000..862a4e1a42
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/README.md
@@ -0,0 +1,232 @@
+[](https://supportukrainenow.org/)
+
+
+
+# PHPMailer – A full-featured email creation and transfer class for PHP
+
+[](https://github.com/PHPMailer/PHPMailer/actions)
+[](https://codecov.io/gh/PHPMailer/PHPMailer)
+[](https://packagist.org/packages/phpmailer/phpmailer)
+[](https://packagist.org/packages/phpmailer/phpmailer)
+[](https://packagist.org/packages/phpmailer/phpmailer)
+[](https://phpmailer.github.io/PHPMailer/)
+[](https://api.securityscorecards.dev/projects/github.com/PHPMailer/PHPMailer)
+
+## Features
+- Probably the world's most popular code for sending email from PHP!
+- Used by many open-source projects: WordPress, Drupal, 1CRM, SugarCRM, Yii, Joomla! and many more
+- Integrated SMTP support – send without a local mail server
+- Send emails with multiple To, CC, BCC, and Reply-to addresses
+- Multipart/alternative emails for mail clients that do not read HTML email
+- Add attachments, including inline
+- Support for UTF-8 content and 8bit, base64, binary, and quoted-printable encodings
+- Full UTF-8 support when using servers that support `SMTPUTF8`.
+- Support for iCal events in multiparts and attachments
+- SMTP authentication with `LOGIN`, `PLAIN`, `CRAM-MD5`, and `XOAUTH2` mechanisms over SMTPS and SMTP+STARTTLS transports
+- Validates email addresses automatically
+- Protects against header injection attacks
+- Error messages in over 50 languages!
+- DKIM and S/MIME signing support
+- Compatible with PHP 5.5 and later, including PHP 8.4
+- Namespaced to prevent name clashes
+- Much more!
+
+## Why you might need it
+Many PHP developers need to send email from their code. The only PHP function that supports this directly is [`mail()`](https://www.php.net/manual/en/function.mail.php). However, it does not provide any assistance for making use of popular features such as authentication, HTML messages, and attachments.
+
+Formatting email correctly is surprisingly difficult. There are myriad overlapping (and conflicting) standards, requiring tight adherence to horribly complicated formatting and encoding rules – the vast majority of code that you'll find online that uses the `mail()` function directly is just plain wrong, if not unsafe!
+
+The PHP `mail()` function usually sends via a local mail server, typically fronted by a `sendmail` binary on Linux, BSD, and macOS platforms, however, Windows usually doesn't include a local mail server; PHPMailer's integrated SMTP client allows email sending on all platforms without needing a local mail server. Be aware though, that the `mail()` function should be avoided when possible; it's both faster and [safer](https://exploitbox.io/paper/Pwning-PHP-Mail-Function-For-Fun-And-RCE.html) to use SMTP to localhost.
+
+*Please* don't be tempted to do it yourself – if you don't use PHPMailer, there are many other excellent libraries that
+you should look at before rolling your own. Try [Symfony Mailer](https://symfony.com/doc/current/mailer.html), [Laminas/Mail](https://docs.laminas.dev/laminas-mail/), [ZetaComponents](https://github.com/zetacomponents/Mail), etc.
+
+## License
+This software is distributed under the [LGPL 2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) license, along with the [GPL Cooperation Commitment](https://gplcc.github.io/gplcc/). Please read [LICENSE](https://github.com/PHPMailer/PHPMailer/blob/master/LICENSE) for information on the software availability and distribution.
+
+## Installation & loading
+PHPMailer is available on [Packagist](https://packagist.org/packages/phpmailer/phpmailer) (using semantic versioning), and installation via [Composer](https://getcomposer.org) is the recommended way to install PHPMailer. Just add this line to your `composer.json` file:
+
+```json
+"phpmailer/phpmailer": "^6.10.0"
+```
+
+or run
+
+```sh
+composer require phpmailer/phpmailer
+```
+
+Note that the `vendor` folder and the `vendor/autoload.php` script are generated by Composer; they are not part of PHPMailer.
+
+If you want to use XOAUTH2 authentication, you will also need to add a dependency on the `league/oauth2-client` and appropriate service adapters package in your `composer.json`, or take a look at
+by @decomplexity's [SendOauth2 wrapper](https://github.com/decomplexity/SendOauth2), especially if you're using Microsoft services.
+
+Alternatively, if you're not using Composer, you
+can [download PHPMailer as a zip file](https://github.com/PHPMailer/PHPMailer/archive/master.zip), (note that docs and examples are not included in the zip file), then copy the contents of the PHPMailer folder into one of the `include_path` directories specified in your PHP configuration and load each class file manually:
+
+```php
+SMTPDebug = SMTP::DEBUG_SERVER; //Enable verbose debug output
+ $mail->isSMTP(); //Send using SMTP
+ $mail->Host = 'smtp.example.com'; //Set the SMTP server to send through
+ $mail->SMTPAuth = true; //Enable SMTP authentication
+ $mail->Username = 'user@example.com'; //SMTP username
+ $mail->Password = 'secret'; //SMTP password
+ $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; //Enable implicit TLS encryption
+ $mail->Port = 465; //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`
+
+ //Recipients
+ $mail->setFrom('from@example.com', 'Mailer');
+ $mail->addAddress('joe@example.net', 'Joe User'); //Add a recipient
+ $mail->addAddress('ellen@example.com'); //Name is optional
+ $mail->addReplyTo('info@example.com', 'Information');
+ $mail->addCC('cc@example.com');
+ $mail->addBCC('bcc@example.com');
+
+ //Attachments
+ $mail->addAttachment('/var/tmp/file.tar.gz'); //Add attachments
+ $mail->addAttachment('/tmp/image.jpg', 'new.jpg'); //Optional name
+
+ //Content
+ $mail->isHTML(true); //Set email format to HTML
+ $mail->Subject = 'Here is the subject';
+ $mail->Body = 'This is the HTML message body in bold!';
+ $mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
+
+ $mail->send();
+ echo 'Message has been sent';
+} catch (Exception $e) {
+ echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
+}
+```
+
+You'll find plenty to play with in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder, which covers many common scenarios including sending through Gmail, building contact forms, sending to mailing lists, and more.
+
+If you are re-using the instance (e.g. when sending to a mailing list), you may need to clear the recipient list to avoid sending duplicate messages. See [the mailing list example](https://github.com/PHPMailer/PHPMailer/blob/master/examples/mailing_list.phps) for further guidance.
+
+That's it. You should now be ready to use PHPMailer!
+
+## Localization
+PHPMailer defaults to English, but in the [language](https://github.com/PHPMailer/PHPMailer/tree/master/language/) folder, you'll find many translations for PHPMailer error messages that you may encounter. Their filenames contain [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) language code for the translations, for example `fr` for French. To specify a language, you need to tell PHPMailer which one to use, like this:
+
+```php
+//To load the French version
+$mail->setLanguage('fr', '/optional/path/to/language/directory/');
+```
+
+We welcome corrections and new languages – if you're looking for corrections, run the [Language/TranslationCompletenessTest.php](https://github.com/PHPMailer/PHPMailer/blob/master/test/Language/TranslationCompletenessTest.php) script in the tests folder and it will show any missing translations.
+
+## Documentation
+Start reading at the [GitHub wiki](https://github.com/PHPMailer/PHPMailer/wiki). If you're having trouble, head for [the troubleshooting guide](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting) as it's frequently updated.
+
+Examples of how to use PHPMailer for common scenarios can be found in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder. If you're looking for a good starting point, we recommend you start with [the Gmail example](https://github.com/PHPMailer/PHPMailer/tree/master/examples/gmail.phps).
+
+To reduce PHPMailer's deployed code footprint, examples are not included if you load PHPMailer via Composer or via [GitHub's zip file download](https://github.com/PHPMailer/PHPMailer/archive/master.zip), so you'll need to either clone the git repository or use the above links to get to the examples directly.
+
+Complete generated API documentation is [available online](https://phpmailer.github.io/PHPMailer/).
+
+You can generate complete API-level documentation by running `phpdoc` in the top-level folder, and documentation will appear in the `docs` folder, though you'll need to have [PHPDocumentor](https://www.phpdoc.org) installed. You may find [the unit tests](https://github.com/PHPMailer/PHPMailer/blob/master/test/PHPMailer/PHPMailerTest.php) a good reference for how to do various operations such as encryption.
+
+If the documentation doesn't cover what you need, search the [many questions on Stack Overflow](https://stackoverflow.com/questions/tagged/phpmailer), and before you ask a question about "SMTP Error: Could not connect to SMTP host.", [read the troubleshooting guide](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting).
+
+## Tests
+[PHPMailer tests](https://github.com/PHPMailer/PHPMailer/tree/master/test/) use PHPUnit 9, with [a polyfill](https://github.com/Yoast/PHPUnit-Polyfills) to let 9-style tests run on older PHPUnit and PHP versions.
+
+[](https://github.com/PHPMailer/PHPMailer/actions)
+
+If this isn't passing, is there something you can do to help?
+
+## Security
+Please disclose any vulnerabilities found responsibly – report security issues to the maintainers privately.
+
+See [SECURITY](https://github.com/PHPMailer/PHPMailer/tree/master/SECURITY.md) and [PHPMailer's security advisories on GitHub](https://github.com/PHPMailer/PHPMailer/security).
+
+## Contributing
+Please submit bug reports, suggestions, and pull requests to the [GitHub issue tracker](https://github.com/PHPMailer/PHPMailer/issues).
+
+We're particularly interested in fixing edge cases, expanding test coverage, and updating translations.
+
+If you found a mistake in the docs, or want to add something, go ahead and amend the wiki – anyone can edit it.
+
+If you have git clones from prior to the move to the PHPMailer GitHub organisation, you'll need to update any remote URLs referencing the old GitHub location with a command like this from within your clone:
+
+```sh
+git remote set-url upstream https://github.com/PHPMailer/PHPMailer.git
+```
+
+Please *don't* use the SourceForge or Google Code projects any more; they are obsolete and no longer maintained.
+
+## Sponsorship
+Development time and resources for PHPMailer are provided by [Smartmessages.net](https://info.smartmessages.net/), the world's only privacy-first email marketing system.
+
+
+
+Donations are very welcome, whether in beer 🍺, T-shirts 👕, or cold, hard cash 💰. Sponsorship through GitHub is a simple and convenient way to say "thank you" to PHPMailer's maintainers and contributors – just click the "Sponsor" button [on the project page](https://github.com/PHPMailer/PHPMailer). If your company uses PHPMailer, consider taking part in Tidelift's enterprise support programme.
+
+## PHPMailer For Enterprise
+
+Available as part of the Tidelift Subscription.
+
+The maintainers of PHPMailer and thousands of other packages are working with Tidelift to deliver commercial
+support and maintenance for the open-source packages you use to build your applications. Save time, reduce risk, and
+improve code health, while paying the maintainers of the exact packages you
+use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-phpmailer-phpmailer?utm_source=packagist-phpmailer-phpmailer&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
+
+## Changelog
+See [changelog](changelog.md).
+
+## History
+- PHPMailer was originally written in 2001 by Brent R. Matzelle as a [SourceForge project](https://sourceforge.net/projects/phpmailer/).
+- [Marcus Bointon](https://github.com/Synchro) (`coolbru` on SF) and Andy Prevost (`codeworxtech`) took over the project in 2004.
+- Became an Apache incubator project on Google Code in 2010, managed by Jim Jagielski.
+- Marcus created [his fork on GitHub](https://github.com/Synchro/PHPMailer) in 2008.
+- Jim and Marcus decide to join forces and use GitHub as the canonical and official repo for PHPMailer in 2013.
+- PHPMailer moves to [the PHPMailer organisation](https://github.com/PHPMailer) on GitHub in 2013.
+
+### What's changed since moving from SourceForge?
+- Official successor to the SourceForge and Google Code projects.
+- Test suite.
+- Continuous integration with GitHub Actions.
+- Composer support.
+- Public development.
+- Additional languages and language strings.
+- CRAM-MD5 authentication support.
+- Preserves full repo history of authors, commits, and branches from the original SourceForge project.
diff --git a/lib/vendor/phpmailer/phpmailer/SECURITY.md b/lib/vendor/phpmailer/phpmailer/SECURITY.md
new file mode 100644
index 0000000000..4f34026dfa
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/SECURITY.md
@@ -0,0 +1,37 @@
+# Security notices relating to PHPMailer
+
+Please disclose any security issues or vulnerabilities found through [Tidelift's coordinated disclosure system](https://tidelift.com/security) or to the maintainers privately.
+
+PHPMailer 6.4.1 and earlier contain a vulnerability that can result in untrusted code being called (if such code is injected into the host project's scope by other means). If the `$patternselect` parameter to `validateAddress()` is set to `'php'` (the default, defined by `PHPMailer::$validator`), and the global namespace contains a function called `php`, it will be called in preference to the built-in validator of the same name. Mitigated in PHPMailer 6.5.0 by denying the use of simple strings as validator function names. Recorded as [CVE-2021-3603](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3603). Reported by [Vikrant Singh Chauhan](mailto:vi@hackberry.xyz) via [huntr.dev](https://www.huntr.dev/).
+
+PHPMailer versions 6.4.1 and earlier contain a possible remote code execution vulnerability through the `$lang_path` parameter of the `setLanguage()` method. If the `$lang_path` parameter is passed unfiltered from user input, it can be set to [a UNC path](https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats#unc-paths), and if an attacker is also able to persuade the server to load a file from that UNC path, a script file under their control may be executed. This vulnerability only applies to systems that resolve UNC paths, typically only Microsoft Windows.
+PHPMailer 6.5.0 mitigates this by no longer treating translation files as PHP code, but by parsing their text content directly. This approach avoids the possibility of executing unknown code while retaining backward compatibility. This isn't ideal, so the current translation format is deprecated and will be replaced in the next major release. Recorded as [CVE-2021-34551](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-34551). Reported by [Jilin Diting Information Technology Co., Ltd](https://listensec.com) via Tidelift.
+
+PHPMailer versions between 6.1.8 and 6.4.0 contain a regression of the earlier CVE-2018-19296 object injection vulnerability as a result of [a fix for Windows UNC paths in 6.1.8](https://github.com/PHPMailer/PHPMailer/commit/e2e07a355ee8ff36aba21d0242c5950c56e4c6f9). Recorded as [CVE-2020-36326](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-36326). Reported by Fariskhi Vidyan via Tidelift. 6.4.1 fixes this issue, and also enforces stricter checks for URL schemes in local path contexts.
+
+PHPMailer versions 6.1.5 and earlier contain an output escaping bug that occurs in `Content-Type` and `Content-Disposition` when filenames passed into `addAttachment` and other methods that accept attachment names contain double quote characters, in contravention of RFC822 3.4.1. No specific vulnerability has been found relating to this, but it could allow file attachments to bypass attachment filters that are based on matching filename extensions. Recorded as [CVE-2020-13625](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-13625). Reported by Elar Lang of Clarified Security.
+
+PHPMailer versions prior to 6.0.6 and 5.2.27 are vulnerable to an object injection attack by passing `phar://` paths into `addAttachment()` and other functions that may receive unfiltered local paths, possibly leading to RCE. Recorded as [CVE-2018-19296](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-19296). See [this article](https://knasmueller.net/5-answers-about-php-phar-exploitation) for more info on this type of vulnerability. Mitigated by blocking the use of paths containing URL-protocol style prefixes such as `phar://`. Reported by Sehun Oh of cyberone.kr.
+
+PHPMailer versions prior to 5.2.24 (released July 26th 2017) have an XSS vulnerability in one of the code examples, [CVE-2017-11503](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-11503). The `code_generator.phps` example did not filter user input prior to output. This file is distributed with a `.phps` extension, so it is not normally executable unless it is explicitly renamed, and the file is not included when PHPMailer is loaded through composer, so it is safe by default. There was also an undisclosed potential XSS vulnerability in the default exception handler (unused by default). Patches for both issues kindly provided by Patrick Monnerat of the Fedora Project.
+
+PHPMailer versions prior to 5.2.22 (released January 9th 2017) have a local file disclosure vulnerability, [CVE-2017-5223](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5223). If content passed into `msgHTML()` is sourced from unfiltered user input, relative paths can map to absolute local file paths and added as attachments. Also note that `addAttachment` (just like `file_get_contents`, `passthru`, `unlink`, etc) should not be passed user-sourced params either! Reported by Yongxiang Li of Asiasecurity.
+
+PHPMailer versions prior to 5.2.20 (released December 28th 2016) are vulnerable to [CVE-2016-10045](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10045) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10045-Vuln-Patch-Bypass.html), and patched by Paul Buonopane (@Zenexer).
+
+PHPMailer versions prior to 5.2.18 (released December 2016) are vulnerable to [CVE-2016-10033](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10033) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html).
+
+PHPMailer versions prior to 5.2.14 (released November 2015) are vulnerable to [CVE-2015-8476](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-8476) an SMTP CRLF injection bug permitting arbitrary message sending.
+
+PHPMailer versions prior to 5.2.10 (released May 2015) are vulnerable to [CVE-2008-5619](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2008-5619), a remote code execution vulnerability in the bundled html2text library. This file was removed in 5.2.10, so if you are using a version prior to that and make use of the html2text function, it's vitally important that you upgrade and remove this file.
+
+PHPMailer versions prior to 2.0.7 and 2.2.1 are vulnerable to [CVE-2012-0796](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2012-0796), an email header injection attack.
+
+Joomla 1.6.0 uses PHPMailer in an unsafe way, allowing it to reveal local file paths, reported in [CVE-2011-3747](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2011-3747).
+
+PHPMailer didn't sanitise the `$lang_path` parameter in `SetLanguage`. This wasn't a problem in itself, but some apps (PHPClassifieds, ATutor) also failed to sanitise user-provided parameters passed to it, permitting semi-arbitrary local file inclusion, reported in [CVE-2010-4914](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2010-4914), [CVE-2007-2021](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-2021) and [CVE-2006-5734](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2006-5734).
+
+PHPMailer 1.7.2 and earlier contained a possible DDoS vulnerability reported in [CVE-2005-1807](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2005-1807).
+
+PHPMailer 1.7 and earlier (June 2003) have a possible vulnerability in the `SendmailSend` method where shell commands may not be sanitised. Reported in [CVE-2007-3215](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-3215).
+
diff --git a/lib/vendor/phpmailer/phpmailer/SMTPUTF8.md b/lib/vendor/phpmailer/phpmailer/SMTPUTF8.md
new file mode 100644
index 0000000000..ca284ee216
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/SMTPUTF8.md
@@ -0,0 +1,48 @@
+# A short history of UTF-8 in email
+
+## Background
+
+For most of its existence, SMTP has been a 7-bit channel, only supporting US-ASCII characters. This has been a problem for many languages, especially those that use non-Latin scripts, and has led to the development of various workarounds.
+
+The first major improvement, introduced in 1994 in [RFC 1652](https://www.rfc-editor.org/rfc/rfc1652) and extended in 2011 in [RFC 6152](https://www.rfc-editor.org/rfc/rfc6152), was the addition of the `8BITMIME` SMTP extension, which allowed raw 8-bit data to be included in message bodies sent over SMTP.
+This allowed the message *contents* to contain 8-bit data, including things like UTF-8 text, even though the SMTP protocol itself was still firmly 7-bit. This worked by having the server switch to 8-bit after the headers, and then back to 7-bit after the completion of a `DATA` command.
+
+From 1996, messages could support [RFC 2047 encoding](https://www.rfc-editor.org/rfc/rfc2047), which permitted inserting characters from any character set into header *values* (but not names), but only by encoding them in somewhat unreadable ways to allow them to survive passage through a 7-bit channel. An example with a subject of "Schrödinger's cat" would be:
+
+```
+Subject: =?utf-8?Q=Schr=C3=B6dinger=92s_Cat?=
+```
+
+Here the accented `ö` is encoded as `=C3=B6`, which is the UTF-8 encoding of the 2-byte character, and the whole thing is wrapped in `=?utf-8?Q?` to indicate that it uses the UTF-8 charset and `quoted-printable` encoding. This is a bit of a hack, and not very human-friendly, but it works.
+
+Similarly, 8-bit message bodies could be encoded using the same `quoted-printable` and `base64` content transfer encoding (CTE) schemes, which preserved the 8-bit content while encoding it in a format that could survive transmission through a 7-bit channel.
+
+Domain names were originally also stuck in a 7-bit world, actually even more constrained to only a subset of the US-ASCII character set. But of course, many people want to have domains in their own language/script. Internationalized domain name (IDN) permitted this, using yet another complex encoding scheme called punycode, defined for domain names in 2003 in [RFC 3492](https://www.rfc-editor.org/rfc/rfc3492). This finally allowed the domain part (after the `@`) of email addresses to contain UTF-8, though it was actually an illusion preserved by email client applications. For example, an address of
+`user@café.example.com` translates to
+`user@xn--caf-dma.example.com` in punycode, rendering it mostly unreadable, but 7-bit friendly, and remaining compatible with email clients that don't know about IDN.
+
+The one remaining part of email that could not handle UTF-8 is the local part of email addresses (the part before the `@`).
+
+I've only mentioned UTF-8 here, but most of these approaches also allowed other character sets that were popular, such as [the ISO-8859 family](https://en.wikipedia.org/wiki/ISO/IEC_8859). However, UTF-8 solves so many problems that these other character sets are gradually falling out of favour, as UTF-8 can support all languages.
+
+This patchwork of overlapping approaches has served us well, but we have to admit that it's a mess.
+
+## SMTPUTF8
+
+`SMTPUTF8` is another SMTP extension, defined in [RFC 6531](https://www.rfc-editor.org/rfc/rfc6531) in 2012. This essentially solves the whole problem, allowing the entire SMTP conversation — commands, headers, and message bodies — to be sent in raw, unencoded UTF-8.
+
+But there's a problem with this approach: adoption. If you send a UTF-8 message to a recipient whose mail server doesn't support this format, the sender has to somehow downgrade the message to make it survive a transition to 7-bit. This is a hard problem to solve, especially since there is no way to make a 7-bit system support UTF-8 in the local parts of addresses. This downgrade problem is what held up the adoption of `SMTPUTF8` in PHPMailer for many years, but in that time the *de facto* approach has become to simply fail in that situation, and tell the recipient it's time they upgraded their mail server 😅.
+
+The vast majority of large email providers (gmail, Yahoo, Microsoft, etc), mail servers (postfix, exim, IIS, etc), and mail clients (Apple Mail, Outlook, Thunderbird, etc) now all support SMTPUTF8, so the need for backward compatibility is no longer what it was.
+
+## SMTPUTF8 in PHPMailer
+
+Several other PHP email libraries have implemented a halfway solution to `SMTPUTF8`, adding only the ability to support UTF-8 in email addresses, not elsewhere in the protocol. I wanted PHPMailer to do it "the right way", and this has taken much longer. PHPMailer now supports UTF-8 everywhere, and does not need to use transfer or header encodings for UTF-8 text when connecting to an `SMTPUTF8`-capable mail server.
+
+This support is handled automatically: if you add an email address that requires UTF-8, PHPMailer will use UTF-8 for everything. If not, it will fall back to 7-bit and encode the message as necessary.
+
+The one place you will need to be careful is in the selection of the address validator. By default, PHPMailer uses PHP's built-in `filter_var` validator, which does not allow UTF-8 email addresses. When PHPMailer spots that you have submitted a UTF-8 address, but have not altered the default validator, it will automatically switch to using a UTF-8-compatible validator. As soon as you do this, any SMTP connection you make will *require* that the server you connect to supports `SMTPUTF8`. You can select this validator explicitly by setting `PHPMailer::$validator = 'eai'` (an acronym for Email Address Internationalization).
+
+### Postfix gotcha
+
+Postfix has supported `SMTPUTF8` for a long time, but it has a peculiarity that it does not always advertise that it does so. However, rather surprisingly, if you use UTF-8 in the conversation, it will work anyway.
diff --git a/lib/vendor/phpmailer/phpmailer/VERSION b/lib/vendor/phpmailer/phpmailer/VERSION
new file mode 100644
index 0000000000..cf79bf90ee
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/VERSION
@@ -0,0 +1 @@
+6.10.0
diff --git a/lib/vendor/phpmailer/phpmailer/composer.json b/lib/vendor/phpmailer/phpmailer/composer.json
new file mode 100644
index 0000000000..7b008b7c57
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/composer.json
@@ -0,0 +1,80 @@
+{
+ "name": "phpmailer/phpmailer",
+ "type": "library",
+ "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+ "authors": [
+ {
+ "name": "Marcus Bointon",
+ "email": "phpmailer@synchromedia.co.uk"
+ },
+ {
+ "name": "Jim Jagielski",
+ "email": "jimjag@gmail.com"
+ },
+ {
+ "name": "Andy Prevost",
+ "email": "codeworxtech@users.sourceforge.net"
+ },
+ {
+ "name": "Brent R. Matzelle"
+ }
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/Synchro",
+ "type": "github"
+ }
+ ],
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ },
+ "lock": false
+ },
+ "require": {
+ "php": ">=5.5.0",
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-hash": "*"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "doctrine/annotations": "^1.2.6 || ^1.13.3",
+ "php-parallel-lint/php-console-highlighter": "^1.0.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "phpcompatibility/php-compatibility": "^9.3.5",
+ "roave/security-advisories": "dev-latest",
+ "squizlabs/php_codesniffer": "^3.7.2",
+ "yoast/phpunit-polyfills": "^1.0.4"
+ },
+ "suggest": {
+ "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
+ "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+ "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+ "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+ "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+ "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+ "psr/log": "For optional PSR-3 debug logging",
+ "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication",
+ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
+ },
+ "autoload": {
+ "psr-4": {
+ "PHPMailer\\PHPMailer\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "PHPMailer\\Test\\": "test/"
+ }
+ },
+ "license": "LGPL-2.1-only",
+ "scripts": {
+ "check": "./vendor/bin/phpcs",
+ "test": "./vendor/bin/phpunit --no-coverage",
+ "coverage": "./vendor/bin/phpunit",
+ "lint": [
+ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . --show-deprecated -e php,phps --exclude vendor --exclude .git --exclude build"
+ ]
+ }
+}
diff --git a/lib/vendor/phpmailer/phpmailer/get_oauth_token.php b/lib/vendor/phpmailer/phpmailer/get_oauth_token.php
new file mode 100644
index 0000000000..0e54a00b63
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/get_oauth_token.php
@@ -0,0 +1,182 @@
+
+ * @author Jim Jagielski (jimjag)
+ * @author Andy Prevost (codeworxtech)
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+/**
+ * Get an OAuth2 token from an OAuth2 provider.
+ * * Install this script on your server so that it's accessible
+ * as [https/http]:////get_oauth_token.php
+ * e.g.: http://localhost/phpmailer/get_oauth_token.php
+ * * Ensure dependencies are installed with 'composer install'
+ * * Set up an app in your Google/Yahoo/Microsoft account
+ * * Set the script address as the app's redirect URL
+ * If no refresh token is obtained when running this file,
+ * revoke access to your app and run the script again.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * Aliases for League Provider Classes
+ * Make sure you have added these to your composer.json and run `composer install`
+ * Plenty to choose from here:
+ * @see https://oauth2-client.thephpleague.com/providers/thirdparty/
+ */
+//@see https://github.com/thephpleague/oauth2-google
+use League\OAuth2\Client\Provider\Google;
+//@see https://packagist.org/packages/hayageek/oauth2-yahoo
+use Hayageek\OAuth2\Client\Provider\Yahoo;
+//@see https://github.com/stevenmaguire/oauth2-microsoft
+use Stevenmaguire\OAuth2\Client\Provider\Microsoft;
+//@see https://github.com/greew/oauth2-azure-provider
+use Greew\OAuth2\Client\Provider\Azure;
+
+if (!isset($_GET['code']) && !isset($_POST['provider'])) {
+ ?>
+
+
+
+
+
+ $clientId,
+ 'clientSecret' => $clientSecret,
+ 'redirectUri' => $redirectUri,
+ 'accessType' => 'offline'
+];
+
+$options = [];
+$provider = null;
+
+switch ($providerName) {
+ case 'Google':
+ $provider = new Google($params);
+ $options = [
+ 'scope' => [
+ 'https://mail.google.com/'
+ ]
+ ];
+ break;
+ case 'Yahoo':
+ $provider = new Yahoo($params);
+ break;
+ case 'Microsoft':
+ $provider = new Microsoft($params);
+ $options = [
+ 'scope' => [
+ 'wl.imap',
+ 'wl.offline_access'
+ ]
+ ];
+ break;
+ case 'Azure':
+ $params['tenantId'] = $tenantId;
+
+ $provider = new Azure($params);
+ $options = [
+ 'scope' => [
+ 'https://outlook.office.com/SMTP.Send',
+ 'offline_access'
+ ]
+ ];
+ break;
+}
+
+if (null === $provider) {
+ exit('Provider missing');
+}
+
+if (!isset($_GET['code'])) {
+ //If we don't have an authorization code then get one
+ $authUrl = $provider->getAuthorizationUrl($options);
+ $_SESSION['oauth2state'] = $provider->getState();
+ header('Location: ' . $authUrl);
+ exit;
+ //Check given state against previously stored one to mitigate CSRF attack
+} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
+ unset($_SESSION['oauth2state']);
+ unset($_SESSION['provider']);
+ exit('Invalid state');
+} else {
+ unset($_SESSION['provider']);
+ //Try to get an access token (using the authorization code grant)
+ $token = $provider->getAccessToken(
+ 'authorization_code',
+ [
+ 'code' => $_GET['code']
+ ]
+ );
+ //Use this to interact with an API on the users behalf
+ //Use this to get a new access token if the old one expires
+ echo 'Refresh Token: ', htmlspecialchars($token->getRefreshToken());
+}
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-af.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-af.php
new file mode 100644
index 0000000000..0b2a72d524
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-af.php
@@ -0,0 +1,26 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'خطأ SMTP : لا يمكن تأكيد الهوية.';
+$PHPMAILER_LANG['connect_host'] = 'خطأ SMTP: لا يمكن الاتصال بالخادم SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'خطأ SMTP: لم يتم قبول المعلومات .';
+$PHPMAILER_LANG['empty_message'] = 'نص الرسالة فارغ';
+$PHPMAILER_LANG['encoding'] = 'ترميز غير معروف: ';
+$PHPMAILER_LANG['execute'] = 'لا يمكن تنفيذ : ';
+$PHPMAILER_LANG['file_access'] = 'لا يمكن الوصول للملف: ';
+$PHPMAILER_LANG['file_open'] = 'خطأ في الملف: لا يمكن فتحه: ';
+$PHPMAILER_LANG['from_failed'] = 'خطأ على مستوى عنوان المرسل : ';
+$PHPMAILER_LANG['instantiate'] = 'لا يمكن توفير خدمة البريد.';
+$PHPMAILER_LANG['invalid_address'] = 'الإرسال غير ممكن لأن عنوان البريد الإلكتروني غير صالح: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' برنامج الإرسال غير مدعوم.';
+$PHPMAILER_LANG['provide_address'] = 'يجب توفير عنوان البريد الإلكتروني لمستلم واحد على الأقل.';
+$PHPMAILER_LANG['recipients_failed'] = 'خطأ SMTP: الأخطاء التالية فشل في الارسال لكل من : ';
+$PHPMAILER_LANG['signing'] = 'خطأ في التوقيع: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() غير ممكن.';
+$PHPMAILER_LANG['smtp_error'] = 'خطأ على مستوى الخادم SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'لا يمكن تعيين أو إعادة تعيين متغير: ';
+$PHPMAILER_LANG['extension_missing'] = 'الإضافة غير موجودة: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-as.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-as.php
new file mode 100644
index 0000000000..327dfbafaf
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-as.php
@@ -0,0 +1,35 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP ত্ৰুটি: প্ৰমাণীকৰণ কৰিব নোৱাৰি';
+$PHPMAILER_LANG['buggy_php'] = 'আপোনাৰ PHP সংস্কৰণ এটা বাগৰ দ্বাৰা প্ৰভাৱিত হয় যাৰ ফলত নষ্ট বাৰ্তা হব পাৰে । ইয়াক সমাধান কৰিবলে, প্ৰেৰণ কৰিবলে SMTP ব্যৱহাৰ কৰক, আপোনাৰ php.ini ত mail.add_x_header বিকল্প নিষ্ক্ৰিয় কৰক, MacOS বা Linux লৈ সলনি কৰক, বা আপোনাৰ PHP সংস্কৰণ 7.0.17+ বা 7.1.3+ লৈ সলনি কৰক ।';
+$PHPMAILER_LANG['connect_host'] = 'SMTP ত্ৰুটি: SMTP চাৰ্ভাৰৰ সৈতে সংযোগ কৰিবলে অক্ষম';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP ত্ৰুটি: তথ্য গ্ৰহণ কৰা হোৱা নাই';
+$PHPMAILER_LANG['empty_message'] = 'বাৰ্তাৰ মূখ্য অংশ খালী।';
+$PHPMAILER_LANG['encoding'] = 'অজ্ঞাত এনকোডিং: ';
+$PHPMAILER_LANG['execute'] = 'এক্সিকিউট কৰিব নোৱাৰি: ';
+$PHPMAILER_LANG['extension_missing'] = 'সম্প্ৰসাৰণ নোহোৱা হৈছে: ';
+$PHPMAILER_LANG['file_access'] = 'ফাইল অভিগম কৰিবলে অক্ষম: ';
+$PHPMAILER_LANG['file_open'] = 'ফাইল ত্ৰুটি: ফাইল খোলিবলৈ অক্ষম: ';
+$PHPMAILER_LANG['from_failed'] = 'নিম্নলিখিত প্ৰেৰকৰ ঠিকনা(সমূহ) ব্যৰ্থ: ';
+$PHPMAILER_LANG['instantiate'] = 'মেইল ফাংচনৰ এটা উদাহৰণ সৃষ্টি কৰিবলে অক্ষম';
+$PHPMAILER_LANG['invalid_address'] = 'প্ৰেৰণ কৰিব নোৱাৰি: অবৈধ ইমেইল ঠিকনা: ';
+$PHPMAILER_LANG['invalid_header'] = 'অবৈধ হেডাৰৰ নাম বা মান';
+$PHPMAILER_LANG['invalid_hostentry'] = 'অবৈধ হোষ্টেন্ট্ৰি: ';
+$PHPMAILER_LANG['invalid_host'] = 'অবৈধ হস্ট:';
+$PHPMAILER_LANG['mailer_not_supported'] = 'মেইলাৰ সমৰ্থিত নহয়।';
+$PHPMAILER_LANG['provide_address'] = 'আপুনি অন্ততঃ এটা গন্তব্য ইমেইল ঠিকনা দিব লাগিব';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP ত্ৰুটি: নিম্নলিখিত গন্তব্যস্থানসমূহ ব্যৰ্থ: ';
+$PHPMAILER_LANG['signing'] = 'স্বাক্ষৰ কৰাত ব্যৰ্থ: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP কড: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'অতিৰিক্ত SMTP তথ্য: ';
+$PHPMAILER_LANG['smtp_detail'] = 'বিৱৰণ:';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP সংযোগ() ব্যৰ্থ';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP চাৰ্ভাৰৰ ত্ৰুটি: ';
+$PHPMAILER_LANG['variable_set'] = 'চলক নিৰ্ধাৰণ কৰিব পৰা নগল: ';
+$PHPMAILER_LANG['extension_missing'] = 'অনুপস্থিত সম্প্ৰসাৰণ: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-az.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-az.php
new file mode 100644
index 0000000000..552167ef62
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-az.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela prijava.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Nije moguće spojiti se sa SMTP serverom.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.';
+$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.';
+$PHPMAILER_LANG['encoding'] = 'Nepoznata kriptografija: ';
+$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: ';
+$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: ';
+$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: ';
+$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje sa navedenih e-mail adresa nije uspjelo: ';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedene e-mail adrese nije uspjelo: ';
+$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.';
+$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.';
+$PHPMAILER_LANG['provide_address'] = 'Definišite barem jednu adresu primaoca.';
+$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP server nije uspjelo.';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP greška: ';
+$PHPMAILER_LANG['variable_set'] = 'Nije moguće postaviti varijablu ili je vratiti nazad: ';
+$PHPMAILER_LANG['extension_missing'] = 'Nedostaje ekstenzija: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-be.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-be.php
new file mode 100644
index 0000000000..9e92ddaaf7
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-be.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Памылка SMTP: памылка ідэнтыфікацыі.';
+$PHPMAILER_LANG['connect_host'] = 'Памылка SMTP: нельга ўстанавіць сувязь з SMTP-серверам.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Памылка SMTP: звесткі непрынятыя.';
+$PHPMAILER_LANG['empty_message'] = 'Пустое паведамленне.';
+$PHPMAILER_LANG['encoding'] = 'Невядомая кадыроўка тэксту: ';
+$PHPMAILER_LANG['execute'] = 'Нельга выканаць каманду: ';
+$PHPMAILER_LANG['file_access'] = 'Няма доступу да файла: ';
+$PHPMAILER_LANG['file_open'] = 'Нельга адкрыць файл: ';
+$PHPMAILER_LANG['from_failed'] = 'Няправільны адрас адпраўніка: ';
+$PHPMAILER_LANG['instantiate'] = 'Нельга прымяніць функцыю mail().';
+$PHPMAILER_LANG['invalid_address'] = 'Нельга даслаць паведамленне, няправільны email атрымальніка: ';
+$PHPMAILER_LANG['provide_address'] = 'Запоўніце, калі ласка, правільны email атрымальніка.';
+$PHPMAILER_LANG['mailer_not_supported'] = ' - паштовы сервер не падтрымліваецца.';
+$PHPMAILER_LANG['recipients_failed'] = 'Памылка SMTP: няправільныя атрымальнікі: ';
+$PHPMAILER_LANG['signing'] = 'Памылка подпісу паведамлення: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Памылка сувязі з SMTP-серверам.';
+$PHPMAILER_LANG['smtp_error'] = 'Памылка SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Нельга ўстанавіць або перамяніць значэнне пераменнай: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-bg.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-bg.php
new file mode 100644
index 0000000000..c41f675dfd
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-bg.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: Не може да се удостовери пред сървъра.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: Не може да се свърже с SMTP хоста.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: данните не са приети.';
+$PHPMAILER_LANG['empty_message'] = 'Съдържанието на съобщението е празно';
+$PHPMAILER_LANG['encoding'] = 'Неизвестно кодиране: ';
+$PHPMAILER_LANG['execute'] = 'Не може да се изпълни: ';
+$PHPMAILER_LANG['file_access'] = 'Няма достъп до файл: ';
+$PHPMAILER_LANG['file_open'] = 'Файлова грешка: Не може да се отвори файл: ';
+$PHPMAILER_LANG['from_failed'] = 'Следните адреси за подател са невалидни: ';
+$PHPMAILER_LANG['instantiate'] = 'Не може да се инстанцира функцията mail.';
+$PHPMAILER_LANG['invalid_address'] = 'Невалиден адрес: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' - пощенски сървър не се поддържа.';
+$PHPMAILER_LANG['provide_address'] = 'Трябва да предоставите поне един email адрес за получател.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: Следните адреси за Получател са невалидни: ';
+$PHPMAILER_LANG['signing'] = 'Грешка при подписване: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP провален connect().';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP сървърна грешка: ';
+$PHPMAILER_LANG['variable_set'] = 'Не може да се установи или възстанови променлива: ';
+$PHPMAILER_LANG['extension_missing'] = 'Липсва разширение: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-bn.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-bn.php
new file mode 100644
index 0000000000..4736510804
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-bn.php
@@ -0,0 +1,35 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP ত্রুটি: প্রমাণীকরণ করতে অক্ষম৷';
+$PHPMAILER_LANG['buggy_php'] = 'আপনার PHP সংস্করণ একটি বাগ দ্বারা প্রভাবিত হয় যার ফলে দূষিত বার্তা হতে পারে। এটি ঠিক করতে, পাঠাতে SMTP ব্যবহার করুন, আপনার php.ini এ mail.add_x_header বিকল্পটি নিষ্ক্রিয় করুন, MacOS বা Linux-এ স্যুইচ করুন, অথবা আপনার PHP সংস্করণকে 7.0.17+ বা 7.1.3+ এ পরিবর্তন করুন।';
+$PHPMAILER_LANG['connect_host'] = 'SMTP ত্রুটি: SMTP সার্ভারের সাথে সংযোগ করতে অক্ষম৷';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP ত্রুটি: ডেটা গ্রহণ করা হয়নি৷';
+$PHPMAILER_LANG['empty_message'] = 'বার্তার অংশটি খালি।';
+$PHPMAILER_LANG['encoding'] = 'অজানা এনকোডিং: ';
+$PHPMAILER_LANG['execute'] = 'নির্বাহ করতে অক্ষম: ';
+$PHPMAILER_LANG['extension_missing'] = 'এক্সটেনশন অনুপস্থিত:';
+$PHPMAILER_LANG['file_access'] = 'ফাইল অ্যাক্সেস করতে অক্ষম: ';
+$PHPMAILER_LANG['file_open'] = 'ফাইল ত্রুটি: ফাইল খুলতে অক্ষম: ';
+$PHPMAILER_LANG['from_failed'] = 'নিম্নলিখিত প্রেরকের ঠিকানা(গুলি) ব্যর্থ হয়েছে: ';
+$PHPMAILER_LANG['instantiate'] = 'মেল ফাংশনের একটি উদাহরণ তৈরি করতে অক্ষম৷';
+$PHPMAILER_LANG['invalid_address'] = 'পাঠাতে অক্ষম: অবৈধ ইমেল ঠিকানা: ';
+$PHPMAILER_LANG['invalid_header'] = 'অবৈধ হেডার নাম বা মান';
+$PHPMAILER_LANG['invalid_hostentry'] = 'অবৈধ হোস্টেন্ট্রি: ';
+$PHPMAILER_LANG['invalid_host'] = 'অবৈধ হোস্ট:';
+$PHPMAILER_LANG['mailer_not_supported'] = 'মেইলার সমর্থিত নয়।';
+$PHPMAILER_LANG['provide_address'] = 'আপনাকে অবশ্যই অন্তত একটি গন্তব্য ইমেল ঠিকানা প্রদান করতে হবে৷';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP ত্রুটি: নিম্নলিখিত গন্তব্যগুলি ব্যর্থ হয়েছে: ';
+$PHPMAILER_LANG['signing'] = 'স্বাক্ষর করতে ব্যর্থ হয়েছে: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP কোড: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'অতিরিক্ত SMTP তথ্য:';
+$PHPMAILER_LANG['smtp_detail'] = 'বর্ণনা: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP সংযোগ() ব্যর্থ হয়েছে৷';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP সার্ভার ত্রুটি: ';
+$PHPMAILER_LANG['variable_set'] = 'পরিবর্তনশীল সেট করা যায়নি: ';
+$PHPMAILER_LANG['extension_missing'] = 'অনুপস্থিত এক্সটেনশন: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ca.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ca.php
new file mode 100644
index 0000000000..34684855a5
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ca.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Error SMTP: No s’ha pogut autenticar.';
+$PHPMAILER_LANG['connect_host'] = 'Error SMTP: No es pot connectar al servidor SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Dades no acceptades.';
+$PHPMAILER_LANG['empty_message'] = 'El cos del missatge està buit.';
+$PHPMAILER_LANG['encoding'] = 'Codificació desconeguda: ';
+$PHPMAILER_LANG['execute'] = 'No es pot executar: ';
+$PHPMAILER_LANG['file_access'] = 'No es pot accedir a l’arxiu: ';
+$PHPMAILER_LANG['file_open'] = 'Error d’Arxiu: No es pot obrir l’arxiu: ';
+$PHPMAILER_LANG['from_failed'] = 'La(s) següent(s) adreces de remitent han fallat: ';
+$PHPMAILER_LANG['instantiate'] = 'No s’ha pogut crear una instància de la funció Mail.';
+$PHPMAILER_LANG['invalid_address'] = 'Adreça d’email invalida: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no està suportat';
+$PHPMAILER_LANG['provide_address'] = 'S’ha de proveir almenys una adreça d’email com a destinatari.';
+$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Els següents destinataris han fallat: ';
+$PHPMAILER_LANG['signing'] = 'Error al signar: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Ha fallat el SMTP Connect().';
+$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'No s’ha pogut establir o restablir la variable: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-cs.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-cs.php
new file mode 100644
index 0000000000..e770a1a265
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-cs.php
@@ -0,0 +1,28 @@
+
+ * Rewrite and extension of the work by Mikael Stokkebro
+ *
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP fejl: Login mislykkedes.';
+$PHPMAILER_LANG['buggy_php'] = 'Din version af PHP er berørt af en fejl, som gør at dine beskeder muligvis vises forkert. For at rette dette kan du skifte til SMTP, slå mail.add_x_header headeren i din php.ini fil fra, skifte til MacOS eller Linux eller opgradere din version af PHP til 7.0.17+ eller 7.1.3+.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP fejl: Forbindelse til SMTP serveren kunne ikke oprettes.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fejl: Data blev ikke accepteret.';
+$PHPMAILER_LANG['empty_message'] = 'Meddelelsen er uden indhold';
+$PHPMAILER_LANG['encoding'] = 'Ukendt encode-format: ';
+$PHPMAILER_LANG['execute'] = 'Kunne ikke afvikle: ';
+$PHPMAILER_LANG['extension_missing'] = 'Udvidelse mangler: ';
+$PHPMAILER_LANG['file_access'] = 'Kunne ikke tilgå filen: ';
+$PHPMAILER_LANG['file_open'] = 'Fil fejl: Kunne ikke åbne filen: ';
+$PHPMAILER_LANG['from_failed'] = 'Følgende afsenderadresse er forkert: ';
+$PHPMAILER_LANG['instantiate'] = 'Email funktionen kunne ikke initialiseres.';
+$PHPMAILER_LANG['invalid_address'] = 'Udgyldig adresse: ';
+$PHPMAILER_LANG['invalid_header'] = 'Ugyldig header navn eller værdi';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Ugyldig hostentry: ';
+$PHPMAILER_LANG['invalid_host'] = 'Ugyldig vært: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer understøttes ikke.';
+$PHPMAILER_LANG['provide_address'] = 'Indtast mindst en modtagers email adresse.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP fejl: Følgende modtagere fejlede: ';
+$PHPMAILER_LANG['signing'] = 'Signeringsfejl: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP kode: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'Yderligere SMTP info: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fejlede.';
+$PHPMAILER_LANG['smtp_detail'] = 'Detalje: ';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP server fejl: ';
+$PHPMAILER_LANG['variable_set'] = 'Kunne ikke definere eller nulstille variablen: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-de.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-de.php
new file mode 100644
index 0000000000..e7e59d2b67
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-de.php
@@ -0,0 +1,28 @@
+
+ * @author Crystopher Glodzienski Cardoso
+ * @author Daniel Cruz
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.';
+$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP está afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.';
+$PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.';
+$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.';
+$PHPMAILER_LANG['encoding'] = 'Codificación desconocida: ';
+$PHPMAILER_LANG['execute'] = 'Imposible ejecutar: ';
+$PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: ';
+$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: ';
+$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: ';
+$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: ';
+$PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.';
+$PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: ';
+$PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Hostentry inválido: ';
+$PHPMAILER_LANG['invalid_host'] = 'Host inválido: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no está soportado.';
+$PHPMAILER_LANG['provide_address'] = 'Debe proporcionar al menos una dirección de email de destino.';
+$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Los siguientes destinos fallaron: ';
+$PHPMAILER_LANG['signing'] = 'Error al firmar: ';
+$PHPMAILER_LANG['smtp_code'] = 'Código del servidor SMTP: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'Información adicional del servidor SMTP: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.';
+$PHPMAILER_LANG['smtp_detail'] = 'Detalle: ';
+$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-et.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-et.php
new file mode 100644
index 0000000000..93addc9e33
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-et.php
@@ -0,0 +1,28 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP Viga: Autoriseerimise viga.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Viga: Ei õnnestunud luua ühendust SMTP serveriga.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Viga: Vigased andmed.';
+$PHPMAILER_LANG['empty_message'] = 'Tühi kirja sisu';
+$PHPMAILER_LANG["encoding"] = 'Tundmatu kodeering: ';
+$PHPMAILER_LANG['execute'] = 'Tegevus ebaõnnestus: ';
+$PHPMAILER_LANG['file_access'] = 'Pole piisavalt õiguseid järgneva faili avamiseks: ';
+$PHPMAILER_LANG['file_open'] = 'Faili Viga: Faili avamine ebaõnnestus: ';
+$PHPMAILER_LANG['from_failed'] = 'Järgnev saatja e-posti aadress on vigane: ';
+$PHPMAILER_LANG['instantiate'] = 'mail funktiooni käivitamine ebaõnnestus.';
+$PHPMAILER_LANG['invalid_address'] = 'Saatmine peatatud, e-posti address vigane: ';
+$PHPMAILER_LANG['provide_address'] = 'Te peate määrama vähemalt ühe saaja e-posti aadressi.';
+$PHPMAILER_LANG['mailer_not_supported'] = ' maileri tugi puudub.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Viga: Järgnevate saajate e-posti aadressid on vigased: ';
+$PHPMAILER_LANG["signing"] = 'Viga allkirjastamisel: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() ebaõnnestus.';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP serveri viga: ';
+$PHPMAILER_LANG['variable_set'] = 'Ei õnnestunud määrata või lähtestada muutujat: ';
+$PHPMAILER_LANG['extension_missing'] = 'Nõutud laiendus on puudu: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fa.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fa.php
new file mode 100644
index 0000000000..295a47f95c
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fa.php
@@ -0,0 +1,28 @@
+
+ * @author Mohammad Hossein Mojtahedi
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'خطای SMTP: احراز هویت با شکست مواجه شد.';
+$PHPMAILER_LANG['connect_host'] = 'خطای SMTP: اتصال به سرور SMTP برقرار نشد.';
+$PHPMAILER_LANG['data_not_accepted'] = 'خطای SMTP: دادهها نادرست هستند.';
+$PHPMAILER_LANG['empty_message'] = 'بخش متن پیام خالی است.';
+$PHPMAILER_LANG['encoding'] = 'کدگذاری ناشناخته: ';
+$PHPMAILER_LANG['execute'] = 'امکان اجرا وجود ندارد: ';
+$PHPMAILER_LANG['file_access'] = 'امکان دسترسی به فایل وجود ندارد: ';
+$PHPMAILER_LANG['file_open'] = 'خطای File: امکان بازکردن فایل وجود ندارد: ';
+$PHPMAILER_LANG['from_failed'] = 'آدرس فرستنده اشتباه است: ';
+$PHPMAILER_LANG['instantiate'] = 'امکان معرفی تابع ایمیل وجود ندارد.';
+$PHPMAILER_LANG['invalid_address'] = 'آدرس ایمیل معتبر نیست: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer پشتیبانی نمیشود.';
+$PHPMAILER_LANG['provide_address'] = 'باید حداقل یک آدرس گیرنده وارد کنید.';
+$PHPMAILER_LANG['recipients_failed'] = 'خطای SMTP: ارسال به آدرس گیرنده با خطا مواجه شد: ';
+$PHPMAILER_LANG['signing'] = 'خطا در امضا: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'خطا در اتصال به SMTP.';
+$PHPMAILER_LANG['smtp_error'] = 'خطا در SMTP Server: ';
+$PHPMAILER_LANG['variable_set'] = 'امکان ارسال یا ارسال مجدد متغیرها وجود ندارد: ';
+$PHPMAILER_LANG['extension_missing'] = 'افزونه موجود نیست: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fi.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fi.php
new file mode 100644
index 0000000000..6d1e637390
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fi.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP feilur: Kundi ikki góðkenna.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP feilur: Kundi ikki knýta samband við SMTP vert.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP feilur: Data ikki góðkent.';
+//$PHPMAILER_LANG['empty_message'] = 'Message body empty';
+$PHPMAILER_LANG['encoding'] = 'Ókend encoding: ';
+$PHPMAILER_LANG['execute'] = 'Kundi ikki útføra: ';
+$PHPMAILER_LANG['file_access'] = 'Kundi ikki tilganga fílu: ';
+$PHPMAILER_LANG['file_open'] = 'Fílu feilur: Kundi ikki opna fílu: ';
+$PHPMAILER_LANG['from_failed'] = 'fylgjandi Frá/From adressa miseydnaðist: ';
+$PHPMAILER_LANG['instantiate'] = 'Kuni ikki instantiera mail funktión.';
+//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' er ikki supporterað.';
+$PHPMAILER_LANG['provide_address'] = 'Tú skal uppgeva minst móttakara-emailadressu(r).';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Feilur: Fylgjandi móttakarar miseydnaðust: ';
+//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
+//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
+//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: ';
+//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fr.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fr.php
new file mode 100644
index 0000000000..a6d582d833
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-fr.php
@@ -0,0 +1,36 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Erro SMTP: Non puido ser autentificado.';
+$PHPMAILER_LANG['connect_host'] = 'Erro SMTP: Non puido conectar co servidor SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Erro SMTP: Datos non aceptados.';
+$PHPMAILER_LANG['empty_message'] = 'Corpo da mensaxe vacía';
+$PHPMAILER_LANG['encoding'] = 'Codificación descoñecida: ';
+$PHPMAILER_LANG['execute'] = 'Non puido ser executado: ';
+$PHPMAILER_LANG['file_access'] = 'Nob puido acceder ó arquivo: ';
+$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: No puido abrir o arquivo: ';
+$PHPMAILER_LANG['from_failed'] = 'A(s) seguinte(s) dirección(s) de remitente(s) deron erro: ';
+$PHPMAILER_LANG['instantiate'] = 'Non puido crear unha instancia da función Mail.';
+$PHPMAILER_LANG['invalid_address'] = 'Non puido envia-lo correo: dirección de email inválida: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer non está soportado.';
+$PHPMAILER_LANG['provide_address'] = 'Debe engadir polo menos unha dirección de email coma destino.';
+$PHPMAILER_LANG['recipients_failed'] = 'Erro SMTP: Os seguintes destinos fallaron: ';
+$PHPMAILER_LANG['signing'] = 'Erro ó firmar: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallou.';
+$PHPMAILER_LANG['smtp_error'] = 'Erro do servidor SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Non puidemos axustar ou reaxustar a variábel: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-he.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-he.php
new file mode 100644
index 0000000000..b123aa5fc0
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-he.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'שגיאת SMTP: פעולת האימות נכשלה.';
+$PHPMAILER_LANG['connect_host'] = 'שגיאת SMTP: לא הצלחתי להתחבר לשרת SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'שגיאת SMTP: מידע לא התקבל.';
+$PHPMAILER_LANG['empty_message'] = 'גוף ההודעה ריק';
+$PHPMAILER_LANG['invalid_address'] = 'כתובת שגויה: ';
+$PHPMAILER_LANG['encoding'] = 'קידוד לא מוכר: ';
+$PHPMAILER_LANG['execute'] = 'לא הצלחתי להפעיל את: ';
+$PHPMAILER_LANG['file_access'] = 'לא ניתן לגשת לקובץ: ';
+$PHPMAILER_LANG['file_open'] = 'שגיאת קובץ: לא ניתן לגשת לקובץ: ';
+$PHPMAILER_LANG['from_failed'] = 'כתובות הנמענים הבאות נכשלו: ';
+$PHPMAILER_LANG['instantiate'] = 'לא הצלחתי להפעיל את פונקציית המייל.';
+$PHPMAILER_LANG['mailer_not_supported'] = ' אינה נתמכת.';
+$PHPMAILER_LANG['provide_address'] = 'חובה לספק לפחות כתובת אחת של מקבל המייל.';
+$PHPMAILER_LANG['recipients_failed'] = 'שגיאת SMTP: הנמענים הבאים נכשלו: ';
+$PHPMAILER_LANG['signing'] = 'שגיאת חתימה: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
+$PHPMAILER_LANG['smtp_error'] = 'שגיאת שרת SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'לא ניתן לקבוע או לשנות את המשתנה: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hi.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hi.php
new file mode 100644
index 0000000000..d2856e057b
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hi.php
@@ -0,0 +1,35 @@
+
+ * Rewrite and extension of the work by Jayanti Suthar
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP त्रुटि: प्रामाणिकता की जांच नहीं हो सका। ';
+$PHPMAILER_LANG['buggy_php'] = 'PHP का आपका संस्करण एक बग से प्रभावित है जिसके परिणामस्वरूप संदेश दूषित हो सकते हैं. इसे ठीक करने हेतु, भेजने के लिए SMTP का उपयोग करे, अपने php.ini में mail.add_x_header विकल्प को अक्षम करें, MacOS या Linux पर जाए, या अपने PHP संस्करण को 7.0.17+ या 7.1.3+ बदले.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP त्रुटि: SMTP सर्वर से कनेक्ट नहीं हो सका। ';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP त्रुटि: डेटा स्वीकार नहीं किया जाता है। ';
+$PHPMAILER_LANG['empty_message'] = 'संदेश खाली है। ';
+$PHPMAILER_LANG['encoding'] = 'अज्ञात एन्कोडिंग प्रकार। ';
+$PHPMAILER_LANG['execute'] = 'आदेश को निष्पादित करने में विफल। ';
+$PHPMAILER_LANG['extension_missing'] = 'एक्सटेन्षन गायब है: ';
+$PHPMAILER_LANG['file_access'] = 'फ़ाइल उपलब्ध नहीं है। ';
+$PHPMAILER_LANG['file_open'] = 'फ़ाइल त्रुटि: फाइल को खोला नहीं जा सका। ';
+$PHPMAILER_LANG['from_failed'] = 'प्रेषक का पता गलत है। ';
+$PHPMAILER_LANG['instantiate'] = 'मेल फ़ंक्शन कॉल नहीं कर सकता है।';
+$PHPMAILER_LANG['invalid_address'] = 'पता गलत है। ';
+$PHPMAILER_LANG['invalid_header'] = 'अमान्य हेडर नाम या मान';
+$PHPMAILER_LANG['invalid_hostentry'] = 'अमान्य hostentry: ';
+$PHPMAILER_LANG['invalid_host'] = 'अमान्य होस्ट: ';
+$PHPMAILER_LANG['mailer_not_supported'] = 'मेल सर्वर के साथ काम नहीं करता है। ';
+$PHPMAILER_LANG['provide_address'] = 'आपको कम से कम एक प्राप्तकर्ता का ई-मेल पता प्रदान करना होगा।';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP त्रुटि: निम्न प्राप्तकर्ताओं को पते भेजने में विफल। ';
+$PHPMAILER_LANG['signing'] = 'साइनअप त्रुटि: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP कोड: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'अतिरिक्त SMTP जानकारी: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP का connect () फ़ंक्शन विफल हुआ। ';
+$PHPMAILER_LANG['smtp_detail'] = 'विवरण: ';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP सर्वर त्रुटि। ';
+$PHPMAILER_LANG['variable_set'] = 'चर को बना या संशोधित नहीं किया जा सकता। ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hr.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hr.php
new file mode 100644
index 0000000000..cacb6c37e5
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hr.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela autentikacija.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Ne mogu se spojiti na SMTP poslužitelj.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.';
+$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.';
+$PHPMAILER_LANG['encoding'] = 'Nepoznati encoding: ';
+$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: ';
+$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: ';
+$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: ';
+$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje s navedenih e-mail adresa nije uspjelo: ';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedenih e-mail adresa nije uspjelo: ';
+$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.';
+$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.';
+$PHPMAILER_LANG['provide_address'] = 'Definirajte barem jednu adresu primatelja.';
+$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP poslužitelj nije uspjelo.';
+$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP poslužitelja: ';
+$PHPMAILER_LANG['variable_set'] = 'Ne mogu postaviti varijablu niti ju vratiti nazad: ';
+$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hu.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hu.php
new file mode 100644
index 0000000000..e6b58b0dbe
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-hu.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP -ի սխալ: չհաջողվեց ստուգել իսկությունը.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP -ի սխալ: չհաջողվեց կապ հաստատել SMTP սերվերի հետ.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP -ի սխալ: տվյալները ընդունված չեն.';
+$PHPMAILER_LANG['empty_message'] = 'Հաղորդագրությունը դատարկ է';
+$PHPMAILER_LANG['encoding'] = 'Կոդավորման անհայտ տեսակ: ';
+$PHPMAILER_LANG['execute'] = 'Չհաջողվեց իրականացնել հրամանը: ';
+$PHPMAILER_LANG['file_access'] = 'Ֆայլը հասանելի չէ: ';
+$PHPMAILER_LANG['file_open'] = 'Ֆայլի սխալ: ֆայլը չհաջողվեց բացել: ';
+$PHPMAILER_LANG['from_failed'] = 'Ուղարկողի հետևյալ հասցեն սխալ է: ';
+$PHPMAILER_LANG['instantiate'] = 'Հնարավոր չէ կանչել mail ֆունկցիան.';
+$PHPMAILER_LANG['invalid_address'] = 'Հասցեն սխալ է: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' փոստային սերվերի հետ չի աշխատում.';
+$PHPMAILER_LANG['provide_address'] = 'Անհրաժեշտ է տրամադրել գոնե մեկ ստացողի e-mail հասցե.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP -ի սխալ: չի հաջողվել ուղարկել հետևյալ ստացողների հասցեներին: ';
+$PHPMAILER_LANG['signing'] = 'Ստորագրման սխալ: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP -ի connect() ֆունկցիան չի հաջողվել';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP սերվերի սխալ: ';
+$PHPMAILER_LANG['variable_set'] = 'Չի հաջողվում ստեղծել կամ վերափոխել փոփոխականը: ';
+$PHPMAILER_LANG['extension_missing'] = 'Հավելվածը բացակայում է: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-id.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-id.php
new file mode 100644
index 0000000000..212a11f135
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-id.php
@@ -0,0 +1,31 @@
+
+ * @author @januridp
+ * @author Ian Mustafa
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Kesalahan SMTP: Tidak dapat mengotentikasi.';
+$PHPMAILER_LANG['connect_host'] = 'Kesalahan SMTP: Tidak dapat terhubung ke host SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Kesalahan SMTP: Data tidak diterima.';
+$PHPMAILER_LANG['empty_message'] = 'Isi pesan kosong';
+$PHPMAILER_LANG['encoding'] = 'Pengkodean karakter tidak dikenali: ';
+$PHPMAILER_LANG['execute'] = 'Tidak dapat menjalankan proses: ';
+$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses berkas: ';
+$PHPMAILER_LANG['file_open'] = 'Kesalahan Berkas: Berkas tidak dapat dibuka: ';
+$PHPMAILER_LANG['from_failed'] = 'Alamat pengirim berikut mengakibatkan kesalahan: ';
+$PHPMAILER_LANG['instantiate'] = 'Tidak dapat menginisialisasi fungsi surel.';
+$PHPMAILER_LANG['invalid_address'] = 'Gagal terkirim, alamat surel tidak sesuai: ';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Gagal terkirim, entri host tidak sesuai: ';
+$PHPMAILER_LANG['invalid_host'] = 'Gagal terkirim, host tidak sesuai: ';
+$PHPMAILER_LANG['provide_address'] = 'Harus tersedia minimal satu alamat tujuan';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tidak didukung';
+$PHPMAILER_LANG['recipients_failed'] = 'Kesalahan SMTP: Alamat tujuan berikut menyebabkan kesalahan: ';
+$PHPMAILER_LANG['signing'] = 'Kesalahan dalam penandatangan SSL: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() gagal.';
+$PHPMAILER_LANG['smtp_error'] = 'Kesalahan pada pelayan SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Tidak dapat mengatur atau mengatur ulang variabel: ';
+$PHPMAILER_LANG['extension_missing'] = 'Ekstensi PHP tidak tersedia: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-it.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-it.php
new file mode 100644
index 0000000000..08a6b73331
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-it.php
@@ -0,0 +1,28 @@
+
+ * @author Stefano Sabatini
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Impossibile autenticarsi.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Impossibile connettersi all\'host SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dati non accettati dal server.';
+$PHPMAILER_LANG['empty_message'] = 'Il corpo del messaggio è vuoto';
+$PHPMAILER_LANG['encoding'] = 'Codifica dei caratteri sconosciuta: ';
+$PHPMAILER_LANG['execute'] = 'Impossibile eseguire l\'operazione: ';
+$PHPMAILER_LANG['file_access'] = 'Impossibile accedere al file: ';
+$PHPMAILER_LANG['file_open'] = 'File Error: Impossibile aprire il file: ';
+$PHPMAILER_LANG['from_failed'] = 'I seguenti indirizzi mittenti hanno generato errore: ';
+$PHPMAILER_LANG['instantiate'] = 'Impossibile istanziare la funzione mail';
+$PHPMAILER_LANG['invalid_address'] = 'Impossibile inviare, l\'indirizzo email non è valido: ';
+$PHPMAILER_LANG['provide_address'] = 'Deve essere fornito almeno un indirizzo ricevente';
+$PHPMAILER_LANG['mailer_not_supported'] = 'Mailer non supportato';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: I seguenti indirizzi destinatari hanno generato un errore: ';
+$PHPMAILER_LANG['signing'] = 'Errore nella firma: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallita.';
+$PHPMAILER_LANG['smtp_error'] = 'Errore del server SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Impossibile impostare o resettare la variabile: ';
+$PHPMAILER_LANG['extension_missing'] = 'Estensione mancante: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ja.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ja.php
new file mode 100644
index 0000000000..d01869cec8
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ja.php
@@ -0,0 +1,37 @@
+
+ * @author Yoshi Sakai
+ * @author Arisophy
+ * @author ARAKI Musashi
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTPエラー: 認証できませんでした。';
+$PHPMAILER_LANG['buggy_php'] = 'ご利用のバージョンのPHPには不具合があり、メッセージが破損するおそれがあります。問題の解決は以下のいずれかを行ってください。SMTPでの送信に切り替える。php.iniのmail.add_x_headerをoffにする。MacOSまたはLinuxに切り替える。PHPバージョン7.0.17以降または7.1.3以降にアップグレードする。';
+$PHPMAILER_LANG['connect_host'] = 'SMTPエラー: SMTPホストに接続できませんでした。';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTPエラー: データが受け付けられませんでした。';
+$PHPMAILER_LANG['empty_message'] = 'メール本文が空です。';
+$PHPMAILER_LANG['encoding'] = '不明なエンコーディング: ';
+$PHPMAILER_LANG['execute'] = '実行できませんでした: ';
+$PHPMAILER_LANG['extension_missing'] = '拡張機能が見つかりません: ';
+$PHPMAILER_LANG['file_access'] = 'ファイルにアクセスできません: ';
+$PHPMAILER_LANG['file_open'] = 'ファイルエラー: ファイルを開けません: ';
+$PHPMAILER_LANG['from_failed'] = 'Fromアドレスを登録する際にエラーが発生しました: ';
+$PHPMAILER_LANG['instantiate'] = 'メール関数が正常に動作しませんでした。';
+$PHPMAILER_LANG['invalid_address'] = '不正なメールアドレス: ';
+$PHPMAILER_LANG['invalid_header'] = '不正なヘッダー名またはその内容';
+$PHPMAILER_LANG['invalid_hostentry'] = '不正なホストエントリー: ';
+$PHPMAILER_LANG['invalid_host'] = '不正なホスト: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' メーラーがサポートされていません。';
+$PHPMAILER_LANG['provide_address'] = '少なくとも1つメールアドレスを 指定する必要があります。';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTPエラー: 次の受信者アドレスに 間違いがあります: ';
+$PHPMAILER_LANG['signing'] = '署名エラー: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTPコード: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'SMTP追加情報: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP接続に失敗しました。';
+$PHPMAILER_LANG['smtp_detail'] = '詳細: ';
+$PHPMAILER_LANG['smtp_error'] = 'SMTPサーバーエラー: ';
+$PHPMAILER_LANG['variable_set'] = '変数が存在しません: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ka.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ka.php
new file mode 100644
index 0000000000..51fe403b40
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ka.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP შეცდომა: ავტორიზაცია შეუძლებელია.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP შეცდომა: SMTP სერვერთან დაკავშირება შეუძლებელია.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP შეცდომა: მონაცემები არ იქნა მიღებული.';
+$PHPMAILER_LANG['encoding'] = 'კოდირების უცნობი ტიპი: ';
+$PHPMAILER_LANG['execute'] = 'შეუძლებელია შემდეგი ბრძანების შესრულება: ';
+$PHPMAILER_LANG['file_access'] = 'შეუძლებელია წვდომა ფაილთან: ';
+$PHPMAILER_LANG['file_open'] = 'ფაილური სისტემის შეცდომა: არ იხსნება ფაილი: ';
+$PHPMAILER_LANG['from_failed'] = 'გამგზავნის არასწორი მისამართი: ';
+$PHPMAILER_LANG['instantiate'] = 'mail ფუნქციის გაშვება ვერ ხერხდება.';
+$PHPMAILER_LANG['provide_address'] = 'გთხოვთ მიუთითოთ ერთი ადრესატის e-mail მისამართი მაინც.';
+$PHPMAILER_LANG['mailer_not_supported'] = ' - საფოსტო სერვერის მხარდაჭერა არ არის.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP შეცდომა: შემდეგ მისამართებზე გაგზავნა ვერ მოხერხდა: ';
+$PHPMAILER_LANG['empty_message'] = 'შეტყობინება ცარიელია';
+$PHPMAILER_LANG['invalid_address'] = 'არ გაიგზავნა, e-mail მისამართის არასწორი ფორმატი: ';
+$PHPMAILER_LANG['signing'] = 'ხელმოწერის შეცდომა: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'შეცდომა SMTP სერვერთან დაკავშირებისას';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP სერვერის შეცდომა: ';
+$PHPMAILER_LANG['variable_set'] = 'შეუძლებელია შემდეგი ცვლადის შექმნა ან შეცვლა: ';
+$PHPMAILER_LANG['extension_missing'] = 'ბიბლიოთეკა არ არსებობს: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ko.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ko.php
new file mode 100644
index 0000000000..8c97dd947c
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ko.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP 오류: 인증할 수 없습니다.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP 오류: SMTP 호스트에 접속할 수 없습니다.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 오류: 데이터가 받아들여지지 않았습니다.';
+$PHPMAILER_LANG['empty_message'] = '메세지 내용이 없습니다';
+$PHPMAILER_LANG['encoding'] = '알 수 없는 인코딩: ';
+$PHPMAILER_LANG['execute'] = '실행 불가: ';
+$PHPMAILER_LANG['file_access'] = '파일 접근 불가: ';
+$PHPMAILER_LANG['file_open'] = '파일 오류: 파일을 열 수 없습니다: ';
+$PHPMAILER_LANG['from_failed'] = '다음 From 주소에서 오류가 발생했습니다: ';
+$PHPMAILER_LANG['instantiate'] = 'mail 함수를 인스턴스화할 수 없습니다';
+$PHPMAILER_LANG['invalid_address'] = '잘못된 주소: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' 메일러는 지원되지 않습니다.';
+$PHPMAILER_LANG['provide_address'] = '적어도 한 개 이상의 수신자 메일 주소를 제공해야 합니다.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP 오류: 다음 수신자에서 오류가 발생했습니다: ';
+$PHPMAILER_LANG['signing'] = '서명 오류: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 연결을 실패하였습니다.';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP 서버 오류: ';
+$PHPMAILER_LANG['variable_set'] = '변수 설정 및 초기화 불가: ';
+$PHPMAILER_LANG['extension_missing'] = '확장자 없음: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ku.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ku.php
new file mode 100644
index 0000000000..cf3bda69f2
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ku.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'هەڵەی SMTP : نەتوانرا کۆدەکە پشتڕاست بکرێتەوە ';
+$PHPMAILER_LANG['connect_host'] = 'هەڵەی SMTP: نەتوانرا پەیوەندی بە سێرڤەرەوە بکات SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'هەڵەی SMTP: ئەو زانیاریانە قبوڵ نەکرا.';
+$PHPMAILER_LANG['empty_message'] = 'پەیامەکە بەتاڵە';
+$PHPMAILER_LANG['encoding'] = 'کۆدکردنی نەزانراو : ';
+$PHPMAILER_LANG['execute'] = 'ناتوانرێت جێبەجێ بکرێت: ';
+$PHPMAILER_LANG['file_access'] = 'ناتوانرێت دەستت بگات بە فایلەکە: ';
+$PHPMAILER_LANG['file_open'] = 'هەڵەی پەڕگە(فایل): ناتوانرێت بکرێتەوە: ';
+$PHPMAILER_LANG['from_failed'] = 'هەڵە لە ئاستی ناونیشانی نێرەر: ';
+$PHPMAILER_LANG['instantiate'] = 'ناتوانرێت خزمەتگوزاری پۆستە پێشکەش بکرێت.';
+$PHPMAILER_LANG['invalid_address'] = 'نەتوانرا بنێردرێت ، چونکە ناونیشانی ئیمەیڵەکە نادروستە: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' مەیلەر پشتگیری ناکات';
+$PHPMAILER_LANG['provide_address'] = 'دەبێت ناونیشانی ئیمەیڵی لانیکەم یەک وەرگر دابین بکرێت.';
+$PHPMAILER_LANG['recipients_failed'] = ' هەڵەی SMTP: ئەم هەڵانەی خوارەوەشکستی هێنا لە ناردن بۆ هەردووکیان: ';
+$PHPMAILER_LANG['signing'] = 'هەڵەی واژۆ: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect()پەیوەندی شکستی هێنا .';
+$PHPMAILER_LANG['smtp_error'] = 'هەڵەی ئاستی سێرڤەری SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'ناتوانرێت بیگۆڕیت یان دوبارە بینێریتەوە: ';
+$PHPMAILER_LANG['extension_missing'] = 'درێژکراوە نەماوە: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-lt.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-lt.php
new file mode 100644
index 0000000000..4f115b1c58
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-lt.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP klaida: autentifikacija nepavyko.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP klaida: nepavyksta prisijungti prie SMTP stoties.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP klaida: duomenys nepriimti.';
+$PHPMAILER_LANG['empty_message'] = 'Laiško turinys tuščias';
+$PHPMAILER_LANG['encoding'] = 'Neatpažinta koduotė: ';
+$PHPMAILER_LANG['execute'] = 'Nepavyko įvykdyti komandos: ';
+$PHPMAILER_LANG['file_access'] = 'Byla nepasiekiama: ';
+$PHPMAILER_LANG['file_open'] = 'Bylos klaida: Nepavyksta atidaryti: ';
+$PHPMAILER_LANG['from_failed'] = 'Neteisingas siuntėjo adresas: ';
+$PHPMAILER_LANG['instantiate'] = 'Nepavyko paleisti mail funkcijos.';
+$PHPMAILER_LANG['invalid_address'] = 'Neteisingas adresas: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' pašto stotis nepalaikoma.';
+$PHPMAILER_LANG['provide_address'] = 'Nurodykite bent vieną gavėjo adresą.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP klaida: nepavyko išsiųsti šiems gavėjams: ';
+$PHPMAILER_LANG['signing'] = 'Prisijungimo klaida: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP susijungimo klaida';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP stoties klaida: ';
+$PHPMAILER_LANG['variable_set'] = 'Nepavyko priskirti reikšmės kintamajam: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-lv.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-lv.php
new file mode 100644
index 0000000000..679b18cf9f
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-lv.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP kļūda: Autorizācija neizdevās.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Kļūda: Nevar izveidot savienojumu ar SMTP serveri.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Kļūda: Nepieņem informāciju.';
+$PHPMAILER_LANG['empty_message'] = 'Ziņojuma teksts ir tukšs';
+$PHPMAILER_LANG['encoding'] = 'Neatpazīts kodējums: ';
+$PHPMAILER_LANG['execute'] = 'Neizdevās izpildīt komandu: ';
+$PHPMAILER_LANG['file_access'] = 'Fails nav pieejams: ';
+$PHPMAILER_LANG['file_open'] = 'Faila kļūda: Nevar atvērt failu: ';
+$PHPMAILER_LANG['from_failed'] = 'Nepareiza sūtītāja adrese: ';
+$PHPMAILER_LANG['instantiate'] = 'Nevar palaist sūtīšanas funkciju.';
+$PHPMAILER_LANG['invalid_address'] = 'Nepareiza adrese: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' sūtītājs netiek atbalstīts.';
+$PHPMAILER_LANG['provide_address'] = 'Lūdzu, norādiet vismaz vienu adresātu.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP kļūda: neizdevās nosūtīt šādiem saņēmējiem: ';
+$PHPMAILER_LANG['signing'] = 'Autorizācijas kļūda: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP savienojuma kļūda';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP servera kļūda: ';
+$PHPMAILER_LANG['variable_set'] = 'Nevar piešķirt mainīgā vērtību: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-mg.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-mg.php
new file mode 100644
index 0000000000..8a94f6a044
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-mg.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Hadisoana SMTP: Tsy nahomby ny fanamarinana.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Tsy afaka mampifandray amin\'ny mpampiantrano SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP diso: tsy voarakitra ny angona.';
+$PHPMAILER_LANG['empty_message'] = 'Tsy misy ny votoaty mailaka.';
+$PHPMAILER_LANG['encoding'] = 'Tsy fantatra encoding: ';
+$PHPMAILER_LANG['execute'] = 'Tsy afaka manatanteraka ity baiko manaraka ity: ';
+$PHPMAILER_LANG['file_access'] = 'Tsy nahomby ny fidirana amin\'ity rakitra ity: ';
+$PHPMAILER_LANG['file_open'] = 'Hadisoana diso: Tsy afaka nanokatra ity file manaraka ity: ';
+$PHPMAILER_LANG['from_failed'] = 'Ny adiresy iraka manaraka dia diso: ';
+$PHPMAILER_LANG['instantiate'] = 'Tsy afaka nanomboka ny hetsika mail.';
+$PHPMAILER_LANG['invalid_address'] = 'Tsy mety ny adiresy: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tsy manohana.';
+$PHPMAILER_LANG['provide_address'] = 'Alefaso azafady iray adiresy iray farafahakeliny.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Tsy mety ireo mpanaraka ireto: ';
+$PHPMAILER_LANG['signing'] = 'Error nandritra ny sonia:';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Tsy nahomby ny fifandraisana tamin\'ny server SMTP.';
+$PHPMAILER_LANG['smtp_error'] = 'Fahadisoana tamin\'ny server SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Tsy azo atao ny mametraka na mamerina ny variable: ';
+$PHPMAILER_LANG['extension_missing'] = 'Tsy hita ny ampahany: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-mn.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-mn.php
new file mode 100644
index 0000000000..04d262c724
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-mn.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Ralat SMTP: Tidak dapat pengesahan.';
+$PHPMAILER_LANG['connect_host'] = 'Ralat SMTP: Tidak dapat menghubungi hos pelayan SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Ralat SMTP: Data tidak diterima oleh pelayan.';
+$PHPMAILER_LANG['empty_message'] = 'Tiada isi untuk mesej';
+$PHPMAILER_LANG['encoding'] = 'Pengekodan tidak diketahui: ';
+$PHPMAILER_LANG['execute'] = 'Tidak dapat melaksanakan: ';
+$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses fail: ';
+$PHPMAILER_LANG['file_open'] = 'Ralat Fail: Tidak dapat membuka fail: ';
+$PHPMAILER_LANG['from_failed'] = 'Berikut merupakan ralat dari alamat e-mel: ';
+$PHPMAILER_LANG['instantiate'] = 'Tidak dapat memberi contoh fungsi e-mel.';
+$PHPMAILER_LANG['invalid_address'] = 'Alamat emel tidak sah: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' jenis penghantar emel tidak disokong.';
+$PHPMAILER_LANG['provide_address'] = 'Anda perlu menyediakan sekurang-kurangnya satu alamat e-mel penerima.';
+$PHPMAILER_LANG['recipients_failed'] = 'Ralat SMTP: Penerima e-mel berikut telah gagal: ';
+$PHPMAILER_LANG['signing'] = 'Ralat pada tanda tangan: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() telah gagal.';
+$PHPMAILER_LANG['smtp_error'] = 'Ralat pada pelayan SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Tidak boleh menetapkan atau menetapkan semula pembolehubah: ';
+$PHPMAILER_LANG['extension_missing'] = 'Sambungan hilang: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-nb.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-nb.php
new file mode 100644
index 0000000000..c9621a164f
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-nb.php
@@ -0,0 +1,33 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP-fout: authenticatie mislukt.';
+$PHPMAILER_LANG['buggy_php'] = 'PHP versie gededecteerd die onderhavig is aan een bug die kan resulteren in gecorrumpeerde berichten. Om dit te voorkomen, gebruik SMTP voor het verzenden van berichten, zet de mail.add_x_header optie in uw php.ini file uit, gebruik MacOS of Linux, of pas de gebruikte PHP versie aan naar versie 7.0.17+ or 7.1.3+.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP-fout: kon niet verbinden met SMTP-host.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-fout: data niet geaccepteerd.';
+$PHPMAILER_LANG['empty_message'] = 'Berichttekst is leeg';
+$PHPMAILER_LANG['encoding'] = 'Onbekende codering: ';
+$PHPMAILER_LANG['execute'] = 'Kon niet uitvoeren: ';
+$PHPMAILER_LANG['extension_missing'] = 'Extensie afwezig: ';
+$PHPMAILER_LANG['file_access'] = 'Kreeg geen toegang tot bestand: ';
+$PHPMAILER_LANG['file_open'] = 'Bestandsfout: kon bestand niet openen: ';
+$PHPMAILER_LANG['from_failed'] = 'Het volgende afzendersadres is mislukt: ';
+$PHPMAILER_LANG['instantiate'] = 'Kon mailfunctie niet initialiseren.';
+$PHPMAILER_LANG['invalid_address'] = 'Ongeldig adres: ';
+$PHPMAILER_LANG['invalid_header'] = 'Ongeldige header naam of waarde';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Ongeldige hostentry: ';
+$PHPMAILER_LANG['invalid_host'] = 'Ongeldige host: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer wordt niet ondersteund.';
+$PHPMAILER_LANG['provide_address'] = 'Er moet minstens één ontvanger worden opgegeven.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP-fout: de volgende ontvangers zijn mislukt: ';
+$PHPMAILER_LANG['signing'] = 'Signeerfout: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP code: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'Aanvullende SMTP informatie: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Verbinding mislukt.';
+$PHPMAILER_LANG['smtp_detail'] = 'Detail: ';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP-serverfout: ';
+$PHPMAILER_LANG['variable_set'] = 'Kan de volgende variabele niet instellen of resetten: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-pl.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-pl.php
new file mode 100644
index 0000000000..cb7b2c2107
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-pl.php
@@ -0,0 +1,33 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Erro SMTP: Falha na autenticação.';
+$PHPMAILER_LANG['buggy_php'] = 'A sua versão do PHP tem um bug que pode causar mensagens corrompidas. Para resolver, utilize o envio por SMTP, desative a opção mail.add_x_header no ficheiro php.ini, mude para MacOS ou Linux, ou atualize o PHP para a versão 7.0.17+ ou 7.1.3+.';
+$PHPMAILER_LANG['connect_host'] = 'Erro SMTP: Não foi possível ligar ao servidor SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Erro SMTP: Dados não aceites.';
+$PHPMAILER_LANG['empty_message'] = 'A mensagem de e-mail está vazia.';
+$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: ';
+$PHPMAILER_LANG['execute'] = 'Não foi possível executar: ';
+$PHPMAILER_LANG['extension_missing'] = 'Extensão em falta: ';
+$PHPMAILER_LANG['file_access'] = 'Não foi possível aceder ao ficheiro: ';
+$PHPMAILER_LANG['file_open'] = 'Erro ao abrir o ficheiro: ';
+$PHPMAILER_LANG['from_failed'] = 'O envio falhou para o seguinte endereço do remetente: ';
+$PHPMAILER_LANG['instantiate'] = 'Não foi possível instanciar a função mail.';
+$PHPMAILER_LANG['invalid_address'] = 'Endereço de e-mail inválido: ';
+$PHPMAILER_LANG['invalid_header'] = 'Nome ou valor do cabeçalho inválido.';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Entrada de host inválida: ';
+$PHPMAILER_LANG['invalid_host'] = 'Host inválido: ';
+$PHPMAILER_LANG['mailer_not_supported'] = 'O cliente de e-mail não é suportado.';
+$PHPMAILER_LANG['provide_address'] = 'Deve fornecer pelo menos um endereço de destinatário.';
+$PHPMAILER_LANG['recipients_failed'] = 'Erro SMTP: Falha no envio para os seguintes destinatários: ';
+$PHPMAILER_LANG['signing'] = 'Erro ao assinar: ';
+$PHPMAILER_LANG['smtp_code'] = 'Código SMTP: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'Informações adicionais SMTP: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Falha na função SMTP connect().';
+$PHPMAILER_LANG['smtp_detail'] = 'Detalhes: ';
+$PHPMAILER_LANG['smtp_error'] = 'Erro do servidor SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php
new file mode 100644
index 0000000000..5239865a6f
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-pt_br.php
@@ -0,0 +1,38 @@
+
+ * @author Lucas Guimarães
+ * @author Phelipe Alves
+ * @author Fabio Beneditto
+ * @author Geidson Benício Coelho
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Erro de SMTP: Não foi possível autenticar.';
+$PHPMAILER_LANG['buggy_php'] = 'Sua versão do PHP é afetada por um bug que por resultar em messagens corrompidas. Para corrigir, mude para enviar usando SMTP, desative a opção mail.add_x_header em seu php.ini, mude para MacOS ou Linux, ou atualize seu PHP para versão 7.0.17+ ou 7.1.3+ ';
+$PHPMAILER_LANG['connect_host'] = 'Erro de SMTP: Não foi possível conectar ao servidor SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Erro de SMTP: Dados rejeitados.';
+$PHPMAILER_LANG['empty_message'] = 'Mensagem vazia';
+$PHPMAILER_LANG['encoding'] = 'Codificação desconhecida: ';
+$PHPMAILER_LANG['execute'] = 'Não foi possível executar: ';
+$PHPMAILER_LANG['extension_missing'] = 'Extensão não existe: ';
+$PHPMAILER_LANG['file_access'] = 'Não foi possível acessar o arquivo: ';
+$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: Não foi possível abrir o arquivo: ';
+$PHPMAILER_LANG['from_failed'] = 'Os seguintes remetentes falharam: ';
+$PHPMAILER_LANG['instantiate'] = 'Não foi possível instanciar a função mail.';
+$PHPMAILER_LANG['invalid_address'] = 'Endereço de e-mail inválido: ';
+$PHPMAILER_LANG['invalid_header'] = 'Nome ou valor de cabeçalho inválido';
+$PHPMAILER_LANG['invalid_hostentry'] = 'hostentry inválido: ';
+$PHPMAILER_LANG['invalid_host'] = 'host inválido: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer não é suportado.';
+$PHPMAILER_LANG['provide_address'] = 'Você deve informar pelo menos um destinatário.';
+$PHPMAILER_LANG['recipients_failed'] = 'Erro de SMTP: Os seguintes destinatários falharam: ';
+$PHPMAILER_LANG['signing'] = 'Erro de Assinatura: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falhou.';
+$PHPMAILER_LANG['smtp_code'] = 'Código do servidor SMTP: ';
+$PHPMAILER_LANG['smtp_error'] = 'Erro de servidor SMTP: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'Informações adicionais do servidor SMTP: ';
+$PHPMAILER_LANG['smtp_detail'] = 'Detalhes do servidor SMTP: ';
+$PHPMAILER_LANG['variable_set'] = 'Não foi possível definir ou redefinir a variável: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ro.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ro.php
new file mode 100644
index 0000000000..45bef91553
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ro.php
@@ -0,0 +1,33 @@
+
+ * @author Foster Snowhill
+ * @author ProjectSoft
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Ошибка SMTP: не удалось пройти аутентификацию.';
+$PHPMAILER_LANG['buggy_php'] = 'В вашей версии PHP есть ошибка, которая может привести к повреждению сообщений. Чтобы исправить, переключитесь на отправку по SMTP, отключите опцию mail.add_x_header в ваш php.ini, переключитесь на MacOS или Linux или обновите PHP до версии 7.0.17+ или 7.1.3+.';
+$PHPMAILER_LANG['connect_host'] = 'Ошибка SMTP: не удается подключиться к SMTP-серверу.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Ошибка SMTP: данные не приняты.';
+$PHPMAILER_LANG['empty_message'] = 'Пустое сообщение';
+$PHPMAILER_LANG['encoding'] = 'Неизвестная кодировка: ';
+$PHPMAILER_LANG['execute'] = 'Невозможно выполнить команду: ';
+$PHPMAILER_LANG['extension_missing'] = 'Расширение отсутствует: ';
+$PHPMAILER_LANG['file_access'] = 'Нет доступа к файлу: ';
+$PHPMAILER_LANG['file_open'] = 'Файловая ошибка: не удаётся открыть файл: ';
+$PHPMAILER_LANG['from_failed'] = 'Неверный адрес отправителя: ';
+$PHPMAILER_LANG['instantiate'] = 'Невозможно запустить функцию mail().';
+$PHPMAILER_LANG['invalid_address'] = 'Не отправлено из-за неправильного формата email-адреса: ';
+$PHPMAILER_LANG['invalid_header'] = 'Неверное имя или значение заголовка';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Неверная запись хоста: ';
+$PHPMAILER_LANG['invalid_host'] = 'Неверный хост: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' — почтовый сервер не поддерживается.';
+$PHPMAILER_LANG['provide_address'] = 'Вы должны указать хотя бы один адрес электронной почты получателя.';
+$PHPMAILER_LANG['recipients_failed'] = 'Ошибка SMTP: Ошибка следующих получателей: ';
+$PHPMAILER_LANG['signing'] = 'Ошибка подписи: ';
+$PHPMAILER_LANG['smtp_code'] = 'Код SMTP: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'Дополнительная информация SMTP: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Ошибка соединения с SMTP-сервером.';
+$PHPMAILER_LANG['smtp_detail'] = 'Детали: ';
+$PHPMAILER_LANG['smtp_error'] = 'Ошибка SMTP-сервера: ';
+$PHPMAILER_LANG['variable_set'] = 'Невозможно установить или сбросить переменную: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-si.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-si.php
new file mode 100644
index 0000000000..dce502aa02
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-si.php
@@ -0,0 +1,34 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP දෝෂය: සත්යාපනය අසාර්ථක විය.';
+$PHPMAILER_LANG['buggy_php'] = 'ඔබගේ PHP version එකෙහි පවතින දෝෂයක් නිසා email පණිවිඩ දෝෂ සහගත වීමේ හැකියාවක් ඇත. මෙය විසදීම සදහා SMTP භාවිතා කිරීම, mail.add_x_header INI setting එක අක්රීය කිරීම, MacOS හෝ Linux වලට මාරු වීම, හෝ ඔබගේ PHP version එක 7.0.17+ හෝ 7.1.3+ වලට අලුත් කිරීම කරගන්න.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP දෝෂය: සම්බන්ධ වීමට නොහැකි විය.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP දෝෂය: දත්ත පිළිගනු නොලැබේ.';
+$PHPMAILER_LANG['empty_message'] = 'පණිවිඩ අන්තර්ගතය හිස්';
+$PHPMAILER_LANG['encoding'] = 'නොදන්නා කේතනය: ';
+$PHPMAILER_LANG['execute'] = 'ක්රියාත්මක කළ නොහැකි විය: ';
+$PHPMAILER_LANG['extension_missing'] = 'Extension එක නොමැත: ';
+$PHPMAILER_LANG['file_access'] = 'File එකට ප්රවේශ විය නොහැකි විය: ';
+$PHPMAILER_LANG['file_open'] = 'File දෝෂය: File එක විවෘත කළ නොහැක: ';
+$PHPMAILER_LANG['from_failed'] = 'පහත From ලිපිනයන් අසාර්ථක විය: ';
+$PHPMAILER_LANG['instantiate'] = 'mail function එක ක්රියාත්මක කළ නොහැක.';
+$PHPMAILER_LANG['invalid_address'] = 'වලංගු නොවන ලිපිනය: ';
+$PHPMAILER_LANG['invalid_header'] = 'වලංගු නොවන header නාමයක් හෝ අගයක්';
+$PHPMAILER_LANG['invalid_hostentry'] = 'වලංගු නොවන hostentry එකක්: ';
+$PHPMAILER_LANG['invalid_host'] = 'වලංගු නොවන host එකක්: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer සහාය නොදක්වයි.';
+$PHPMAILER_LANG['provide_address'] = 'ඔබ අවම වශයෙන් එක් ලබන්නෙකුගේ ඊමේල් ලිපිනයක් සැපයිය යුතුය.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP දෝෂය: පහත ලබන්නන් අසමත් විය: ';
+$PHPMAILER_LANG['signing'] = 'Sign කිරීමේ දෝෂය: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP කේතය: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'අමතර SMTP තොරතුරු: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP සම්බන්ධය අසාර්ථක විය.';
+$PHPMAILER_LANG['smtp_detail'] = 'තොරතුරු: ';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP දෝෂය: ';
+$PHPMAILER_LANG['variable_set'] = 'Variable එක සැකසීමට හෝ නැවත සැකසීමට නොහැක: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sk.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sk.php
new file mode 100644
index 0000000000..028f5bc496
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sk.php
@@ -0,0 +1,30 @@
+
+ * @author Peter Orlický
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Chyba autentifikácie.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Nebolo možné nadviazať spojenie so SMTP serverom.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dáta neboli prijaté';
+$PHPMAILER_LANG['empty_message'] = 'Prázdne telo správy.';
+$PHPMAILER_LANG['encoding'] = 'Neznáme kódovanie: ';
+$PHPMAILER_LANG['execute'] = 'Nedá sa vykonať: ';
+$PHPMAILER_LANG['file_access'] = 'Súbor nebol nájdený: ';
+$PHPMAILER_LANG['file_open'] = 'File Error: Súbor sa otvoriť pre čítanie: ';
+$PHPMAILER_LANG['from_failed'] = 'Následujúca adresa From je nesprávna: ';
+$PHPMAILER_LANG['instantiate'] = 'Nedá sa vytvoriť inštancia emailovej funkcie.';
+$PHPMAILER_LANG['invalid_address'] = 'Neodoslané, emailová adresa je nesprávna: ';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Záznam hostiteľa je nesprávny: ';
+$PHPMAILER_LANG['invalid_host'] = 'Hostiteľ je nesprávny: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' emailový klient nieje podporovaný.';
+$PHPMAILER_LANG['provide_address'] = 'Musíte zadať aspoň jednu emailovú adresu príjemcu.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Adresy príjemcov niesu správne ';
+$PHPMAILER_LANG['signing'] = 'Chyba prihlasovania: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() zlyhalo.';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP chyba serveru: ';
+$PHPMAILER_LANG['variable_set'] = 'Nemožno nastaviť alebo resetovať premennú: ';
+$PHPMAILER_LANG['extension_missing'] = 'Chýba rozšírenie: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sl.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sl.php
new file mode 100644
index 0000000000..3e00c25963
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sl.php
@@ -0,0 +1,36 @@
+
+ * @author Filip Š
+ * @author Blaž Oražem
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP napaka: Avtentikacija ni uspela.';
+$PHPMAILER_LANG['buggy_php'] = 'Na vašo PHP različico vpliva napaka, ki lahko povzroči poškodovana sporočila. Če želite težavo odpraviti, preklopite na pošiljanje prek SMTP, onemogočite možnost mail.add_x_header v vaši php.ini datoteki, preklopite na MacOS ali Linux, ali nadgradite vašo PHP zaličico na 7.0.17+ ali 7.1.3+.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP napaka: Vzpostavljanje povezave s SMTP gostiteljem ni uspelo.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP napaka: Strežnik zavrača podatke.';
+$PHPMAILER_LANG['empty_message'] = 'E-poštno sporočilo nima vsebine.';
+$PHPMAILER_LANG['encoding'] = 'Nepoznan tip kodiranja: ';
+$PHPMAILER_LANG['execute'] = 'Operacija ni uspela: ';
+$PHPMAILER_LANG['extension_missing'] = 'Manjkajoča razširitev: ';
+$PHPMAILER_LANG['file_access'] = 'Nimam dostopa do datoteke: ';
+$PHPMAILER_LANG['file_open'] = 'Ne morem odpreti datoteke: ';
+$PHPMAILER_LANG['from_failed'] = 'Neveljaven e-naslov pošiljatelja: ';
+$PHPMAILER_LANG['instantiate'] = 'Ne morem inicializirati mail funkcije.';
+$PHPMAILER_LANG['invalid_address'] = 'E-poštno sporočilo ni bilo poslano. E-naslov je neveljaven: ';
+$PHPMAILER_LANG['invalid_header'] = 'Neveljavno ime ali vrednost glave';
+$PHPMAILER_LANG['invalid_hostentry'] = 'Neveljaven vnos gostitelja: ';
+$PHPMAILER_LANG['invalid_host'] = 'Neveljaven gostitelj: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer ni podprt.';
+$PHPMAILER_LANG['provide_address'] = 'Prosimo, vnesite vsaj enega naslovnika.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP napaka: Sledeči naslovniki so neveljavni: ';
+$PHPMAILER_LANG['signing'] = 'Napaka pri podpisovanju: ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP koda: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'Dodatne informacije o SMTP: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Ne morem vzpostaviti povezave s SMTP strežnikom.';
+$PHPMAILER_LANG['smtp_detail'] = 'Podrobnosti: ';
+$PHPMAILER_LANG['smtp_error'] = 'Napaka SMTP strežnika: ';
+$PHPMAILER_LANG['variable_set'] = 'Ne morem nastaviti oz. ponastaviti spremenljivke: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr.php
new file mode 100644
index 0000000000..0b5280f75e
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr.php
@@ -0,0 +1,28 @@
+
+ * @author Miloš Milanović
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: аутентификација није успела.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: повезивање са SMTP сервером није успело.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: подаци нису прихваћени.';
+$PHPMAILER_LANG['empty_message'] = 'Садржај поруке је празан.';
+$PHPMAILER_LANG['encoding'] = 'Непознато кодирање: ';
+$PHPMAILER_LANG['execute'] = 'Није могуће извршити наредбу: ';
+$PHPMAILER_LANG['file_access'] = 'Није могуће приступити датотеци: ';
+$PHPMAILER_LANG['file_open'] = 'Није могуће отворити датотеку: ';
+$PHPMAILER_LANG['from_failed'] = 'SMTP грешка: слање са следећих адреса није успело: ';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: слање на следеће адресе није успело: ';
+$PHPMAILER_LANG['instantiate'] = 'Није могуће покренути mail функцију.';
+$PHPMAILER_LANG['invalid_address'] = 'Порука није послата. Неисправна адреса: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' мејлер није подржан.';
+$PHPMAILER_LANG['provide_address'] = 'Дефинишите бар једну адресу примаоца.';
+$PHPMAILER_LANG['signing'] = 'Грешка приликом пријаве: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Повезивање са SMTP сервером није успело.';
+$PHPMAILER_LANG['smtp_error'] = 'Грешка SMTP сервера: ';
+$PHPMAILER_LANG['variable_set'] = 'Није могуће задати нити ресетовати променљиву: ';
+$PHPMAILER_LANG['extension_missing'] = 'Недостаје проширење: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr_latn.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr_latn.php
new file mode 100644
index 0000000000..62138329ac
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sr_latn.php
@@ -0,0 +1,28 @@
+
+ * @author Miloš Milanović
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP greška: autentifikacija nije uspela.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP greška: povezivanje sa SMTP serverom nije uspelo.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP greška: podaci nisu prihvaćeni.';
+$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.';
+$PHPMAILER_LANG['encoding'] = 'Nepoznato kodiranje: ';
+$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: ';
+$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: ';
+$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: ';
+$PHPMAILER_LANG['from_failed'] = 'SMTP greška: slanje sa sledećih adresa nije uspelo: ';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP greška: slanje na sledeće adrese nije uspelo: ';
+$PHPMAILER_LANG['instantiate'] = 'Nije moguće pokrenuti mail funkciju.';
+$PHPMAILER_LANG['invalid_address'] = 'Poruka nije poslata. Neispravna adresa: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' majler nije podržan.';
+$PHPMAILER_LANG['provide_address'] = 'Definišite bar jednu adresu primaoca.';
+$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Povezivanje sa SMTP serverom nije uspelo.';
+$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP servera: ';
+$PHPMAILER_LANG['variable_set'] = 'Nije moguće zadati niti resetovati promenljivu: ';
+$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sv.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sv.php
new file mode 100644
index 0000000000..9872c19219
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-sv.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP fel: Kunde inte autentisera.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP fel: Kunde inte ansluta till SMTP-server.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fel: Data accepterades inte.';
+//$PHPMAILER_LANG['empty_message'] = 'Message body empty';
+$PHPMAILER_LANG['encoding'] = 'Okänt encode-format: ';
+$PHPMAILER_LANG['execute'] = 'Kunde inte köra: ';
+$PHPMAILER_LANG['file_access'] = 'Ingen åtkomst till fil: ';
+$PHPMAILER_LANG['file_open'] = 'Fil fel: Kunde inte öppna fil: ';
+$PHPMAILER_LANG['from_failed'] = 'Följande avsändaradress är felaktig: ';
+$PHPMAILER_LANG['instantiate'] = 'Kunde inte initiera e-postfunktion.';
+$PHPMAILER_LANG['invalid_address'] = 'Felaktig adress: ';
+$PHPMAILER_LANG['provide_address'] = 'Du måste ange minst en mottagares e-postadress.';
+$PHPMAILER_LANG['mailer_not_supported'] = ' mailer stöds inte.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP fel: Följande mottagare är felaktig: ';
+$PHPMAILER_LANG['signing'] = 'Signeringsfel: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() misslyckades.';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP serverfel: ';
+$PHPMAILER_LANG['variable_set'] = 'Kunde inte definiera eller återställa variabel: ';
+$PHPMAILER_LANG['extension_missing'] = 'Tillägg ej tillgängligt: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-tl.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-tl.php
new file mode 100644
index 0000000000..d15bed1c83
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-tl.php
@@ -0,0 +1,28 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Hindi mapatotohanan.';
+$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Hindi makakonekta sa SMTP host.';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Ang datos ay hindi naitanggap.';
+$PHPMAILER_LANG['empty_message'] = 'Walang laman ang mensahe';
+$PHPMAILER_LANG['encoding'] = 'Hindi alam ang encoding: ';
+$PHPMAILER_LANG['execute'] = 'Hindi maisasagawa: ';
+$PHPMAILER_LANG['file_access'] = 'Hindi ma-access ang file: ';
+$PHPMAILER_LANG['file_open'] = 'File Error: Hindi mabuksan ang file: ';
+$PHPMAILER_LANG['from_failed'] = 'Ang sumusunod na address ay nabigo: ';
+$PHPMAILER_LANG['instantiate'] = 'Hindi maisimulan ang instance ng mail function.';
+$PHPMAILER_LANG['invalid_address'] = 'Hindi wasto ang address na naibigay: ';
+$PHPMAILER_LANG['mailer_not_supported'] = 'Ang mailer ay hindi suportado.';
+$PHPMAILER_LANG['provide_address'] = 'Kailangan mong magbigay ng kahit isang email address na tatanggap.';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: Ang mga sumusunod na tatanggap ay nabigo: ';
+$PHPMAILER_LANG['signing'] = 'Hindi ma-sign: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Ang SMTP connect() ay nabigo.';
+$PHPMAILER_LANG['smtp_error'] = 'Ang server ng SMTP ay nabigo: ';
+$PHPMAILER_LANG['variable_set'] = 'Hindi matatakda o ma-reset ang mga variables: ';
+$PHPMAILER_LANG['extension_missing'] = 'Nawawala ang extension: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-tr.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-tr.php
new file mode 100644
index 0000000000..3c45bc1c35
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-tr.php
@@ -0,0 +1,38 @@
+
+ * @fixed by Boris Yurchenko
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Помилка SMTP: помилка авторизації.';
+$PHPMAILER_LANG['connect_host'] = 'Помилка SMTP: не вдається під\'єднатися до SMTP-серверу.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Помилка SMTP: дані не прийнято.';
+$PHPMAILER_LANG['encoding'] = 'Невідоме кодування: ';
+$PHPMAILER_LANG['execute'] = 'Неможливо виконати команду: ';
+$PHPMAILER_LANG['file_access'] = 'Немає доступу до файлу: ';
+$PHPMAILER_LANG['file_open'] = 'Помилка файлової системи: не вдається відкрити файл: ';
+$PHPMAILER_LANG['from_failed'] = 'Невірна адреса відправника: ';
+$PHPMAILER_LANG['instantiate'] = 'Неможливо запустити функцію mail().';
+$PHPMAILER_LANG['provide_address'] = 'Будь ласка, введіть хоча б одну email-адресу отримувача.';
+$PHPMAILER_LANG['mailer_not_supported'] = ' - поштовий сервер не підтримується.';
+$PHPMAILER_LANG['recipients_failed'] = 'Помилка SMTP: не вдалося відправлення для таких отримувачів: ';
+$PHPMAILER_LANG['empty_message'] = 'Пусте повідомлення';
+$PHPMAILER_LANG['invalid_address'] = 'Не відправлено через неправильний формат email-адреси: ';
+$PHPMAILER_LANG['signing'] = 'Помилка підпису: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Помилка з\'єднання з SMTP-сервером';
+$PHPMAILER_LANG['smtp_error'] = 'Помилка SMTP-сервера: ';
+$PHPMAILER_LANG['variable_set'] = 'Неможливо встановити або скинути змінну: ';
+$PHPMAILER_LANG['extension_missing'] = 'Розширення відсутнє: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ur.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ur.php
new file mode 100644
index 0000000000..0b9de0f127
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-ur.php
@@ -0,0 +1,30 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP خرابی: تصدیق کرنے سے قاصر۔';
+$PHPMAILER_LANG['connect_host'] = 'SMTP خرابی: سرور سے منسلک ہونے سے قاصر۔';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP خرابی: ڈیٹا قبول نہیں کیا گیا۔';
+$PHPMAILER_LANG['empty_message'] = 'پیغام کی باڈی خالی ہے۔';
+$PHPMAILER_LANG['encoding'] = 'نامعلوم انکوڈنگ: ';
+$PHPMAILER_LANG['execute'] = 'عمل کرنے کے قابل نہیں ';
+$PHPMAILER_LANG['file_access'] = 'فائل تک رسائی سے قاصر:';
+$PHPMAILER_LANG['file_open'] = 'فائل کی خرابی: فائل کو کھولنے سے قاصر:';
+$PHPMAILER_LANG['from_failed'] = 'درج ذیل بھیجنے والے کا پتہ ناکام ہو گیا:';
+$PHPMAILER_LANG['instantiate'] = 'میل فنکشن کی مثال بنانے سے قاصر۔';
+$PHPMAILER_LANG['invalid_address'] = 'بھیجنے سے قاصر: غلط ای میل پتہ:';
+$PHPMAILER_LANG['mailer_not_supported'] = ' میلر تعاون یافتہ نہیں ہے۔';
+$PHPMAILER_LANG['provide_address'] = 'آپ کو کم از کم ایک منزل کا ای میل پتہ فراہم کرنا چاہیے۔';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP خرابی: درج ذیل پتہ پر نہیں بھیجا جاسکا: ';
+$PHPMAILER_LANG['signing'] = 'دستخط کی خرابی: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP ملنا ناکام ہوا';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP سرور کی خرابی: ';
+$PHPMAILER_LANG['variable_set'] = 'متغیر سیٹ نہیں کیا جا سکا: ';
+$PHPMAILER_LANG['extension_missing'] = 'ایکٹینشن موجود نہیں ہے۔ ';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP سرور کوڈ: ';
+$PHPMAILER_LANG['smtp_code_ex'] = 'اضافی SMTP سرور کی معلومات:';
+$PHPMAILER_LANG['invalid_header'] = 'غلط ہیڈر کا نام یا قدر';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-vi.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-vi.php
new file mode 100644
index 0000000000..d65576e2d4
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-vi.php
@@ -0,0 +1,27 @@
+
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'Lỗi SMTP: Không thể xác thực.';
+$PHPMAILER_LANG['connect_host'] = 'Lỗi SMTP: Không thể kết nối máy chủ SMTP.';
+$PHPMAILER_LANG['data_not_accepted'] = 'Lỗi SMTP: Dữ liệu không được chấp nhận.';
+$PHPMAILER_LANG['empty_message'] = 'Không có nội dung';
+$PHPMAILER_LANG['encoding'] = 'Mã hóa không xác định: ';
+$PHPMAILER_LANG['execute'] = 'Không thực hiện được: ';
+$PHPMAILER_LANG['file_access'] = 'Không thể truy cập tệp tin ';
+$PHPMAILER_LANG['file_open'] = 'Lỗi Tập tin: Không thể mở tệp tin: ';
+$PHPMAILER_LANG['from_failed'] = 'Lỗi địa chỉ gửi đi: ';
+$PHPMAILER_LANG['instantiate'] = 'Không dùng được các hàm gửi thư.';
+$PHPMAILER_LANG['invalid_address'] = 'Đại chỉ emai không đúng: ';
+$PHPMAILER_LANG['mailer_not_supported'] = ' trình gửi thư không được hỗ trợ.';
+$PHPMAILER_LANG['provide_address'] = 'Bạn phải cung cấp ít nhất một địa chỉ người nhận.';
+$PHPMAILER_LANG['recipients_failed'] = 'Lỗi SMTP: lỗi địa chỉ người nhận: ';
+$PHPMAILER_LANG['signing'] = 'Lỗi đăng nhập: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'Lỗi kết nối với SMTP';
+$PHPMAILER_LANG['smtp_error'] = 'Lỗi máy chủ smtp ';
+$PHPMAILER_LANG['variable_set'] = 'Không thể thiết lập hoặc thiết lập lại biến: ';
+//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh.php
new file mode 100644
index 0000000000..35e4e7000e
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh.php
@@ -0,0 +1,29 @@
+
+ * @author Peter Dave Hello <@PeterDaveHello/>
+ * @author Jason Chiang
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP 錯誤:登入失敗。';
+$PHPMAILER_LANG['connect_host'] = 'SMTP 錯誤:無法連線到 SMTP 主機。';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 錯誤:無法接受的資料。';
+$PHPMAILER_LANG['empty_message'] = '郵件內容為空';
+$PHPMAILER_LANG['encoding'] = '未知編碼: ';
+$PHPMAILER_LANG['execute'] = '無法執行:';
+$PHPMAILER_LANG['file_access'] = '無法存取檔案:';
+$PHPMAILER_LANG['file_open'] = '檔案錯誤:無法開啟檔案:';
+$PHPMAILER_LANG['from_failed'] = '發送地址錯誤:';
+$PHPMAILER_LANG['instantiate'] = '未知函數呼叫。';
+$PHPMAILER_LANG['invalid_address'] = '因為電子郵件地址無效,無法傳送: ';
+$PHPMAILER_LANG['mailer_not_supported'] = '不支援的發信客戶端。';
+$PHPMAILER_LANG['provide_address'] = '必須提供至少一個收件人地址。';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP 錯誤:以下收件人地址錯誤:';
+$PHPMAILER_LANG['signing'] = '電子簽章錯誤: ';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 連線失敗';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP 伺服器錯誤: ';
+$PHPMAILER_LANG['variable_set'] = '無法設定或重設變數: ';
+$PHPMAILER_LANG['extension_missing'] = '遺失模組 Extension: ';
diff --git a/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php
new file mode 100644
index 0000000000..03d491165b
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/language/phpmailer.lang-zh_cn.php
@@ -0,0 +1,36 @@
+
+ * @author young
+ * @author Teddysun
+ */
+
+$PHPMAILER_LANG['authenticate'] = 'SMTP 错误:登录失败。';
+$PHPMAILER_LANG['buggy_php'] = '您的 PHP 版本存在漏洞,可能会导致消息损坏。为修复此问题,请切换到使用 SMTP 发送,在您的 php.ini 中禁用 mail.add_x_header 选项。切换到 MacOS 或 Linux,或将您的 PHP 升级到 7.0.17+ 或 7.1.3+ 版本。';
+$PHPMAILER_LANG['connect_host'] = 'SMTP 错误:无法连接到 SMTP 主机。';
+$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 错误:数据不被接受。';
+$PHPMAILER_LANG['empty_message'] = '邮件正文为空。';
+$PHPMAILER_LANG['encoding'] = '未知编码:';
+$PHPMAILER_LANG['execute'] = '无法执行:';
+$PHPMAILER_LANG['extension_missing'] = '缺少扩展名:';
+$PHPMAILER_LANG['file_access'] = '无法访问文件:';
+$PHPMAILER_LANG['file_open'] = '文件错误:无法打开文件:';
+$PHPMAILER_LANG['from_failed'] = '发送地址错误:';
+$PHPMAILER_LANG['instantiate'] = '未知函数调用。';
+$PHPMAILER_LANG['invalid_address'] = '发送失败,电子邮箱地址是无效的:';
+$PHPMAILER_LANG['mailer_not_supported'] = '发信客户端不被支持。';
+$PHPMAILER_LANG['provide_address'] = '必须提供至少一个收件人地址。';
+$PHPMAILER_LANG['recipients_failed'] = 'SMTP 错误:收件人地址错误:';
+$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP服务器连接失败。';
+$PHPMAILER_LANG['smtp_error'] = 'SMTP服务器出错:';
+$PHPMAILER_LANG['variable_set'] = '无法设置或重置变量:';
+$PHPMAILER_LANG['invalid_header'] = '无效的标题名称或值';
+$PHPMAILER_LANG['invalid_hostentry'] = '无效的hostentry: ';
+$PHPMAILER_LANG['invalid_host'] = '无效的主机:';
+$PHPMAILER_LANG['signing'] = '签名错误:';
+$PHPMAILER_LANG['smtp_code'] = 'SMTP代码: ';
+$PHPMAILER_LANG['smtp_code_ex'] = '附加SMTP信息: ';
+$PHPMAILER_LANG['smtp_detail'] = '详情:';
diff --git a/lib/vendor/phpmailer/phpmailer/src/DSNConfigurator.php b/lib/vendor/phpmailer/phpmailer/src/DSNConfigurator.php
new file mode 100644
index 0000000000..7058c1f05e
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/src/DSNConfigurator.php
@@ -0,0 +1,245 @@
+
+ * @author Jim Jagielski (jimjag)
+ * @author Andy Prevost (codeworxtech)
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2023 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * Configure PHPMailer with DSN string.
+ *
+ * @see https://en.wikipedia.org/wiki/Data_source_name
+ *
+ * @author Oleg Voronkovich
+ */
+class DSNConfigurator
+{
+ /**
+ * Create new PHPMailer instance configured by DSN.
+ *
+ * @param string $dsn DSN
+ * @param bool $exceptions Should we throw external exceptions?
+ *
+ * @return PHPMailer
+ */
+ public static function mailer($dsn, $exceptions = null)
+ {
+ static $configurator = null;
+
+ if (null === $configurator) {
+ $configurator = new DSNConfigurator();
+ }
+
+ return $configurator->configure(new PHPMailer($exceptions), $dsn);
+ }
+
+ /**
+ * Configure PHPMailer instance with DSN string.
+ *
+ * @param PHPMailer $mailer PHPMailer instance
+ * @param string $dsn DSN
+ *
+ * @return PHPMailer
+ */
+ public function configure(PHPMailer $mailer, $dsn)
+ {
+ $config = $this->parseDSN($dsn);
+
+ $this->applyConfig($mailer, $config);
+
+ return $mailer;
+ }
+
+ /**
+ * Parse DSN string.
+ *
+ * @param string $dsn DSN
+ *
+ * @throws Exception If DSN is malformed
+ *
+ * @return array Configuration
+ */
+ private function parseDSN($dsn)
+ {
+ $config = $this->parseUrl($dsn);
+
+ if (false === $config || !isset($config['scheme']) || !isset($config['host'])) {
+ throw new Exception('Malformed DSN');
+ }
+
+ if (isset($config['query'])) {
+ parse_str($config['query'], $config['query']);
+ }
+
+ return $config;
+ }
+
+ /**
+ * Apply configuration to mailer.
+ *
+ * @param PHPMailer $mailer PHPMailer instance
+ * @param array $config Configuration
+ *
+ * @throws Exception If scheme is invalid
+ */
+ private function applyConfig(PHPMailer $mailer, $config)
+ {
+ switch ($config['scheme']) {
+ case 'mail':
+ $mailer->isMail();
+ break;
+ case 'sendmail':
+ $mailer->isSendmail();
+ break;
+ case 'qmail':
+ $mailer->isQmail();
+ break;
+ case 'smtp':
+ case 'smtps':
+ $mailer->isSMTP();
+ $this->configureSMTP($mailer, $config);
+ break;
+ default:
+ throw new Exception(
+ sprintf(
+ 'Invalid scheme: "%s". Allowed values: "mail", "sendmail", "qmail", "smtp", "smtps".',
+ $config['scheme']
+ )
+ );
+ }
+
+ if (isset($config['query'])) {
+ $this->configureOptions($mailer, $config['query']);
+ }
+ }
+
+ /**
+ * Configure SMTP.
+ *
+ * @param PHPMailer $mailer PHPMailer instance
+ * @param array $config Configuration
+ */
+ private function configureSMTP($mailer, $config)
+ {
+ $isSMTPS = 'smtps' === $config['scheme'];
+
+ if ($isSMTPS) {
+ $mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+ }
+
+ $mailer->Host = $config['host'];
+
+ if (isset($config['port'])) {
+ $mailer->Port = $config['port'];
+ } elseif ($isSMTPS) {
+ $mailer->Port = SMTP::DEFAULT_SECURE_PORT;
+ }
+
+ $mailer->SMTPAuth = isset($config['user']) || isset($config['pass']);
+
+ if (isset($config['user'])) {
+ $mailer->Username = $config['user'];
+ }
+
+ if (isset($config['pass'])) {
+ $mailer->Password = $config['pass'];
+ }
+ }
+
+ /**
+ * Configure options.
+ *
+ * @param PHPMailer $mailer PHPMailer instance
+ * @param array $options Options
+ *
+ * @throws Exception If option is unknown
+ */
+ private function configureOptions(PHPMailer $mailer, $options)
+ {
+ $allowedOptions = get_object_vars($mailer);
+
+ unset($allowedOptions['Mailer']);
+ unset($allowedOptions['SMTPAuth']);
+ unset($allowedOptions['Username']);
+ unset($allowedOptions['Password']);
+ unset($allowedOptions['Hostname']);
+ unset($allowedOptions['Port']);
+ unset($allowedOptions['ErrorInfo']);
+
+ $allowedOptions = \array_keys($allowedOptions);
+
+ foreach ($options as $key => $value) {
+ if (!in_array($key, $allowedOptions)) {
+ throw new Exception(
+ sprintf(
+ 'Unknown option: "%s". Allowed values: "%s"',
+ $key,
+ implode('", "', $allowedOptions)
+ )
+ );
+ }
+
+ switch ($key) {
+ case 'AllowEmpty':
+ case 'SMTPAutoTLS':
+ case 'SMTPKeepAlive':
+ case 'SingleTo':
+ case 'UseSendmailOptions':
+ case 'do_verp':
+ case 'DKIM_copyHeaderFields':
+ $mailer->$key = (bool) $value;
+ break;
+ case 'Priority':
+ case 'SMTPDebug':
+ case 'WordWrap':
+ $mailer->$key = (int) $value;
+ break;
+ default:
+ $mailer->$key = $value;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parse a URL.
+ * Wrapper for the built-in parse_url function to work around a bug in PHP 5.5.
+ *
+ * @param string $url URL
+ *
+ * @return array|false
+ */
+ protected function parseUrl($url)
+ {
+ if (\PHP_VERSION_ID >= 50600 || false === strpos($url, '?')) {
+ return parse_url($url);
+ }
+
+ $chunks = explode('?', $url);
+ if (is_array($chunks)) {
+ $result = parse_url($chunks[0]);
+ if (is_array($result)) {
+ $result['query'] = $chunks[1];
+ }
+ return $result;
+ }
+
+ return false;
+ }
+}
diff --git a/lib/vendor/phpmailer/phpmailer/src/Exception.php b/lib/vendor/phpmailer/phpmailer/src/Exception.php
new file mode 100644
index 0000000000..09c1a2cfef
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/src/Exception.php
@@ -0,0 +1,40 @@
+
+ * @author Jim Jagielski (jimjag)
+ * @author Andy Prevost (codeworxtech)
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer exception handler.
+ *
+ * @author Marcus Bointon
+ */
+class Exception extends \Exception
+{
+ /**
+ * Prettify error message output.
+ *
+ * @return string
+ */
+ public function errorMessage()
+ {
+ return '' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . " \n";
+ }
+}
diff --git a/lib/vendor/phpmailer/phpmailer/src/OAuth.php b/lib/vendor/phpmailer/phpmailer/src/OAuth.php
new file mode 100644
index 0000000000..a7e958860c
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/src/OAuth.php
@@ -0,0 +1,139 @@
+
+ * @author Jim Jagielski (jimjag)
+ * @author Andy Prevost (codeworxtech)
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+use League\OAuth2\Client\Grant\RefreshToken;
+use League\OAuth2\Client\Provider\AbstractProvider;
+use League\OAuth2\Client\Token\AccessToken;
+
+/**
+ * OAuth - OAuth2 authentication wrapper class.
+ * Uses the oauth2-client package from the League of Extraordinary Packages.
+ *
+ * @see https://oauth2-client.thephpleague.com
+ *
+ * @author Marcus Bointon (Synchro/coolbru)
+ */
+class OAuth implements OAuthTokenProvider
+{
+ /**
+ * An instance of the League OAuth Client Provider.
+ *
+ * @var AbstractProvider
+ */
+ protected $provider;
+
+ /**
+ * The current OAuth access token.
+ *
+ * @var AccessToken
+ */
+ protected $oauthToken;
+
+ /**
+ * The user's email address, usually used as the login ID
+ * and also the from address when sending email.
+ *
+ * @var string
+ */
+ protected $oauthUserEmail = '';
+
+ /**
+ * The client secret, generated in the app definition of the service you're connecting to.
+ *
+ * @var string
+ */
+ protected $oauthClientSecret = '';
+
+ /**
+ * The client ID, generated in the app definition of the service you're connecting to.
+ *
+ * @var string
+ */
+ protected $oauthClientId = '';
+
+ /**
+ * The refresh token, used to obtain new AccessTokens.
+ *
+ * @var string
+ */
+ protected $oauthRefreshToken = '';
+
+ /**
+ * OAuth constructor.
+ *
+ * @param array $options Associative array containing
+ * `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements
+ */
+ public function __construct($options)
+ {
+ $this->provider = $options['provider'];
+ $this->oauthUserEmail = $options['userName'];
+ $this->oauthClientSecret = $options['clientSecret'];
+ $this->oauthClientId = $options['clientId'];
+ $this->oauthRefreshToken = $options['refreshToken'];
+ }
+
+ /**
+ * Get a new RefreshToken.
+ *
+ * @return RefreshToken
+ */
+ protected function getGrant()
+ {
+ return new RefreshToken();
+ }
+
+ /**
+ * Get a new AccessToken.
+ *
+ * @return AccessToken
+ */
+ protected function getToken()
+ {
+ return $this->provider->getAccessToken(
+ $this->getGrant(),
+ ['refresh_token' => $this->oauthRefreshToken]
+ );
+ }
+
+ /**
+ * Generate a base64-encoded OAuth token.
+ *
+ * @return string
+ */
+ public function getOauth64()
+ {
+ //Get a new token if it's not available or has expired
+ if (null === $this->oauthToken || $this->oauthToken->hasExpired()) {
+ $this->oauthToken = $this->getToken();
+ }
+
+ return base64_encode(
+ 'user=' .
+ $this->oauthUserEmail .
+ "\001auth=Bearer " .
+ $this->oauthToken .
+ "\001\001"
+ );
+ }
+}
diff --git a/lib/vendor/phpmailer/phpmailer/src/OAuthTokenProvider.php b/lib/vendor/phpmailer/phpmailer/src/OAuthTokenProvider.php
new file mode 100644
index 0000000000..cbda1a1296
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/src/OAuthTokenProvider.php
@@ -0,0 +1,44 @@
+
+ * @author Jim Jagielski (jimjag)
+ * @author Andy Prevost (codeworxtech)
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * OAuthTokenProvider - OAuth2 token provider interface.
+ * Provides base64 encoded OAuth2 auth strings for SMTP authentication.
+ *
+ * @see OAuth
+ * @see SMTP::authenticate()
+ *
+ * @author Peter Scopes (pdscopes)
+ * @author Marcus Bointon (Synchro/coolbru)
+ */
+interface OAuthTokenProvider
+{
+ /**
+ * Generate a base64-encoded OAuth token ensuring that the access token has not expired.
+ * The string to be base 64 encoded should be in the form:
+ * "user=\001auth=Bearer \001\001"
+ *
+ * @return string
+ */
+ public function getOauth64();
+}
diff --git a/lib/vendor/phpmailer/phpmailer/src/PHPMailer.php b/lib/vendor/phpmailer/phpmailer/src/PHPMailer.php
new file mode 100644
index 0000000000..2444bcf351
--- /dev/null
+++ b/lib/vendor/phpmailer/phpmailer/src/PHPMailer.php
@@ -0,0 +1,5362 @@
+
+ * @author Jim Jagielski (jimjag)
+ * @author Andy Prevost (codeworxtech)
+ * @author Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
+ * @note This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ *
+ * @author Marcus Bointon (Synchro/coolbru)
+ * @author Jim Jagielski (jimjag)
+ * @author Andy Prevost (codeworxtech)
+ * @author Brent R. Matzelle (original founder)
+ */
+class PHPMailer
+{
+ const CHARSET_ASCII = 'us-ascii';
+ const CHARSET_ISO88591 = 'iso-8859-1';
+ const CHARSET_UTF8 = 'utf-8';
+
+ const CONTENT_TYPE_PLAINTEXT = 'text/plain';
+ const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
+ const CONTENT_TYPE_TEXT_HTML = 'text/html';
+ const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
+ const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
+ const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
+
+ const ENCODING_7BIT = '7bit';
+ const ENCODING_8BIT = '8bit';
+ const ENCODING_BASE64 = 'base64';
+ const ENCODING_BINARY = 'binary';
+ const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
+
+ const ENCRYPTION_STARTTLS = 'tls';
+ const ENCRYPTION_SMTPS = 'ssl';
+
+ const ICAL_METHOD_REQUEST = 'REQUEST';
+ const ICAL_METHOD_PUBLISH = 'PUBLISH';
+ const ICAL_METHOD_REPLY = 'REPLY';
+ const ICAL_METHOD_ADD = 'ADD';
+ const ICAL_METHOD_CANCEL = 'CANCEL';
+ const ICAL_METHOD_REFRESH = 'REFRESH';
+ const ICAL_METHOD_COUNTER = 'COUNTER';
+ const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
+
+ /**
+ * Email priority.
+ * Options: null (default), 1 = High, 3 = Normal, 5 = low.
+ * When null, the header is not set at all.
+ *
+ * @var int|null
+ */
+ public $Priority;
+
+ /**
+ * The character set of the message.
+ *
+ * @var string
+ */
+ public $CharSet = self::CHARSET_ISO88591;
+
+ /**
+ * The MIME Content-type of the message.
+ *
+ * @var string
+ */
+ public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
+
+ /**
+ * The message encoding.
+ * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
+ *
+ * @var string
+ */
+ public $Encoding = self::ENCODING_8BIT;
+
+ /**
+ * Holds the most recent mailer error message.
+ *
+ * @var string
+ */
+ public $ErrorInfo = '';
+
+ /**
+ * The From email address for the message.
+ *
+ * @var string
+ */
+ public $From = '';
+
+ /**
+ * The From name of the message.
+ *
+ * @var string
+ */
+ public $FromName = '';
+
+ /**
+ * The envelope sender of the message.
+ * This will usually be turned into a Return-Path header by the receiver,
+ * and is the address that bounces will be sent to.
+ * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
+ *
+ * @var string
+ */
+ public $Sender = '';
+
+ /**
+ * The Subject of the message.
+ *
+ * @var string
+ */
+ public $Subject = '';
+
+ /**
+ * An HTML or plain text message body.
+ * If HTML then call isHTML(true).
+ *
+ * @var string
+ */
+ public $Body = '';
+
+ /**
+ * The plain-text message body.
+ * This body can be read by mail clients that do not have HTML email
+ * capability such as mutt & Eudora.
+ * Clients that can read HTML will view the normal Body.
+ *
+ * @var string
+ */
+ public $AltBody = '';
+
+ /**
+ * An iCal message part body.
+ * Only supported in simple alt or alt_inline message types
+ * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
+ *
+ * @see https://kigkonsult.se/iCalcreator/
+ *
+ * @var string
+ */
+ public $Ical = '';
+
+ /**
+ * Value-array of "method" in Contenttype header "text/calendar"
+ *
+ * @var string[]
+ */
+ protected static $IcalMethods = [
+ self::ICAL_METHOD_REQUEST,
+ self::ICAL_METHOD_PUBLISH,
+ self::ICAL_METHOD_REPLY,
+ self::ICAL_METHOD_ADD,
+ self::ICAL_METHOD_CANCEL,
+ self::ICAL_METHOD_REFRESH,
+ self::ICAL_METHOD_COUNTER,
+ self::ICAL_METHOD_DECLINECOUNTER,
+ ];
+
+ /**
+ * The complete compiled MIME message body.
+ *
+ * @var string
+ */
+ protected $MIMEBody = '';
+
+ /**
+ * The complete compiled MIME message headers.
+ *
+ * @var string
+ */
+ protected $MIMEHeader = '';
+
+ /**
+ * Extra headers that createHeader() doesn't fold in.
+ *
+ * @var string
+ */
+ protected $mailHeader = '';
+
+ /**
+ * Word-wrap the message body to this number of chars.
+ * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
+ *
+ * @see static::STD_LINE_LENGTH
+ *
+ * @var int
+ */
+ public $WordWrap = 0;
+
+ /**
+ * Which method to use to send mail.
+ * Options: "mail", "sendmail", or "smtp".
+ *
+ * @var string
+ */
+ public $Mailer = 'mail';
+
+ /**
+ * The path to the sendmail program.
+ *
+ * @var string
+ */
+ public $Sendmail = '/usr/sbin/sendmail';
+
+ /**
+ * Whether mail() uses a fully sendmail-compatible MTA.
+ * One which supports sendmail's "-oi -f" options.
+ *
+ * @var bool
+ */
+ public $UseSendmailOptions = true;
+
+ /**
+ * The email address that a reading confirmation should be sent to, also known as read receipt.
+ *
+ * @var string
+ */
+ public $ConfirmReadingTo = '';
+
+ /**
+ * The hostname to use in the Message-ID header and as default HELO string.
+ * If empty, PHPMailer attempts to find one with, in order,
+ * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
+ * 'localhost.localdomain'.
+ *
+ * @see PHPMailer::$Helo
+ *
+ * @var string
+ */
+ public $Hostname = '';
+
+ /**
+ * An ID to be used in the Message-ID header.
+ * If empty, a unique id will be generated.
+ * You can set your own, but it must be in the format "",
+ * as defined in RFC5322 section 3.6.4 or it will be ignored.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4
+ *
+ * @var string
+ */
+ public $MessageID = '';
+
+ /**
+ * The message Date to be used in the Date header.
+ * If empty, the current date will be added.
+ *
+ * @var string
+ */
+ public $MessageDate = '';
+
+ /**
+ * SMTP hosts.
+ * Either a single hostname or multiple semicolon-delimited hostnames.
+ * You can also specify a different port
+ * for each host by using this format: [hostname:port]
+ * (e.g. "smtp1.example.com:25;smtp2.example.com").
+ * You can also specify encryption type, for example:
+ * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
+ * Hosts will be tried in order.
+ *
+ * @var string
+ */
+ public $Host = 'localhost';
+
+ /**
+ * The default SMTP server port.
+ *
+ * @var int
+ */
+ public $Port = 25;
+
+ /**
+ * The SMTP HELO/EHLO name used for the SMTP connection.
+ * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
+ * one with the same method described above for $Hostname.
+ *
+ * @see PHPMailer::$Hostname
+ *
+ * @var string
+ */
+ public $Helo = '';
+
+ /**
+ * What kind of encryption to use on the SMTP connection.
+ * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
+ *
+ * @var string
+ */
+ public $SMTPSecure = '';
+
+ /**
+ * Whether to enable TLS encryption automatically if a server supports it,
+ * even if `SMTPSecure` is not set to 'tls'.
+ * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
+ *
+ * @var bool
+ */
+ public $SMTPAutoTLS = true;
+
+ /**
+ * Whether to use SMTP authentication.
+ * Uses the Username and Password properties.
+ *
+ * @see PHPMailer::$Username
+ * @see PHPMailer::$Password
+ *
+ * @var bool
+ */
+ public $SMTPAuth = false;
+
+ /**
+ * Options array passed to stream_context_create when connecting via SMTP.
+ *
+ * @var array
+ */
+ public $SMTPOptions = [];
+
+ /**
+ * SMTP username.
+ *
+ * @var string
+ */
+ public $Username = '';
+
+ /**
+ * SMTP password.
+ *
+ * @var string
+ */
+ public $Password = '';
+
+ /**
+ * SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2.
+ * If not specified, the first one from that list that the server supports will be selected.
+ *
+ * @var string
+ */
+ public $AuthType = '';
+
+ /**
+ * SMTP SMTPXClient command attributes
+ *
+ * @var array
+ */
+ protected $SMTPXClient = [];
+
+ /**
+ * An implementation of the PHPMailer OAuthTokenProvider interface.
+ *
+ * @var OAuthTokenProvider
+ */
+ protected $oauth;
+
+ /**
+ * The SMTP server timeout in seconds.
+ * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+ *
+ * @var int
+ */
+ public $Timeout = 300;
+
+ /**
+ * Comma separated list of DSN notifications
+ * 'NEVER' under no circumstances a DSN must be returned to the sender.
+ * If you use NEVER all other notifications will be ignored.
+ * 'SUCCESS' will notify you when your mail has arrived at its destination.
+ * 'FAILURE' will arrive if an error occurred during delivery.
+ * 'DELAY' will notify you if there is an unusual delay in delivery, but the actual
+ * delivery's outcome (success or failure) is not yet decided.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc3461.html#section-4.1 for more information about NOTIFY
+ */
+ public $dsn = '';
+
+ /**
+ * SMTP class debug output mode.
+ * Debug output level.
+ * Options:
+ * @see SMTP::DEBUG_OFF: No output
+ * @see SMTP::DEBUG_CLIENT: Client messages
+ * @see SMTP::DEBUG_SERVER: Client and server messages
+ * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
+ * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
+ *
+ * @see SMTP::$do_debug
+ *
+ * @var int
+ */
+ public $SMTPDebug = 0;
+
+ /**
+ * How to handle debug output.
+ * Options:
+ * * `echo` Output plain-text as-is, appropriate for CLI
+ * * `html` Output escaped, line breaks converted to ` `, appropriate for browser output
+ * * `error_log` Output to error log as configured in php.ini
+ * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
+ * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
+ *
+ * ```php
+ * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
+ * ```
+ *
+ * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
+ * level output is used:
+ *
+ * ```php
+ * $mail->Debugoutput = new myPsr3Logger;
+ * ```
+ *
+ * @see SMTP::$Debugoutput
+ *
+ * @var string|callable|\Psr\Log\LoggerInterface
+ */
+ public $Debugoutput = 'echo';
+
+ /**
+ * Whether to keep the SMTP connection open after each message.
+ * If this is set to true then the connection will remain open after a send,
+ * and closing the connection will require an explicit call to smtpClose().
+ * It's a good idea to use this if you are sending multiple messages as it reduces overhead.
+ * See the mailing list example for how to use it.
+ *
+ * @var bool
+ */
+ public $SMTPKeepAlive = false;
+
+ /**
+ * Whether to split multiple to addresses into multiple messages
+ * or send them all in one message.
+ * Only supported in `mail` and `sendmail` transports, not in SMTP.
+ *
+ * @var bool
+ *
+ * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
+ */
+ public $SingleTo = false;
+
+ /**
+ * Storage for addresses when SingleTo is enabled.
+ *
+ * @var array
+ */
+ protected $SingleToArray = [];
+
+ /**
+ * Whether to generate VERP addresses on send.
+ * Only applicable when sending via SMTP.
+ *
+ * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
+ * @see https://www.postfix.org/VERP_README.html Postfix VERP info
+ *
+ * @var bool
+ */
+ public $do_verp = false;
+
+ /**
+ * Whether to allow sending messages with an empty body.
+ *
+ * @var bool
+ */
+ public $AllowEmpty = false;
+
+ /**
+ * DKIM selector.
+ *
+ * @var string
+ */
+ public $DKIM_selector = '';
+
+ /**
+ * DKIM Identity.
+ * Usually the email address used as the source of the email.
+ *
+ * @var string
+ */
+ public $DKIM_identity = '';
+
+ /**
+ * DKIM passphrase.
+ * Used if your key is encrypted.
+ *
+ * @var string
+ */
+ public $DKIM_passphrase = '';
+
+ /**
+ * DKIM signing domain name.
+ *
+ * @example 'example.com'
+ *
+ * @var string
+ */
+ public $DKIM_domain = '';
+
+ /**
+ * DKIM Copy header field values for diagnostic use.
+ *
+ * @var bool
+ */
+ public $DKIM_copyHeaderFields = true;
+
+ /**
+ * DKIM Extra signing headers.
+ *
+ * @example ['List-Unsubscribe', 'List-Help']
+ *
+ * @var array
+ */
+ public $DKIM_extraHeaders = [];
+
+ /**
+ * DKIM private key file path.
+ *
+ * @var string
+ */
+ public $DKIM_private = '';
+
+ /**
+ * DKIM private key string.
+ *
+ * If set, takes precedence over `$DKIM_private`.
+ *
+ * @var string
+ */
+ public $DKIM_private_string = '';
+
+ /**
+ * Callback Action function name.
+ *
+ * The function that handles the result of the send email action.
+ * It is called out by send() for each email sent.
+ *
+ * Value can be any php callable: https://www.php.net/is_callable
+ *
+ * Parameters:
+ * bool $result result of the send action
+ * array $to email addresses of the recipients
+ * array $cc cc email addresses
+ * array $bcc bcc email addresses
+ * string $subject the subject
+ * string $body the email body
+ * string $from email address of sender
+ * string $extra extra information of possible use
+ * "smtp_transaction_id' => last smtp transaction id
+ *
+ * @var string
+ */
+ public $action_function = '';
+
+ /**
+ * What to put in the X-Mailer header.
+ * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
+ *
+ * @var string|null
+ */
+ public $XMailer = '';
+
+ /**
+ * Which validator to use by default when validating email addresses.
+ * May be a callable to inject your own validator, but there are several built-in validators.
+ * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
+ *
+ * If CharSet is UTF8, the validator is left at the default value,
+ * and you send to addresses that use non-ASCII local parts, then
+ * PHPMailer automatically changes to the 'eai' validator.
+ *
+ * @see PHPMailer::validateAddress()
+ *
+ * @var string|callable
+ */
+ public static $validator = 'php';
+
+ /**
+ * An instance of the SMTP sender class.
+ *
+ * @var SMTP
+ */
+ protected $smtp;
+
+ /**
+ * The array of 'to' names and addresses.
+ *
+ * @var array
+ */
+ protected $to = [];
+
+ /**
+ * The array of 'cc' names and addresses.
+ *
+ * @var array
+ */
+ protected $cc = [];
+
+ /**
+ * The array of 'bcc' names and addresses.
+ *
+ * @var array
+ */
+ protected $bcc = [];
+
+ /**
+ * The array of reply-to names and addresses.
+ *
+ * @var array
+ */
+ protected $ReplyTo = [];
+
+ /**
+ * An array of all kinds of addresses.
+ * Includes all of $to, $cc, $bcc.
+ *
+ * @see PHPMailer::$to
+ * @see PHPMailer::$cc
+ * @see PHPMailer::$bcc
+ *
+ * @var array
+ */
+ protected $all_recipients = [];
+
+ /**
+ * An array of names and addresses queued for validation.
+ * In send(), valid and non duplicate entries are moved to $all_recipients
+ * and one of $to, $cc, or $bcc.
+ * This array is used only for addresses with IDN.
+ *
+ * @see PHPMailer::$to
+ * @see PHPMailer::$cc
+ * @see PHPMailer::$bcc
+ * @see PHPMailer::$all_recipients
+ *
+ * @var array
+ */
+ protected $RecipientsQueue = [];
+
+ /**
+ * An array of reply-to names and addresses queued for validation.
+ * In send(), valid and non duplicate entries are moved to $ReplyTo.
+ * This array is used only for addresses with IDN.
+ *
+ * @see PHPMailer::$ReplyTo
+ *
+ * @var array
+ */
+ protected $ReplyToQueue = [];
+
+ /**
+ * Whether the need for SMTPUTF8 has been detected. Set by
+ * preSend() if necessary.
+ *
+ * @var bool
+ */
+ public $UseSMTPUTF8 = false;
+
+ /**
+ * The array of attachments.
+ *
+ * @var array
+ */
+ protected $attachment = [];
+
+ /**
+ * The array of custom headers.
+ *
+ * @var array
+ */
+ protected $CustomHeader = [];
+
+ /**
+ * The most recent Message-ID (including angular brackets).
+ *
+ * @var string
+ */
+ protected $lastMessageID = '';
+
+ /**
+ * The message's MIME type.
+ *
+ * @var string
+ */
+ protected $message_type = '';
+
+ /**
+ * The array of MIME boundary strings.
+ *
+ * @var array
+ */
+ protected $boundary = [];
+
+ /**
+ * The array of available text strings for the current language.
+ *
+ * @var array
+ */
+ protected $language = [];
+
+ /**
+ * The number of errors encountered.
+ *
+ * @var int
+ */
+ protected $error_count = 0;
+
+ /**
+ * The S/MIME certificate file path.
+ *
+ * @var string
+ */
+ protected $sign_cert_file = '';
+
+ /**
+ * The S/MIME key file path.
+ *
+ * @var string
+ */
+ protected $sign_key_file = '';
+
+ /**
+ * The optional S/MIME extra certificates ("CA Chain") file path.
+ *
+ * @var string
+ */
+ protected $sign_extracerts_file = '';
+
+ /**
+ * The S/MIME password for the key.
+ * Used only if the key is encrypted.
+ *
+ * @var string
+ */
+ protected $sign_key_pass = '';
+
+ /**
+ * Whether to throw exceptions for errors.
+ *
+ * @var bool
+ */
+ protected $exceptions = false;
+
+ /**
+ * Unique ID used for message ID and boundaries.
+ *
+ * @var string
+ */
+ protected $uniqueid = '';
+
+ /**
+ * The PHPMailer Version number.
+ *
+ * @var string
+ */
+ const VERSION = '6.10.0';
+
+ /**
+ * Error severity: message only, continue processing.
+ *
+ * @var int
+ */
+ const STOP_MESSAGE = 0;
+
+ /**
+ * Error severity: message, likely ok to continue processing.
+ *
+ * @var int
+ */
+ const STOP_CONTINUE = 1;
+
+ /**
+ * Error severity: message, plus full stop, critical error reached.
+ *
+ * @var int
+ */
+ const STOP_CRITICAL = 2;
+
+ /**
+ * The SMTP standard CRLF line break.
+ * If you want to change line break format, change static::$LE, not this.
+ */
+ const CRLF = "\r\n";
+
+ /**
+ * "Folding White Space" a white space string used for line folding.
+ */
+ const FWS = ' ';
+
+ /**
+ * SMTP RFC standard line ending; Carriage Return, Line Feed.
+ *
+ * @var string
+ */
+ protected static $LE = self::CRLF;
+
+ /**
+ * The maximum line length supported by mail().
+ *
+ * Background: mail() will sometimes corrupt messages
+ * with headers longer than 65 chars, see #818.
+ *
+ * @var int
+ */
+ const MAIL_MAX_LINE_LENGTH = 63;
+
+ /**
+ * The maximum line length allowed by RFC 2822 section 2.1.1.
+ *
+ * @var int
+ */
+ const MAX_LINE_LENGTH = 998;
+
+ /**
+ * The lower maximum line length allowed by RFC 2822 section 2.1.1.
+ * This length does NOT include the line break
+ * 76 means that lines will be 77 or 78 chars depending on whether
+ * the line break format is LF or CRLF; both are valid.
+ *
+ * @var int
+ */
+ const STD_LINE_LENGTH = 76;
+
+ /**
+ * Constructor.
+ *
+ * @param bool $exceptions Should we throw external exceptions?
+ */
+ public function __construct($exceptions = null)
+ {
+ if (null !== $exceptions) {
+ $this->exceptions = (bool) $exceptions;
+ }
+ //Pick an appropriate debug output format automatically
+ $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
+ }
+
+ /**
+ * Destructor.
+ */
+ public function __destruct()
+ {
+ //Close any open SMTP connection nicely
+ $this->smtpClose();
+ }
+
+ /**
+ * Call mail() in a safe_mode-aware fashion.
+ * Also, unless sendmail_path points to sendmail (or something that
+ * claims to be sendmail), don't pass params (not a perfect fix,
+ * but it will do).
+ *
+ * @param string $to To
+ * @param string $subject Subject
+ * @param string $body Message Body
+ * @param string $header Additional Header(s)
+ * @param string|null $params Params
+ *
+ * @return bool
+ */
+ private function mailPassthru($to, $subject, $body, $header, $params)
+ {
+ //Check overloading of mail function to avoid double-encoding
+ if ((int)ini_get('mbstring.func_overload') & 1) {
+ $subject = $this->secureHeader($subject);
+ } else {
+ $subject = $this->encodeHeader($this->secureHeader($subject));
+ }
+ //Calling mail() with null params breaks
+ $this->edebug('Sending with mail()');
+ $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
+ $this->edebug("Envelope sender: {$this->Sender}");
+ $this->edebug("To: {$to}");
+ $this->edebug("Subject: {$subject}");
+ $this->edebug("Headers: {$header}");
+ if (!$this->UseSendmailOptions || null === $params) {
+ $result = @mail($to, $subject, $body, $header);
+ } else {
+ $this->edebug("Additional params: {$params}");
+ $result = @mail($to, $subject, $body, $header, $params);
+ }
+ $this->edebug('Result: ' . ($result ? 'true' : 'false'));
+ return $result;
+ }
+
+ /**
+ * Output debugging info via a user-defined method.
+ * Only generates output if debug output is enabled.
+ *
+ * @see PHPMailer::$Debugoutput
+ * @see PHPMailer::$SMTPDebug
+ *
+ * @param string $str
+ */
+ protected function edebug($str)
+ {
+ if ($this->SMTPDebug <= 0) {
+ return;
+ }
+ //Is this a PSR-3 logger?
+ if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
+ $this->Debugoutput->debug(rtrim($str, "\r\n"));
+
+ return;
+ }
+ //Avoid clash with built-in function names
+ if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
+ call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
+
+ return;
+ }
+ switch ($this->Debugoutput) {
+ case 'error_log':
+ //Don't output, just log
+ /** @noinspection ForgottenDebugOutputInspection */
+ error_log($str);
+ break;
+ case 'html':
+ //Cleans up output a bit for a better looking, HTML-safe output
+ echo htmlentities(
+ preg_replace('/[\r\n]+/', '', $str),
+ ENT_QUOTES,
+ 'UTF-8'
+ ), " \n";
+ break;
+ case 'echo':
+ default:
+ //Normalize line breaks
+ $str = preg_replace('/\r\n|\r/m', "\n", $str);
+ echo gmdate('Y-m-d H:i:s'),
+ "\t",
+ //Trim trailing space
+ trim(
+ //Indent for readability, except for trailing break
+ str_replace(
+ "\n",
+ "\n \t ",
+ trim($str)
+ )
+ ),
+ "\n";
+ }
+ }
+
+ /**
+ * Sets message type to HTML or plain.
+ *
+ * @param bool $isHtml True for HTML mode
+ */
+ public function isHTML($isHtml = true)
+ {
+ if ($isHtml) {
+ $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
+ } else {
+ $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
+ }
+ }
+
+ /**
+ * Send messages using SMTP.
+ */
+ public function isSMTP()
+ {
+ $this->Mailer = 'smtp';
+ }
+
+ /**
+ * Send messages using PHP's mail() function.
+ */
+ public function isMail()
+ {
+ $this->Mailer = 'mail';
+ }
+
+ /**
+ * Send messages using $Sendmail.
+ */
+ public function isSendmail()
+ {
+ $ini_sendmail_path = ini_get('sendmail_path');
+
+ if (false === stripos($ini_sendmail_path, 'sendmail')) {
+ $this->Sendmail = '/usr/sbin/sendmail';
+ } else {
+ $this->Sendmail = $ini_sendmail_path;
+ }
+ $this->Mailer = 'sendmail';
+ }
+
+ /**
+ * Send messages using qmail.
+ */
+ public function isQmail()
+ {
+ $ini_sendmail_path = ini_get('sendmail_path');
+
+ if (false === stripos($ini_sendmail_path, 'qmail')) {
+ $this->Sendmail = '/var/qmail/bin/qmail-inject';
+ } else {
+ $this->Sendmail = $ini_sendmail_path;
+ }
+ $this->Mailer = 'qmail';
+ }
+
+ /**
+ * Add a "To" address.
+ *
+ * @param string $address The email address to send to
+ * @param string $name
+ *
+ * @throws Exception
+ *
+ * @return bool true on success, false if address already used or invalid in some way
+ */
+ public function addAddress($address, $name = '')
+ {
+ return $this->addOrEnqueueAnAddress('to', $address, $name);
+ }
+
+ /**
+ * Add a "CC" address.
+ *
+ * @param string $address The email address to send to
+ * @param string $name
+ *
+ * @throws Exception
+ *
+ * @return bool true on success, false if address already used or invalid in some way
+ */
+ public function addCC($address, $name = '')
+ {
+ return $this->addOrEnqueueAnAddress('cc', $address, $name);
+ }
+
+ /**
+ * Add a "BCC" address.
+ *
+ * @param string $address The email address to send to
+ * @param string $name
+ *
+ * @throws Exception
+ *
+ * @return bool true on success, false if address already used or invalid in some way
+ */
+ public function addBCC($address, $name = '')
+ {
+ return $this->addOrEnqueueAnAddress('bcc', $address, $name);
+ }
+
+ /**
+ * Add a "Reply-To" address.
+ *
+ * @param string $address The email address to reply to
+ * @param string $name
+ *
+ * @throws Exception
+ *
+ * @return bool true on success, false if address already used or invalid in some way
+ */
+ public function addReplyTo($address, $name = '')
+ {
+ return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
+ }
+
+ /**
+ * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
+ * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
+ * be modified after calling this function), addition of such addresses is delayed until send().
+ * Addresses that have been added already return false, but do not throw exceptions.
+ *
+ * @param string $kind One of 'to', 'cc', 'bcc', or 'Reply-To'
+ * @param string $address The email address
+ * @param string $name An optional username associated with the address
+ *
+ * @throws Exception
+ *
+ * @return bool true on success, false if address already used or invalid in some way
+ */
+ protected function addOrEnqueueAnAddress($kind, $address, $name)
+ {
+ $pos = false;
+ if ($address !== null) {
+ $address = trim($address);
+ $pos = strrpos($address, '@');
+ }
+ if (false === $pos) {
+ //At-sign is missing.
+ $error_message = sprintf(
+ '%s (%s): %s',
+ $this->lang('invalid_address'),
+ $kind,
+ $address
+ );
+ $this->setError($error_message);
+ $this->edebug($error_message);
+ if ($this->exceptions) {
+ throw new Exception($error_message);
+ }
+
+ return false;
+ }
+ if ($name !== null && is_string($name)) {
+ $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
+ } else {
+ $name = '';
+ }
+ $params = [$kind, $address, $name];
+ //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
+ //Domain is assumed to be whatever is after the last @ symbol in the address
+ if ($this->has8bitChars(substr($address, ++$pos))) {
+ if (static::idnSupported()) {
+ if ('Reply-To' !== $kind) {
+ if (!array_key_exists($address, $this->RecipientsQueue)) {
+ $this->RecipientsQueue[$address] = $params;
+
+ return true;
+ }
+ } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
+ $this->ReplyToQueue[$address] = $params;
+
+ return true;
+ }
+ }
+ //We have an 8-bit domain, but we are missing the necessary extensions to support it
+ //Or we are already sending to this address
+ return false;
+ }
+
+ //Immediately add standard addresses without IDN.
+ return call_user_func_array([$this, 'addAnAddress'], $params);
+ }
+
+ /**
+ * Set the boundaries to use for delimiting MIME parts.
+ * If you override this, ensure you set all 3 boundaries to unique values.
+ * The default boundaries include a "=_" sequence which cannot occur in quoted-printable bodies,
+ * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7
+ *
+ * @return void
+ */
+ public function setBoundaries()
+ {
+ $this->uniqueid = $this->generateId();
+ $this->boundary[1] = 'b1=_' . $this->uniqueid;
+ $this->boundary[2] = 'b2=_' . $this->uniqueid;
+ $this->boundary[3] = 'b3=_' . $this->uniqueid;
+ }
+
+ /**
+ * Add an address to one of the recipient arrays or to the ReplyTo array.
+ * Addresses that have been added already return false, but do not throw exceptions.
+ *
+ * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo'
+ * @param string $address The email address to send, resp. to reply to
+ * @param string $name
+ *
+ * @throws Exception
+ *
+ * @return bool true on success, false if address already used or invalid in some way
+ */
+ protected function addAnAddress($kind, $address, $name = '')
+ {
+ if (
+ self::$validator === 'php' &&
+ ((bool) preg_match('/[\x80-\xFF]/', $address))
+ ) {
+ //The caller has not altered the validator and is sending to an address
+ //with UTF-8, so assume that they want UTF-8 support instead of failing
+ $this->CharSet = self::CHARSET_UTF8;
+ self::$validator = 'eai';
+ }
+ if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
+ $error_message = sprintf(
+ '%s: %s',
+ $this->lang('Invalid recipient kind'),
+ $kind
+ );
+ $this->setError($error_message);
+ $this->edebug($error_message);
+ if ($this->exceptions) {
+ throw new Exception($error_message);
+ }
+
+ return false;
+ }
+ if (!static::validateAddress($address)) {
+ $error_message = sprintf(
+ '%s (%s): %s',
+ $this->lang('invalid_address'),
+ $kind,
+ $address
+ );
+ $this->setError($error_message);
+ $this->edebug($error_message);
+ if ($this->exceptions) {
+ throw new Exception($error_message);
+ }
+
+ return false;
+ }
+ if ('Reply-To' !== $kind) {
+ if (!array_key_exists(strtolower($address), $this->all_recipients)) {
+ $this->{$kind}[] = [$address, $name];
+ $this->all_recipients[strtolower($address)] = true;
+
+ return true;
+ }
+ } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
+ $this->ReplyTo[strtolower($address)] = [$address, $name];
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
+ * of the form "display name " into an array of name/address pairs.
+ * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
+ * Note that quotes in the name part are removed.
+ *
+ * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
+ *
+ * @param string $addrstr The address list string
+ * @param bool $useimap Whether to use the IMAP extension to parse the list
+ * @param string $charset The charset to use when decoding the address list string.
+ *
+ * @return array
+ */
+ public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)
+ {
+ $addresses = [];
+ if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
+ //Use this built-in parser if it's available
+ $list = imap_rfc822_parse_adrlist($addrstr, '');
+ // Clear any potential IMAP errors to get rid of notices being thrown at end of script.
+ imap_errors();
+ foreach ($list as $address) {
+ if (
+ '.SYNTAX-ERROR.' !== $address->host &&
+ static::validateAddress($address->mailbox . '@' . $address->host)
+ ) {
+ //Decode the name part if it's present and encoded
+ if (
+ property_exists($address, 'personal') &&
+ //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
+ defined('MB_CASE_UPPER') &&
+ preg_match('/^=\?.*\?=$/s', $address->personal)
+ ) {
+ $origCharset = mb_internal_encoding();
+ mb_internal_encoding($charset);
+ //Undo any RFC2047-encoded spaces-as-underscores
+ $address->personal = str_replace('_', '=20', $address->personal);
+ //Decode the name
+ $address->personal = mb_decode_mimeheader($address->personal);
+ mb_internal_encoding($origCharset);
+ }
+
+ $addresses[] = [
+ 'name' => (property_exists($address, 'personal') ? $address->personal : ''),
+ 'address' => $address->mailbox . '@' . $address->host,
+ ];
+ }
+ }
+ } else {
+ //Use this simpler parser
+ $list = explode(',', $addrstr);
+ foreach ($list as $address) {
+ $address = trim($address);
+ //Is there a separate name part?
+ if (strpos($address, '<') === false) {
+ //No separate name, just use the whole thing
+ if (static::validateAddress($address)) {
+ $addresses[] = [
+ 'name' => '',
+ 'address' => $address,
+ ];
+ }
+ } else {
+ list($name, $email) = explode('<', $address);
+ $email = trim(str_replace('>', '', $email));
+ $name = trim($name);
+ if (static::validateAddress($email)) {
+ //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
+ //If this name is encoded, decode it
+ if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) {
+ $origCharset = mb_internal_encoding();
+ mb_internal_encoding($charset);
+ //Undo any RFC2047-encoded spaces-as-underscores
+ $name = str_replace('_', '=20', $name);
+ //Decode the name
+ $name = mb_decode_mimeheader($name);
+ mb_internal_encoding($origCharset);
+ }
+ $addresses[] = [
+ //Remove any surrounding quotes and spaces from the name
+ 'name' => trim($name, '\'" '),
+ 'address' => $email,
+ ];
+ }
+ }
+ }
+ }
+
+ return $addresses;
+ }
+
+ /**
+ * Set the From and FromName properties.
+ *
+ * @param string $address
+ * @param string $name
+ * @param bool $auto Whether to also set the Sender address, defaults to true
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ public function setFrom($address, $name = '', $auto = true)
+ {
+ $address = trim((string)$address);
+ $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
+ //Don't validate now addresses with IDN. Will be done in send().
+ $pos = strrpos($address, '@');
+ if (
+ (false === $pos)
+ || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
+ && !static::validateAddress($address))
+ ) {
+ $error_message = sprintf(
+ '%s (From): %s',
+ $this->lang('invalid_address'),
+ $address
+ );
+ $this->setError($error_message);
+ $this->edebug($error_message);
+ if ($this->exceptions) {
+ throw new Exception($error_message);
+ }
+
+ return false;
+ }
+ $this->From = $address;
+ $this->FromName = $name;
+ if ($auto && empty($this->Sender)) {
+ $this->Sender = $address;
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the Message-ID header of the last email.
+ * Technically this is the value from the last time the headers were created,
+ * but it's also the message ID of the last sent message except in
+ * pathological cases.
+ *
+ * @return string
+ */
+ public function getLastMessageID()
+ {
+ return $this->lastMessageID;
+ }
+
+ /**
+ * Check that a string looks like an email address.
+ * Validation patterns supported:
+ * * `auto` Pick best pattern automatically;
+ * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
+ * * `pcre` Use old PCRE implementation;
+ * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
+ * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
+ * * `eai` Use a pattern similar to the HTML5 spec for 'email' and to firefox, extended to support EAI (RFC6530).
+ * * `noregex` Don't use a regex: super fast, really dumb.
+ * Alternatively you may pass in a callable to inject your own validator, for example:
+ *
+ * ```php
+ * PHPMailer::validateAddress('user@example.com', function($address) {
+ * return (strpos($address, '@') !== false);
+ * });
+ * ```
+ *
+ * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
+ *
+ * @param string $address The email address to check
+ * @param string|callable $patternselect Which pattern to use
+ *
+ * @return bool
+ */
+ public static function validateAddress($address, $patternselect = null)
+ {
+ if (null === $patternselect) {
+ $patternselect = static::$validator;
+ }
+ //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603
+ if (is_callable($patternselect) && !is_string($patternselect)) {
+ return call_user_func($patternselect, $address);
+ }
+ //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
+ if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
+ return false;
+ }
+ switch ($patternselect) {
+ case 'pcre': //Kept for BC
+ case 'pcre8':
+ /*
+ * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
+ * is based.
+ * In addition to the addresses allowed by filter_var, also permits:
+ * * dotless domains: `a@b`
+ * * comments: `1234 @ local(blah) .machine .example`
+ * * quoted elements: `'"test blah"@example.org'`
+ * * numeric TLDs: `a@b.123`
+ * * unbracketed IPv4 literals: `a@192.168.0.1`
+ * * IPv6 literals: 'first.last@[IPv6:a1::]'
+ * Not all of these will necessarily work for sending!
+ *
+ * @copyright 2009-2010 Michael Rushton
+ * Feel free to use and redistribute this code. But please keep this copyright notice.
+ */
+ return (bool) preg_match(
+ '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
+ '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
+ '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
+ '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
+ '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
+ '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
+ '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
+ '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
+ '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
+ $address
+ );
+ case 'html5':
+ /*
+ * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
+ *
+ * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
+ */
+ return (bool) preg_match(
+ '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
+ '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
+ $address
+ );
+ case 'eai':
+ /*
+ * This is the pattern used in the HTML5 spec for validation of 'email' type
+ * form input elements (as above), modified to accept Unicode email addresses.
+ * This is also more lenient than Firefox' html5 spec, in order to make the regex faster.
+ * 'eai' is an acronym for Email Address Internationalization.
+ * This validator is selected automatically if you attempt to use recipient addresses
+ * that contain Unicode characters in the local part.
+ *
+ * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
+ * @see https://en.wikipedia.org/wiki/International_email
+ */
+ return (bool) preg_match(
+ '/^[-\p{L}\p{N}\p{M}.!#$%&\'*+\/=?^_`{|}~]+@[\p{L}\p{N}\p{M}](?:[\p{L}\p{N}\p{M}-]{0,61}' .
+ '[\p{L}\p{N}\p{M}])?(?:\.[\p{L}\p{N}\p{M}]' .
+ '(?:[-\p{L}\p{N}\p{M}]{0,61}[\p{L}\p{N}\p{M}])?)*$/usD',
+ $address
+ );
+ case 'php':
+ default:
+ return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
+ }
+ }
+
+ /**
+ * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
+ * `intl` and `mbstring` PHP extensions.
+ *
+ * @return bool `true` if required functions for IDN support are present
+ */
+ public static function idnSupported()
+ {
+ return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
+ }
+
+ /**
+ * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
+ * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
+ * This function silently returns unmodified address if:
+ * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
+ * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
+ * or fails for any reason (e.g. domain contains characters not allowed in an IDN).
+ *
+ * @see PHPMailer::$CharSet
+ *
+ * @param string $address The email address to convert
+ *
+ * @return string The encoded address in ASCII form
+ */
+ public function punyencodeAddress($address)
+ {
+ //Verify we have required functions, CharSet, and at-sign.
+ $pos = strrpos($address, '@');
+ if (
+ !empty($this->CharSet) &&
+ false !== $pos &&
+ static::idnSupported()
+ ) {
+ $domain = substr($address, ++$pos);
+ //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
+ if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
+ //Convert the domain from whatever charset it's in to UTF-8
+ $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
+ //Ignore IDE complaints about this line - method signature changed in PHP 5.4
+ $errorcode = 0;
+ if (defined('INTL_IDNA_VARIANT_UTS46')) {
+ //Use the current punycode standard (appeared in PHP 7.2)
+ $punycode = idn_to_ascii(
+ $domain,
+ \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI |
+ \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII,
+ \INTL_IDNA_VARIANT_UTS46
+ );
+ } elseif (defined('INTL_IDNA_VARIANT_2003')) {
+ //Fall back to this old, deprecated/removed encoding
+ $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003);
+ } else {
+ //Fall back to a default we don't know about
+ $punycode = idn_to_ascii($domain, $errorcode);
+ }
+ if (false !== $punycode) {
+ return substr($address, 0, $pos) . $punycode;
+ }
+ }
+ }
+
+ return $address;
+ }
+
+ /**
+ * Create a message and send it.
+ * Uses the sending method specified by $Mailer.
+ *
+ * @throws Exception
+ *
+ * @return bool false on error - See the ErrorInfo property for details of the error
+ */
+ public function send()
+ {
+ try {
+ if (!$this->preSend()) {
+ return false;
+ }
+
+ return $this->postSend();
+ } catch (Exception $exc) {
+ $this->mailHeader = '';
+ $this->setError($exc->getMessage());
+ if ($this->exceptions) {
+ throw $exc;
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Prepare a message for sending.
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ public function preSend()
+ {
+ if (
+ 'smtp' === $this->Mailer
+ || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
+ ) {
+ //SMTP mandates RFC-compliant line endings
+ //and it's also used with mail() on Windows
+ static::setLE(self::CRLF);
+ } else {
+ //Maintain backward compatibility with legacy Linux command line mailers
+ static::setLE(PHP_EOL);
+ }
+ //Check for buggy PHP versions that add a header with an incorrect line break
+ if (
+ 'mail' === $this->Mailer
+ && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
+ || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
+ && ini_get('mail.add_x_header') === '1'
+ && stripos(PHP_OS, 'WIN') === 0
+ ) {
+ trigger_error($this->lang('buggy_php'), E_USER_WARNING);
+ }
+
+ try {
+ $this->error_count = 0; //Reset errors
+ $this->mailHeader = '';
+
+ //The code below tries to support full use of Unicode,
+ //while remaining compatible with legacy SMTP servers to
+ //the greatest degree possible: If the message uses
+ //Unicode in the local parts of any addresses, it is sent
+ //using SMTPUTF8. If not, it it sent using
+ //punycode-encoded domains and plain SMTP.
+ if (
+ static::CHARSET_UTF8 === strtolower($this->CharSet) &&
+ ($this->anyAddressHasUnicodeLocalPart($this->RecipientsQueue) ||
+ $this->anyAddressHasUnicodeLocalPart(array_keys($this->all_recipients)) ||
+ $this->anyAddressHasUnicodeLocalPart($this->ReplyToQueue) ||
+ $this->addressHasUnicodeLocalPart($this->From))
+ ) {
+ $this->UseSMTPUTF8 = true;
+ }
+ //Dequeue recipient and Reply-To addresses with IDN
+ foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
+ if (!$this->UseSMTPUTF8) {
+ $params[1] = $this->punyencodeAddress($params[1]);
+ }
+ call_user_func_array([$this, 'addAnAddress'], $params);
+ }
+ if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
+ throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
+ }
+
+ //Validate From, Sender, and ConfirmReadingTo addresses
+ foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
+ if ($this->{$address_kind} === null) {
+ $this->{$address_kind} = '';
+ continue;
+ }
+ $this->{$address_kind} = trim($this->{$address_kind});
+ if (empty($this->{$address_kind})) {
+ continue;
+ }
+ $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind});
+ if (!static::validateAddress($this->{$address_kind})) {
+ $error_message = sprintf(
+ '%s (%s): %s',
+ $this->lang('invalid_address'),
+ $address_kind,
+ $this->{$address_kind}
+ );
+ $this->setError($error_message);
+ $this->edebug($error_message);
+ if ($this->exceptions) {
+ throw new Exception($error_message);
+ }
+
+ return false;
+ }
+ }
+
+ //Set whether the message is multipart/alternative
+ if ($this->alternativeExists()) {
+ $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
+ }
+
+ $this->setMessageType();
+ //Refuse to send an empty message unless we are specifically allowing it
+ if (!$this->AllowEmpty && empty($this->Body)) {
+ throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
+ }
+
+ //Trim subject consistently
+ $this->Subject = trim($this->Subject);
+ //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
+ $this->MIMEHeader = '';
+ $this->MIMEBody = $this->createBody();
+ //createBody may have added some headers, so retain them
+ $tempheaders = $this->MIMEHeader;
+ $this->MIMEHeader = $this->createHeader();
+ $this->MIMEHeader .= $tempheaders;
+
+ //To capture the complete message when using mail(), create
+ //an extra header list which createHeader() doesn't fold in
+ if ('mail' === $this->Mailer) {
+ if (count($this->to) > 0) {
+ $this->mailHeader .= $this->addrAppend('To', $this->to);
+ } else {
+ $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
+ }
+ $this->mailHeader .= $this->headerLine(
+ 'Subject',
+ $this->encodeHeader($this->secureHeader($this->Subject))
+ );
+ }
+
+ //Sign with DKIM if enabled
+ if (
+ !empty($this->DKIM_domain)
+ && !empty($this->DKIM_selector)
+ && (!empty($this->DKIM_private_string)
+ || (!empty($this->DKIM_private)
+ && static::isPermittedPath($this->DKIM_private)
+ && file_exists($this->DKIM_private)
+ )
+ )
+ ) {
+ $header_dkim = $this->DKIM_Add(
+ $this->MIMEHeader . $this->mailHeader,
+ $this->encodeHeader($this->secureHeader($this->Subject)),
+ $this->MIMEBody
+ );
+ $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
+ static::normalizeBreaks($header_dkim) . static::$LE;
+ }
+
+ return true;
+ } catch (Exception $exc) {
+ $this->setError($exc->getMessage());
+ if ($this->exceptions) {
+ throw $exc;
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Actually send a message via the selected mechanism.
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ public function postSend()
+ {
+ try {
+ //Choose the mailer and send through it
+ switch ($this->Mailer) {
+ case 'sendmail':
+ case 'qmail':
+ return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
+ case 'smtp':
+ return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
+ case 'mail':
+ return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
+ default:
+ $sendMethod = $this->Mailer . 'Send';
+ if (method_exists($this, $sendMethod)) {
+ return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody);
+ }
+
+ return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
+ }
+ } catch (Exception $exc) {
+ $this->setError($exc->getMessage());
+ $this->edebug($exc->getMessage());
+ if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) {
+ $this->smtp->reset();
+ }
+ if ($this->exceptions) {
+ throw $exc;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Send mail using the $Sendmail program.
+ *
+ * @see PHPMailer::$Sendmail
+ *
+ * @param string $header The message headers
+ * @param string $body The message body
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ protected function sendmailSend($header, $body)
+ {
+ if ($this->Mailer === 'qmail') {
+ $this->edebug('Sending with qmail');
+ } else {
+ $this->edebug('Sending with sendmail');
+ }
+ $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+ //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
+ //A space after `-f` is optional, but there is a long history of its presence
+ //causing problems, so we don't use one
+ //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
+ //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html
+ //Example problem: https://www.drupal.org/node/1057954
+
+ //PHP 5.6 workaround
+ $sendmail_from_value = ini_get('sendmail_from');
+ if (empty($this->Sender) && !empty($sendmail_from_value)) {
+ //PHP config has a sender address we can use
+ $this->Sender = ini_get('sendmail_from');
+ }
+ //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+ if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
+ if ($this->Mailer === 'qmail') {
+ $sendmailFmt = '%s -f%s';
+ } else {
+ $sendmailFmt = '%s -oi -f%s -t';
+ }
+ } else {
+ //allow sendmail to choose a default envelope sender. It may
+ //seem preferable to force it to use the From header as with
+ //SMTP, but that introduces new problems (see
+ //), and
+ //it has historically worked this way.
+ $sendmailFmt = '%s -oi -t';
+ }
+
+ $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
+ $this->edebug('Sendmail path: ' . $this->Sendmail);
+ $this->edebug('Sendmail command: ' . $sendmail);
+ $this->edebug('Envelope sender: ' . $this->Sender);
+ $this->edebug("Headers: {$header}");
+
+ if ($this->SingleTo) {
+ foreach ($this->SingleToArray as $toAddr) {
+ $mail = @popen($sendmail, 'w');
+ if (!$mail) {
+ throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+ }
+ $this->edebug("To: {$toAddr}");
+ fwrite($mail, 'To: ' . $toAddr . "\n");
+ fwrite($mail, $header);
+ fwrite($mail, $body);
+ $result = pclose($mail);
+ $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
+ $this->doCallback(
+ ($result === 0),
+ [[$addrinfo['address'], $addrinfo['name']]],
+ $this->cc,
+ $this->bcc,
+ $this->Subject,
+ $body,
+ $this->From,
+ []
+ );
+ $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
+ if (0 !== $result) {
+ throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+ }
+ }
+ } else {
+ $mail = @popen($sendmail, 'w');
+ if (!$mail) {
+ throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+ }
+ fwrite($mail, $header);
+ fwrite($mail, $body);
+ $result = pclose($mail);
+ $this->doCallback(
+ ($result === 0),
+ $this->to,
+ $this->cc,
+ $this->bcc,
+ $this->Subject,
+ $body,
+ $this->From,
+ []
+ );
+ $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
+ if (0 !== $result) {
+ throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
+ * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
+ *
+ * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
+ *
+ * @param string $string The string to be validated
+ *
+ * @return bool
+ */
+ protected static function isShellSafe($string)
+ {
+ //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg,
+ //but some hosting providers disable it, creating a security problem that we don't want to have to deal with,
+ //so we don't.
+ if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) {
+ return false;
+ }
+
+ if (
+ escapeshellcmd($string) !== $string
+ || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
+ ) {
+ return false;
+ }
+
+ $length = strlen($string);
+
+ for ($i = 0; $i < $length; ++$i) {
+ $c = $string[$i];
+
+ //All other characters have a special meaning in at least one common shell, including = and +.
+ //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
+ //Note that this does permit non-Latin alphanumeric characters based on the current locale.
+ if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check whether a file path is of a permitted type.
+ * Used to reject URLs and phar files from functions that access local file paths,
+ * such as addAttachment.
+ *
+ * @param string $path A relative or absolute path to a file
+ *
+ * @return bool
+ */
+ protected static function isPermittedPath($path)
+ {
+ //Matches scheme definition from https://www.rfc-editor.org/rfc/rfc3986#section-3.1
+ return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
+ }
+
+ /**
+ * Check whether a file path is safe, accessible, and readable.
+ *
+ * @param string $path A relative or absolute path to a file
+ *
+ * @return bool
+ */
+ protected static function fileIsAccessible($path)
+ {
+ if (!static::isPermittedPath($path)) {
+ return false;
+ }
+ $readable = is_file($path);
+ //If not a UNC path (expected to start with \\), check read permission, see #2069
+ if (strpos($path, '\\\\') !== 0) {
+ $readable = $readable && is_readable($path);
+ }
+ return $readable;
+ }
+
+ /**
+ * Send mail using the PHP mail() function.
+ *
+ * @see https://www.php.net/manual/en/book.mail.php
+ *
+ * @param string $header The message headers
+ * @param string $body The message body
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ protected function mailSend($header, $body)
+ {
+ $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+
+ $toArr = [];
+ foreach ($this->to as $toaddr) {
+ $toArr[] = $this->addrFormat($toaddr);
+ }
+ $to = trim(implode(', ', $toArr));
+
+ //If there are no To-addresses (e.g. when sending only to BCC-addresses)
+ //the following should be added to get a correct DKIM-signature.
+ //Compare with $this->preSend()
+ if ($to === '') {
+ $to = 'undisclosed-recipients:;';
+ }
+
+ $params = null;
+ //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
+ //A space after `-f` is optional, but there is a long history of its presence
+ //causing problems, so we don't use one
+ //Exim docs: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
+ //Sendmail docs: https://www.sendmail.org/~ca/email/man/sendmail.html
+ //Example problem: https://www.drupal.org/node/1057954
+ //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+
+ //PHP 5.6 workaround
+ $sendmail_from_value = ini_get('sendmail_from');
+ if (empty($this->Sender) && !empty($sendmail_from_value)) {
+ //PHP config has a sender address we can use
+ $this->Sender = ini_get('sendmail_from');
+ }
+ if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
+ if (self::isShellSafe($this->Sender)) {
+ $params = sprintf('-f%s', $this->Sender);
+ }
+ $old_from = ini_get('sendmail_from');
+ ini_set('sendmail_from', $this->Sender);
+ }
+ $result = false;
+ if ($this->SingleTo && count($toArr) > 1) {
+ foreach ($toArr as $toAddr) {
+ $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
+ $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
+ $this->doCallback(
+ $result,
+ [[$addrinfo['address'], $addrinfo['name']]],
+ $this->cc,
+ $this->bcc,
+ $this->Subject,
+ $body,
+ $this->From,
+ []
+ );
+ }
+ } else {
+ $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
+ $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
+ }
+ if (isset($old_from)) {
+ ini_set('sendmail_from', $old_from);
+ }
+ if (!$result) {
+ throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
+ }
+
+ return true;
+ }
+
+ /**
+ * Get an instance to use for SMTP operations.
+ * Override this function to load your own SMTP implementation,
+ * or set one with setSMTPInstance.
+ *
+ * @return SMTP
+ */
+ public function getSMTPInstance()
+ {
+ if (!is_object($this->smtp)) {
+ $this->smtp = new SMTP();
+ }
+
+ return $this->smtp;
+ }
+
+ /**
+ * Provide an instance to use for SMTP operations.
+ *
+ * @return SMTP
+ */
+ public function setSMTPInstance(SMTP $smtp)
+ {
+ $this->smtp = $smtp;
+
+ return $this->smtp;
+ }
+
+ /**
+ * Provide SMTP XCLIENT attributes
+ *
+ * @param string $name Attribute name
+ * @param ?string $value Attribute value
+ *
+ * @return bool
+ */
+ public function setSMTPXclientAttribute($name, $value)
+ {
+ if (!in_array($name, SMTP::$xclient_allowed_attributes)) {
+ return false;
+ }
+ if (isset($this->SMTPXClient[$name]) && $value === null) {
+ unset($this->SMTPXClient[$name]);
+ } elseif ($value !== null) {
+ $this->SMTPXClient[$name] = $value;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get SMTP XCLIENT attributes
+ *
+ * @return array
+ */
+ public function getSMTPXclientAttributes()
+ {
+ return $this->SMTPXClient;
+ }
+
+ /**
+ * Send mail via SMTP.
+ * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
+ *
+ * @see PHPMailer::setSMTPInstance() to use a different class.
+ *
+ * @uses \PHPMailer\PHPMailer\SMTP
+ *
+ * @param string $header The message headers
+ * @param string $body The message body
+ *
+ * @throws Exception
+ *
+ * @return bool
+ */
+ protected function smtpSend($header, $body)
+ {
+ $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+ $bad_rcpt = [];
+ if (!$this->smtpConnect($this->SMTPOptions)) {
+ throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
+ }
+ //If we have recipient addresses that need Unicode support,
+ //but the server doesn't support it, stop here
+ if ($this->UseSMTPUTF8 && !$this->smtp->getServerExt('SMTPUTF8')) {
+ throw new Exception($this->lang('no_smtputf8'), self::STOP_CRITICAL);
+ }
+ //Sender already validated in preSend()
+ if ('' === $this->Sender) {
+ $smtp_from = $this->From;
+ } else {
+ $smtp_from = $this->Sender;
+ }
+ if (count($this->SMTPXClient)) {
+ $this->smtp->xclient($this->SMTPXClient);
+ }
+ if (!$this->smtp->mail($smtp_from)) {
+ $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
+ throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
+ }
+
+ $callbacks = [];
+ //Attempt to send to all recipients
+ foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
+ foreach ($togroup as $to) {
+ if (!$this->smtp->recipient($to[0], $this->dsn)) {
+ $error = $this->smtp->getError();
+ $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
+ $isSent = false;
+ } else {
+ $isSent = true;
+ }
+
+ $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];
+ }
+ }
+
+ //Only send the DATA command if we have viable recipients
+ if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
+ throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
+ }
+
+ $smtp_transaction_id = $this->smtp->getLastTransactionID();
+
+ if ($this->SMTPKeepAlive) {
+ $this->smtp->reset();
+ } else {
+ $this->smtp->quit();
+ $this->smtp->close();
+ }
+
+ foreach ($callbacks as $cb) {
+ $this->doCallback(
+ $cb['issent'],
+ [[$cb['to'], $cb['name']]],
+ [],
+ [],
+ $this->Subject,
+ $body,
+ $this->From,
+ ['smtp_transaction_id' => $smtp_transaction_id]
+ );
+ }
+
+ //Create error message for any bad addresses
+ if (count($bad_rcpt) > 0) {
+ $errstr = '';
+ foreach ($bad_rcpt as $bad) {
+ $errstr .= $bad['to'] . ': ' . $bad['error'];
+ }
+ throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);
+ }
+
+ return true;
+ }
+
+ /**
+ * Initiate a connection to an SMTP server.
+ * Returns false if the operation failed.
+ *
+ * @param array $options An array of options compatible with stream_context_create()
+ *
+ * @throws Exception
+ *
+ * @uses \PHPMailer\PHPMailer\SMTP
+ *
+ * @return bool
+ */
+ public function smtpConnect($options = null)
+ {
+ if (null === $this->smtp) {
+ $this->smtp = $this->getSMTPInstance();
+ }
+
+ //If no options are provided, use whatever is set in the instance
+ if (null === $options) {
+ $options = $this->SMTPOptions;
+ }
+
+ //Already connected?
+ if ($this->smtp->connected()) {
+ return true;
+ }
+
+ $this->smtp->setTimeout($this->Timeout);
+ $this->smtp->setDebugLevel($this->SMTPDebug);
+ $this->smtp->setDebugOutput($this->Debugoutput);
+ $this->smtp->setVerp($this->do_verp);
+ $this->smtp->setSMTPUTF8($this->UseSMTPUTF8);
+ if ($this->Host === null) {
+ $this->Host = 'localhost';
+ }
+ $hosts = explode(';', $this->Host);
+ $lastexception = null;
+
+ foreach ($hosts as $hostentry) {
+ $hostinfo = [];
+ if (
+ !preg_match(
+ '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
+ trim($hostentry),
+ $hostinfo
+ )
+ ) {
+ $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
+ //Not a valid host entry
+ continue;
+ }
+ //$hostinfo[1]: optional ssl or tls prefix
+ //$hostinfo[2]: the hostname
+ //$hostinfo[3]: optional port number
+ //The host string prefix can temporarily override the current setting for SMTPSecure
+ //If it's not specified, the default value is used
+
+ //Check the host name is a valid name or IP address before trying to use it
+ if (!static::isValidHost($hostinfo[2])) {
+ $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
+ continue;
+ }
+ $prefix = '';
+ $secure = $this->SMTPSecure;
+ $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
+ if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
+ $prefix = 'ssl://';
+ $tls = false; //Can't have SSL and TLS at the same time
+ $secure = static::ENCRYPTION_SMTPS;
+ } elseif ('tls' === $hostinfo[1]) {
+ $tls = true;
+ //TLS doesn't use a prefix
+ $secure = static::ENCRYPTION_STARTTLS;
+ }
+ //Do we need the OpenSSL extension?
+ $sslext = defined('OPENSSL_ALGO_SHA256');
+ if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
+ //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
+ if (!$sslext) {
+ throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
+ }
+ }
+ $host = $hostinfo[2];
+ $port = $this->Port;
+ if (
+ array_key_exists(3, $hostinfo) &&
+ is_numeric($hostinfo[3]) &&
+ $hostinfo[3] > 0 &&
+ $hostinfo[3] < 65536
+ ) {
+ $port = (int) $hostinfo[3];
+ }
+ if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
+ try {
+ if ($this->Helo) {
+ $hello = $this->Helo;
+ } else {
+ $hello = $this->serverHostname();
+ }
+ $this->smtp->hello($hello);
+ //Automatically enable TLS encryption if:
+ //* it's not disabled
+ //* we are not connecting to localhost
+ //* we have openssl extension
+ //* we are not already using SSL
+ //* the server offers STARTTLS
+ if (
+ $this->SMTPAutoTLS &&
+ $this->Host !== 'localhost' &&
+ $sslext &&
+ $secure !== 'ssl' &&
+ $this->smtp->getServerExt('STARTTLS')
+ ) {
+ $tls = true;
+ }
+ if ($tls) {
+ if (!$this->smtp->startTLS()) {
+ $message = $this->getSmtpErrorMessage('connect_host');
+ throw new Exception($message);
+ }
+ //We must resend EHLO after TLS negotiation
+ $this->smtp->hello($hello);
+ }
+ if (
+ $this->SMTPAuth && !$this->smtp->authenticate(
+ $this->Username,
+ $this->Password,
+ $this->AuthType,
+ $this->oauth
+ )
+ ) {
+ throw new Exception($this->lang('authenticate'));
+ }
+
+ return true;
+ } catch (Exception $exc) {
+ $lastexception = $exc;
+ $this->edebug($exc->getMessage());
+ //We must have connected, but then failed TLS or Auth, so close connection nicely
+ $this->smtp->quit();
+ }
+ }
+ }
+ //If we get here, all connection attempts have failed, so close connection hard
+ $this->smtp->close();
+ //As we've caught all exceptions, just report whatever the last one was
+ if ($this->exceptions && null !== $lastexception) {
+ throw $lastexception;
+ }
+ if ($this->exceptions) {
+ // no exception was thrown, likely $this->smtp->connect() failed
+ $message = $this->getSmtpErrorMessage('connect_host');
+ throw new Exception($message);
+ }
+
+ return false;
+ }
+
+ /**
+ * Close the active SMTP session if one exists.
+ */
+ public function smtpClose()
+ {
+ if ((null !== $this->smtp) && $this->smtp->connected()) {
+ $this->smtp->quit();
+ $this->smtp->close();
+ }
+ }
+
+ /**
+ * Set the language for error messages.
+ * The default language is English.
+ *
+ * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr")
+ * Optionally, the language code can be enhanced with a 4-character
+ * script annotation and/or a 2-character country annotation.
+ * @param string $lang_path Path to the language file directory, with trailing separator (slash)
+ * Do not set this from user input!
+ *
+ * @return bool Returns true if the requested language was loaded, false otherwise.
+ */
+ public function setLanguage($langcode = 'en', $lang_path = '')
+ {
+ //Backwards compatibility for renamed language codes
+ $renamed_langcodes = [
+ 'br' => 'pt_br',
+ 'cz' => 'cs',
+ 'dk' => 'da',
+ 'no' => 'nb',
+ 'se' => 'sv',
+ 'rs' => 'sr',
+ 'tg' => 'tl',
+ 'am' => 'hy',
+ ];
+
+ if (array_key_exists($langcode, $renamed_langcodes)) {
+ $langcode = $renamed_langcodes[$langcode];
+ }
+
+ //Define full set of translatable strings in English
+ $PHPMAILER_LANG = [
+ 'authenticate' => 'SMTP Error: Could not authenticate.',
+ 'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' .
+ ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
+ ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
+ 'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
+ 'data_not_accepted' => 'SMTP Error: data not accepted.',
+ 'empty_message' => 'Message body empty',
+ 'encoding' => 'Unknown encoding: ',
+ 'execute' => 'Could not execute: ',
+ 'extension_missing' => 'Extension missing: ',
+ 'file_access' => 'Could not access file: ',
+ 'file_open' => 'File Error: Could not open file: ',
+ 'from_failed' => 'The following From address failed: ',
+ 'instantiate' => 'Could not instantiate mail function.',
+ 'invalid_address' => 'Invalid address: ',
+ 'invalid_header' => 'Invalid header name or value',
+ 'invalid_hostentry' => 'Invalid hostentry: ',
+ 'invalid_host' => 'Invalid host: ',
+ 'mailer_not_supported' => ' mailer is not supported.',
+ 'provide_address' => 'You must provide at least one recipient email address.',
+ 'recipients_failed' => 'SMTP Error: The following recipients failed: ',
+ 'signing' => 'Signing Error: ',
+ 'smtp_code' => 'SMTP code: ',
+ 'smtp_code_ex' => 'Additional SMTP info: ',
+ 'smtp_connect_failed' => 'SMTP connect() failed.',
+ 'smtp_detail' => 'Detail: ',
+ 'smtp_error' => 'SMTP server error: ',
+ 'variable_set' => 'Cannot set or reset variable: ',
+ 'no_smtputf8' => 'Server does not support SMTPUTF8 needed to send to Unicode addresses',
+ ];
+ if (empty($lang_path)) {
+ //Calculate an absolute path so it can work if CWD is not here
+ $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
+ }
+
+ //Validate $langcode
+ $foundlang = true;
+ $langcode = strtolower($langcode);
+ if (
+ !preg_match('/^(?P[a-z]{2})(?P
+
+
+
+