diff --git a/composer.json b/composer.json index 60dcd4c..e526b65 100644 --- a/composer.json +++ b/composer.json @@ -85,16 +85,16 @@ "cweagans/composer-patches": true } }, - "minimum-stability": "beta", + "minimum-stability": "dev", "prefer-stable": true, "repositories": [ { - "type": "composer", - "url": "https://packagist.org" + "type": "path", + "url": "packages/*/*" }, { - "type": "path", - "url": "packages/*" + "type": "composer", + "url": "https://packagist.org" } ], "extra": { @@ -106,14 +106,6 @@ "askvortsov/flarum-pwa": [ "patches/askvortsov-flarum-pwa-src-pushsender-php.patch" ], - "flarum/core": { - "0": "patches/flarum-core-js-src-common-helpers-avatar-tsx.patch", - "1": "patches/flarum-core-js-src-common-models-user-tsx.patch", - "2": "patches/flarum-core-src-forum-forumserviceprovider-php.patch", - "3": "patches/flarum-core-src-admin-adminserviceprovider-php.patch", - "4": "patches/flarum-core-js-package-json.patch", - "Get User Ip from CDN": "patches/core-1.patch" - }, "fof/sitemap": { "Remove User in Sitemap": "patches/fof-sitemap-1.patch" }, @@ -132,4 +124,4 @@ "require-dev": { "symplify/vendor-patches": "^11.2" } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index fbcdd4f..1eebf59 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b4d33bb8fe2c21ba0a7c3831289811f2", + "content-hash": "10c4fe3db4c5973cda9622b8dddff071", "packages": [ { "name": "afrux/forum-widgets-core", @@ -2155,17 +2155,11 @@ }, { "name": "flarum/core", - "version": "v1.8.9", - "source": { - "type": "git", - "url": "https://github.com/flarum/flarum-core.git", - "reference": "c09efebdcfb4cdc396bee0efafa0a81946fc813f" - }, + "version": "dev-main", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/flarum/flarum-core/zipball/c09efebdcfb4cdc396bee0efafa0a81946fc813f", - "reference": "c09efebdcfb4cdc396bee0efafa0a81946fc813f", - "shasum": "" + "type": "path", + "url": "packages/flarum/core", + "reference": "ff5b2bb9a5fc7f8ccdb8dc39c12af2ec7a0101d0" }, "require": { "components/font-awesome": "^5.14.0", @@ -2225,41 +2219,60 @@ }, "type": "library", "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, "flarum-cli": { + "excludeScaffolding": [ + "LICENSE.md", + "js/tsconfig.json", + "js/webpack.config.js" + ], "modules": { + "backendTesting": true, "js": true, - "css": true, - "admin": true, - "forum": true, "gitConf": true, - "styleci": true, - "jsCommon": true, + "githubActions": true, "prettier": true, "typescript": true, "bundlewatch": true, "editorConfig": true, - "githubActions": true, - "backendTesting": true - }, - "excludeScaffolding": [ - "LICENSE.md", - "js/tsconfig.json", - "js/webpack.config.js" - ] - }, - "branch-alias": { - "dev-main": "1.x-dev" + "styleci": true, + "admin": true, + "forum": true, + "jsCommon": true, + "css": true + } } }, "autoload": { + "psr-4": { + "Flarum\\": "src/" + }, "files": [ "src/helpers.php" - ], + ] + }, + "autoload-dev": { "psr-4": { - "Flarum\\": "src/" + "Flarum\\Tests\\": "tests/" } }, - "notification-url": "https://packagist.org/downloads/", + "scripts": { + "test": [ + "@test:unit", + "@test:integration" + ], + "test:unit": [ + "phpunit -c tests/phpunit.unit.xml" + ], + "test:integration": [ + "phpunit -c tests/phpunit.integration.xml" + ], + "test:setup": [ + "@php tests/integration/setup.php" + ] + }, "license": [ "MIT" ], @@ -2277,27 +2290,29 @@ "forum" ], "support": { - "chat": "https://flarum.org/chat", + "issues": "https://github.com/flarum/framework/issues", + "source": "https://github.com/flarum/flarum-core", "docs": "https://docs.flarum.org", "forum": "https://discuss.flarum.org", - "issues": "https://github.com/flarum/framework/issues", - "source": "https://github.com/flarum/flarum-core" + "chat": "https://flarum.org/chat" }, "funding": [ { - "url": "https://github.com/sponsors/flarum", - "type": "github" + "type": "opencollective", + "url": "https://opencollective.com/flarum" }, { - "url": "https://opencollective.com/flarum", - "type": "opencollective" + "type": "github", + "url": "https://github.com/sponsors/flarum" }, { - "url": "https://flarum.org/donate", - "type": "other" + "type": "other", + "url": "https://flarum.org/donate" } ], - "time": "2024-11-20T13:51:54+00:00" + "transport-options": { + "relative": true + } }, { "name": "flarum/emoji", @@ -4638,36 +4653,34 @@ }, { "name": "google/auth", - "version": "v1.44.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/googleapis/google-auth-library-php.git", - "reference": "5670e56307d7a2eac931f677c0e59a4f8abb2e43" + "reference": "f1f0d0319e2e7750ebfaa523c78819792a9ed9f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/5670e56307d7a2eac931f677c0e59a4f8abb2e43", - "reference": "5670e56307d7a2eac931f677c0e59a4f8abb2e43", + "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/f1f0d0319e2e7750ebfaa523c78819792a9ed9f7", + "reference": "f1f0d0319e2e7750ebfaa523c78819792a9ed9f7", "shasum": "" }, "require": { - "firebase/php-jwt": "^6.0", - "guzzlehttp/guzzle": "^7.4.5", - "guzzlehttp/psr7": "^2.4.5", - "php": "^8.1", - "psr/cache": "^2.0||^3.0", - "psr/http-message": "^1.1||^2.0" + "firebase/php-jwt": "^5.5||^6.0", + "guzzlehttp/guzzle": "^6.2.1|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": "^7.1||^8.0", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/http-message": "^1.0" }, "require-dev": { - "guzzlehttp/promises": "^2.0", - "kelvinmo/simplejwt": "0.7.1", - "phpseclib/phpseclib": "^3.0.35", - "phpspec/prophecy-phpunit": "^2.1", - "phpunit/phpunit": "^9.6", + "guzzlehttp/promises": "0.1.1|^1.3", + "kelvinmo/simplejwt": "0.7.0", + "phpseclib/phpseclib": "^2.0.31||^3.0", + "phpspec/prophecy-phpunit": "^1.1||^2.0", + "phpunit/phpunit": "^7.5||^9.0.0", "sebastian/comparator": ">=1.2.3", - "squizlabs/php_codesniffer": "^3.5", - "symfony/process": "^6.0||^7.0", - "webmozart/assert": "^1.11" + "squizlabs/php_codesniffer": "^3.5" }, "suggest": { "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2." @@ -4692,9 +4705,9 @@ "support": { "docs": "https://googleapis.github.io/google-auth-library-php/main/", "issues": "https://github.com/googleapis/google-auth-library-php/issues", - "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.44.0" + "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.26.0" }, - "time": "2024-12-04T15:34:58+00:00" + "time": "2023-04-05T15:11:57+00:00" }, { "name": "google/cloud-core", @@ -4871,22 +4884,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.8.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "f4152d9eb85c445fe1f992001d1748e8bec070d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4152d9eb85c445fe1f992001d1748e8bec070d2", + "reference": "f4152d9eb85c445fe1f992001d1748e8bec070d2", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.6.3", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -4977,7 +4990,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.8.2" }, "funding": [ { @@ -4993,7 +5006,7 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2024-07-18T11:12:18+00:00" }, { "name": "guzzlehttp/promises", @@ -5076,44 +5089,38 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b", + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" }, "provide": { - "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, "autoload": { + "files": [ + "src/functions_include.php" + ], "psr-4": { "GuzzleHttp\\Psr7\\": "src/" } @@ -5152,11 +5159,6 @@ "name": "Tobias Schultze", "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" } ], "description": "PSR-7 message implementation that also provides common utility methods", @@ -5172,7 +5174,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/1.9.1" }, "funding": [ { @@ -5188,7 +5190,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2023-04-17T16:00:37+00:00" }, { "name": "hikarilan/flarum-passkey-login", @@ -7061,16 +7063,16 @@ }, { "name": "kreait/firebase-php", - "version": "5.26.5", + "version": "5.20.1", "source": { "type": "git", "url": "https://github.com/kreait/firebase-php.git", - "reference": "dd915017e0a33532deb5e2b82c0a6536dd408123" + "reference": "5c5c8f3e5d2ae07ed4e5e521392609b4f812b67c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kreait/firebase-php/zipball/dd915017e0a33532deb5e2b82c0a6536dd408123", - "reference": "dd915017e0a33532deb5e2b82c0a6536dd408123", + "url": "https://api.github.com/repos/kreait/firebase-php/zipball/5c5c8f3e5d2ae07ed4e5e521392609b4f812b67c", + "reference": "5c5c8f3e5d2ae07ed4e5e521392609b4f812b67c", "shasum": "" }, "require": { @@ -7078,33 +7080,31 @@ "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", - "google/auth": "^1.18", - "google/cloud-core": "^1.42.2", - "google/cloud-storage": "^1.24.1", - "guzzlehttp/guzzle": "^6.5.5|^7.3", + "google/auth": "^1.8", + "google/cloud-core": "^1.36", + "google/cloud-storage": "^1.14", + "guzzlehttp/guzzle": "^6.2.1|^7.0", "guzzlehttp/promises": "^1.4", - "guzzlehttp/psr7": "^1.7|^2.0", - "kreait/clock": "^1.1", - "kreait/firebase-tokens": "^1.16.1", - "mtdowling/jmespath.php": "^2.6.1", + "guzzlehttp/psr7": "^1.7", + "kreait/clock": "^1.0.1", + "kreait/firebase-tokens": "^1.15.0", + "mtdowling/jmespath.php": "^2.3", "php": "^7.4|^8.0", - "psr/cache": "^1.0.1|^2.0|^3.0", - "psr/log": "^1.1|^2.0|^3.0", + "psr/cache": "^1.0", + "psr/http-message": "^1.0", + "psr/log": "^1.1", "psr/simple-cache": "^1.0", "riverline/multipart-parser": "^2.0.8", - "symfony/polyfill-php80": "^1.23", - "symfony/polyfill-php81": "^1.23" + "symfony/polyfill-php80": "^1.22" }, "require-dev": { - "giggsey/libphonenumber-for-php": "^8.12.27", - "google/cloud-firestore": "^1.19.3", + "giggsey/libphonenumber-for-php": "^8.12.23", + "google/cloud-firestore": "^1.19.2", "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5.10", - "rector/rector": "^0.12.5", - "symfony/var-dumper": "^5.4|^6.0", - "symplify/easy-coding-standard": "^10.0" + "phpstan/phpstan": "^0.12.86", + "phpstan/phpstan-phpunit": "^0.12.19", + "phpunit/phpunit": "^9.5.4", + "symfony/var-dumper": "^5.2.7" }, "suggest": { "giggsey/libphonenumber-for-php": "^8.9 to validate phone numbers before attempting to send them to Firebase", @@ -7114,8 +7114,7 @@ "extra": { "branch-alias": { "dev-4.x": "4.x-dev", - "dev-5.x": "5.x-dev", - "dev-6.x": "6.x-dev" + "dev-5.x": "5.x-dev" } }, "autoload": { @@ -7154,7 +7153,7 @@ "type": "github" } ], - "time": "2023-06-10T06:51:27+00:00" + "time": "2021-05-12T13:41:54+00:00" }, { "name": "kreait/firebase-tokens", @@ -9632,24 +9631,24 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", + "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", "shasum": "" }, "require": { - "php": "^8" + "php": "^7|^8" }, "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" }, "type": "library", "autoload": { @@ -9695,39 +9694,84 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:36:18+00:00" + "time": "2024-05-08T12:18:48+00:00" }, { - "name": "paragonie/sodium_compat", - "version": "v2.1.0", + "name": "paragonie/random_compat", + "version": "v9.99.100", "source": { "type": "git", - "url": "https://github.com/paragonie/sodium_compat.git", - "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb" + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/a673d5f310477027cead2e2f2b6db5d8368157cb", - "reference": "a673d5f310477027cead2e2f2b6db5d8368157cb", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", "shasum": "" }, "require": { - "php": "^8.1", - "php-64bit": "*" + "php": ">= 7" }, "require-dev": { - "phpunit/phpunit": "^7|^8|^9", - "vimeo/psalm": "^4|^5" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { - "ext-sodium": "Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" + "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": "paragonie/sodium_compat", + "version": "v1.21.1", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37" }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/bb312875dcdd20680419564fe42ba1d9564b9e37", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37", + "shasum": "" + }, + "require": { + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", "autoload": { "files": [ "autoload.php" @@ -9784,9 +9828,9 @@ ], "support": { "issues": "https://github.com/paragonie/sodium_compat/issues", - "source": "https://github.com/paragonie/sodium_compat/tree/v2.1.0" + "source": "https://github.com/paragonie/sodium_compat/tree/v1.21.1" }, - "time": "2024-09-04T12:51:01+00:00" + "time": "2024-04-22T22:05:04+00:00" }, { "name": "predis/predis", @@ -9851,20 +9895,20 @@ }, { "name": "psr/cache", - "version": "3.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, "type": "library", "extra": { @@ -9884,7 +9928,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for caching libraries", @@ -9894,9 +9938,9 @@ "psr-6" ], "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" + "source": "https://github.com/php-fig/cache/tree/master" }, - "time": "2021-02-03T23:26:27+00:00" + "time": "2016-08-06T20:24:11+00:00" }, { "name": "psr/clock", @@ -14705,26 +14749,25 @@ }, { "name": "web-token/jwt-library", - "version": "3.4.6", + "version": "3.3.5", "source": { "type": "git", "url": "https://github.com/web-token/jwt-library.git", - "reference": "1a25c8ced3e2b3c31d32dcfad215cbd8cb812f28" + "reference": "5ddb8f1064039cff2d81d0d9f668a4bbff4b62cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-token/jwt-library/zipball/1a25c8ced3e2b3c31d32dcfad215cbd8cb812f28", - "reference": "1a25c8ced3e2b3c31d32dcfad215cbd8cb812f28", + "url": "https://api.github.com/repos/web-token/jwt-library/zipball/5ddb8f1064039cff2d81d0d9f668a4bbff4b62cb", + "reference": "5ddb8f1064039cff2d81d0d9f668a4bbff4b62cb", "shasum": "" }, "require": { "brick/math": "^0.9|^0.10|^0.11|^0.12", "ext-json": "*", "ext-mbstring": "*", - "paragonie/constant_time_encoding": "^2.6|^3.0", - "paragonie/sodium_compat": "^1.20|^2.0", + "paragonie/constant_time_encoding": "^2.6", + "paragonie/sodium_compat": "^1.20", "php": ">=8.1", - "psr/cache": "^3.0", "psr/clock": "^1.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", @@ -14787,7 +14830,7 @@ ], "support": { "issues": "https://github.com/web-token/jwt-library/issues", - "source": "https://github.com/web-token/jwt-library/tree/3.4.6" + "source": "https://github.com/web-token/jwt-library/tree/3.3.5" }, "funding": [ { @@ -14799,7 +14842,7 @@ "type": "patreon" } ], - "time": "2024-07-02T16:35:11+00:00" + "time": "2024-04-03T07:57:45+00:00" }, { "name": "web-token/jwt-signature", @@ -15243,7 +15286,7 @@ } ], "aliases": [], - "minimum-stability": "beta", + "minimum-stability": "dev", "stability-flags": { "flarum-lang/chinese-simplified": 20 }, diff --git a/js/dist-core/admin.js b/js/dist-core/admin.js deleted file mode 100644 index 4c27c80..0000000 --- a/js/dist-core/admin.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! For license information please see admin.js.LICENSE.txt */ -(()=>{var t={9423:()=>{!function(t){"use strict";var e=function e(n,r){this.options=t.extend({},e.DEFAULTS,r);var i=this.options.target===e.DEFAULTS.target?t(this.options.target):t(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",t.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",t.proxy(this.checkPositionWithEventLoop,this)),this.$element=t(n),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function n(n){return this.each((function(){var r=t(this),i=r.data("bs.affix"),o="object"==typeof n&&n;i||r.data("bs.affix",i=new e(this,o)),"string"==typeof n&&i[n]()}))}e.VERSION="3.4.1",e.RESET="affix affix-top affix-bottom",e.DEFAULTS={offset:0,target:window},e.prototype.getState=function(t,e,n,r){var i=this.$target.scrollTop(),o=this.$element.offset(),a=this.$target.height();if(null!=n&&"top"==this.affixed)return i=t-r&&"bottom"},e.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(e.RESET).addClass("affix");var t=this.$target.scrollTop(),n=this.$element.offset();return this.pinnedOffset=n.top-t},e.prototype.checkPositionWithEventLoop=function(){setTimeout(t.proxy(this.checkPosition,this),1)},e.prototype.checkPosition=function(){if(this.$element.is(":visible")){var n=this.$element.height(),r=this.options.offset,i=r.top,o=r.bottom,a=Math.max(t(document).height(),t(document.body).height());"object"!=typeof r&&(o=i=r),"function"==typeof i&&(i=r.top(this.$element)),"function"==typeof o&&(o=r.bottom(this.$element));var s=this.getState(a,n,i,o);if(this.affixed!=s){null!=this.unpin&&this.$element.css("top","");var u="affix"+(s?"-"+s:""),l=t.Event(u+".bs.affix");if(this.$element.trigger(l),l.isDefaultPrevented())return;this.affixed=s,this.unpin="bottom"==s?this.getPinnedOffset():null,this.$element.removeClass(e.RESET).addClass(u).trigger(u.replace("affix","affixed")+".bs.affix")}"bottom"==s&&this.$element.offset({top:a-n-o})}};var r=t.fn.affix;t.fn.affix=n,t.fn.affix.Constructor=e,t.fn.affix.noConflict=function(){return t.fn.affix=r,this},t(window).on("load",(function(){t('[data-spy="affix"]').each((function(){var e=t(this),r=e.data();r.offset=r.offset||{},null!=r.offsetBottom&&(r.offset.bottom=r.offsetBottom),null!=r.offsetTop&&(r.offset.top=r.offsetTop),n.call(e,r)}))}))}(jQuery)},6298:()=>{!function(t){"use strict";var e='[data-toggle="dropdown"]',n=function(e){t(e).on("click.bs.dropdown",this.toggle)};function r(e){var n=e.attr("data-target");n||(n=(n=e.attr("href"))&&/#[A-Za-z]/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,""));var r="#"!==n?t(document).find(n):null;return r&&r.length?r:e.parent()}function i(n){n&&3===n.which||(t(".dropdown-backdrop").remove(),t(e).each((function(){var e=t(this),i=r(e),o={relatedTarget:this};i.hasClass("open")&&(n&&"click"==n.type&&/input|textarea/i.test(n.target.tagName)&&t.contains(i[0],n.target)||(i.trigger(n=t.Event("hide.bs.dropdown",o)),n.isDefaultPrevented()||(e.attr("aria-expanded","false"),i.removeClass("open").trigger(t.Event("hidden.bs.dropdown",o)))))})))}n.VERSION="3.4.1",n.prototype.toggle=function(e){var n=t(this);if(!n.is(".disabled, :disabled")){var o=r(n),a=o.hasClass("open");if(i(),!a){"ontouchstart"in document.documentElement&&!o.closest(".navbar-nav").length&&t(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(t(this)).on("click",i);var s={relatedTarget:this};if(o.trigger(e=t.Event("show.bs.dropdown",s)),e.isDefaultPrevented())return;n.trigger("focus").attr("aria-expanded","true"),o.toggleClass("open").trigger(t.Event("shown.bs.dropdown",s))}return!1}},n.prototype.keydown=function(n){if(/(38|40|27|32)/.test(n.which)&&!/input|textarea/i.test(n.target.tagName)){var i=t(this);if(n.preventDefault(),n.stopPropagation(),!i.is(".disabled, :disabled")){var o=r(i),a=o.hasClass("open");if(!a&&27!=n.which||a&&27==n.which)return 27==n.which&&o.find(e).trigger("focus"),i.trigger("click");var s=o.find(".dropdown-menu li:not(.disabled):visible a");if(s.length){var u=s.index(n.target);38==n.which&&u>0&&u--,40==n.which&&u{!function(t){"use strict";var e=["sanitize","whiteList","sanitizeFn"],n=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],r=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,i=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function o(e,o){var a=e.nodeName.toLowerCase();if(-1!==t.inArray(a,o))return-1===t.inArray(a,n)||Boolean(e.nodeValue.match(r)||e.nodeValue.match(i));for(var s=t(o).filter((function(t,e){return e instanceof RegExp})),u=0,l=s.length;u
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]}},s.prototype.init=function(e,n,r){if(this.enabled=!0,this.type=e,this.$element=t(n),this.options=this.getOptions(r),this.$viewport=this.options.viewport&&t(document).find(t.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var i=this.options.trigger.split(" "),o=i.length;o--;){var a=i[o];if("click"==a)this.$element.on("click."+this.type,this.options.selector,t.proxy(this.toggle,this));else if("manual"!=a){var s="hover"==a?"mouseenter":"focusin",u="hover"==a?"mouseleave":"focusout";this.$element.on(s+"."+this.type,this.options.selector,t.proxy(this.enter,this)),this.$element.on(u+"."+this.type,this.options.selector,t.proxy(this.leave,this))}}this.options.selector?this._options=t.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},s.prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.getOptions=function(n){var r=this.$element.data();for(var i in r)r.hasOwnProperty(i)&&-1!==t.inArray(i,e)&&delete r[i];return(n=t.extend({},this.getDefaults(),r,n)).delay&&"number"==typeof n.delay&&(n.delay={show:n.delay,hide:n.delay}),n.sanitize&&(n.template=a(n.template,n.whiteList,n.sanitizeFn)),n},s.prototype.getDelegateOptions=function(){var e={},n=this.getDefaults();return this._options&&t.each(this._options,(function(t,r){n[t]!=r&&(e[t]=r)})),e},s.prototype.enter=function(e){var n=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);if(n||(n=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,n)),e instanceof t.Event&&(n.inState["focusin"==e.type?"focus":"hover"]=!0),n.tip().hasClass("in")||"in"==n.hoverState)n.hoverState="in";else{if(clearTimeout(n.timeout),n.hoverState="in",!n.options.delay||!n.options.delay.show)return n.show();n.timeout=setTimeout((function(){"in"==n.hoverState&&n.show()}),n.options.delay.show)}},s.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},s.prototype.leave=function(e){var n=e instanceof this.constructor?e:t(e.currentTarget).data("bs."+this.type);if(n||(n=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,n)),e instanceof t.Event&&(n.inState["focusout"==e.type?"focus":"hover"]=!1),!n.isInStateTrue()){if(clearTimeout(n.timeout),n.hoverState="out",!n.options.delay||!n.options.delay.hide)return n.hide();n.timeout=setTimeout((function(){"out"==n.hoverState&&n.hide()}),n.options.delay.hide)}},s.prototype.show=function(){var e=t.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(e);var n=t.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(e.isDefaultPrevented()||!n)return;var r=this,i=this.tip(),o=this.getUID(this.type);this.setContent(),i.attr("id",o),this.$element.attr("aria-describedby",o),this.options.animation&&i.addClass("fade");var a="function"==typeof this.options.placement?this.options.placement.call(this,i[0],this.$element[0]):this.options.placement,u=/\s?auto?\s?/i,l=u.test(a);l&&(a=a.replace(u,"")||"top"),i.detach().css({top:0,left:0,display:"block"}).addClass(a).data("bs."+this.type,this),this.options.container?i.appendTo(t(document).find(this.options.container)):i.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var c=this.getPosition(),d=i[0].offsetWidth,f=i[0].offsetHeight;if(l){var p=a,h=this.getPosition(this.$viewport);a="bottom"==a&&c.bottom+f>h.bottom?"top":"top"==a&&c.top-fh.width?"left":"left"==a&&c.left-da.top+a.height&&(i.top=a.top+a.height-u)}else{var l=e.left-o,c=e.left+o+n;la.right&&(i.left=a.left+a.width-c)}return i},s.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},s.prototype.getUID=function(t){do{t+=~~(1e6*Math.random())}while(document.getElementById(t));return t},s.prototype.tip=function(){if(!this.$tip&&(this.$tip=t(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},s.prototype.enable=function(){this.enabled=!0},s.prototype.disable=function(){this.enabled=!1},s.prototype.toggleEnabled=function(){this.enabled=!this.enabled},s.prototype.toggle=function(e){var n=this;e&&((n=t(e.currentTarget).data("bs."+this.type))||(n=new this.constructor(e.currentTarget,this.getDelegateOptions()),t(e.currentTarget).data("bs."+this.type,n))),e?(n.inState.click=!n.inState.click,n.isInStateTrue()?n.enter(n):n.leave(n)):n.tip().hasClass("in")?n.leave(n):n.enter(n)},s.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide((function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null}))},s.prototype.sanitizeHtml=function(t){return a(t,this.options.whiteList,this.options.sanitizeFn)};var u=t.fn.tooltip;t.fn.tooltip=function(e){return this.each((function(){var n=t(this),r=n.data("bs.tooltip"),i="object"==typeof e&&e;!r&&/destroy|hide/.test(e)||(r||n.data("bs.tooltip",r=new s(this,i)),"string"==typeof e&&r[e]())}))},t.fn.tooltip.Constructor=s,t.fn.tooltip.noConflict=function(){return t.fn.tooltip=u,this}}(jQuery)},2646:()=>{!function(t){"use strict";t.fn.emulateTransitionEnd=function(e){var n=!1,r=this;return t(this).one("bsTransitionEnd",(function(){n=!0})),setTimeout((function(){n||t(r).trigger(t.support.transition.end)}),e),this},t((function(){t.support.transition=function(){var t=document.createElement("bootstrap"),e={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var n in e)if(void 0!==t.style[n])return{end:e[n]};return!1}(),t.support.transition&&(t.event.special.bsTransitionEnd={bindType:t.support.transition.end,delegateType:t.support.transition.end,handle:function(e){if(t(e.target).is(this))return e.handleObj.handler.apply(this,arguments)}})}))}(jQuery)},1028:t=>{var e=function(t){this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),document.body.appendChild(this.canvas),this.width=this.canvas.width=t.width,this.height=this.canvas.height=t.height,this.context.drawImage(t,0,0,this.width,this.height)};e.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)},e.prototype.update=function(t){this.context.putImageData(t,0,0)},e.prototype.getPixelCount=function(){return this.width*this.height},e.prototype.getImageData=function(){return this.context.getImageData(0,0,this.width,this.height)},e.prototype.removeCanvas=function(){this.canvas.parentNode.removeChild(this.canvas)};var n=function(){};if(n.prototype.getColor=function(t,e){return this.getPalette(t,5,e)[0]},n.prototype.getPalette=function(t,n,r){void 0===n&&(n=10),(void 0===r||1>r)&&(r=10);for(var o,a,s,u,l=new e(t),c=l.getImageData().data,d=l.getPixelCount(),f=[],p=0;d>p;p+=r)a=c[0+(o=4*p)],s=c[o+1],u=c[o+2],c[o+3]>=125&&(a>250&&s>250&&u>250||f.push([a,s,u]));var h=i.quantize(f,n),m=h?h.palette():null;return l.removeCanvas(),m},!r)var r={map:function(t,e){var n={};return e?t.map((function(t,r){return n.index=r,e.call(n,t)})):t.slice()},naturalOrder:function(t,e){return e>t?-1:t>e?1:0},sum:function(t,e){var n={};return t.reduce(e?function(t,r,i){return n.index=i,t+e.call(n,r)}:function(t,e){return t+e},0)},max:function(t,e){return Math.max.apply(null,e?r.map(t,e):t)}};var i=function(){function t(t,e,n){return(t<<2*u)+(e<>l,i=e[1]>>l,o=e[2]>>l,n=t(r,i,o),a[n]=(a[n]||0)+1})),a}function a(t,e){var r,i,o,a=1e6,s=0,u=1e6,c=0,d=1e6,f=0;return t.forEach((function(t){r=t[0]>>l,i=t[1]>>l,o=t[2]>>l,a>r?a=r:r>s&&(s=r),u>i?u=i:i>c&&(c=i),d>o?d=o:o>f&&(f=o)})),new n(a,s,u,c,d,f,e)}function s(e,n){if(n.count()){var i=n.r2-n.r1+1,o=n.g2-n.g1+1,a=n.b2-n.b1+1,s=r.max([i,o,a]);if(1==n.count())return[n.copy()];var u,l,c,d,f=0,p=[],h=[];if(s==i)for(u=n.r1;u<=n.r2;u++){for(d=0,l=n.g1;l<=n.g2;l++)for(c=n.b1;c<=n.b2;c++)d+=e[t(u,l,c)]||0;f+=d,p[u]=f}else if(s==o)for(u=n.g1;u<=n.g2;u++){for(d=0,l=n.r1;l<=n.r2;l++)for(c=n.b1;c<=n.b2;c++)d+=e[t(l,u,c)]||0;f+=d,p[u]=f}else for(u=n.b1;u<=n.b2;u++){for(d=0,l=n.r1;l<=n.r2;l++)for(c=n.g1;c<=n.g2;c++)d+=e[t(l,c,u)]||0;f+=d,p[u]=f}return p.forEach((function(t,e){h[e]=f-t})),function(t){var e,r,i,o,a,s=t+"1",l=t+"2",c=0;for(u=n[s];u<=n[l];u++)if(p[u]>f/2){for(i=n.copy(),o=n.copy(),e=u-n[s],a=(r=n[l]-u)>=e?Math.min(n[l]-1,~~(u+r/2)):Math.max(n[s],~~(u-1-e/2));!p[a];)a++;for(c=h[a];!c&&p[a-1];)c=h[--a];return i[l]=a,o[s]=i[l]+1,[i,o]}}(s==i?"r":s==o?"g":"b")}}var u=5,l=8-u;return n.prototype={volume:function(t){var e=this;return(!e._volume||t)&&(e._volume=(e.r2-e.r1+1)*(e.g2-e.g1+1)*(e.b2-e.b1+1)),e._volume},count:function(e){var n=this,r=n.histo;if(!n._count_set||e){var i,o,a,s=0;for(i=n.r1;i<=n.r2;i++)for(o=n.g1;o<=n.g2;o++)for(a=n.b1;a<=n.b2;a++)index=t(i,o,a),s+=r[index]||0;n._count=s,n._count_set=!0}return n._count},copy:function(){var t=this;return new n(t.r1,t.r2,t.g1,t.g2,t.b1,t.b2,t.histo)},avg:function(e){var n=this,r=n.histo;if(!n._avg||e){var i,o,a,s,l=0,c=1<<8-u,d=0,f=0,p=0;for(o=n.r1;o<=n.r2;o++)for(a=n.g1;a<=n.g2;a++)for(s=n.b1;s<=n.b2;s++)l+=i=r[t(o,a,s)]||0,d+=i*(o+.5)*c,f+=i*(a+.5)*c,p+=i*(s+.5)*c;n._avg=l?[~~(d/l),~~(f/l),~~(p/l)]:[~~(c*(n.r1+n.r2+1)/2),~~(c*(n.g1+n.g2+1)/2),~~(c*(n.b1+n.b2+1)/2)]}return n._avg},contains:function(t){var e=this,n=t[0]>>l;return gval=t[1]>>l,bval=t[2]>>l,n>=e.r1&&n<=e.r2&&gval>=e.g1&&gval<=e.g2&&bval>=e.b1&&bval<=e.b2}},i.prototype={push:function(t){this.vboxes.push({vbox:t,color:t.avg()})},palette:function(){return this.vboxes.map((function(t){return t.color}))},size:function(){return this.vboxes.size()},map:function(t){for(var e=this.vboxes,n=0;n(n=Math.sqrt(Math.pow(t[0]-i.peek(o).color[0],2)+Math.pow(t[1]-i.peek(o).color[1],2)+Math.pow(t[2]-i.peek(o).color[2],2)))||void 0===e)&&(e=n,r=i.peek(o).color);return r},forcebw:function(){var t=this.vboxes;t.sort((function(t,e){return r.naturalOrder(r.sum(t.color),r.sum(e.color))}));var e=t[0].color;e[0]<5&&e[1]<5&&e[2]<5&&(t[0].color=[0,0,0]);var n=t.length-1,i=t[n].color;i[0]>251&&i[1]>251&&i[2]>251&&(t[n].color=[255,255,255])}},{quantize:function(t,n){function u(t,e){for(var n,r=1,i=0;1e3>i;)if((n=t.pop()).count()){var o=s(l,n),a=o[0],u=o[1];if(!a)return;if(t.push(a),u&&(t.push(u),r++),r>=e)return;if(i++>1e3)return}else t.push(n),i++}if(!t.length||2>n||n>256)return!1;var l=o(t);l.forEach((function(){}));var c=a(t,l),d=new e((function(t,e){return r.naturalOrder(t.count(),e.count())}));d.push(c),u(d,.75*n);for(var f=new e((function(t,e){return r.naturalOrder(t.count()*t.volume(),e.count()*e.volume())}));d.size();)f.push(d.pop());u(f,n-f.size());for(var p=new i;f.size();)p.push(f.pop());return p}}}();t.exports=n},9555:function(t){t.exports=function(){"use strict";var t=6e4,e=36e5,n="millisecond",r="second",i="minute",o="hour",a="day",s="week",u="month",l="quarter",c="year",d="date",f="Invalid Date",p=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,h=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,m={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},v=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},g={s:v,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+v(r,2,"0")+":"+v(i,2,"0")},m:function t(e,n){if(e.date()1)return t(a[0])}else{var s=e.name;b[s]=e,i=s}return!r&&i&&(y=i),i||!r&&y},E=function(t,e){if(w(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new A(n)},C=g;C.l=x,C.i=w,C.w=function(t,e){return E(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var A=function(){function m(t){this.$L=x(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[D]=!0}var v=m.prototype;return v.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(C.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(p);if(r){var i=r[2]-1||0,o=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,o)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,o)}}return new Date(e)}(t),this.init()},v.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},v.$utils=function(){return C},v.isValid=function(){return!(this.$d.toString()===f)},v.isSame=function(t,e){var n=E(t);return this.startOf(e)<=n&&n<=this.endOf(e)},v.isAfter=function(t,e){return E(t)0,v<=m.r||!m.r){v<=1&&h>0&&(m=f[h-1]);var g=d[m.l];s&&(v=s(""+v)),l="string"==typeof g?g.replace("%d",v):g(v,r,m.l,c);break}}if(r)return l;var y=c?d.future:d.past;return"function"==typeof y?y(l):y.replace("%s",l)},r.to=function(t,e){return o(t,e,this,!0)},r.from=function(t,e){return o(t,e,this)};var a=function(t){return t.$u?n.utc():n()};r.toNow=function(t){return this.to(a(this),t)},r.fromNow=function(t){return this.from(a(this),t)}}}()},6648:(t,e,n)=>{"use strict";t.exports=function(){if("object"==typeof globalThis)return globalThis;var t;try{t=this||new Function("return this")()}catch(t){if("object"==typeof window)return window;if("object"==typeof self)return self;if(void 0!==n.g)return n.g}return t}()},5426:function(){!function(t){function e(e){if("string"==typeof e.data&&(e.data={keys:e.data}),e.data&&e.data.keys&&"string"==typeof e.data.keys){var n=e.handler,r=e.data.keys.toLowerCase().split(" ");e.handler=function(e){if(this===e.target||!(t.hotkeys.options.filterInputAcceptingElements&&t.hotkeys.textInputTypes.test(e.target.nodeName)||t.hotkeys.options.filterContentEditable&&t(e.target).attr("contenteditable")||t.hotkeys.options.filterTextInputs&&t.inArray(e.target.type,t.hotkeys.textAcceptingInputTypes)>-1)){var i="keypress"!==e.type&&t.hotkeys.specialKeys[e.which],o=String.fromCharCode(e.which).toLowerCase(),a="",s={};t.each(["alt","ctrl","shift"],(function(t,n){e[n+"Key"]&&i!==n&&(a+=n+"+")})),e.metaKey&&!e.ctrlKey&&"meta"!==i&&(a+="meta+"),e.metaKey&&"meta"!==i&&a.indexOf("alt+ctrl+shift+")>-1&&(a=a.replace("alt+ctrl+shift+","hyper+")),i?s[a+i]=!0:(s[a+o]=!0,s[a+t.hotkeys.shiftNums[o]]=!0,"shift+"===a&&(s[t.hotkeys.shiftNums[o]]=!0));for(var u=0,l=r.length;u","/":"?","\\":"|"},textAcceptingInputTypes:["text","password","number","email","url","range","date","month","week","time","datetime","datetime-local","search","color","tel"],textInputTypes:/textarea|input|select/i,options:{filterInputAcceptingElements:!0,filterTextInputs:!0,filterContentEditable:!0}},t.each(["keydown","keyup","keypress"],(function(){t.event.special[this]={add:e}}))}(jQuery||this.jQuery||window.jQuery)},6663:function(t,e){var n;!function(e,n){"use strict";"object"==typeof t.exports?t.exports=e.document?n(e,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return n(t)}:n(e)}("undefined"!=typeof window?window:this,(function(r,i){"use strict";var o=[],a=Object.getPrototypeOf,s=o.slice,u=o.flat?function(t){return o.flat.call(t)}:function(t){return o.concat.apply([],t)},l=o.push,c=o.indexOf,d={},f=d.toString,p=d.hasOwnProperty,h=p.toString,m=h.call(Object),v={},g=function(t){return"function"==typeof t&&"number"!=typeof t.nodeType&&"function"!=typeof t.item},y=function(t){return null!=t&&t===t.window},b=r.document,D={type:!0,src:!0,nonce:!0,noModule:!0};function w(t,e,n){var r,i,o=(n=n||b).createElement("script");if(o.text=t,e)for(r in D)(i=e[r]||e.getAttribute&&e.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?d[f.call(t)]||"object":typeof t}var E="3.7.1",C=/HTML$/i,A=function t(e,n){return new t.fn.init(e,n)};function F(t){var e=!!t&&"length"in t&&t.length,n=x(t);return!g(t)&&!y(t)&&("array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t)}function N(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}A.fn=A.prototype={jquery:E,constructor:A,length:0,toArray:function(){return s.call(this)},get:function(t){return null==t?s.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){var e=A.merge(this.constructor(),t);return e.prevObject=this,e},each:function(t){return A.each(this,t)},map:function(t){return this.pushStack(A.map(this,(function(e,n){return t.call(e,n,e)})))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(A.grep(this,(function(t,e){return(e+1)%2})))},odd:function(){return this.pushStack(A.grep(this,(function(t,e){return e%2})))},eq:function(t){var e=this.length,n=+t+(t<0?e:0);return this.pushStack(n>=0&&n+~]|"+S+")"+S+"*"),H=new RegExp(S+"|>"),q=new RegExp(L),U=new RegExp("^"+P+"$"),z={ID:new RegExp("^#("+P+")"),CLASS:new RegExp("^\\.("+P+")"),TAG:new RegExp("^("+P+"|[*])"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+L),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+S+"*(even|odd|(([+-]|)(\\d*)n|)"+S+"*(?:([+-]|)"+S+"*(\\d+)|))"+S+"*\\)|)","i"),bool:new RegExp("^(?:"+F+")$","i"),needsContext:new RegExp("^"+S+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+S+"*((?:-\\d)?\\d*)"+S+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,V=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Y=/[+~]/,K=new RegExp("\\\\[\\da-fA-F]{1,6}"+S+"?|\\\\([^\\r\\n\\f])","g"),X=function(t,e){var n="0x"+t.slice(1)-65536;return e||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},J=function(){ut()},Q=ft((function(t){return!0===t.disabled&&N(t,"fieldset")}),{dir:"parentNode",next:"legend"});try{m.apply(o=s.call(j.childNodes),j.childNodes),o[j.childNodes.length].nodeType}catch(t){m={apply:function(t,e){I.apply(t,s.call(e))},call:function(t){I.apply(t,s.call(arguments,1))}}}function Z(t,e,n,r){var i,o,a,s,l,c,p,h=e&&e.ownerDocument,y=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==y&&9!==y&&11!==y)return n;if(!r&&(ut(e),e=e||u,d)){if(11!==y&&(l=V.exec(t)))if(i=l[1]){if(9===y){if(!(a=e.getElementById(i)))return n;if(a.id===i)return m.call(n,a),n}else if(h&&(a=h.getElementById(i))&&Z.contains(e,a)&&a.id===i)return m.call(n,a),n}else{if(l[2])return m.apply(n,e.getElementsByTagName(t)),n;if((i=l[3])&&e.getElementsByClassName)return m.apply(n,e.getElementsByClassName(i)),n}if(!(E[t+" "]||f&&f.test(t))){if(p=t,h=e,1===y&&(H.test(t)||R.test(t))){for((h=Y.test(t)&&st(e.parentNode)||e)==e&&v.scope||((s=e.getAttribute("id"))?s=A.escapeSelector(s):e.setAttribute("id",s=g)),o=(c=ct(t)).length;o--;)c[o]=(s?"#"+s:":scope")+" "+dt(c[o]);p=c.join(",")}try{return m.apply(n,h.querySelectorAll(p)),n}catch(e){E(t,!0)}finally{s===g&&e.removeAttribute("id")}}}return yt(t.replace(B,"$1"),e,n,r)}function tt(){var t=[];return function n(r,i){return t.push(r+" ")>e.cacheLength&&delete n[t.shift()],n[r+" "]=i}}function et(t){return t[g]=!0,t}function nt(t){var e=u.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function rt(t){return function(e){return N(e,"input")&&e.type===t}}function it(t){return function(e){return(N(e,"input")||N(e,"button"))&&e.type===t}}function ot(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&Q(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function at(t){return et((function(e){return e=+e,et((function(n,r){for(var i,o=t([],n.length,e),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))}))}))}function st(t){return t&&void 0!==t.getElementsByTagName&&t}function ut(t){var n,r=t?t.ownerDocument||t:j;return r!=u&&9===r.nodeType&&r.documentElement?(l=(u=r).documentElement,d=!A.isXMLDoc(u),h=l.matches||l.webkitMatchesSelector||l.msMatchesSelector,l.msMatchesSelector&&j!=u&&(n=u.defaultView)&&n.top!==n&&n.addEventListener("unload",J),v.getById=nt((function(t){return l.appendChild(t).id=A.expando,!u.getElementsByName||!u.getElementsByName(A.expando).length})),v.disconnectedMatch=nt((function(t){return h.call(t,"*")})),v.scope=nt((function(){return u.querySelectorAll(":scope")})),v.cssHas=nt((function(){try{return u.querySelector(":has(*,:jqfake)"),!1}catch(t){return!0}})),v.getById?(e.filter.ID=function(t){var e=t.replace(K,X);return function(t){return t.getAttribute("id")===e}},e.find.ID=function(t,e){if(void 0!==e.getElementById&&d){var n=e.getElementById(t);return n?[n]:[]}}):(e.filter.ID=function(t){var e=t.replace(K,X);return function(t){var n=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return n&&n.value===e}},e.find.ID=function(t,e){if(void 0!==e.getElementById&&d){var n,r,i,o=e.getElementById(t);if(o){if((n=o.getAttributeNode("id"))&&n.value===t)return[o];for(i=e.getElementsByName(t),r=0;o=i[r++];)if((n=o.getAttributeNode("id"))&&n.value===t)return[o]}return[]}}),e.find.TAG=function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):e.querySelectorAll(t)},e.find.CLASS=function(t,e){if(void 0!==e.getElementsByClassName&&d)return e.getElementsByClassName(t)},f=[],nt((function(t){var e;l.appendChild(t).innerHTML="",t.querySelectorAll("[selected]").length||f.push("\\["+S+"*(?:value|"+F+")"),t.querySelectorAll("[id~="+g+"-]").length||f.push("~="),t.querySelectorAll("a#"+g+"+*").length||f.push(".#.+[+~]"),t.querySelectorAll(":checked").length||f.push(":checked"),(e=u.createElement("input")).setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),l.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&f.push(":enabled",":disabled"),(e=u.createElement("input")).setAttribute("name",""),t.appendChild(e),t.querySelectorAll("[name='']").length||f.push("\\["+S+"*name"+S+"*="+S+"*(?:''|\"\")")})),v.cssHas||f.push(":has"),f=f.length&&new RegExp(f.join("|")),C=function(t,e){if(t===e)return a=!0,0;var n=!t.compareDocumentPosition-!e.compareDocumentPosition;return n||(1&(n=(t.ownerDocument||t)==(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!v.sortDetached&&e.compareDocumentPosition(t)===n?t===u||t.ownerDocument==j&&Z.contains(j,t)?-1:e===u||e.ownerDocument==j&&Z.contains(j,e)?1:i?c.call(i,t)-c.call(i,e):0:4&n?-1:1)},u):u}for(t in Z.matches=function(t,e){return Z(t,null,null,e)},Z.matchesSelector=function(t,e){if(ut(t),d&&!E[e+" "]&&(!f||!f.test(e)))try{var n=h.call(t,e);if(n||v.disconnectedMatch||t.document&&11!==t.document.nodeType)return n}catch(t){E(e,!0)}return Z(e,u,null,[t]).length>0},Z.contains=function(t,e){return(t.ownerDocument||t)!=u&&ut(t),A.contains(t,e)},Z.attr=function(t,n){(t.ownerDocument||t)!=u&&ut(t);var r=e.attrHandle[n.toLowerCase()],i=r&&p.call(e.attrHandle,n.toLowerCase())?r(t,n,!d):void 0;return void 0!==i?i:t.getAttribute(n)},Z.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},A.uniqueSort=function(t){var e,n=[],r=0,o=0;if(a=!v.sortStable,i=!v.sortStable&&s.call(t,0),T.call(t,C),a){for(;e=t[o++];)e===t[o]&&(r=n.push(o));for(;r--;)k.call(t,n[r],1)}return i=null,t},A.fn.uniqueSort=function(){return this.pushStack(A.uniqueSort(s.apply(this)))},e=A.expr={cacheLength:50,createPseudo:et,match:z,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(K,X),t[3]=(t[3]||t[4]||t[5]||"").replace(K,X),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||Z.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&Z.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return z.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&q.test(n)&&(e=ct(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(K,X).toLowerCase();return"*"===t?function(){return!0}:function(t){return N(t,e)}},CLASS:function(t){var e=D[t+" "];return e||(e=new RegExp("(^|"+S+")"+t+"("+S+"|$)"))&&D(t,(function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")}))},ATTR:function(t,e,n){return function(r){var i=Z.attr(r,t);return null==i?"!="===e:!e||(i+="","="===e?i===n:"!="===e?i!==n:"^="===e?n&&0===i.indexOf(n):"*="===e?n&&i.indexOf(n)>-1:"$="===e?n&&i.slice(-n.length)===n:"~="===e?(" "+i.replace(M," ")+" ").indexOf(n)>-1:"|="===e&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(t,e,n,r,i){var o="nth"!==t.slice(0,3),a="last"!==t.slice(-4),s="of-type"===e;return 1===r&&0===i?function(t){return!!t.parentNode}:function(e,n,u){var l,c,d,f,p,h=o!==a?"nextSibling":"previousSibling",m=e.parentNode,v=s&&e.nodeName.toLowerCase(),b=!u&&!s,D=!1;if(m){if(o){for(;h;){for(d=e;d=d[h];)if(s?N(d,v):1===d.nodeType)return!1;p=h="only"===t&&!p&&"nextSibling"}return!0}if(p=[a?m.firstChild:m.lastChild],a&&b){for(D=(f=(l=(c=m[g]||(m[g]={}))[t]||[])[0]===y&&l[1])&&l[2],d=f&&m.childNodes[f];d=++f&&d&&d[h]||(D=f=0)||p.pop();)if(1===d.nodeType&&++D&&d===e){c[t]=[y,f,D];break}}else if(b&&(D=f=(l=(c=e[g]||(e[g]={}))[t]||[])[0]===y&&l[1]),!1===D)for(;(d=++f&&d&&d[h]||(D=f=0)||p.pop())&&(!(s?N(d,v):1===d.nodeType)||!++D||(b&&((c=d[g]||(d[g]={}))[t]=[y,D]),d!==e)););return(D-=i)===r||D%r==0&&D/r>=0}}},PSEUDO:function(t,n){var r,i=e.pseudos[t]||e.setFilters[t.toLowerCase()]||Z.error("unsupported pseudo: "+t);return i[g]?i(n):i.length>1?(r=[t,t,"",n],e.setFilters.hasOwnProperty(t.toLowerCase())?et((function(t,e){for(var r,o=i(t,n),a=o.length;a--;)t[r=c.call(t,o[a])]=!(e[r]=o[a])})):function(t){return i(t,0,r)}):i}},pseudos:{not:et((function(t){var e=[],n=[],r=gt(t.replace(B,"$1"));return r[g]?et((function(t,e,n,i){for(var o,a=r(t,null,i,[]),s=t.length;s--;)(o=a[s])&&(t[s]=!(e[s]=o))})):function(t,i,o){return e[0]=t,r(e,null,o,n),e[0]=null,!n.pop()}})),has:et((function(t){return function(e){return Z(t,e).length>0}})),contains:et((function(t){return t=t.replace(K,X),function(e){return(e.textContent||A.text(e)).indexOf(t)>-1}})),lang:et((function(t){return U.test(t||"")||Z.error("unsupported lang: "+t),t=t.replace(K,X).toLowerCase(),function(e){var n;do{if(n=d?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(n=n.toLowerCase())===t||0===n.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}})),target:function(t){var e=r.location&&r.location.hash;return e&&e.slice(1)===t.id},root:function(t){return t===l},focus:function(t){return t===function(){try{return u.activeElement}catch(t){}}()&&u.hasFocus()&&!!(t.type||t.href||~t.tabIndex)},enabled:ot(!1),disabled:ot(!0),checked:function(t){return N(t,"input")&&!!t.checked||N(t,"option")&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!e.pseudos.empty(t)},header:function(t){return G.test(t.nodeName)},input:function(t){return W.test(t.nodeName)},button:function(t){return N(t,"input")&&"button"===t.type||N(t,"button")},text:function(t){var e;return N(t,"input")&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:at((function(){return[0]})),last:at((function(t,e){return[e-1]})),eq:at((function(t,e,n){return[n<0?n+e:n]})),even:at((function(t,e){for(var n=0;ne?e:n;--r>=0;)t.push(r);return t})),gt:at((function(t,e,n){for(var r=n<0?n+e:n;++r1?function(e,n,r){for(var i=t.length;i--;)if(!t[i](e,n,r))return!1;return!0}:t[0]}function ht(t,e,n,r,i){for(var o,a=[],s=0,u=t.length,l=null!=e;s-1&&(o[l]=!(a[l]=f))}}else p=ht(p===a?p.splice(g,p.length):p),i?i(null,a,p,u):m.apply(a,p)}))}function vt(t){for(var r,i,o,a=t.length,s=e.relative[t[0].type],u=s||e.relative[" "],l=s?1:0,d=ft((function(t){return t===r}),u,!0),f=ft((function(t){return c.call(r,t)>-1}),u,!0),p=[function(t,e,i){var o=!s&&(i||e!=n)||((r=e).nodeType?d(t,e,i):f(t,e,i));return r=null,o}];l1&&pt(p),l>1&&dt(t.slice(0,l-1).concat({value:" "===t[l-2].type?"*":""})).replace(B,"$1"),i,l0,o=t.length>0,a=function(a,s,l,c,f){var p,h,v,g=0,b="0",D=a&&[],w=[],x=n,E=a||o&&e.find.TAG("*",f),C=y+=null==x?1:Math.random()||.1,F=E.length;for(f&&(n=s==u||s||f);b!==F&&null!=(p=E[b]);b++){if(o&&p){for(h=0,s||p.ownerDocument==u||(ut(p),l=!d);v=t[h++];)if(v(p,s||u,l)){m.call(c,p);break}f&&(y=C)}i&&((p=!v&&p)&&g--,a&&D.push(p))}if(g+=b,i&&b!==g){for(h=0;v=r[h++];)v(D,w,s,l);if(a){if(g>0)for(;b--;)D[b]||w[b]||(w[b]=_.call(c));w=ht(w)}m.apply(c,w),f&&!a&&w.length>0&&g+r.length>1&&A.uniqueSort(c)}return f&&(y=C,n=x),D};return i?et(a):a}(a,o)),s.selector=t}return s}function yt(t,n,r,i){var o,a,s,u,l,c="function"==typeof t&&t,f=!i&&ct(t=c.selector||t);if(r=r||[],1===f.length){if((a=f[0]=f[0].slice(0)).length>2&&"ID"===(s=a[0]).type&&9===n.nodeType&&d&&e.relative[a[1].type]){if(!(n=(e.find.ID(s.matches[0].replace(K,X),n)||[])[0]))return r;c&&(n=n.parentNode),t=t.slice(a.shift().value.length)}for(o=z.needsContext.test(t)?0:a.length;o--&&(s=a[o],!e.relative[u=s.type]);)if((l=e.find[u])&&(i=l(s.matches[0].replace(K,X),Y.test(a[0].type)&&st(n.parentNode)||n))){if(a.splice(o,1),!(t=i.length&&dt(a)))return m.apply(r,i),r;break}}return(c||gt(t,f))(i,n,!d,r,!n||Y.test(t)&&st(n.parentNode)||n),r}lt.prototype=e.filters=e.pseudos,e.setFilters=new lt,v.sortStable=g.split("").sort(C).join("")===g,ut(),v.sortDetached=nt((function(t){return 1&t.compareDocumentPosition(u.createElement("fieldset"))})),A.find=Z,A.expr[":"]=A.expr.pseudos,A.unique=A.uniqueSort,Z.compile=gt,Z.select=yt,Z.setDocument=ut,Z.tokenize=ct,Z.escape=A.escapeSelector,Z.getText=A.text,Z.isXML=A.isXMLDoc,Z.selectors=A.expr,Z.support=A.support,Z.uniqueSort=A.uniqueSort}();var L=function(t,e,n){for(var r=[],i=void 0!==n;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(i&&A(t).is(n))break;r.push(t)}return r},M=function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n},$=A.expr.match.needsContext,R=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function H(t,e,n){return g(e)?A.grep(t,(function(t,r){return!!e.call(t,r,t)!==n})):e.nodeType?A.grep(t,(function(t){return t===e!==n})):"string"!=typeof e?A.grep(t,(function(t){return c.call(e,t)>-1!==n})):A.filter(e,t,n)}A.filter=function(t,e,n){var r=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===r.nodeType?A.find.matchesSelector(r,t)?[r]:[]:A.find.matches(t,A.grep(e,(function(t){return 1===t.nodeType})))},A.fn.extend({find:function(t){var e,n,r=this.length,i=this;if("string"!=typeof t)return this.pushStack(A(t).filter((function(){for(e=0;e1?A.uniqueSort(n):n},filter:function(t){return this.pushStack(H(this,t||[],!1))},not:function(t){return this.pushStack(H(this,t||[],!0))},is:function(t){return!!H(this,"string"==typeof t&&$.test(t)?A(t):t||[],!1).length}});var q,U=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(A.fn.init=function(t,e,n){var r,i;if(!t)return this;if(n=n||q,"string"==typeof t){if(!(r="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:U.exec(t))||!r[1]&&e)return!e||e.jquery?(e||n).find(t):this.constructor(e).find(t);if(r[1]){if(e=e instanceof A?e[0]:e,A.merge(this,A.parseHTML(r[1],e&&e.nodeType?e.ownerDocument||e:b,!0)),R.test(r[1])&&A.isPlainObject(e))for(r in e)g(this[r])?this[r](e[r]):this.attr(r,e[r]);return this}return(i=b.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):g(t)?void 0!==n.ready?n.ready(t):t(A):A.makeArray(t,this)}).prototype=A.fn,q=A(b);var z=/^(?:parents|prev(?:Until|All))/,W={children:!0,contents:!0,next:!0,prev:!0};function G(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}A.fn.extend({has:function(t){var e=A(t,this),n=e.length;return this.filter((function(){for(var t=0;t-1:1===n.nodeType&&A.find.matchesSelector(n,t))){o.push(n);break}return this.pushStack(o.length>1?A.uniqueSort(o):o)},index:function(t){return t?"string"==typeof t?c.call(A(t),this[0]):c.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(A.uniqueSort(A.merge(this.get(),A(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),A.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return L(t,"parentNode")},parentsUntil:function(t,e,n){return L(t,"parentNode",n)},next:function(t){return G(t,"nextSibling")},prev:function(t){return G(t,"previousSibling")},nextAll:function(t){return L(t,"nextSibling")},prevAll:function(t){return L(t,"previousSibling")},nextUntil:function(t,e,n){return L(t,"nextSibling",n)},prevUntil:function(t,e,n){return L(t,"previousSibling",n)},siblings:function(t){return M((t.parentNode||{}).firstChild,t)},children:function(t){return M(t.firstChild)},contents:function(t){return null!=t.contentDocument&&a(t.contentDocument)?t.contentDocument:(N(t,"template")&&(t=t.content||t),A.merge([],t.childNodes))}},(function(t,e){A.fn[t]=function(n,r){var i=A.map(this,e,n);return"Until"!==t.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=A.filter(r,i)),this.length>1&&(W[t]||A.uniqueSort(i),z.test(t)&&i.reverse()),this.pushStack(i)}}));var V=/[^\x20\t\r\n\f]+/g;function Y(t){return t}function K(t){throw t}function X(t,e,n,r){var i;try{t&&g(i=t.promise)?i.call(t).done(e).fail(n):t&&g(i=t.then)?i.call(t,e,n):e.apply(void 0,[t].slice(r))}catch(t){n.apply(void 0,[t])}}A.Callbacks=function(t){t="string"==typeof t?function(t){var e={};return A.each(t.match(V)||[],(function(t,n){e[n]=!0})),e}(t):A.extend({},t);var e,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||t.once,r=e=!0;a.length;s=-1)for(n=a.shift();++s-1;)o.splice(n,1),n<=s&&s--})),this},has:function(t){return t?A.inArray(t,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||e||(o=n=""),this},locked:function(){return!!i},fireWith:function(t,n){return i||(n=[t,(n=n||[]).slice?n.slice():n],a.push(n),e||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},A.extend({Deferred:function(t){var e=[["notify","progress",A.Callbacks("memory"),A.Callbacks("memory"),2],["resolve","done",A.Callbacks("once memory"),A.Callbacks("once memory"),0,"resolved"],["reject","fail",A.Callbacks("once memory"),A.Callbacks("once memory"),1,"rejected"]],n="pending",i={state:function(){return n},always:function(){return o.done(arguments).fail(arguments),this},catch:function(t){return i.then(null,t)},pipe:function(){var t=arguments;return A.Deferred((function(n){A.each(e,(function(e,r){var i=g(t[r[4]])&&t[r[4]];o[r[1]]((function(){var t=i&&i.apply(this,arguments);t&&g(t.promise)?t.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[r[0]+"With"](this,i?[t]:arguments)}))})),t=null})).promise()},then:function(t,n,i){var o=0;function a(t,e,n,i){return function(){var s=this,u=arguments,l=function(){var r,l;if(!(t=o&&(n!==K&&(s=void 0,u=[r]),e.rejectWith(s,u))}};t?c():(A.Deferred.getErrorHook?c.error=A.Deferred.getErrorHook():A.Deferred.getStackHook&&(c.error=A.Deferred.getStackHook()),r.setTimeout(c))}}return A.Deferred((function(r){e[0][3].add(a(0,r,g(i)?i:Y,r.notifyWith)),e[1][3].add(a(0,r,g(t)?t:Y)),e[2][3].add(a(0,r,g(n)?n:K))})).promise()},promise:function(t){return null!=t?A.extend(t,i):i}},o={};return A.each(e,(function(t,r){var a=r[2],s=r[5];i[r[1]]=a.add,s&&a.add((function(){n=s}),e[3-t][2].disable,e[3-t][3].disable,e[0][2].lock,e[0][3].lock),a.add(r[3].fire),o[r[0]]=function(){return o[r[0]+"With"](this===o?void 0:this,arguments),this},o[r[0]+"With"]=a.fireWith})),i.promise(o),t&&t.call(o,o),o},when:function(t){var e=arguments.length,n=e,r=Array(n),i=s.call(arguments),o=A.Deferred(),a=function(t){return function(n){r[t]=this,i[t]=arguments.length>1?s.call(arguments):n,--e||o.resolveWith(r,i)}};if(e<=1&&(X(t,o.done(a(n)).resolve,o.reject,!e),"pending"===o.state()||g(i[n]&&i[n].then)))return o.then();for(;n--;)X(i[n],a(n),o.reject);return o.promise()}});var J=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;A.Deferred.exceptionHook=function(t,e){r.console&&r.console.warn&&t&&J.test(t.name)&&r.console.warn("jQuery.Deferred exception: "+t.message,t.stack,e)},A.readyException=function(t){r.setTimeout((function(){throw t}))};var Q=A.Deferred();function Z(){b.removeEventListener("DOMContentLoaded",Z),r.removeEventListener("load",Z),A.ready()}A.fn.ready=function(t){return Q.then(t).catch((function(t){A.readyException(t)})),this},A.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--A.readyWait:A.isReady)||(A.isReady=!0,!0!==t&&--A.readyWait>0||Q.resolveWith(b,[A]))}}),A.ready.then=Q.then,"complete"===b.readyState||"loading"!==b.readyState&&!b.documentElement.doScroll?r.setTimeout(A.ready):(b.addEventListener("DOMContentLoaded",Z),r.addEventListener("load",Z));var tt=function t(e,n,r,i,o,a,s){var u=0,l=e.length,c=null==r;if("object"===x(r))for(u in o=!0,r)t(e,n,u,r[u],!0,a,s);else if(void 0!==i&&(o=!0,g(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(t,e,n){return c.call(A(t),n)})),n))for(;u1,null,!0)},removeData:function(t){return this.each((function(){ut.remove(this,t)}))}}),A.extend({queue:function(t,e,n){var r;if(t)return e=(e||"fx")+"queue",r=st.get(t,e),n&&(!r||Array.isArray(n)?r=st.access(t,e,A.makeArray(n)):r.push(n)),r||[]},dequeue:function(t,e){e=e||"fx";var n=A.queue(t,e),r=n.length,i=n.shift(),o=A._queueHooks(t,e);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===e&&n.unshift("inprogress"),delete o.stop,i.call(t,(function(){A.dequeue(t,e)}),o)),!r&&o&&o.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return st.get(t,n)||st.access(t,n,{empty:A.Callbacks("once memory").add((function(){st.remove(t,[e+"queue",n])}))})}}),A.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length\x20\t\r\n\f]*)/i,Nt=/^$|^module$|\/(?:java|ecma)script/i;Et=b.createDocumentFragment().appendChild(b.createElement("div")),(Ct=b.createElement("input")).setAttribute("type","radio"),Ct.setAttribute("checked","checked"),Ct.setAttribute("name","t"),Et.appendChild(Ct),v.checkClone=Et.cloneNode(!0).cloneNode(!0).lastChild.checked,Et.innerHTML="",v.noCloneChecked=!!Et.cloneNode(!0).lastChild.defaultValue,Et.innerHTML="",v.option=!!Et.lastChild;var _t={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Tt(t,e){var n;return n=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&N(t,e)?A.merge([t],n):n}function kt(t,e){for(var n=0,r=t.length;n",""]);var St=/<|&#?\w+;/;function Bt(t,e,n,r,i){for(var o,a,s,u,l,c,d=e.createDocumentFragment(),f=[],p=0,h=t.length;p-1)i&&i.push(o);else if(l=vt(o),a=Tt(d.appendChild(o),"script"),l&&kt(a),n)for(c=0;o=a[c++];)Nt.test(o.type||"")&&n.push(o);return d}var Pt=/^([^.]*)(?:\.(.+)|)/;function Ot(){return!0}function jt(){return!1}function It(t,e,n,r,i,o){var a,s;if("object"==typeof e){for(s in"string"!=typeof n&&(r=r||n,n=void 0),e)It(t,s,n,r,e[s],o);return t}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=jt;else if(!i)return t;return 1===o&&(a=i,i=function(t){return A().off(t),a.apply(this,arguments)},i.guid=a.guid||(a.guid=A.guid++)),t.each((function(){A.event.add(this,e,i,r,n)}))}function Lt(t,e,n){n?(st.set(t,e,!1),A.event.add(t,e,{namespace:!1,handler:function(t){var n,r=st.get(this,e);if(1&t.isTrigger&&this[e]){if(r)(A.event.special[e]||{}).delegateType&&t.stopPropagation();else if(r=s.call(arguments),st.set(this,e,r),this[e](),n=st.get(this,e),st.set(this,e,!1),r!==n)return t.stopImmediatePropagation(),t.preventDefault(),n}else r&&(st.set(this,e,A.event.trigger(r[0],r.slice(1),this)),t.stopPropagation(),t.isImmediatePropagationStopped=Ot)}})):void 0===st.get(t,e)&&A.event.add(t,e,Ot)}A.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,d,f,p,h,m,v=st.get(t);if(ot(t))for(n.handler&&(n=(o=n).handler,i=o.selector),i&&A.find.matchesSelector(mt,i),n.guid||(n.guid=A.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return void 0!==A&&A.event.triggered!==e.type?A.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(V)||[""]).length;l--;)p=m=(s=Pt.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),p&&(d=A.event.special[p]||{},p=(i?d.delegateType:d.bindType)||p,d=A.event.special[p]||{},c=A.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&A.expr.match.needsContext.test(i),namespace:h.join(".")},o),(f=u[p])||((f=u[p]=[]).delegateCount=0,d.setup&&!1!==d.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(p,a)),d.add&&(d.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,c):f.push(c),A.event.global[p]=!0)},remove:function(t,e,n,r,i){var o,a,s,u,l,c,d,f,p,h,m,v=st.hasData(t)&&st.get(t);if(v&&(u=v.events)){for(l=(e=(e||"").match(V)||[""]).length;l--;)if(p=m=(s=Pt.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),p){for(d=A.event.special[p]||{},f=u[p=(r?d.delegateType:d.bindType)||p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=f.length;o--;)c=f[o],!i&&m!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,d.remove&&d.remove.call(t,c));a&&!f.length&&(d.teardown&&!1!==d.teardown.call(t,h,v.handle)||A.removeEvent(t,p,v.handle),delete u[p])}else for(p in u)A.event.remove(t,p+e[l],n,r,!0);A.isEmptyObject(u)&&st.remove(t,"handle events")}},dispatch:function(t){var e,n,r,i,o,a,s=new Array(arguments.length),u=A.event.fix(t),l=(st.get(this,"events")||Object.create(null))[u.type]||[],c=A.event.special[u.type]||{};for(s[0]=u,e=1;e=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==t.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:A.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\s*$/g;function Ht(t,e){return N(t,"table")&&N(11!==e.nodeType?e:e.firstChild,"tr")&&A(t).children("tbody")[0]||t}function qt(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function Ut(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function zt(t,e){var n,r,i,o,a,s;if(1===e.nodeType){if(st.hasData(t)&&(s=st.get(t).events))for(i in st.remove(e,"handle events"),s)for(n=0,r=s[i].length;n1&&"string"==typeof h&&!v.checkClone&&$t.test(h))return t.each((function(i){var o=t.eq(i);m&&(e[0]=h.call(this,i,o.html())),Gt(o,e,n,r)}));if(f&&(o=(i=Bt(e,t[0].ownerDocument,!1,t,r)).firstChild,1===i.childNodes.length&&(i=o),o||r)){for(s=(a=A.map(Tt(i,"script"),qt)).length;d0&&kt(a,!u&&Tt(t,"script")),s},cleanData:function(t){for(var e,n,r,i=A.event.special,o=0;void 0!==(n=t[o]);o++)if(ot(n)){if(e=n[st.expando]){if(e.events)for(r in e.events)i[r]?A.event.remove(n,r):A.removeEvent(n,r,e.handle);n[st.expando]=void 0}n[ut.expando]&&(n[ut.expando]=void 0)}}}),A.fn.extend({detach:function(t){return Vt(this,t,!0)},remove:function(t){return Vt(this,t)},text:function(t){return tt(this,(function(t){return void 0===t?A.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)}))}),null,t,arguments.length)},append:function(){return Gt(this,arguments,(function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Ht(this,t).appendChild(t)}))},prepend:function(){return Gt(this,arguments,(function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=Ht(this,t);e.insertBefore(t,e.firstChild)}}))},before:function(){return Gt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this)}))},after:function(){return Gt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)}))},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(A.cleanData(Tt(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map((function(){return A.clone(this,t,e)}))},html:function(t){return tt(this,(function(t){var e=this[0]||{},n=0,r=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!Mt.test(t)&&!_t[(Ft.exec(t)||["",""])[1].toLowerCase()]){t=A.htmlPrefilter(t);try{for(;n=0&&(u+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-o-u-s-.5))||0),u+l}function ce(t,e,n){var r=Xt(t),i=(!v.boxSizingReliable()||n)&&"border-box"===A.css(t,"boxSizing",!1,r),o=i,a=Zt(t,e,r),s="offset"+e[0].toUpperCase()+e.slice(1);if(Yt.test(a)){if(!n)return a;a="auto"}return(!v.boxSizingReliable()&&i||!v.reliableTrDimensions()&&N(t,"tr")||"auto"===a||!parseFloat(a)&&"inline"===A.css(t,"display",!1,r))&&t.getClientRects().length&&(i="border-box"===A.css(t,"boxSizing",!1,r),(o=s in t)&&(a=t[s])),(a=parseFloat(a)||0)+le(t,e,n||(i?"border":"content"),o,r,a)+"px"}function de(t,e,n,r,i){return new de.prototype.init(t,e,n,r,i)}A.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=Zt(t,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(t,e,n,r){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var i,o,a,s=it(e),u=Kt.test(e),l=t.style;if(u||(e=ie(s)),a=A.cssHooks[e]||A.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(t,!1,r))?i:l[e];"string"==(o=typeof n)&&(i=pt.exec(n))&&i[1]&&(n=bt(t,e,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(A.cssNumber[s]?"":"px")),v.clearCloneStyle||""!==n||0!==e.indexOf("background")||(l[e]="inherit"),a&&"set"in a&&void 0===(n=a.set(t,n,r))||(u?l.setProperty(e,n):l[e]=n))}},css:function(t,e,n,r){var i,o,a,s=it(e);return Kt.test(e)||(e=ie(s)),(a=A.cssHooks[e]||A.cssHooks[s])&&"get"in a&&(i=a.get(t,!0,n)),void 0===i&&(i=Zt(t,e,r)),"normal"===i&&e in se&&(i=se[e]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),A.each(["height","width"],(function(t,e){A.cssHooks[e]={get:function(t,n,r){if(n)return!oe.test(A.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?ce(t,e,r):Jt(t,ae,(function(){return ce(t,e,r)}))},set:function(t,n,r){var i,o=Xt(t),a=!v.scrollboxSize()&&"absolute"===o.position,s=(a||r)&&"border-box"===A.css(t,"boxSizing",!1,o),u=r?le(t,e,r,s,o):0;return s&&a&&(u-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(o[e])-le(t,e,"border",!1,o)-.5)),u&&(i=pt.exec(n))&&"px"!==(i[3]||"px")&&(t.style[e]=n,n=A.css(t,e)),ue(0,n,u)}}})),A.cssHooks.marginLeft=te(v.reliableMarginLeft,(function(t,e){if(e)return(parseFloat(Zt(t,"marginLeft"))||t.getBoundingClientRect().left-Jt(t,{marginLeft:0},(function(){return t.getBoundingClientRect().left})))+"px"})),A.each({margin:"",padding:"",border:"Width"},(function(t,e){A.cssHooks[t+e]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[t+ht[r]+e]=o[r]||o[r-2]||o[0];return i}},"margin"!==t&&(A.cssHooks[t+e].set=ue)})),A.fn.extend({css:function(t,e){return tt(this,(function(t,e,n){var r,i,o={},a=0;if(Array.isArray(e)){for(r=Xt(t),i=e.length;a1)}}),A.Tween=de,de.prototype={constructor:de,init:function(t,e,n,r,i,o){this.elem=t,this.prop=n,this.easing=i||A.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=r,this.unit=o||(A.cssNumber[n]?"":"px")},cur:function(){var t=de.propHooks[this.prop];return t&&t.get?t.get(this):de.propHooks._default.get(this)},run:function(t){var e,n=de.propHooks[this.prop];return this.options.duration?this.pos=e=A.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):de.propHooks._default.set(this),this}},de.prototype.init.prototype=de.prototype,de.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=A.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){A.fx.step[t.prop]?A.fx.step[t.prop](t):1!==t.elem.nodeType||!A.cssHooks[t.prop]&&null==t.elem.style[ie(t.prop)]?t.elem[t.prop]=t.now:A.style(t.elem,t.prop,t.now+t.unit)}}},de.propHooks.scrollTop=de.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},A.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},A.fx=de.prototype.init,A.fx.step={};var fe,pe,he=/^(?:toggle|show|hide)$/,me=/queueHooks$/;function ve(){pe&&(!1===b.hidden&&r.requestAnimationFrame?r.requestAnimationFrame(ve):r.setTimeout(ve,A.fx.interval),A.fx.tick())}function ge(){return r.setTimeout((function(){fe=void 0})),fe=Date.now()}function ye(t,e){var n,r=0,i={height:t};for(e=e?1:0;r<4;r+=2-e)i["margin"+(n=ht[r])]=i["padding"+n]=t;return e&&(i.opacity=i.width=t),i}function be(t,e,n){for(var r,i=(De.tweeners[e]||[]).concat(De.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(t){return this.each((function(){A.removeAttr(this,t)}))}}),A.extend({attr:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===t.getAttribute?A.prop(t,e,n):(1===o&&A.isXMLDoc(t)||(i=A.attrHooks[e.toLowerCase()]||(A.expr.match.bool.test(e)?we:void 0)),void 0!==n?null===n?void A.removeAttr(t,e):i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:(t.setAttribute(e,n+""),n):i&&"get"in i&&null!==(r=i.get(t,e))?r:null==(r=A.find.attr(t,e))?void 0:r)},attrHooks:{type:{set:function(t,e){if(!v.radioValue&&"radio"===e&&N(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,r=0,i=e&&e.match(V);if(i&&1===t.nodeType)for(;n=i[r++];)t.removeAttribute(n)}}),we={set:function(t,e,n){return!1===e?A.removeAttr(t,n):t.setAttribute(n,n),n}},A.each(A.expr.match.bool.source.match(/\w+/g),(function(t,e){var n=xe[e]||A.find.attr;xe[e]=function(t,e,r){var i,o,a=e.toLowerCase();return r||(o=xe[a],xe[a]=i,i=null!=n(t,e,r)?a:null,xe[a]=o),i}}));var Ee=/^(?:input|select|textarea|button)$/i,Ce=/^(?:a|area)$/i;function Ae(t){return(t.match(V)||[]).join(" ")}function Fe(t){return t.getAttribute&&t.getAttribute("class")||""}function Ne(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(V)||[]}A.fn.extend({prop:function(t,e){return tt(this,A.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each((function(){delete this[A.propFix[t]||t]}))}}),A.extend({prop:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&A.isXMLDoc(t)||(e=A.propFix[e]||e,i=A.propHooks[e]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:t[e]=n:i&&"get"in i&&null!==(r=i.get(t,e))?r:t[e]},propHooks:{tabIndex:{get:function(t){var e=A.find.attr(t,"tabindex");return e?parseInt(e,10):Ee.test(t.nodeName)||Ce.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),v.optSelected||(A.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),A.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],(function(){A.propFix[this.toLowerCase()]=this})),A.fn.extend({addClass:function(t){var e,n,r,i,o,a;return g(t)?this.each((function(e){A(this).addClass(t.call(this,e,Fe(this)))})):(e=Ne(t)).length?this.each((function(){if(r=Fe(this),n=1===this.nodeType&&" "+Ae(r)+" "){for(o=0;o-1;)n=n.replace(" "+i+" "," ");a=Ae(n),r!==a&&this.setAttribute("class",a)}})):this:this.attr("class","")},toggleClass:function(t,e){var n,r,i,o,a=typeof t,s="string"===a||Array.isArray(t);return g(t)?this.each((function(n){A(this).toggleClass(t.call(this,n,Fe(this),e),e)})):"boolean"==typeof e&&s?e?this.addClass(t):this.removeClass(t):(n=Ne(t),this.each((function(){if(s)for(o=A(this),i=0;i-1)return!0;return!1}});var _e=/\r/g;A.fn.extend({val:function(t){var e,n,r,i=this[0];return arguments.length?(r=g(t),this.each((function(n){var i;1===this.nodeType&&(null==(i=r?t.call(this,n,A(this).val()):t)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=A.map(i,(function(t){return null==t?"":t+""}))),(e=A.valHooks[this.type]||A.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,i,"value")||(this.value=i))}))):i?(e=A.valHooks[i.type]||A.valHooks[i.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(_e,""):null==n?"":n:void 0}}),A.extend({valHooks:{option:{get:function(t){var e=A.find.attr(t,"value");return null!=e?e:Ae(A.text(t))}},select:{get:function(t){var e,n,r,i=t.options,o=t.selectedIndex,a="select-one"===t.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(t.selectedIndex=-1),o}}}}),A.each(["radio","checkbox"],(function(){A.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=A.inArray(A(t).val(),e)>-1}},v.checkOn||(A.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}));var Te=r.location,ke={guid:Date.now()},Se=/\?/;A.parseXML=function(t){var e,n;if(!t||"string"!=typeof t)return null;try{e=(new r.DOMParser).parseFromString(t,"text/xml")}catch(t){}return n=e&&e.getElementsByTagName("parsererror")[0],e&&!n||A.error("Invalid XML: "+(n?A.map(n.childNodes,(function(t){return t.textContent})).join("\n"):t)),e};var Be=/^(?:focusinfocus|focusoutblur)$/,Pe=function(t){t.stopPropagation()};A.extend(A.event,{trigger:function(t,e,n,i){var o,a,s,u,l,c,d,f,h=[n||b],m=p.call(t,"type")?t.type:t,v=p.call(t,"namespace")?t.namespace.split("."):[];if(a=f=s=n=n||b,3!==n.nodeType&&8!==n.nodeType&&!Be.test(m+A.event.triggered)&&(m.indexOf(".")>-1&&(v=m.split("."),m=v.shift(),v.sort()),l=m.indexOf(":")<0&&"on"+m,(t=t[A.expando]?t:new A.Event(m,"object"==typeof t&&t)).isTrigger=i?2:3,t.namespace=v.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+v.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=n),e=null==e?[t]:A.makeArray(e,[t]),d=A.event.special[m]||{},i||!d.trigger||!1!==d.trigger.apply(n,e))){if(!i&&!d.noBubble&&!y(n)){for(u=d.delegateType||m,Be.test(u+m)||(a=a.parentNode);a;a=a.parentNode)h.push(a),s=a;s===(n.ownerDocument||b)&&h.push(s.defaultView||s.parentWindow||r)}for(o=0;(a=h[o++])&&!t.isPropagationStopped();)f=a,t.type=o>1?u:d.bindType||m,(c=(st.get(a,"events")||Object.create(null))[t.type]&&st.get(a,"handle"))&&c.apply(a,e),(c=l&&a[l])&&c.apply&&ot(a)&&(t.result=c.apply(a,e),!1===t.result&&t.preventDefault());return t.type=m,i||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(h.pop(),e)||!ot(n)||l&&g(n[m])&&!y(n)&&((s=n[l])&&(n[l]=null),A.event.triggered=m,t.isPropagationStopped()&&f.addEventListener(m,Pe),n[m](),t.isPropagationStopped()&&f.removeEventListener(m,Pe),A.event.triggered=void 0,s&&(n[l]=s)),t.result}},simulate:function(t,e,n){var r=A.extend(new A.Event,n,{type:t,isSimulated:!0});A.event.trigger(r,null,e)}}),A.fn.extend({trigger:function(t,e){return this.each((function(){A.event.trigger(t,e,this)}))},triggerHandler:function(t,e){var n=this[0];if(n)return A.event.trigger(t,e,n,!0)}});var Oe=/\[\]$/,je=/\r?\n/g,Ie=/^(?:submit|button|image|reset|file)$/i,Le=/^(?:input|select|textarea|keygen)/i;function Me(t,e,n,r){var i;if(Array.isArray(e))A.each(e,(function(e,i){n||Oe.test(t)?r(t,i):Me(t+"["+("object"==typeof i&&null!=i?e:"")+"]",i,n,r)}));else if(n||"object"!==x(e))r(t,e);else for(i in e)Me(t+"["+i+"]",e[i],n,r)}A.param=function(t,e){var n,r=[],i=function(t,e){var n=g(e)?e():e;r[r.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(null==t)return"";if(Array.isArray(t)||t.jquery&&!A.isPlainObject(t))A.each(t,(function(){i(this.name,this.value)}));else for(n in t)Me(n,t[n],e,i);return r.join("&")},A.fn.extend({serialize:function(){return A.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var t=A.prop(this,"elements");return t?A.makeArray(t):this})).filter((function(){var t=this.type;return this.name&&!A(this).is(":disabled")&&Le.test(this.nodeName)&&!Ie.test(t)&&(this.checked||!At.test(t))})).map((function(t,e){var n=A(this).val();return null==n?null:Array.isArray(n)?A.map(n,(function(t){return{name:e.name,value:t.replace(je,"\r\n")}})):{name:e.name,value:n.replace(je,"\r\n")}})).get()}});var $e=/%20/g,Re=/#.*$/,He=/([?&])_=[^&]*/,qe=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ue=/^(?:GET|HEAD)$/,ze=/^\/\//,We={},Ge={},Ve="*/".concat("*"),Ye=b.createElement("a");function Ke(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var r,i=0,o=e.toLowerCase().match(V)||[];if(g(n))for(;r=o[i++];)"+"===r[0]?(r=r.slice(1)||"*",(t[r]=t[r]||[]).unshift(n)):(t[r]=t[r]||[]).push(n)}}function Xe(t,e,n,r){var i={},o=t===Ge;function a(s){var u;return i[s]=!0,A.each(t[s]||[],(function(t,s){var l=s(e,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(e.dataTypes.unshift(l),a(l),!1)})),u}return a(e.dataTypes[0])||!i["*"]&&a("*")}function Je(t,e){var n,r,i=A.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((i[n]?t:r||(r={}))[n]=e[n]);return r&&A.extend(!0,t,r),t}Ye.href=Te.href,A.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Te.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Te.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ve,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":A.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Je(Je(t,A.ajaxSettings),e):Je(A.ajaxSettings,t)},ajaxPrefilter:Ke(We),ajaxTransport:Ke(Ge),ajax:function(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var n,i,o,a,s,u,l,c,d,f,p=A.ajaxSetup({},e),h=p.context||p,m=p.context&&(h.nodeType||h.jquery)?A(h):A.event,v=A.Deferred(),g=A.Callbacks("once memory"),y=p.statusCode||{},D={},w={},x="canceled",E={readyState:0,getResponseHeader:function(t){var e;if(l){if(!a)for(a={};e=qe.exec(o);)a[e[1].toLowerCase()+" "]=(a[e[1].toLowerCase()+" "]||[]).concat(e[2]);e=a[t.toLowerCase()+" "]}return null==e?null:e.join(", ")},getAllResponseHeaders:function(){return l?o:null},setRequestHeader:function(t,e){return null==l&&(t=w[t.toLowerCase()]=w[t.toLowerCase()]||t,D[t]=e),this},overrideMimeType:function(t){return null==l&&(p.mimeType=t),this},statusCode:function(t){var e;if(t)if(l)E.always(t[E.status]);else for(e in t)y[e]=[y[e],t[e]];return this},abort:function(t){var e=t||x;return n&&n.abort(e),C(0,e),this}};if(v.promise(E),p.url=((t||p.url||Te.href)+"").replace(ze,Te.protocol+"//"),p.type=e.method||e.type||p.method||p.type,p.dataTypes=(p.dataType||"*").toLowerCase().match(V)||[""],null==p.crossDomain){u=b.createElement("a");try{u.href=p.url,u.href=u.href,p.crossDomain=Ye.protocol+"//"+Ye.host!=u.protocol+"//"+u.host}catch(t){p.crossDomain=!0}}if(p.data&&p.processData&&"string"!=typeof p.data&&(p.data=A.param(p.data,p.traditional)),Xe(We,p,e,E),l)return E;for(d in(c=A.event&&p.global)&&0==A.active++&&A.event.trigger("ajaxStart"),p.type=p.type.toUpperCase(),p.hasContent=!Ue.test(p.type),i=p.url.replace(Re,""),p.hasContent?p.data&&p.processData&&0===(p.contentType||"").indexOf("application/x-www-form-urlencoded")&&(p.data=p.data.replace($e,"+")):(f=p.url.slice(i.length),p.data&&(p.processData||"string"==typeof p.data)&&(i+=(Se.test(i)?"&":"?")+p.data,delete p.data),!1===p.cache&&(i=i.replace(He,"$1"),f=(Se.test(i)?"&":"?")+"_="+ke.guid+++f),p.url=i+f),p.ifModified&&(A.lastModified[i]&&E.setRequestHeader("If-Modified-Since",A.lastModified[i]),A.etag[i]&&E.setRequestHeader("If-None-Match",A.etag[i])),(p.data&&p.hasContent&&!1!==p.contentType||e.contentType)&&E.setRequestHeader("Content-Type",p.contentType),E.setRequestHeader("Accept",p.dataTypes[0]&&p.accepts[p.dataTypes[0]]?p.accepts[p.dataTypes[0]]+("*"!==p.dataTypes[0]?", "+Ve+"; q=0.01":""):p.accepts["*"]),p.headers)E.setRequestHeader(d,p.headers[d]);if(p.beforeSend&&(!1===p.beforeSend.call(h,E,p)||l))return E.abort();if(x="abort",g.add(p.complete),E.done(p.success),E.fail(p.error),n=Xe(Ge,p,e,E)){if(E.readyState=1,c&&m.trigger("ajaxSend",[E,p]),l)return E;p.async&&p.timeout>0&&(s=r.setTimeout((function(){E.abort("timeout")}),p.timeout));try{l=!1,n.send(D,C)}catch(t){if(l)throw t;C(-1,t)}}else C(-1,"No Transport");function C(t,e,a,u){var d,f,b,D,w,x=e;l||(l=!0,s&&r.clearTimeout(s),n=void 0,o=u||"",E.readyState=t>0?4:0,d=t>=200&&t<300||304===t,a&&(D=function(t,e,n){for(var r,i,o,a,s=t.contents,u=t.dataTypes;"*"===u[0];)u.shift(),void 0===r&&(r=t.mimeType||e.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||t.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(p,E,a)),!d&&A.inArray("script",p.dataTypes)>-1&&A.inArray("json",p.dataTypes)<0&&(p.converters["text script"]=function(){}),D=function(t,e,n,r){var i,o,a,s,u,l={},c=t.dataTypes.slice();if(c[1])for(a in t.converters)l[a.toLowerCase()]=t.converters[a];for(o=c.shift();o;)if(t.responseFields[o]&&(n[t.responseFields[o]]=e),!u&&r&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&t.throws)e=a(e);else try{e=a(e)}catch(t){return{state:"parsererror",error:a?t:"No conversion from "+u+" to "+o}}}return{state:"success",data:e}}(p,D,E,d),d?(p.ifModified&&((w=E.getResponseHeader("Last-Modified"))&&(A.lastModified[i]=w),(w=E.getResponseHeader("etag"))&&(A.etag[i]=w)),204===t||"HEAD"===p.type?x="nocontent":304===t?x="notmodified":(x=D.state,f=D.data,d=!(b=D.error))):(b=x,!t&&x||(x="error",t<0&&(t=0))),E.status=t,E.statusText=(e||x)+"",d?v.resolveWith(h,[f,x,E]):v.rejectWith(h,[E,x,b]),E.statusCode(y),y=void 0,c&&m.trigger(d?"ajaxSuccess":"ajaxError",[E,p,d?f:b]),g.fireWith(h,[E,x]),c&&(m.trigger("ajaxComplete",[E,p]),--A.active||A.event.trigger("ajaxStop")))}return E},getJSON:function(t,e,n){return A.get(t,e,n,"json")},getScript:function(t,e){return A.get(t,void 0,e,"script")}}),A.each(["get","post"],(function(t,e){A[e]=function(t,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),A.ajax(A.extend({url:t,type:e,dataType:i,data:n,success:r},A.isPlainObject(t)&&t))}})),A.ajaxPrefilter((function(t){var e;for(e in t.headers)"content-type"===e.toLowerCase()&&(t.contentType=t.headers[e]||"")})),A._evalUrl=function(t,e,n){return A.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(t){A.globalEval(t,e,n)}})},A.fn.extend({wrapAll:function(t){var e;return this[0]&&(g(t)&&(t=t.call(this[0])),e=A(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map((function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t})).append(this)),this},wrapInner:function(t){return g(t)?this.each((function(e){A(this).wrapInner(t.call(this,e))})):this.each((function(){var e=A(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)}))},wrap:function(t){var e=g(t);return this.each((function(n){A(this).wrapAll(e?t.call(this,n):t)}))},unwrap:function(t){return this.parent(t).not("body").each((function(){A(this).replaceWith(this.childNodes)})),this}}),A.expr.pseudos.hidden=function(t){return!A.expr.pseudos.visible(t)},A.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},A.ajaxSettings.xhr=function(){try{return new r.XMLHttpRequest}catch(t){}};var Qe={0:200,1223:204},Ze=A.ajaxSettings.xhr();v.cors=!!Ze&&"withCredentials"in Ze,v.ajax=Ze=!!Ze,A.ajaxTransport((function(t){var e,n;if(v.cors||Ze&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];for(a in t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest"),i)s.setRequestHeader(a,i[a]);e=function(t){return function(){e&&(e=n=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===t?s.abort():"error"===t?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Qe[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=e(),n=s.onerror=s.ontimeout=e("error"),void 0!==s.onabort?s.onabort=n:s.onreadystatechange=function(){4===s.readyState&&r.setTimeout((function(){e&&n()}))},e=e("abort");try{s.send(t.hasContent&&t.data||null)}catch(t){if(e)throw t}},abort:function(){e&&e()}}})),A.ajaxPrefilter((function(t){t.crossDomain&&(t.contents.script=!1)})),A.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return A.globalEval(t),t}}}),A.ajaxPrefilter("script",(function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")})),A.ajaxTransport("script",(function(t){var e,n;if(t.crossDomain||t.scriptAttrs)return{send:function(r,i){e=A("', + json_encode($payload) + ); + + return new HtmlResponse($content); + } + + protected function makeLoggedInResponse(User $user) + { + $response = $this->makeResponse(['loggedIn' => true]); + + $token = RememberAccessToken::generate($user->id); + + return $this->rememberer->remember($response, $token); + } +} diff --git a/packages/flarum/core/src/Forum/Content/AssertRegistered.php b/packages/flarum/core/src/Forum/Content/AssertRegistered.php new file mode 100644 index 0000000..e17240e --- /dev/null +++ b/packages/flarum/core/src/Forum/Content/AssertRegistered.php @@ -0,0 +1,22 @@ +assertRegistered(); + } +} diff --git a/packages/flarum/core/src/Forum/Content/Discussion.php b/packages/flarum/core/src/Forum/Content/Discussion.php new file mode 100644 index 0000000..9add89c --- /dev/null +++ b/packages/flarum/core/src/Forum/Content/Discussion.php @@ -0,0 +1,130 @@ +api = $api; + $this->url = $url; + $this->view = $view; + } + + public function __invoke(Document $document, Request $request) + { + $queryParams = $request->getQueryParams(); + $id = Arr::get($queryParams, 'id'); + $near = intval(Arr::get($queryParams, 'near')); + $page = max(1, intval(Arr::get($queryParams, 'page')), 1 + intdiv($near, 20)); + + $params = [ + 'id' => $id, + 'page' => [ + 'near' => $near, + 'offset' => ($page - 1) * 20, + 'limit' => 20 + ] + ]; + + $apiDocument = $this->getApiDocument($request, $id, $params); + + $getResource = function ($link) use ($apiDocument) { + return Arr::first($apiDocument->included, function ($value) use ($link) { + return $value->type === $link->type && $value->id === $link->id; + }); + }; + + $url = function ($newQueryParams) use ($queryParams, $apiDocument) { + $newQueryParams = array_merge($queryParams, $newQueryParams); + unset($newQueryParams['id']); + unset($newQueryParams['near']); + + if (Arr::get($newQueryParams, 'page') == 1) { + unset($newQueryParams['page']); + } + + $queryString = http_build_query($newQueryParams); + + return $this->url->to('forum')->route('discussion', ['id' => $apiDocument->data->attributes->slug]). + ($queryString ? '?'.$queryString : ''); + }; + + $posts = []; + + foreach ($apiDocument->included as $resource) { + if ($resource->type === 'posts' && isset($resource->relationships->discussion) && isset($resource->attributes->contentHtml)) { + $posts[] = $resource; + } + } + + $hasPrevPage = $page > 1; + $hasNextPage = $page < 1 + intval($apiDocument->data->attributes->commentCount / 20); + + $document->title = $apiDocument->data->attributes->title; + $document->content = $this->view->make('flarum.forum::frontend.content.discussion', compact('apiDocument', 'page', 'hasPrevPage', 'hasNextPage', 'getResource', 'posts', 'url')); + $document->payload['apiDocument'] = $apiDocument; + + $document->canonicalUrl = $url([]); + $document->page = $page; + $document->hasNextPage = $hasNextPage; + + return $document; + } + + /** + * Get the result of an API request to show a discussion. + * + * @throws RouteNotFoundException + */ + protected function getApiDocument(Request $request, string $id, array $params) + { + $params['bySlug'] = true; + $response = $this->api + ->withParentRequest($request) + ->withQueryParams($params) + ->get("/discussions/$id"); + $statusCode = $response->getStatusCode(); + + if ($statusCode === 404) { + throw new RouteNotFoundException; + } + + return json_decode($response->getBody()); + } +} diff --git a/packages/flarum/core/src/Forum/Content/Index.php b/packages/flarum/core/src/Forum/Content/Index.php new file mode 100644 index 0000000..3f02af7 --- /dev/null +++ b/packages/flarum/core/src/Forum/Content/Index.php @@ -0,0 +1,110 @@ +api = $api; + $this->view = $view; + $this->settings = $settings; + $this->url = $url; + $this->translator = $translator; + } + + public function __invoke(Document $document, Request $request) + { + $queryParams = $request->getQueryParams(); + + $sort = Arr::pull($queryParams, 'sort'); + $q = Arr::pull($queryParams, 'q'); + $page = max(1, intval(Arr::pull($queryParams, 'page'))); + $filters = Arr::pull($queryParams, 'filter', []); + + $sortMap = resolve('flarum.forum.discussions.sortmap'); + + $params = [ + 'sort' => $sort && isset($sortMap[$sort]) ? $sortMap[$sort] : '', + 'filter' => $filters, + 'page' => ['offset' => ($page - 1) * 20, 'limit' => 20] + ]; + + if ($q) { + $params['filter']['q'] = $q; + } + + $apiDocument = $this->getApiDocument($request, $params); + $defaultRoute = $this->settings->get('default_route'); + + $document->title = $this->translator->trans('core.forum.index.meta_title_text'); + $document->content = $this->view->make('flarum.forum::frontend.content.index', compact('apiDocument', 'page')); + $document->payload['apiDocument'] = $apiDocument; + + $document->canonicalUrl = $this->url->to('forum')->base().($defaultRoute === '/all' ? '' : $request->getUri()->getPath()); + $document->page = $page; + $document->hasNextPage = isset($apiDocument->links->next); + + return $document; + } + + /** + * Get the result of an API request to list discussions. + * + * @param Request $request + * @param array $params + * @return object + */ + protected function getApiDocument(Request $request, array $params) + { + return json_decode($this->api->withParentRequest($request)->withQueryParams($params)->get('/discussions')->getBody()); + } +} diff --git a/packages/flarum/core/src/Forum/Content/User.php b/packages/flarum/core/src/Forum/Content/User.php new file mode 100644 index 0000000..50a67f7 --- /dev/null +++ b/packages/flarum/core/src/Forum/Content/User.php @@ -0,0 +1,72 @@ +api = $api; + $this->url = $url; + } + + public function __invoke(Document $document, Request $request) + { + $queryParams = $request->getQueryParams(); + $username = Arr::get($queryParams, 'username'); + + $apiDocument = $this->getApiDocument($request, $username); + $user = $apiDocument->data->attributes; + + $document->title = $user->displayName; + $document->canonicalUrl = $this->url->to('forum')->route('user', ['username' => $user->slug]); + $document->payload['apiDocument'] = $apiDocument; + + return $document; + } + + /** + * Get the result of an API request to show a user. + * + * @throws ModelNotFoundException + */ + protected function getApiDocument(Request $request, string $username) + { + $response = $this->api->withParentRequest($request)->withQueryParams(['bySlug' => true])->get("/users/$username"); + $statusCode = $response->getStatusCode(); + + if ($statusCode === 404) { + throw new ModelNotFoundException; + } + + return json_decode($response->getBody()); + } +} diff --git a/packages/flarum/core/src/Forum/Controller/ConfirmEmailController.php b/packages/flarum/core/src/Forum/Controller/ConfirmEmailController.php new file mode 100644 index 0000000..f57572a --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/ConfirmEmailController.php @@ -0,0 +1,70 @@ +bus = $bus; + $this->url = $url; + $this->authenticator = $authenticator; + } + + /** + * @param Request $request + * @return ResponseInterface + */ + public function handle(Request $request): ResponseInterface + { + $token = Arr::get($request->getQueryParams(), 'token'); + + $user = $this->bus->dispatch( + new ConfirmEmail($token) + ); + + $session = $request->getAttribute('session'); + $token = SessionAccessToken::generate($user->id); + $this->authenticator->logIn($session, $token); + + return new RedirectResponse($this->url->to('forum')->base()); + } +} diff --git a/packages/flarum/core/src/Forum/Controller/ConfirmEmailViewController.php b/packages/flarum/core/src/Forum/Controller/ConfirmEmailViewController.php new file mode 100644 index 0000000..865d1fa --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/ConfirmEmailViewController.php @@ -0,0 +1,46 @@ +view = $view; + } + + /** + * @param Request $request + * @return \Illuminate\Contracts\View\View + */ + public function render(Request $request) + { + $token = Arr::get($request->getQueryParams(), 'token'); + + $token = EmailToken::validOrFail($token); + + return $this->view->make('flarum.forum::confirm-email') + ->with('csrfToken', $request->getAttribute('session')->token()); + } +} diff --git a/packages/flarum/core/src/Forum/Controller/GlobalLogOutController.php b/packages/flarum/core/src/Forum/Controller/GlobalLogOutController.php new file mode 100644 index 0000000..0a9b3ed --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/GlobalLogOutController.php @@ -0,0 +1,74 @@ +events = $events; + $this->authenticator = $authenticator; + $this->rememberer = $rememberer; + $this->url = $url; + } + + public function handle(Request $request): ResponseInterface + { + $session = $request->getAttribute('session'); + $actor = RequestUtil::getActor($request); + + $actor->assertRegistered(); + + $this->authenticator->logOut($session); + + $actor->accessTokens()->delete(); + $actor->emailTokens()->delete(); + $actor->passwordTokens()->delete(); + + $this->events->dispatch(new LoggedOut($actor, true)); + + return $this->rememberer->forget(new EmptyResponse()); + } +} diff --git a/packages/flarum/core/src/Forum/Controller/LogInController.php b/packages/flarum/core/src/Forum/Controller/LogInController.php new file mode 100644 index 0000000..86de9ce --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/LogInController.php @@ -0,0 +1,104 @@ +users = $users; + $this->apiClient = $apiClient; + $this->authenticator = $authenticator; + $this->events = $events; + $this->rememberer = $rememberer; + $this->validator = $validator; + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request): ResponseInterface + { + $body = $request->getParsedBody(); + $params = Arr::only($body, ['identification', 'password', 'remember']); + + $this->validator->assertValid($body); + + $response = $this->apiClient->withParentRequest($request)->withBody($params)->post('/token'); + + if ($response->getStatusCode() === 200) { + $data = json_decode($response->getBody()); + + $token = AccessToken::findValid($data->token); + + $session = $request->getAttribute('session'); + $this->authenticator->logIn($session, $token); + + $this->events->dispatch(new LoggedIn($this->users->findOrFail($data->userId), $token)); + + if ($token instanceof RememberAccessToken) { + $response = $this->rememberer->remember($response, $token); + } + } + + return $response; + } +} diff --git a/packages/flarum/core/src/Forum/Controller/LogOutController.php b/packages/flarum/core/src/Forum/Controller/LogOutController.php new file mode 100644 index 0000000..b38bded --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/LogOutController.php @@ -0,0 +1,155 @@ +events = $events; + $this->authenticator = $authenticator; + $this->rememberer = $rememberer; + $this->view = $view; + $this->url = $url; + $this->config = $config; + } + + /** + * @param Request $request + * @return ResponseInterface + * @throws TokenMismatchException + */ + public function handle(Request $request): ResponseInterface + { + $session = $request->getAttribute('session'); + $actor = RequestUtil::getActor($request); + $base = $this->url->to('forum')->base(); + + $returnUrl = Arr::get($request->getQueryParams(), 'return'); + $return = $this->sanitizeReturnUrl((string) $returnUrl, $base); + + // If there is no user logged in, return to the index or the return url if it's set. + if ($actor->isGuest()) { + return new RedirectResponse($return); + } + + // If a valid CSRF token hasn't been provided, show a view which will + // allow the user to press a button to complete the log out process. + $csrfToken = $session->token(); + + if (Arr::get($request->getQueryParams(), 'token') !== $csrfToken) { + $view = $this->view->make('flarum.forum::log-out') + ->with('url', $this->url->to('forum')->route('logout').'?token='.$csrfToken.($returnUrl ? '&return='.urlencode($return) : '')); + + return new HtmlResponse($view->render()); + } + + $accessToken = $session->get('access_token'); + $response = new RedirectResponse($return); + + $this->authenticator->logOut($session); + + $actor->accessTokens()->where('token', $accessToken)->delete(); + + $this->events->dispatch(new LoggedOut($actor, false)); + + return $this->rememberer->forget($response); + } + + protected function sanitizeReturnUrl(string $url, string $base): Uri + { + if (empty($url)) { + return new Uri($base); + } + + try { + $parsedUrl = new Uri($url); + } catch (\InvalidArgumentException $e) { + return new Uri($base); + } + + if (in_array($parsedUrl->getHost(), $this->getAllowedRedirectDomains())) { + return $parsedUrl; + } + + return new Uri($base); + } + + protected function getAllowedRedirectDomains(): array + { + $forumUri = $this->config->url(); + + return array_merge( + [$forumUri->getHost()], + $this->config->offsetGet('redirectDomains') ?? [] + ); + } +} diff --git a/packages/flarum/core/src/Forum/Controller/RegisterController.php b/packages/flarum/core/src/Forum/Controller/RegisterController.php new file mode 100644 index 0000000..e732101 --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/RegisterController.php @@ -0,0 +1,73 @@ +api = $api; + $this->authenticator = $authenticator; + $this->rememberer = $rememberer; + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request): ResponseInterface + { + $params = ['data' => ['attributes' => $request->getParsedBody()]]; + + $response = $this->api->withParentRequest($request)->withBody($params)->post('/users'); + + $body = json_decode($response->getBody()); + + if (isset($body->data)) { + $userId = $body->data->id; + + $token = RememberAccessToken::generate($userId); + + $session = $request->getAttribute('session'); + $this->authenticator->logIn($session, $token); + + $response = $this->rememberer->remember($response, $token); + } + + return $response; + } +} diff --git a/packages/flarum/core/src/Forum/Controller/ResetPasswordController.php b/packages/flarum/core/src/Forum/Controller/ResetPasswordController.php new file mode 100644 index 0000000..c698efb --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/ResetPasswordController.php @@ -0,0 +1,54 @@ +view = $view; + } + + /** + * @param Request $request + * @return \Illuminate\Contracts\View\View + * @throws \Flarum\User\Exception\InvalidConfirmationTokenException + */ + public function render(Request $request) + { + $token = Arr::get($request->getQueryParams(), 'token'); + + $token = PasswordToken::findOrFail($token); + + if ($token->created_at < new DateTime('-1 day')) { + throw new InvalidConfirmationTokenException; + } + + return $this->view->make('flarum.forum::reset-password') + ->with('passwordToken', $token->token) + ->with('csrfToken', $request->getAttribute('session')->token()); + } +} diff --git a/packages/flarum/core/src/Forum/Controller/SavePasswordController.php b/packages/flarum/core/src/Forum/Controller/SavePasswordController.php new file mode 100644 index 0000000..6543db2 --- /dev/null +++ b/packages/flarum/core/src/Forum/Controller/SavePasswordController.php @@ -0,0 +1,107 @@ +url = $url; + $this->authenticator = $authenticator; + $this->validator = $validator; + $this->validatorFactory = $validatorFactory; + $this->events = $events; + } + + /** + * @param Request $request + * @return ResponseInterface + */ + public function handle(Request $request): ResponseInterface + { + $input = $request->getParsedBody(); + + $token = PasswordToken::findOrFail(Arr::get($input, 'passwordToken')); + + $password = Arr::get($input, 'password'); + + try { + // todo: probably shouldn't use the user validator for this, + // passwords should be validated separately + $this->validator->assertValid(compact('password')); + + $validator = $this->validatorFactory->make($input, ['password' => 'required|confirmed']); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + } catch (ValidationException $e) { + $request->getAttribute('session')->put('errors', new MessageBag($e->errors())); + + // @todo: must return a 422 instead, look into renderable exceptions. + return new RedirectResponse($this->url->to('forum')->route('resetPassword', ['token' => $token->token])); + } + + $token->user->changePassword($password); + $token->user->save(); + + $this->dispatchEventsFor($token->user); + + $session = $request->getAttribute('session'); + $accessToken = SessionAccessToken::generate($token->user->id); + $this->authenticator->logIn($session, $accessToken); + + return new RedirectResponse($this->url->to('forum')->base()); + } +} diff --git a/packages/flarum/core/src/Forum/ForumServiceProvider.php b/packages/flarum/core/src/Forum/ForumServiceProvider.php new file mode 100644 index 0000000..57579e4 --- /dev/null +++ b/packages/flarum/core/src/Forum/ForumServiceProvider.php @@ -0,0 +1,233 @@ +container->extend(UrlGenerator::class, function (UrlGenerator $url, Container $container) { + return $url->addCollection('forum', $container->make('flarum.forum.routes')); + }); + + $this->container->singleton('flarum.forum.routes', function (Container $container) { + $routes = new RouteCollection; + $this->populateRoutes($routes, $container); + + return $routes; + }); + + $this->container->afterResolving('flarum.forum.routes', function (RouteCollection $routes, Container $container) { + $this->setDefaultRoute($routes, $container); + }); + + $this->container->singleton('flarum.forum.middleware', function () { + return [ + HttpMiddleware\InjectActorReference::class, + 'flarum.forum.error_handler', + HttpMiddleware\ParseJsonBody::class, + HttpMiddleware\CollectGarbage::class, + HttpMiddleware\StartSession::class, + HttpMiddleware\RememberFromCookie::class, + HttpMiddleware\AuthenticateWithSession::class, + HttpMiddleware\SetLocale::class, + 'flarum.forum.route_resolver', + HttpMiddleware\CheckCsrfToken::class, + HttpMiddleware\ShareErrorsFromSession::class, + HttpMiddleware\FlarumPromotionHeader::class, + HttpMiddleware\ReferrerPolicyHeader::class, + HttpMiddleware\ContentTypeOptionsHeader::class + ]; + }); + + $this->container->bind('flarum.forum.error_handler', function (Container $container) { + return new HttpMiddleware\HandleErrors( + $container->make(Registry::class), + $container['flarum.config']->inDebugMode() ? $container->make(WhoopsFormatter::class) : $container->make(ViewFormatter::class), + $container->tagged(Reporter::class) + ); + }); + + $this->container->bind('flarum.forum.route_resolver', function (Container $container) { + return new HttpMiddleware\ResolveRoute($container->make('flarum.forum.routes')); + }); + + $this->container->singleton('flarum.forum.handler', function (Container $container) { + $pipe = new MiddlewarePipe; + + foreach ($container->make('flarum.forum.middleware') as $middleware) { + $pipe->pipe($container->make($middleware)); + } + + $pipe->pipe(new HttpMiddleware\ExecuteRoute()); + + return $pipe; + }); + + $this->container->bind('flarum.assets.forum', function (Container $container) { + /** @var Assets $assets */ + $assets = $container->make('flarum.assets.factory')('forum'); + + $assets->js(function (SourceCollector $sources) use ($container) { + $sources->addFile(__DIR__.'/../../js/dist/forum.js'); + $sources->addString(function () use ($container) { + return $container->make(Formatter::class)->getJs(); + }); + }); + + $assets->css(function (SourceCollector $sources) use ($container) { + $sources->addFile(__DIR__.'/../../less/forum.less'); + $sources->addString(function () use ($container) { + return $container->make(SettingsRepositoryInterface::class)->get('custom_less', ''); + }); + }); + + $container->make(AddTranslations::class)->forFrontend('forum')->to($assets); + $container->make(AddLocaleAssets::class)->to($assets); + + return $assets; + }); + + $this->container->bind('flarum.frontend.forum', function (Container $container) { + return $container->make('flarum.frontend.factory')('forum'); + }); + + $this->container->singleton('flarum.forum.discussions.sortmap', function () { + return [ + 'latest' => '-lastPostedAt', + 'top' => '-commentCount', + 'newest' => '-createdAt', + 'oldest' => 'createdAt' + ]; + }); + } + + public function boot(Container $container, Dispatcher $events, Factory $view) + { + $this->loadViewsFrom(__DIR__.'/../../views', 'flarum.forum'); + + $view->share([ + 'translator' => $container->make(TranslatorInterface::class), + 'settings' => $container->make(SettingsRepositoryInterface::class) + ]); + + $events->listen( + [Enabled::class, Disabled::class, ClearingCache::class], + function () use ($container) { + $recompile = new RecompileFrontendAssets( + $container->make('flarum.assets.forum'), + $container->make(LocaleManager::class) + ); + $recompile->flush(); + } + ); + + $events->listen( + Saved::class, + function (Saved $event) use ($container) { + $recompile = new RecompileFrontendAssets( + $container->make('flarum.assets.forum'), + $container->make(LocaleManager::class) + ); + $recompile->whenSettingsSaved($event); + + $validator = new ValidateCustomLess( + $container->make('flarum.assets.forum'), + $container->make('flarum.locales'), + $container, + $container->make('flarum.less.config') + ); + $validator->whenSettingsSaved($event); + } + ); + + $events->listen( + Saving::class, + function (Saving $event) use ($container) { + $validator = new ValidateCustomLess( + $container->make('flarum.assets.forum'), + $container->make('flarum.locales'), + $container, + $container->make('flarum.less.config') + ); + $validator->whenSettingsSaving($event); + } + ); + } + + /** + * Populate the forum client routes. + * + * @param RouteCollection $routes + * @param Container $container + */ + protected function populateRoutes(RouteCollection $routes, Container $container) + { + $factory = $container->make(RouteHandlerFactory::class); + + $callback = include __DIR__.'/routes.php'; + $callback($routes, $factory); + } + + /** + * Determine the default route. + * + * @param RouteCollection $routes + * @param Container $container + */ + protected function setDefaultRoute(RouteCollection $routes, Container $container) + { + $factory = $container->make(RouteHandlerFactory::class); + $defaultRoute = $container->make('flarum.settings')->get('default_route'); + + if (isset($routes->getRouteData()[0]['GET'][$defaultRoute]['handler'])) { + $toDefaultController = $routes->getRouteData()[0]['GET'][$defaultRoute]['handler']; + } else { + $toDefaultController = $factory->toForum(Content\Index::class); + } + + $routes->get( + '/', + 'default', + $toDefaultController + ); + } +} diff --git a/packages/flarum/core/src/Forum/LogInValidator.php b/packages/flarum/core/src/Forum/LogInValidator.php new file mode 100644 index 0000000..33035ae --- /dev/null +++ b/packages/flarum/core/src/Forum/LogInValidator.php @@ -0,0 +1,20 @@ +assets = $assets; + $this->locales = $locales; + $this->container = $container; + $this->customLessSettings = $customLessSettings; + } + + public function whenSettingsSaving(Saving $event) + { + if (! isset($event->settings['custom_less']) && ! $this->hasDirtyCustomLessSettings($event)) { + return; + } + + // Restrict what features can be used in custom LESS + if (isset($event->settings['custom_less']) && preg_match('/@import|data-uri\s*\(/i', $event->settings['custom_less'])) { + $translator = $this->container->make(TranslatorInterface::class); + + throw new ValidationException([ + 'custom_less' => $translator->trans('core.admin.appearance.custom_styles_cannot_use_less_features') + ]); + } + + // We haven't saved the settings yet, but we want to trial a full + // recompile of the CSS to see if this custom LESS will break + // anything. In order to do that, we will temporarily override the + // settings repository with the new settings so that the recompile + // is effective. We will also use a dummy filesystem so that nothing + // is actually written yet. + + $settings = $this->container->make(SettingsRepositoryInterface::class); + + $this->container->extend( + SettingsRepositoryInterface::class, + function ($settings) use ($event) { + return new OverrideSettingsRepository($settings, $event->settings); + } + ); + + $assetsDir = $this->assets->getAssetsDir(); + $this->assets->setAssetsDir(new FilesystemAdapter(new Filesystem(new NullAdapter))); + + try { + $this->assets->makeCss()->commit(); + + foreach ($this->locales->getLocales() as $locale => $name) { + $this->assets->makeLocaleCss($locale)->commit(); + } + } catch (Less_Exception_Parser $e) { + throw new ValidationException(['custom_less' => $e->getMessage()]); + } + + $this->assets->setAssetsDir($assetsDir); + $this->container->instance(SettingsRepositoryInterface::class, $settings); + } + + public function whenSettingsSaved(Saved $event) + { + if (! isset($event->settings['custom_less']) && ! $this->hasDirtyCustomLessSettings($event)) { + return; + } + + $this->assets->makeCss()->flush(); + + foreach ($this->locales->getLocales() as $locale => $name) { + $this->assets->makeLocaleCss($locale)->flush(); + } + } + + /** + * @param Saved|Saving $event + * @return bool + */ + protected function hasDirtyCustomLessSettings($event): bool + { + if (empty($this->customLessSettings)) { + return false; + } + + $dirtySettings = array_intersect( + array_keys($event->settings), + array_map(function ($setting) { + return $setting['key']; + }, $this->customLessSettings) + ); + + return ! empty($dirtySettings); + } +} diff --git a/packages/flarum/core/src/Forum/routes.php b/packages/flarum/core/src/Forum/routes.php new file mode 100644 index 0000000..d0264cb --- /dev/null +++ b/packages/flarum/core/src/Forum/routes.php @@ -0,0 +1,93 @@ +get( + '/all', + 'index', + $route->toForum(Content\Index::class) + ); + + $map->get( + '/d/{id:\d+(?:-[^/]*)?}[/{near:[^/]*}]', + 'discussion', + $route->toForum(Content\Discussion::class) + ); + + $map->get( + '/u/{username}[/{filter:[^/]*}]', + 'user', + $route->toForum(Content\User::class) + ); + + $map->get( + '/settings', + 'settings', + $route->toForum(Content\AssertRegistered::class) + ); + + $map->get( + '/notifications', + 'notifications', + $route->toForum(Content\AssertRegistered::class) + ); + + $map->get( + '/logout', + 'logout', + $route->toController(Controller\LogOutController::class) + ); + + $map->post( + '/global-logout', + 'globalLogout', + $route->toController(Controller\GlobalLogOutController::class) + ); + + $map->post( + '/login', + 'login', + $route->toController(Controller\LogInController::class) + ); + + $map->post( + '/register', + 'register', + $route->toController(Controller\RegisterController::class) + ); + + $map->get( + '/confirm/{token}', + 'confirmEmail', + $route->toController(Controller\ConfirmEmailViewController::class), + ); + + $map->post( + '/confirm/{token}', + 'confirmEmail.submit', + $route->toController(Controller\ConfirmEmailController::class), + ); + + $map->get( + '/reset/{token}', + 'resetPassword', + $route->toController(Controller\ResetPasswordController::class) + ); + + $map->post( + '/reset', + 'savePassword', + $route->toController(Controller\SavePasswordController::class) + ); +}; diff --git a/packages/flarum/core/src/Foundation/AbstractServiceProvider.php b/packages/flarum/core/src/Foundation/AbstractServiceProvider.php new file mode 100644 index 0000000..22e32ca --- /dev/null +++ b/packages/flarum/core/src/Foundation/AbstractServiceProvider.php @@ -0,0 +1,43 @@ +app = $container; + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function register() + { + } +} diff --git a/packages/flarum/core/src/Foundation/AbstractValidator.php b/packages/flarum/core/src/Foundation/AbstractValidator.php new file mode 100644 index 0000000..5c0690b --- /dev/null +++ b/packages/flarum/core/src/Foundation/AbstractValidator.php @@ -0,0 +1,135 @@ +configuration[] = $callable; + } + + /** + * @var array + */ + protected $rules = []; + + /** + * @var Factory + */ + protected $validator; + + /** + * @var TranslatorInterface + */ + protected $translator; + + /** + * @param Factory $validator + * @param TranslatorInterface $translator + */ + public function __construct(Factory $validator, TranslatorInterface $translator) + { + $this->validator = $validator; + $this->translator = $translator; + } + + /** + * Throw an exception if a model is not valid. + * + * @param array $attributes + */ + public function assertValid(array $attributes) + { + $validator = $this->makeValidator($attributes); + + if ($validator->fails()) { + throw new ValidationException($validator); + } + } + + /** + * @return array + */ + protected function getRules() + { + return $this->rules; + } + + /** + * @return array + */ + protected function getMessages() + { + return []; + } + + /** + * @return array + */ + protected function getAttributeNames() + { + $cache = resolve(Cache::class); + + if ($cache->get(self::$CORE_VALIDATION_CACHE_KEY) !== null) { + return $cache->get(self::$CORE_VALIDATION_CACHE_KEY); + } + + $extId = $this->getClassExtensionId(); + $attributeNames = []; + + foreach (array_keys($this->rules) as $attribute) { + $key = $extId ? "$extId.validation.attributes.$attribute" : "validation.attributes.$attribute"; + $attributeNames[$attribute] = $this->translator->trans($key); + } + + $cache->forever(self::$CORE_VALIDATION_CACHE_KEY, $attributeNames); + + return $attributeNames; + } + + /** + * Make a new validator instance for this model. + * + * @param array $attributes + * @return \Illuminate\Validation\Validator + */ + protected function makeValidator(array $attributes) + { + $rules = Arr::only($this->getRules(), array_keys($attributes)); + + $validator = $this->validator->make($attributes, $rules, $this->getMessages()); + $validator->setAttributeNames($this->getAttributeNames()); + + foreach ($this->configuration as $callable) { + $callable($this, $validator); + } + + return $validator; + } +} diff --git a/packages/flarum/core/src/Foundation/AppInterface.php b/packages/flarum/core/src/Foundation/AppInterface.php new file mode 100644 index 0000000..3418181 --- /dev/null +++ b/packages/flarum/core/src/Foundation/AppInterface.php @@ -0,0 +1,23 @@ +container = $container; + $this->paths = $paths; + + $this->registerBaseBindings(); + $this->registerBaseServiceProviders(); + $this->registerCoreContainerAliases(); + } + + /** + * @param string $key + * @param mixed $default + * @return mixed + */ + public function config($key, $default = null) + { + $config = $this->container->make('flarum.config'); + + return $config[$key] ?? $default; + } + + /** + * Check if Flarum is in debug mode. + * + * @return bool + */ + public function inDebugMode() + { + return $this->config('debug', true); + } + + /** + * Get the URL to the Flarum installation. + * + * @param string $path + * @return string + */ + public function url($path = null) + { + $config = $this->container->make('flarum.config'); + $url = (string) $config->url(); + + if ($path) { + $url .= '/'.($config["paths.$path"] ?? $path); + } + + return $url; + } + + /** + * Register the basic bindings into the container. + */ + protected function registerBaseBindings() + { + \Illuminate\Container\Container::setInstance($this->container); + + /** + * Needed for the laravel framework code. + * Use container inside flarum instead. + */ + $this->container->instance('app', $this->container); + $this->container->alias('app', \Illuminate\Container\Container::class); + + $this->container->instance('container', $this->container); + $this->container->alias('container', \Illuminate\Container\Container::class); + + $this->container->instance('flarum', $this); + $this->container->alias('flarum', self::class); + + $this->container->instance('flarum.paths', $this->paths); + $this->container->alias('flarum.paths', Paths::class); + } + + /** + * Register all of the base service providers. + */ + protected function registerBaseServiceProviders() + { + $this->register(new EventServiceProvider($this->container)); + } + + /** + * Register a service provider with the application. + * + * @param ServiceProvider|string $provider + * @param array $options + * @param bool $force + * @return ServiceProvider + */ + public function register($provider, $options = [], $force = false) + { + if (($registered = $this->getProvider($provider)) && ! $force) { + return $registered; + } + + // If the given "provider" is a string, we will resolve it, passing in the + // application instance automatically for the developer. This is simply + // a more convenient way of specifying your service provider classes. + if (is_string($provider)) { + $provider = $this->resolveProviderClass($provider); + } + + $provider->register(); + + // Once we have registered the service we will iterate through the options + // and set each of them on the application so they will be available on + // the actual loading of the service objects and for developer usage. + foreach ($options as $key => $value) { + $this[$key] = $value; + } + + $this->markAsRegistered($provider); + + // If the application has already booted, we will call this boot method on + // the provider class so it has an opportunity to do its boot logic and + // will be ready for any usage by the developer's application logics. + if ($this->booted) { + $this->bootProvider($provider); + } + + return $provider; + } + + /** + * Get the registered service provider instance if it exists. + * + * @param ServiceProvider|string $provider + * @return ServiceProvider|null + */ + public function getProvider($provider) + { + $name = is_string($provider) ? $provider : get_class($provider); + + return Arr::first($this->serviceProviders, function ($key, $value) use ($name) { + return $value instanceof $name; + }); + } + + /** + * Resolve a service provider instance from the class name. + * + * @param string $provider + * @return ServiceProvider + */ + public function resolveProviderClass($provider) + { + return new $provider($this->container); + } + + /** + * Mark the given provider as registered. + * + * @param ServiceProvider $provider + * @return void + */ + protected function markAsRegistered($provider) + { + $this->container['events']->dispatch($class = get_class($provider), [$provider]); + + $this->serviceProviders[] = $provider; + + $this->loadedProviders[$class] = true; + } + + /** + * Determine if the application has booted. + * + * @return bool + */ + public function isBooted() + { + return $this->booted; + } + + /** + * Boot the application's service providers. + * + * @return void + */ + public function boot() + { + if ($this->booted) { + return; + } + + // Once the application has booted we will also fire some "booted" callbacks + // for any listeners that need to do work after this initial booting gets + // finished. This is useful when ordering the boot-up processes we run. + $this->fireAppCallbacks($this->bootingCallbacks); + + array_walk($this->serviceProviders, function ($p) { + $this->bootProvider($p); + }); + + $this->booted = true; + + $this->fireAppCallbacks($this->bootedCallbacks); + } + + /** + * Boot the given service provider. + * + * @param ServiceProvider $provider + * @return mixed + */ + protected function bootProvider(ServiceProvider $provider) + { + if (method_exists($provider, 'boot')) { + return $this->container->call([$provider, 'boot']); + } + } + + /** + * Register a new boot listener. + * + * @param mixed $callback + * @return void + */ + public function booting($callback) + { + $this->bootingCallbacks[] = $callback; + } + + /** + * Register a new "booted" listener. + * + * @param mixed $callback + * @return void + */ + public function booted($callback) + { + $this->bootedCallbacks[] = $callback; + + if ($this->isBooted()) { + $this->fireAppCallbacks([$callback]); + } + } + + /** + * Call the booting callbacks for the application. + * + * @param array $callbacks + * @return void + */ + protected function fireAppCallbacks(array $callbacks) + { + foreach ($callbacks as $callback) { + call_user_func($callback, $this); + } + } + + /** + * Register the core class aliases in the container. + */ + public function registerCoreContainerAliases() + { + $aliases = [ + 'app' => [\Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class, \Psr\Container\ContainerInterface::class], + 'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class], + 'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class], + 'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class], + 'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class], + 'db' => [\Illuminate\Database\DatabaseManager::class], + 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], + 'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], + 'files' => [\Illuminate\Filesystem\Filesystem::class], + 'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], + 'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class], + 'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class], + 'hash' => [\Illuminate\Contracts\Hashing\Hasher::class], + 'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class], + 'validator' => [\Illuminate\Validation\Factory::class, \Illuminate\Contracts\Validation\Factory::class], + 'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class], + ]; + + foreach ($aliases as $key => $aliases) { + foreach ((array) $aliases as $alias) { + $this->container->alias($key, $alias); + } + } + } +} diff --git a/packages/flarum/core/src/Foundation/ApplicationInfoProvider.php b/packages/flarum/core/src/Foundation/ApplicationInfoProvider.php new file mode 100644 index 0000000..c15c6de --- /dev/null +++ b/packages/flarum/core/src/Foundation/ApplicationInfoProvider.php @@ -0,0 +1,212 @@ +settings = $settings; + $this->translator = $translator; + $this->schedule = $schedule; + $this->db = $db; + $this->config = $config; + $this->session = $session; + $this->sessionHandler = $sessionHandler; + $this->queue = $queue; + } + + /** + * Identify if any tasks are registered with the scheduler. + * + * @return bool + */ + public function scheduledTasksRegistered(): bool + { + return count($this->schedule->events()) > 0; + } + + /** + * Gets the current status of the scheduler. + * + * @return string + */ + public function getSchedulerStatus(): string + { + $status = $this->settings->get('schedule.last_run'); + + if (! $status) { + return $this->translator->trans('core.admin.dashboard.status.scheduler.never-run'); + } + + // If the schedule has not run in the last 5 minutes, mark it as inactive. + return Carbon::parse($status) > Carbon::now()->subMinutes(5) + ? $this->translator->trans('core.admin.dashboard.status.scheduler.active') + : $this->translator->trans('core.admin.dashboard.status.scheduler.inactive'); + } + + /** + * Identify the queue driver in use. + * + * @return string + */ + public function identifyQueueDriver(): string + { + // Get class name + $queue = get_class($this->queue); + // Drop the namespace + $queue = Str::afterLast($queue, '\\'); + // Lowercase the class name + $queue = strtolower($queue); + // Drop everything like queue SyncQueue, RedisQueue + $queue = str_replace('queue', '', $queue); + + return $queue; + } + + /** + * Identify the version of the database we are connected to. + * + * @return string + */ + public function identifyDatabaseVersion(): string + { + return $this->db->selectOne('select version() as version')->version; + } + + /** + * Reports on the session driver in use based on three scenarios: + * 1. If the configured session driver is valid and in use, it will be returned. + * 2. If the configured session driver is invalid, fallback to the default one and mention it. + * 3. If the actual used driver (i.e `session.handler`) is different from the current one (configured or default), mention it. + */ + public function identifySessionDriver(bool $forWeb = false): string + { + /* + * Get the configured driver and fallback to the default one. + */ + $defaultDriver = $this->session->getDefaultDriver(); + $configuredDriver = Arr::get($this->config, 'session.driver', $defaultDriver); + $driver = $configuredDriver; + + try { + // Try to get the configured driver instance. + // Driver instances are created on demand. + $this->session->driver($configuredDriver); + } catch (InvalidArgumentException $e) { + // An exception is thrown if the configured driver is not a valid driver. + // So we fallback to the default driver. + $driver = $defaultDriver; + } + + /* + * Get actual driver name from its class name. + * And compare that to the current configured driver. + */ + // Get class name + $handlerName = get_class($this->sessionHandler); + // Drop the namespace + $handlerName = Str::afterLast($handlerName, '\\'); + // Lowercase the class name + $handlerName = strtolower($handlerName); + // Drop everything like sessionhandler FileSessionHandler, DatabaseSessionHandler ..etc + $handlerName = str_replace('sessionhandler', '', $handlerName); + + if ($driver !== $handlerName) { + return $forWeb ? $handlerName : "$handlerName (Code override. Configured to $configuredDriver)"; + } + + if ($driver !== $configuredDriver) { + return $forWeb ? $driver : "$driver (Fallback default driver. Configured to invalid driver $configuredDriver)"; + } + + return $driver; + } + + /** + * Identifiy the current PHP version. + * + * @return string + */ + public function identifyPHPVersion(): string + { + return PHP_VERSION; + } +} diff --git a/packages/flarum/core/src/Foundation/Config.php b/packages/flarum/core/src/Foundation/Config.php new file mode 100644 index 0000000..5163c97 --- /dev/null +++ b/packages/flarum/core/src/Foundation/Config.php @@ -0,0 +1,76 @@ +data = $data; + + $this->requireKeys('url'); + } + + public function url(): UriInterface + { + return new Uri(rtrim($this->data['url'], '/')); + } + + public function inDebugMode(): bool + { + return $this->data['debug'] ?? false; + } + + public function inMaintenanceMode(): bool + { + return $this->data['offline'] ?? false; + } + + private function requireKeys(...$keys) + { + foreach ($keys as $key) { + if (! array_key_exists($key, $this->data)) { + throw new InvalidArgumentException( + "Configuration is invalid without a $key key" + ); + } + } + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return Arr::get($this->data, $offset); + } + + public function offsetExists($offset): bool + { + return Arr::has($this->data, $offset); + } + + public function offsetSet($offset, $value): void + { + throw new RuntimeException('The Config is immutable'); + } + + public function offsetUnset($offset): void + { + throw new RuntimeException('The Config is immutable'); + } +} diff --git a/packages/flarum/core/src/Foundation/Console/AssetsPublishCommand.php b/packages/flarum/core/src/Foundation/Console/AssetsPublishCommand.php new file mode 100644 index 0000000..917667e --- /dev/null +++ b/packages/flarum/core/src/Foundation/Console/AssetsPublishCommand.php @@ -0,0 +1,82 @@ +container = $container; + $this->paths = $paths; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('assets:publish') + ->setDescription('Publish core and extension assets.'); + } + + /** + * {@inheritdoc} + */ + protected function fire() + { + $this->info('Publishing core assets...'); + + $target = $this->container->make('filesystem')->disk('flarum-assets'); + $local = new Filesystem(); + + $pathPrefix = $this->paths->vendor.'/components/font-awesome/webfonts'; + $assetFiles = $local->allFiles($pathPrefix); + + foreach ($assetFiles as $fullPath) { + $relPath = substr($fullPath, strlen($pathPrefix)); + $target->put("fonts/$relPath", $local->get($fullPath)); + } + + $this->info('Publishing extension assets...'); + + $extensions = $this->container->make(ExtensionManager::class); + $extensions->getMigrator()->setOutput($this->output); + + foreach ($extensions->getEnabledExtensions() as $name => $extension) { + if ($extension->hasAssets()) { + $this->info('Publishing for extension: '.$name); + $extension->copyAssetsTo($target); + } + } + } +} diff --git a/packages/flarum/core/src/Foundation/Console/CacheClearCommand.php b/packages/flarum/core/src/Foundation/Console/CacheClearCommand.php new file mode 100644 index 0000000..ae7c3d7 --- /dev/null +++ b/packages/flarum/core/src/Foundation/Console/CacheClearCommand.php @@ -0,0 +1,80 @@ +cache = $cache; + $this->events = $events; + $this->paths = $paths; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('cache:clear') + ->setDescription('Remove all temporary and generated files'); + } + + /** + * {@inheritdoc} + */ + protected function fire() + { + $this->info('Clearing the cache...'); + + $succeeded = $this->cache->flush(); + + if (! $succeeded) { + $this->error('Could not clear contents of `storage/cache`. Please adjust file permissions and try again. This can frequently be fixed by clearing cache via the `Tools` dropdown on the Administration Dashboard page.'); + + return 1; + } + + $storagePath = $this->paths->storage; + array_map('unlink', glob($storagePath.'/formatter/*')); + array_map('unlink', glob($storagePath.'/locale/*')); + array_map('unlink', glob($storagePath.'/views/*')); + + $this->events->dispatch(new ClearingCache); + } +} diff --git a/packages/flarum/core/src/Foundation/Console/InfoCommand.php b/packages/flarum/core/src/Foundation/Console/InfoCommand.php new file mode 100644 index 0000000..34001b2 --- /dev/null +++ b/packages/flarum/core/src/Foundation/Console/InfoCommand.php @@ -0,0 +1,157 @@ +extensions = $extensions; + $this->config = $config; + $this->settings = $settings; + $this->db = $db; + $this->appInfo = $appInfo; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('info') + ->setDescription("Gather information about Flarum's core and installed extensions"); + } + + /** + * {@inheritdoc} + */ + protected function fire() + { + $coreVersion = $this->findPackageVersion(__DIR__.'/../../../', Application::VERSION); + $this->output->writeln("Flarum core: $coreVersion"); + + $this->output->writeln('PHP version: '.$this->appInfo->identifyPHPVersion()); + $this->output->writeln('MySQL version: '.$this->appInfo->identifyDatabaseVersion()); + + $phpExtensions = implode(', ', get_loaded_extensions()); + $this->output->writeln("Loaded extensions: $phpExtensions"); + + $this->getExtensionTable()->render(); + + $this->output->writeln('Base URL: '.$this->config->url()); + $this->output->writeln('Installation path: '.getcwd()); + $this->output->writeln('Queue driver: '.$this->appInfo->identifyQueueDriver()); + $this->output->writeln('Session driver: '.$this->appInfo->identifySessionDriver()); + + if ($this->appInfo->scheduledTasksRegistered()) { + $this->output->writeln('Scheduler status: '.$this->appInfo->getSchedulerStatus()); + } + + $this->output->writeln('Mail driver: '.$this->settings->get('mail_driver', 'unknown')); + $this->output->writeln('Debug mode: '.($this->config->inDebugMode() ? 'ON' : 'off')); + + if ($this->config->inDebugMode()) { + $this->output->writeln(''); + $this->error( + "Don't forget to turn off debug mode! It should never be turned on in a production system." + ); + } + } + + private function getExtensionTable() + { + $table = (new Table($this->output)) + ->setHeaders([ + ['Flarum Extensions'], + ['ID', 'Version', 'Commit'] + ])->setStyle( + (new TableStyle)->setCellHeaderFormat('%s') + ); + + foreach ($this->extensions->getEnabledExtensions() as $extension) { + $table->addRow([ + $extension->getId(), + $extension->getVersion(), + $this->findPackageVersion($extension->getPath()) + ]); + } + + return $table; + } + + /** + * Try to detect a package's exact version. + * + * If the package seems to be a Git version, we extract the currently + * checked out commit using the command line. + */ + private function findPackageVersion(string $path, string $fallback = null): ?string + { + if (file_exists("$path/.git")) { + $cwd = getcwd(); + chdir($path); + + $output = []; + $status = null; + exec('git rev-parse HEAD 2>&1', $output, $status); + + chdir($cwd); + + if ($status == 0) { + return isset($fallback) ? "$fallback ($output[0])" : $output[0]; + } + } + + return $fallback; + } +} diff --git a/packages/flarum/core/src/Foundation/Console/ScheduleRunCommand.php b/packages/flarum/core/src/Foundation/Console/ScheduleRunCommand.php new file mode 100644 index 0000000..45b2779 --- /dev/null +++ b/packages/flarum/core/src/Foundation/Console/ScheduleRunCommand.php @@ -0,0 +1,43 @@ +settings = $settings; + } + + /** + * {@inheritdoc} + */ + public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler) + { + parent::handle($schedule, $dispatcher, $handler); + + $this->settings->set('schedule.last_run', $this->startedAt); + } +} diff --git a/packages/flarum/core/src/Foundation/ContainerUtil.php b/packages/flarum/core/src/Foundation/ContainerUtil.php new file mode 100644 index 0000000..acc571b --- /dev/null +++ b/packages/flarum/core/src/Foundation/ContainerUtil.php @@ -0,0 +1,36 @@ +make($callback); + + return $callback(...$args); + }; + } + + return $callback; + } +} diff --git a/packages/flarum/core/src/Foundation/DispatchEventsTrait.php b/packages/flarum/core/src/Foundation/DispatchEventsTrait.php new file mode 100644 index 0000000..4445cab --- /dev/null +++ b/packages/flarum/core/src/Foundation/DispatchEventsTrait.php @@ -0,0 +1,36 @@ +releaseEvents() as $event) { + $event->actor = $actor; + + $this->events->dispatch($event); + } + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandler.php b/packages/flarum/core/src/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandler.php new file mode 100644 index 0000000..a6c1c7f --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/ExceptionHandler/IlluminateValidationExceptionHandler.php @@ -0,0 +1,37 @@ +withDetails($this->errorDetails($e)); + } + + protected function errorDetails(ValidationException $e): array + { + $errors = $e->errors(); + + return array_map(function ($field, $messages) { + return [ + 'detail' => implode("\n", $messages), + 'source' => ['pointer' => "/data/attributes/$field"] + ]; + }, array_keys($errors), $errors); + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandler.php b/packages/flarum/core/src/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandler.php new file mode 100644 index 0000000..ceec9bd --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/ExceptionHandler/ValidationExceptionHandler.php @@ -0,0 +1,38 @@ +withDetails(array_merge( + $this->buildDetails($e->getAttributes(), '/data/attributes'), + $this->buildDetails($e->getRelationships(), '/data/relationships') + )); + } + + private function buildDetails(array $messages, $pointer): array + { + return array_map(function ($path, $detail) use ($pointer) { + return [ + 'detail' => $detail, + 'source' => ['pointer' => $pointer.'/'.$path] + ]; + }, array_keys($messages), $messages); + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/HandledError.php b/packages/flarum/core/src/Foundation/ErrorHandling/HandledError.php new file mode 100644 index 0000000..026d1b1 --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/HandledError.php @@ -0,0 +1,77 @@ +error = $error; + $this->type = $type; + $this->statusCode = $statusCode; + } + + public function withDetails(array $details): self + { + $this->details = $details; + + return $this; + } + + public function getException(): Throwable + { + return $this->error; + } + + public function getType(): string + { + return $this->type; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function shouldBeReported(): bool + { + return $this->type === 'unknown'; + } + + public function getDetails(): array + { + return $this->details; + } + + public function hasDetails(): bool + { + return ! empty($this->details); + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/HttpFormatter.php b/packages/flarum/core/src/Foundation/ErrorHandling/HttpFormatter.php new file mode 100644 index 0000000..812c446 --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/HttpFormatter.php @@ -0,0 +1,29 @@ +includeTrace = $includeTrace; + } + + public function format(HandledError $error, Request $request): Response + { + $document = new Document; + + if ($error->hasDetails()) { + $document->setErrors($this->withDetails($error)); + } else { + $document->setErrors($this->default($error)); + } + + return new JsonApiResponse($document, $error->getStatusCode()); + } + + private function default(HandledError $error): array + { + $default = [ + 'status' => (string) $error->getStatusCode(), + 'code' => $error->getType(), + ]; + + if ($this->includeTrace) { + $default['detail'] = (string) $error->getException(); + } + + return [$default]; + } + + private function withDetails(HandledError $error): array + { + $data = [ + 'status' => (string) $error->getStatusCode(), + 'code' => $error->getType(), + ]; + + return array_map( + function ($row) use ($data) { + return array_merge($data, $row); + }, + $error->getDetails() + ); + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/LogReporter.php b/packages/flarum/core/src/Foundation/ErrorHandling/LogReporter.php new file mode 100644 index 0000000..d4cb0c9 --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/LogReporter.php @@ -0,0 +1,34 @@ +logger = $logger; + } + + public function report(Throwable $error) + { + $this->logger->error($error); + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/Registry.php b/packages/flarum/core/src/Foundation/ErrorHandling/Registry.php new file mode 100644 index 0000000..ae0aada --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/Registry.php @@ -0,0 +1,90 @@ +statusMap = $statusMap; + $this->classMap = $classMap; + $this->handlerMap = $handlerMap; + } + + /** + * Map exceptions to handled errors. + * + * This can map internal ({@see \Flarum\Foundation\KnownError}) as well as + * external exceptions (any classes inheriting from \Throwable) to instances + * of {@see \Flarum\Foundation\ErrorHandling\HandledError}. + * + * Even for unknown exceptions, a generic fallback will always be returned. + * + * @param Throwable $error + * @return HandledError + */ + public function handle(Throwable $error): HandledError + { + return $this->handleKnownTypes($error) + ?? $this->handleCustomTypes($error) + ?? HandledError::unknown($error); + } + + private function handleKnownTypes(Throwable $error): ?HandledError + { + $errorType = null; + + if ($error instanceof KnownError) { + $errorType = $error->getType(); + } else { + $errorClass = get_class($error); + if (isset($this->classMap[$errorClass])) { + $errorType = $this->classMap[$errorClass]; + } + } + + if ($errorType) { + return new HandledError( + $error, + $errorType, + $this->statusMap[$errorType] ?? 500 + ); + } + + return null; + } + + private function handleCustomTypes(Throwable $error): ?HandledError + { + $errorClass = get_class($error); + + if (isset($this->handlerMap[$errorClass])) { + $handler = new $this->handlerMap[$errorClass]; + + return $handler->handle($error); + } + + return null; + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/Reporter.php b/packages/flarum/core/src/Foundation/ErrorHandling/Reporter.php new file mode 100644 index 0000000..ca3277c --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/Reporter.php @@ -0,0 +1,23 @@ +view = $view; + $this->translator = $translator; + $this->settings = $settings; + } + + public function format(HandledError $error, Request $request): Response + { + $view = $this->view->make($this->determineView($error)) + ->with('error', $error->getException()) + ->with('message', $this->getMessage($error)); + + return new HtmlResponse($view->render(), $error->getStatusCode()); + } + + const ERRORS_WITH_VIEWS = ['csrf_token_mismatch', 'not_found']; + + private function determineView(HandledError $error): string + { + $type = $error->getType(); + + if (in_array($type, self::ERRORS_WITH_VIEWS)) { + return "flarum.forum::error.$type"; + } else { + return 'flarum.forum::error.default'; + } + } + + private function getMessage(HandledError $error) + { + return $this->getTranslationIfExists($error->getType()) + ?? $this->getTranslationIfExists('unknown') + ?? 'An error occurred while trying to load this page.'; + } + + private function getTranslationIfExists(string $errorType) + { + $key = "core.views.error.$errorType"; + $translation = $this->translator->trans($key, ['forum' => $this->settings->get('forum_title')]); + + return $translation === $key ? null : $translation; + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorHandling/WhoopsFormatter.php b/packages/flarum/core/src/Foundation/ErrorHandling/WhoopsFormatter.php new file mode 100644 index 0000000..8a92010 --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorHandling/WhoopsFormatter.php @@ -0,0 +1,32 @@ +getException(), $request) + ->withStatus($error->getStatusCode()); + } +} diff --git a/packages/flarum/core/src/Foundation/ErrorServiceProvider.php b/packages/flarum/core/src/Foundation/ErrorServiceProvider.php new file mode 100644 index 0000000..7e9d63d --- /dev/null +++ b/packages/flarum/core/src/Foundation/ErrorServiceProvider.php @@ -0,0 +1,77 @@ +container->singleton('flarum.error.statuses', function () { + return [ + // 400 Bad Request + 'csrf_token_mismatch' => 400, + 'invalid_parameter' => 400, + + // 401 Unauthorized + 'invalid_access_token' => 401, + 'not_authenticated' => 401, + + // 403 Forbidden + 'invalid_confirmation_token' => 403, + 'permission_denied' => 403, + + // 404 Not Found + 'not_found' => 404, + + // 405 Method Not Allowed + 'method_not_allowed' => 405, + + // 409 Conflict + 'io_error' => 409, + + // 429 Too Many Requests + 'too_many_requests' => 429, + ]; + }); + + $this->container->singleton('flarum.error.classes', function () { + return [ + InvalidParameterException::class => 'invalid_parameter', + ModelNotFoundException::class => 'not_found', + ]; + }); + + $this->container->singleton('flarum.error.handlers', function () { + return [ + IlluminateValidationException::class => Handling\ExceptionHandler\IlluminateValidationExceptionHandler::class, + ValidationException::class => Handling\ExceptionHandler\ValidationExceptionHandler::class, + ExtensionException\CircularDependenciesException::class => ExtensionException\CircularDependenciesExceptionHandler::class, + ExtensionException\DependentExtensionsException::class => ExtensionException\DependentExtensionsExceptionHandler::class, + ExtensionException\MissingDependenciesException::class => ExtensionException\MissingDependenciesExceptionHandler::class, + ]; + }); + + $this->container->singleton(Handling\Registry::class, function () { + return new Handling\Registry( + $this->container->make('flarum.error.statuses'), + $this->container->make('flarum.error.classes'), + $this->container->make('flarum.error.handlers') + ); + }); + + $this->container->tag(Handling\LogReporter::class, Handling\Reporter::class); + } +} diff --git a/packages/flarum/core/src/Foundation/Event/ClearingCache.php b/packages/flarum/core/src/Foundation/Event/ClearingCache.php new file mode 100644 index 0000000..aaf0506 --- /dev/null +++ b/packages/flarum/core/src/Foundation/Event/ClearingCache.php @@ -0,0 +1,14 @@ +pendingEvents[] = $event; + } + + /** + * Return and reset all pending events. + * + * @return array + */ + public function releaseEvents() + { + $events = $this->pendingEvents; + + $this->pendingEvents = []; + + return $events; + } +} diff --git a/packages/flarum/core/src/Foundation/ExtensionIdTrait.php b/packages/flarum/core/src/Foundation/ExtensionIdTrait.php new file mode 100644 index 0000000..7e2bde4 --- /dev/null +++ b/packages/flarum/core/src/Foundation/ExtensionIdTrait.php @@ -0,0 +1,31 @@ +getExtensions() + ->mapWithKeys(function (Extension $extension) { + return [$extension->getId() => $extension->getNamespace()]; + }) + ->filter(function ($namespace) { + return $namespace && str_starts_with(static::class, $namespace); + }) + ->keys() + ->first(); + } +} diff --git a/packages/flarum/core/src/Foundation/IOException.php b/packages/flarum/core/src/Foundation/IOException.php new file mode 100644 index 0000000..19417b8 --- /dev/null +++ b/packages/flarum/core/src/Foundation/IOException.php @@ -0,0 +1,20 @@ +container = $container; + $this->config = $config; + } + + public function getContainer() + { + return $this->container; + } + + /** + * @return \Psr\Http\Server\RequestHandlerInterface + */ + public function getRequestHandler() + { + if ($this->config->inMaintenanceMode()) { + return $this->container->make('flarum.maintenance.handler'); + } elseif ($this->needsUpdate()) { + return $this->getUpdaterHandler(); + } + + $pipe = new MiddlewarePipe; + + $pipe->pipe(new HttpMiddleware\ProcessIp()); + $pipe->pipe(new BasePath($this->basePath())); + $pipe->pipe(new OriginalMessages); + $pipe->pipe( + new BasePathRouter([ + $this->subPath('api') => 'flarum.api.handler', + $this->subPath('admin') => 'flarum.admin.handler', + '/' => 'flarum.forum.handler', + ]) + ); + $pipe->pipe(new RequestHandler($this->container)); + + return $pipe; + } + + protected function needsUpdate(): bool + { + $settings = $this->container->make(SettingsRepositoryInterface::class); + $version = $settings->get('version'); + + return $version !== Application::VERSION; + } + + /** + * @return \Psr\Http\Server\RequestHandlerInterface + */ + protected function getUpdaterHandler() + { + $pipe = new MiddlewarePipe; + $pipe->pipe(new BasePath($this->basePath())); + $pipe->pipe( + new HttpMiddleware\ResolveRoute($this->container->make('flarum.update.routes')) + ); + $pipe->pipe(new HttpMiddleware\ExecuteRoute()); + + return $pipe; + } + + protected function basePath(): string + { + return $this->config->url()->getPath() ?: '/'; + } + + protected function subPath($pathName): string + { + return '/'.($this->config['paths'][$pathName] ?? $pathName); + } + + /** + * @return \Symfony\Component\Console\Command\Command[] + */ + public function getConsoleCommands() + { + return array_map(function ($command) { + $command = $this->container->make($command); + + if ($command instanceof Command) { + $command->setLaravel($this->container); + } + + return $command; + }, $this->container->make('flarum.console.commands')); + } +} diff --git a/packages/flarum/core/src/Foundation/InstalledSite.php b/packages/flarum/core/src/Foundation/InstalledSite.php new file mode 100644 index 0000000..6ccc248 --- /dev/null +++ b/packages/flarum/core/src/Foundation/InstalledSite.php @@ -0,0 +1,200 @@ +paths = $paths; + $this->config = $config; + } + + /** + * Create and boot a Flarum application instance. + * + * @return InstalledApp + */ + public function bootApp(): AppInterface + { + return new InstalledApp( + $this->bootLaravel(), + $this->config + ); + } + + /** + * @param \Flarum\Extend\ExtenderInterface[] $extenders + * @return InstalledSite + */ + public function extendWith(array $extenders): self + { + $this->extenders = $extenders; + + return $this; + } + + protected function bootLaravel(): Container + { + $container = new \Illuminate\Container\Container; + $laravel = new Application($container, $this->paths); + + $container->instance('env', 'production'); + $container->instance('flarum.config', $this->config); + $container->alias('flarum.config', Config::class); + $container->instance('flarum.debug', $this->config->inDebugMode()); + $container->instance('config', $config = $this->getIlluminateConfig()); + $container->instance('flarum.maintenance.handler', new MaintenanceModeHandler); + + $this->registerLogger($container); + $this->registerCache($container); + + $laravel->register(AdminServiceProvider::class); + $laravel->register(ApiServiceProvider::class); + $laravel->register(BusServiceProvider::class); + $laravel->register(ConsoleServiceProvider::class); + $laravel->register(DatabaseServiceProvider::class); + $laravel->register(DiscussionServiceProvider::class); + $laravel->register(ExtensionServiceProvider::class); + $laravel->register(ErrorServiceProvider::class); + $laravel->register(FilesystemServiceProvider::class); + $laravel->register(FilterServiceProvider::class); + $laravel->register(FormatterServiceProvider::class); + $laravel->register(ForumServiceProvider::class); + $laravel->register(FrontendServiceProvider::class); + $laravel->register(GroupServiceProvider::class); + $laravel->register(HashServiceProvider::class); + $laravel->register(HttpServiceProvider::class); + $laravel->register(LocaleServiceProvider::class); + $laravel->register(MailServiceProvider::class); + $laravel->register(NotificationServiceProvider::class); + $laravel->register(PostServiceProvider::class); + $laravel->register(QueueServiceProvider::class); + $laravel->register(SearchServiceProvider::class); + $laravel->register(SessionServiceProvider::class); + $laravel->register(SettingsServiceProvider::class); + $laravel->register(UpdateServiceProvider::class); + $laravel->register(UserServiceProvider::class); + $laravel->register(ValidationServiceProvider::class); + $laravel->register(ViewServiceProvider::class); + + $laravel->booting(function () use ($container) { + // Run all local-site extenders before booting service providers + // (but after those from "real" extensions, which have been set up + // in a service provider above). + foreach ($this->extenders as $extension) { + $extension->extend($container); + } + }); + + $laravel->boot(); + + return $container; + } + + /** + * @return ConfigRepository + */ + protected function getIlluminateConfig() + { + return new ConfigRepository([ + 'app' => [ + 'timezone' => 'UTC' + ], + 'view' => [ + 'paths' => [], + 'compiled' => $this->paths->storage.'/views', + ], + 'session' => [ + 'lifetime' => 120, + 'files' => $this->paths->storage.'/sessions', + 'cookie' => 'session' + ] + ]); + } + + protected function registerLogger(Container $container) + { + $logPath = $this->paths->storage.'/logs/flarum.log'; + $logLevel = $this->config->inDebugMode() ? Logger::DEBUG : Logger::INFO; + $handler = new RotatingFileHandler($logPath, 0, $logLevel); + $handler->setFormatter(new LineFormatter(null, null, true, true)); + + $container->instance('log', new Logger('flarum', [$handler])); + $container->alias('log', LoggerInterface::class); + } + + protected function registerCache(Container $container) + { + $container->singleton('cache.store', function ($container) { + return new CacheRepository($container->make('cache.filestore')); + }); + $container->alias('cache.store', Repository::class); + + $container->singleton('cache.filestore', function () { + return new FileStore(new Filesystem, $this->paths->storage.'/cache'); + }); + $container->alias('cache.filestore', Store::class); + } +} diff --git a/packages/flarum/core/src/Foundation/KnownError.php b/packages/flarum/core/src/Foundation/KnownError.php new file mode 100644 index 0000000..12c89b8 --- /dev/null +++ b/packages/flarum/core/src/Foundation/KnownError.php @@ -0,0 +1,39 @@ +isApiRequest($request)) { + return $this->apiResponse(); + } + + // By default, return a simple text message. + return new HtmlResponse(self::MESSAGE, 503); + } + + private function isApiRequest(ServerRequestInterface $request): bool + { + return Str::contains( + $request->getHeaderLine('Accept'), + 'application/vnd.api+json' + ); + } + + private function apiResponse(): ResponseInterface + { + return new JsonResponse( + (new Document)->setErrors([ + 'status' => '503', + 'title' => self::MESSAGE + ]), + 503, + ['Content-Type' => 'application/vnd.api+json'] + ); + } +} diff --git a/packages/flarum/core/src/Foundation/Paths.php b/packages/flarum/core/src/Foundation/Paths.php new file mode 100644 index 0000000..a873123 --- /dev/null +++ b/packages/flarum/core/src/Foundation/Paths.php @@ -0,0 +1,44 @@ +paths = array_map(function ($path) { + return rtrim($path, '\/'); + }, $paths); + + // Assume a standard Composer directory structure unless specified + $this->paths['vendor'] = $this->vendor ?? $this->base.'/vendor'; + } + + public function __get($name): ?string + { + return $this->paths[$name] ?? null; + } +} diff --git a/packages/flarum/core/src/Foundation/Site.php b/packages/flarum/core/src/Foundation/Site.php new file mode 100644 index 0000000..9732142 --- /dev/null +++ b/packages/flarum/core/src/Foundation/Site.php @@ -0,0 +1,73 @@ +base)) { + // Instantiate site instance for new installations, + // fallback to localhost for validation of Config for instance in CLI. + return new UninstalledSite( + $paths, + Arr::get($_SERVER, 'REQUEST_URI', 'http://localhost') + ); + } + + return ( + new InstalledSite($paths, static::loadConfig($paths->base)) + )->extendWith(static::loadExtenders($paths->base)); + } + + protected static function hasConfigFile($basePath) + { + return file_exists("$basePath/config.php"); + } + + protected static function loadConfig($basePath): Config + { + $config = include "$basePath/config.php"; + + if (! is_array($config)) { + throw new RuntimeException('config.php should return an array'); + } + + return new Config($config); + } + + protected static function loadExtenders($basePath): array + { + $extenderFile = "$basePath/extend.php"; + + if (! file_exists($extenderFile)) { + return []; + } + + $extenders = require $extenderFile; + + if (! is_array($extenders)) { + return []; + } + + return Arr::flatten($extenders); + } +} diff --git a/packages/flarum/core/src/Foundation/SiteInterface.php b/packages/flarum/core/src/Foundation/SiteInterface.php new file mode 100644 index 0000000..5c11719 --- /dev/null +++ b/packages/flarum/core/src/Foundation/SiteInterface.php @@ -0,0 +1,20 @@ +paths = $paths; + $this->baseUrl = $baseUrl; + } + + /** + * Create and boot a Flarum application instance. + * + * @return AppInterface + */ + public function bootApp(): AppInterface + { + return new Installer( + $this->bootLaravel() + ); + } + + protected function bootLaravel(): Container + { + $container = new \Illuminate\Container\Container; + $laravel = new Application($container, $this->paths); + + $container->instance('env', 'production'); + $container->instance('flarum.config', new Config(['url' => $this->baseUrl])); + $container->alias('flarum.config', Config::class); + $container->instance('flarum.debug', true); + $container->instance('config', $config = $this->getIlluminateConfig()); + + $this->registerLogger($container); + + $laravel->register(ErrorServiceProvider::class); + $laravel->register(LocaleServiceProvider::class); + $laravel->register(FilesystemServiceProvider::class); + $laravel->register(SessionServiceProvider::class); + $laravel->register(ValidationServiceProvider::class); + + $laravel->register(InstallServiceProvider::class); + + $container->singleton( + SettingsRepositoryInterface::class, + UninstalledSettingsRepository::class + ); + + $container->singleton('view', function ($container) { + $engines = new EngineResolver(); + $engines->register('php', function () use ($container) { + return $container->make(PhpEngine::class); + }); + $finder = new FileViewFinder($container->make('files'), []); + $dispatcher = $container->make(Dispatcher::class); + + return new \Illuminate\View\Factory( + $engines, + $finder, + $dispatcher + ); + }); + + $laravel->boot(); + + return $container; + } + + /** + * @return ConfigRepository + */ + protected function getIlluminateConfig() + { + return new ConfigRepository([ + 'session' => [ + 'lifetime' => 120, + 'files' => $this->paths->storage.'/sessions', + 'cookie' => 'session' + ], + 'view' => [ + 'paths' => [], + ], + ]); + } + + protected function registerLogger(Container $container) + { + $logPath = $this->paths->storage.'/logs/flarum-installer.log'; + $handler = new StreamHandler($logPath, Logger::DEBUG); + $handler->setFormatter(new LineFormatter(null, null, true, true)); + + $container->instance('log', new Logger('Flarum Installer', [$handler])); + $container->alias('log', LoggerInterface::class); + } +} diff --git a/packages/flarum/core/src/Foundation/ValidationException.php b/packages/flarum/core/src/Foundation/ValidationException.php new file mode 100644 index 0000000..0f74c43 --- /dev/null +++ b/packages/flarum/core/src/Foundation/ValidationException.php @@ -0,0 +1,38 @@ +attributes = $attributes; + $this->relationships = $relationships; + + $messages = [implode("\n", $attributes), implode("\n", $relationships)]; + + parent::__construct(implode("\n", $messages)); + } + + public function getAttributes() + { + return $this->attributes; + } + + public function getRelationships() + { + return $this->relationships; + } +} diff --git a/packages/flarum/core/src/Frontend/AddLocaleAssets.php b/packages/flarum/core/src/Frontend/AddLocaleAssets.php new file mode 100644 index 0000000..8dc089d --- /dev/null +++ b/packages/flarum/core/src/Frontend/AddLocaleAssets.php @@ -0,0 +1,47 @@ +locales = $locales; + } + + public function to(Assets $assets) + { + $assets->localeJs(function (SourceCollector $sources, string $locale) { + foreach ($this->locales->getJsFiles($locale) as $file) { + $sources->addFile($file); + } + }); + + $assets->localeCss(function (SourceCollector $sources, string $locale) { + foreach ($this->locales->getCssFiles($locale) as $file) { + $sources->addFile($file); + } + }); + } +} diff --git a/packages/flarum/core/src/Frontend/AddTranslations.php b/packages/flarum/core/src/Frontend/AddTranslations.php new file mode 100644 index 0000000..139724e --- /dev/null +++ b/packages/flarum/core/src/Frontend/AddTranslations.php @@ -0,0 +1,71 @@ +locales = $locales; + $this->filter = $filter; + } + + public function forFrontend(string $name) + { + $this->filter = function (string $id) use ($name) { + return preg_match('/^.+(?:\.|::)(?:'.$name.'|lib)\./', $id); + }; + + return $this; + } + + public function to(Assets $assets) + { + $assets->localeJs(function (SourceCollector $sources, string $locale) { + $sources->addString(function () use ($locale) { + $translations = $this->getTranslations($locale); + + return 'flarum.core.app.translator.addTranslations('.json_encode($translations).')'; + }); + }); + } + + private function getTranslations(string $locale) + { + $catalogue = $this->locales->getTranslator()->getCatalogue($locale); + $translations = $catalogue->all('messages'); + + while ($catalogue = $catalogue->getFallbackCatalogue()) { + $translations = array_replace($catalogue->all('messages'), $translations); + } + + return Arr::only( + $translations, + array_filter(array_keys($translations), $this->filter) + ); + } +} diff --git a/packages/flarum/core/src/Frontend/Assets.php b/packages/flarum/core/src/Frontend/Assets.php new file mode 100644 index 0000000..6750b7a --- /dev/null +++ b/packages/flarum/core/src/Frontend/Assets.php @@ -0,0 +1,242 @@ + [], + 'css' => [], + 'localeJs' => [], + 'localeCss' => [] + ]; + + /** + * @var string + */ + protected $name; + + /** + * @var Cloud + */ + protected $assetsDir; + + /** + * @var string + */ + protected $cacheDir; + + /** + * @var array + */ + protected $lessImportDirs; + + /** + * @var array + */ + protected $lessImportOverrides = []; + + /** + * @var array + */ + protected $fileSourceOverrides = []; + + /** + * @var array + */ + protected $customFunctions = []; + + public function __construct(string $name, Cloud $assetsDir, string $cacheDir = null, array $lessImportDirs = null, array $customFunctions = []) + { + $this->name = $name; + $this->assetsDir = $assetsDir; + $this->cacheDir = $cacheDir; + $this->lessImportDirs = $lessImportDirs; + $this->customFunctions = $customFunctions; + } + + public function js($sources) + { + $this->addSources('js', $sources); + + return $this; + } + + public function css($callback) + { + $this->addSources('css', $callback); + + return $this; + } + + public function localeJs($callback) + { + $this->addSources('localeJs', $callback); + + return $this; + } + + public function localeCss($callback) + { + $this->addSources('localeCss', $callback); + + return $this; + } + + private function addSources($type, $callback) + { + $this->sources[$type][] = $callback; + } + + private function populate(CompilerInterface $compiler, string $type, string $locale = null) + { + $compiler->addSources(function (SourceCollector $sources) use ($type, $locale) { + foreach ($this->sources[$type] as $callback) { + $callback($sources, $locale); + } + }); + } + + public function makeJs(): JsCompiler + { + $compiler = $this->makeJsCompiler($this->name.'.js'); + + $this->populate($compiler, 'js'); + + return $compiler; + } + + public function makeCss(): LessCompiler + { + $compiler = $this->makeLessCompiler($this->name.'.css'); + + $this->populate($compiler, 'css'); + + return $compiler; + } + + public function makeLocaleJs(string $locale): JsCompiler + { + $compiler = $this->makeJsCompiler($this->name.'-'.$locale.'.js'); + + $this->populate($compiler, 'localeJs', $locale); + + return $compiler; + } + + public function makeLocaleCss(string $locale): LessCompiler + { + $compiler = $this->makeLessCompiler($this->name.'-'.$locale.'.css'); + + $this->populate($compiler, 'localeCss', $locale); + + return $compiler; + } + + protected function makeJsCompiler(string $filename) + { + return resolve(JsCompiler::class, [ + 'assetsDir' => $this->assetsDir, + 'filename' => $filename + ]); + } + + protected function makeLessCompiler(string $filename): LessCompiler + { + $compiler = resolve(LessCompiler::class, [ + 'assetsDir' => $this->assetsDir, + 'filename' => $filename + ]); + + if ($this->cacheDir) { + $compiler->setCacheDir($this->cacheDir.'/less'); + } + + if ($this->lessImportDirs) { + $compiler->setImportDirs($this->lessImportDirs); + } + + if ($this->lessImportOverrides) { + $compiler->setLessImportOverrides($this->lessImportOverrides); + } + + if ($this->fileSourceOverrides) { + $compiler->setFileSourceOverrides($this->fileSourceOverrides); + } + + $compiler->setCustomFunctions($this->customFunctions); + + return $compiler; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } + + public function getAssetsDir(): Cloud + { + return $this->assetsDir; + } + + public function setAssetsDir(Cloud $assetsDir) + { + $this->assetsDir = $assetsDir; + } + + public function getCacheDir(): ?string + { + return $this->cacheDir; + } + + public function setCacheDir(?string $cacheDir) + { + $this->cacheDir = $cacheDir; + } + + public function getLessImportDirs(): array + { + return $this->lessImportDirs; + } + + public function setLessImportDirs(array $lessImportDirs) + { + $this->lessImportDirs = $lessImportDirs; + } + + public function addLessImportOverrides(array $lessImportOverrides) + { + $this->lessImportOverrides = array_merge($this->lessImportOverrides, $lessImportOverrides); + } + + public function addFileSourceOverrides(array $fileSourceOverrides) + { + $this->fileSourceOverrides = array_merge($this->fileSourceOverrides, $fileSourceOverrides); + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/CompilerInterface.php b/packages/flarum/core/src/Frontend/Compiler/CompilerInterface.php new file mode 100644 index 0000000..dd5dcf5 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/CompilerInterface.php @@ -0,0 +1,25 @@ +filesystem = $filesystem; + } + + public function putRevision(string $file, ?string $revision) + { + if ($this->filesystem->exists(static::REV_MANIFEST)) { + $manifest = json_decode($this->filesystem->get(static::REV_MANIFEST), true); + } else { + $manifest = []; + } + + if ($revision) { + $manifest[$file] = $revision; + } else { + unset($manifest[$file]); + } + + $this->filesystem->put(static::REV_MANIFEST, json_encode($manifest)); + } + + public function getRevision(string $file): ?string + { + if ($this->filesystem->exists(static::REV_MANIFEST)) { + $manifest = json_decode($this->filesystem->get(static::REV_MANIFEST), true); + + return Arr::get($manifest, $file); + } + + return null; + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/JsCompiler.php b/packages/flarum/core/src/Frontend/Compiler/JsCompiler.php new file mode 100644 index 0000000..c9c4510 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/JsCompiler.php @@ -0,0 +1,79 @@ +file = $mapFile; + $output = []; + $line = 0; + + // For each of the sources, get their content and add it to the + // output. For file sources, if a sourcemap is present, add it to + // the output sourcemap. + foreach ($sources as $source) { + $content = $source->getContent(); + + if ($source instanceof FileSource) { + $sourceMap = $source->getPath().'.map'; + + if (file_exists($sourceMap)) { + $map->concat($sourceMap, $line); + } + } + + $content = $this->format($content); + $output[] = $content; + $line += substr_count($content, "\n") + 1; + } + + // Add a comment to the end of our file to point to the sourcemap + // we just constructed. We will then store the JS file and the map + // in our asset directory. + $output[] = '//# sourceMappingURL='.$this->assetsDir->url($mapFile); + + $this->assetsDir->put($file, implode("\n", $output)); + $this->assetsDir->put($mapFile, json_encode($map, JSON_UNESCAPED_SLASHES)); + + return true; + } + + protected function format(string $string): string + { + return preg_replace('~//# sourceMappingURL.*$~m', '', $string)."\n"; + } + + /** + * {@inheritdoc} + */ + protected function delete(string $file) + { + parent::delete($file); + + if ($this->assetsDir->exists($mapFile = $file.'.map')) { + $this->assetsDir->delete($mapFile); + } + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/LessCompiler.php b/packages/flarum/core/src/Frontend/Compiler/LessCompiler.php new file mode 100644 index 0000000..3825215 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/LessCompiler.php @@ -0,0 +1,173 @@ +cacheDir; + } + + public function setCacheDir(string $cacheDir) + { + $this->cacheDir = $cacheDir; + } + + public function getImportDirs(): array + { + return $this->importDirs; + } + + public function setImportDirs(array $importDirs) + { + $this->importDirs = $importDirs; + } + + public function setLessImportOverrides(array $lessImportOverrides) + { + $this->lessImportOverrides = new Collection($lessImportOverrides); + } + + public function setFileSourceOverrides(array $fileSourceOverrides) + { + $this->fileSourceOverrides = new Collection($fileSourceOverrides); + } + + public function setCustomFunctions(array $customFunctions) + { + $this->customFunctions = $customFunctions; + } + + /** + * @throws \Less_Exception_Parser + */ + protected function compile(array $sources): string + { + if (! count($sources)) { + return ''; + } + + ini_set('xdebug.max_nesting_level', '200'); + + $parser = new Less_Parser([ + 'compress' => true, + 'cache_dir' => $this->cacheDir, + 'import_dirs' => $this->importDirs, + 'import_callback' => $this->lessImportOverrides ? $this->overrideImports($sources) : null, + ]); + + if ($this->fileSourceOverrides) { + $sources = $this->overrideSources($sources); + } + + foreach ($sources as $source) { + if ($source instanceof FileSource) { + $parser->parseFile($source->getPath()); + } else { + $parser->parse($source->getContent()); + } + } + + foreach ($this->customFunctions as $name => $callback) { + $parser->registerFunction($name, $callback); + } + + return $parser->getCss(); + } + + protected function overrideSources(array $sources): array + { + foreach ($sources as $source) { + if ($source instanceof FileSource) { + $basename = basename($source->getPath()); + $override = $this->fileSourceOverrides + ->where('file', $basename) + ->firstWhere('extensionId', $source->getExtensionId()); + + if ($override) { + $source->setPath($override['newFilePath']); + } + } + } + + return $sources; + } + + protected function overrideImports(array $sources): callable + { + $baseSources = (new Collection($sources))->filter(function ($source) { + return $source instanceof Source\FileSource; + })->map(function (FileSource $source) { + $path = realpath($source->getPath()); + $path = Str::beforeLast($path, '/less/'); + + return [ + 'path' => $path, + 'extensionId' => $source->getExtensionId(), + ]; + })->unique('path'); + + return function ($evald) use ($baseSources): ?array { + $relativeImportPath = Str::of($evald->PathAndUri()[0])->split('/\/less\//'); + $extensionId = $baseSources->where('path', $relativeImportPath->first())->pluck('extensionId')->first(); + + $overrideImport = $this->lessImportOverrides + ->where('file', $relativeImportPath->last()) + ->firstWhere('extensionId', $extensionId); + + if (! $overrideImport) { + return null; + } + + return [$overrideImport['newFilePath'], $evald->PathAndUri()[1]]; + }; + } + + protected function getCacheDifferentiator(): ?array + { + return [ + 'import_dirs' => $this->importDirs + ]; + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/RevisionCompiler.php b/packages/flarum/core/src/Frontend/Compiler/RevisionCompiler.php new file mode 100644 index 0000000..adf2e93 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/RevisionCompiler.php @@ -0,0 +1,200 @@ +assetsDir = $assetsDir; + $this->filename = $filename; + $this->versioner = $versioner ?: new FileVersioner($assetsDir); + } + + public function getFilename(): string + { + return $this->filename; + } + + public function setFilename(string $filename) + { + $this->filename = $filename; + } + + public function commit(bool $force = false) + { + $sources = $this->getSources(); + + $oldRevision = $this->versioner->getRevision($this->filename); + + $newRevision = $this->calculateRevision($sources); + + // In case the previous and current revisions do not match + // Or no file was written yet, let's save the file to disk. + if ($force || $oldRevision !== $newRevision || ! $this->assetsDir->exists($this->filename)) { + if (! $this->save($this->filename, $sources)) { + // If no file was written (because the sources were empty), we + // will set the revision to a special value so that we can tell + // that this file does not have a URL. + $newRevision = static::EMPTY_REVISION; + } + + $this->versioner->putRevision($this->filename, $newRevision); + } + } + + public function addSources(callable $callback) + { + $this->sourcesCallbacks[] = $callback; + } + + /** + * @return SourceInterface[] + */ + protected function getSources(): array + { + $sources = new SourceCollector; + + foreach ($this->sourcesCallbacks as $callback) { + $callback($sources); + } + + return $sources->getSources(); + } + + public function getUrl(): ?string + { + $revision = $this->versioner->getRevision($this->filename); + + if (! $revision) { + $this->commit(); + + $revision = $this->versioner->getRevision($this->filename); + + if (! $revision) { + return null; + } + } + + if ($revision === static::EMPTY_REVISION) { + return null; + } + + $url = $this->assetsDir->url($this->filename); + + // Append revision as GET param to signify that there's been + // a change to the file and it should be refreshed. + return "$url?v=$revision"; + } + + /** + * @param string $file + * @param SourceInterface[] $sources + * @return bool true if the file was written, false if there was nothing to write + */ + protected function save(string $file, array $sources): bool + { + if ($content = $this->compile($sources)) { + $this->assetsDir->put($file, $content); + + return true; + } + + return false; + } + + /** + * @param SourceInterface[] $sources + */ + protected function compile(array $sources): string + { + $output = ''; + + foreach ($sources as $source) { + $output .= $this->format($source->getContent()); + } + + return $output; + } + + protected function format(string $string): string + { + return $string; + } + + /** + * @param SourceInterface[] $sources + */ + protected function calculateRevision(array $sources): string + { + $cacheDifferentiator = [$this->getCacheDifferentiator()]; + + foreach ($sources as $source) { + $cacheDifferentiator[] = $source->getCacheDifferentiator(); + } + + return hash('crc32b', serialize($cacheDifferentiator)); + } + + protected function getCacheDifferentiator(): ?array + { + return null; + } + + public function flush() + { + if ($this->versioner->getRevision($this->filename) !== null) { + $this->delete($this->filename); + + $this->versioner->putRevision($this->filename, null); + } + } + + protected function delete(string $file) + { + if ($this->assetsDir->exists($file)) { + $this->assetsDir->delete($file); + } + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/Source/FileSource.php b/packages/flarum/core/src/Frontend/Compiler/Source/FileSource.php new file mode 100644 index 0000000..6fcd9b6 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/Source/FileSource.php @@ -0,0 +1,75 @@ +path = $path; + $this->extensionId = $extensionId; + } + + /** + * @return string + */ + public function getContent(): string + { + return file_get_contents($this->path); + } + + /** + * @return mixed + */ + public function getCacheDifferentiator() + { + return [$this->path, filemtime($this->path)]; + } + + /** + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + public function setPath(string $path): void + { + $this->path = $path; + } + + public function getExtensionId(): ?string + { + return $this->extensionId; + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/Source/SourceCollector.php b/packages/flarum/core/src/Frontend/Compiler/Source/SourceCollector.php new file mode 100644 index 0000000..452d78e --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/Source/SourceCollector.php @@ -0,0 +1,51 @@ +sources[] = new FileSource($file, $extensionId); + + return $this; + } + + /** + * @param callable $callback + * @return $this + */ + public function addString(callable $callback) + { + $this->sources[] = new StringSource($callback); + + return $this; + } + + /** + * @return SourceInterface[] + */ + public function getSources() + { + return $this->sources; + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/Source/SourceInterface.php b/packages/flarum/core/src/Frontend/Compiler/Source/SourceInterface.php new file mode 100644 index 0000000..71f829c --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/Source/SourceInterface.php @@ -0,0 +1,23 @@ +callback = $callback; + } + + /** + * @return string + */ + public function getContent(): string + { + if (is_null($this->content)) { + $this->content = call_user_func($this->callback); + } + + return $this->content; + } + + /** + * @return mixed + */ + public function getCacheDifferentiator() + { + return $this->getContent(); + } +} diff --git a/packages/flarum/core/src/Frontend/Compiler/VersionerInterface.php b/packages/flarum/core/src/Frontend/Compiler/VersionerInterface.php new file mode 100644 index 0000000..ad5dae4 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Compiler/VersionerInterface.php @@ -0,0 +1,17 @@ +container = $container; + $this->config = $config; + } + + /** + * Sets the frontend to generate assets for. + * + * @param string $name frontend name + * @return $this + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function forFrontend(string $name): Assets + { + $this->assets = $this->container->make('flarum.assets.'.$name); + + return $this; + } + + public function __invoke(Document $document, Request $request) + { + $locale = $request->getAttribute('locale'); + + $compilers = $this->assembleCompilers($locale); + + if ($this->config->inDebugMode()) { + $this->forceCommit(Arr::flatten($compilers)); + } + + $this->addAssetsToDocument($document, $compilers); + } + + /** + * Assembles JS and CSS compilers to be used to generate frontend assets. + * + * @param string|null $locale + * @return array[] + */ + protected function assembleCompilers(?string $locale): array + { + return [ + 'js' => [$this->assets->makeJs(), $this->assets->makeLocaleJs($locale)], + 'css' => [$this->assets->makeCss(), $this->assets->makeLocaleCss($locale)] + ]; + } + + /** + * Adds URLs of frontend JS and CSS to the {@link Document} class. + * + * @param Document $document + * @param array $compilers + * @return void + */ + protected function addAssetsToDocument(Document $document, array $compilers): void + { + $document->js = array_merge($document->js, $this->getUrls($compilers['js'])); + $document->css = array_merge($document->css, $this->getUrls($compilers['css'])); + } + + /** + * Force compilation of assets when in debug mode. + * + * @param array $compilers + */ + protected function forceCommit(array $compilers): void + { + /** @var CompilerInterface $compiler */ + foreach ($compilers as $compiler) { + $compiler->commit(true); + } + } + + /** + * Maps provided {@link CompilerInterface}s to their URLs. + * + * @param CompilerInterface[] $compilers + * @return string[] + */ + protected function getUrls(array $compilers): array + { + return array_filter(array_map(function (CompilerInterface $compiler) { + return $compiler->getUrl(); + }, $compilers)); + } +} diff --git a/packages/flarum/core/src/Frontend/Content/CorePayload.php b/packages/flarum/core/src/Frontend/Content/CorePayload.php new file mode 100644 index 0000000..5f02964 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Content/CorePayload.php @@ -0,0 +1,65 @@ +locales = $locales; + } + + public function __invoke(Document $document, Request $request) + { + $document->payload = array_merge( + $document->payload, + $this->buildPayload($document, $request) + ); + } + + private function buildPayload(Document $document, Request $request) + { + $data = $this->getDataFromApiDocument($document->getForumApiDocument()); + + return [ + 'resources' => $data, + 'session' => [ + 'userId' => RequestUtil::getActor($request)->id, + 'csrfToken' => $request->getAttribute('session')->token() + ], + 'locales' => $this->locales->getLocales(), + 'locale' => $request->getAttribute('locale') + ]; + } + + private function getDataFromApiDocument(array $apiDocument): array + { + $data[] = $apiDocument['data']; + + if (isset($apiDocument['included'])) { + $data = array_merge($data, $apiDocument['included']); + } + + return $data; + } +} diff --git a/packages/flarum/core/src/Frontend/Content/Meta.php b/packages/flarum/core/src/Frontend/Content/Meta.php new file mode 100644 index 0000000..2d0c6dd --- /dev/null +++ b/packages/flarum/core/src/Frontend/Content/Meta.php @@ -0,0 +1,64 @@ +locales = $locales; + } + + public function __invoke(Document $document, Request $request) + { + $document->language = $this->locales->getLocale(); + $document->direction = 'ltr'; + + $document->meta = array_merge($document->meta, $this->buildMeta($document)); + $document->head = array_merge($document->head, $this->buildHead($document)); + } + + private function buildMeta(Document $document) + { + $forumApiDocument = $document->getForumApiDocument(); + + $meta = [ + 'viewport' => 'width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1', + 'description' => Arr::get($forumApiDocument, 'data.attributes.description'), + 'theme-color' => Arr::get($forumApiDocument, 'data.attributes.themePrimaryColor') + ]; + + return $meta; + } + + private function buildHead(Document $document) + { + $head = []; + + if ($faviconUrl = Arr::get($document->getForumApiDocument(), 'data.attributes.faviconUrl')) { + $head['favicon'] = ''; + } + + return $head; + } +} diff --git a/packages/flarum/core/src/Frontend/Controller.php b/packages/flarum/core/src/Frontend/Controller.php new file mode 100644 index 0000000..9e1601c --- /dev/null +++ b/packages/flarum/core/src/Frontend/Controller.php @@ -0,0 +1,35 @@ +frontend = $frontend; + } + + public function handle(Request $request): Response + { + return new HtmlResponse( + $this->frontend->document($request)->render() + ); + } +} diff --git a/packages/flarum/core/src/Frontend/Document.php b/packages/flarum/core/src/Frontend/Document.php new file mode 100644 index 0000000..4b94705 --- /dev/null +++ b/packages/flarum/core/src/Frontend/Document.php @@ -0,0 +1,357 @@ + tag. + * + * @var null|string + */ + public $title; + + /** + * The language of the document, displayed as the value of the attribute `lang` in the tag. + * + * @var null|string + */ + public $language; + + /** + * The text direction of the document, displayed as the value of the attribute `dir` in the tag. + * + * @var null|string + */ + public $direction; + + /** + * The name of the frontend app view to display. + * + * @var string + */ + public $appView = 'flarum::frontend.app'; + + /** + * The name of the frontend layout view to display. + * + * @var string + */ + public $layoutView; + + /** + * The name of the frontend content view to display. + * + * @var string + */ + public $contentView = 'flarum::frontend.content'; + + /** + * The SEO content of the page, displayed within the layout in