diff --git a/.editorconfig b/.editorconfig index 8d06361b3..bb3487439 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,5 +13,5 @@ indent_size = 4 trim_trailing_whitespace = true # 2 space indentation -[*.{yaml,yml}] +[*.{yaml,yml,vue,js,css}] indent_size = 2 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6f9d9b21b..53f95be15 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,11 +12,15 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Extract Tag + run: echo "PACKAGE_VERSION=${{ github.ref }}" >> $GITHUB_ENV + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 7.3 extensions: opcache, gd + tools: composer:v2 coverage: none env: COMPOSER_TOKEN: ${{ secrets.GLOBAL_TOKEN }} @@ -38,13 +42,14 @@ jobs: run: | bash ./build-grav.sh - - name: Upload Grav Release Assets - id: upload-release-asset - uses: alexellis/upload-assets@0.2.3 - env: - GITHUB_TOKEN: ${{ secrets.GLOBAL_TOKEN }} + - name: Upload packages to release + uses: svenstaro/upload-release-action@v2 with: - asset_paths: '["./grav-dist/*.zip"]' + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ env.PACKAGE_VERSION }} + file: ./grav-dist/*.zip + overwrite: true + file_glob: true slack: name: Slack diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e83e0c488..ea22ad291 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -25,6 +25,7 @@ jobs: with: php-version: ${{ matrix.php }} extensions: opcache, gd + tools: composer:v2 coverage: none env: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 26ca72dff..5f80bdaa1 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ tests/cache/* tests/error.log system/templates/testing/* /user/config/versions.yaml +/user/cli/config/security.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index da12f46ff..0ce4cba37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,198 @@ -# v1.7.11 +# v1.7.24 ## mm/dd/2021 +3. [](#bugfix) + * Fixed a bug in `PermissionsReader` in PHP 7.3 + * Fixed `session_store_active` language option (#3464) + * Fixed deprecated warnings on `ArrayAccess` in PHP 8.1 + +# v1.7.23 +## 09/29/2021 + +1. [](#new) + * Added method `Pages::referrerRoute()` to get the referrer route and language + * Added true unique `Utils::uniqueId()` / `{{ unique_id() }}` utilities with length, prefix, and suffix support + * Added `UserObject::isMyself()` method to check if flex user is currently logged in + * Added support for custom form field options validation with `validate: options: key|ignore` +2. [](#improved) + * Replaced GPL `SVG-Sanitizer` with MIT licensed `DOM-Sanitizer` + * `Uri::referrer()` now accepts third parameter, if set to `true`, it returns route without base or language code [#3411](https://github.com/getgrav/grav/issues/3411) + * Updated vendor libs with latest + * Updated with latest language strings via Crowdin.com +3. [](#bugfix) + * Fixed `Folder::move()` throwing an error when target folder is changed by only appending characters to the end [#3445](https://github.com/getgrav/grav/issues/3445) + * Fixed some phpstan issues (all code back to level 1, Framework level 3) + * Fixed form reset causing image uploads to fail when using Flex + +# v1.7.22 +## 09/16/2021 + +1. [](#new) + * Register plugin autoloaders into plugin objects +2. [](#improved) + * Improve Twig 2 compatibility + * Update to customized version of Twig DeferredExtension (Twig 1/2 compatible) +3. [](#bugfix) + * Fixed conflicting `$_original` variable in `Flex Pages` + +# v1.7.21 +## 09/14/2021 + +1. [](#new) + * Added `|yaml` filter to convert input to YAML + * Added `route` and `request` to `onPageNotFound` event + * Added file upload/remove support for `Flex Forms` + * Added support for `flex-required@: not exists` and `flex-required@: '!exists'` in blueprints + * Added `$object->getOriginalData()` to get flex objects data before it was modified with `update()` + * Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages +2. [](#improved) + * Use a simplified text-based `cron` field for scheduler + * Add timestamp to logging output of scheduler jobs to see when they ran +3. [](#bugfix) + * Fixed escaping in PageIndex::getLevelListing() + * Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433) + * Fixed excessive `security.yaml` file creation [#3432](https://github.com/getgrav/grav/issues/3432) + * Fixed incorrect port :0 with nginx unix socket setup [#3439](https://github.com/getgrav/grav/issues/3439) + * Fixed `Session::setFlashCookieObject()` to use the same options as the main session cookie + +# v1.7.20 +## 09/01/2021 + +2. [](#improved) + * Added support for `task` and `action` inside JSON request body + +# v1.7.19 +## 08/31/2021 + +1. [](#new) + * Include active form and request in `onPageTask` and `onPageAction` events (defaults to `null`) + * Added `UserObject::$authorizeCallable` to allow `$user->authorize()` customization +2. [](#improved) + * Added meta support for `UploadedFile` class + * Added support for multiple mime-types per file extension [#3422](https://github.com/getgrav/grav/issues/3422) + * Added `setCurrent()` method to Page Collection [#3398](https://github.com/getgrav/grav/pull/3398) + * Initialize `$grav['uri']` before session +3. [](#bugfix) + * Fixed `Warning: Undefined array key "SERVER_SOFTWARE" in index.php` [#3408](https://github.com/getgrav/grav/issues/3408) + * Fixed error in `loadDirectoryConfig()` if configuration hasn't been saved [#3409](https://github.com/getgrav/grav/issues/3409) + * Fixed GPM not using non-standard cache path [#3410](https://github.com/getgrav/grav/issues/3410) + * Fixed broken `environment://` stream when it doesn't have configuration + * Fixed `Flex Object` missing key field value when using `FolderStorage` + * Fixed broken Twig try tag when catch has not been defined or is empty + * Fixed `FlexForm` serialization + * Fixed form validation for numeric values in PHP 8 + * Fixed `flex-options@` in blueprints duplicating items in array + * Fixed wrong form issue with flex objects after cache clear + * Fixed Flex object types not implementing `MediaInterface` + * Fixed issue with `svgImageFunction()` that was causing broken output + +# v1.7.18 +## 07/19/2021 + +1. [](#improved) + * Added support for loading Flex Directory configuration from main configuration + * Move SVGs that cannot be sanitized to quarantine folder under `log://quarantine` + * Added support for CloudFlare-forwarded client IP in the `URI::ip()` method +1. [](#bugfix) + * Fixed error when using Flex `SimpleStorage` with no entries + * Fixed page search to include slug field [#3316](https://github.com/getgrav/grav/issues/3316) + * Fixed Admin becoming unusable when GPM cannot be reached [#3383](https://github.com/getgrav/grav/issues/3383) + * Fixed `Failed to save entry: Forbidden` when moving a page to a visible page [#3389](https://github.com/getgrav/grav/issues/3389) + * Better support for Symfony local server on linux [#3400](https://github.com/getgrav/grav/pull/3400) + * Fixed `open_basedir()` error with some forms + +# v1.7.17 +## 06/15/2021 + +1. [](#new) + * Interface `FlexDirectoryInterface` now extends `FlexAuthorizeInterface` +1. [](#improved) + * Allow to unset an asset attribute by specifying null (ie, `'defer': null`) + * Support specifying custom attributes to assets in a collection [Read more](https://learn.getgrav.org/17/themes/asset-manager#collections-with-attributes?target=_blank) [#3358](https://github.com/getgrav/grav/issues/3358) + * File `frontmatter.yaml` isn't part of media, ignore it + * Switched default `JQuery` collection to use 3.x rather than 2.x +1. [](#bugfix) + * Fixed missing styles when CSS/JS Pipeline is used and `asset://` folder is missing + * Fixed permission check when moving a page [#3382](https://github.com/getgrav/grav/issues/3382) + +# v1.7.16 +## 06/02/2021 + +1. [](#new) + * Added 'addFrame()' method to ImageMedium [#3323](https://github.com/getgrav/grav/pull/3323) +1. [](#improved) + * Set `cache.clear_images_by_default` to `false` by default + * Improve error on bad nested form data [#3364](https://github.com/getgrav/grav/issues/3364) +1. [](#bugfix) + * Improve Plugin and Theme initialization to fix PHP8 bug [#3368](https://github.com/getgrav/grav/issues/3368) + * Fixed `pathinfo()` twig filter in PHP7 + * Fixed the first visible child page getting ordering number `999999.` [#3365](https://github.com/getgrav/grav/issues/3365) + * Fixed flex pages search using only folder name [#3316](https://github.com/getgrav/grav/issues/3316) + * Fixed flex pages using wrong type in `onBlueprintCreated` event [#3157](https://github.com/getgrav/grav/issues/3157) + * Fixed wrong SRI paths invoked when Grav instance as a sub folder [#3358](https://github.com/getgrav/grav/issues/3358) + * Fixed SRI trying to calculate remote assets, only ever set integrity for local files. Use the SRI provided by the remote source and manually add it in the `addJs/addCss` call for remote support. [#3358](https://github.com/getgrav/grav/issues/3358) + * Fix for weird regex issue with latest PHP versions on Intel Macs causing params to not parse properly in URI object + +# v1.7.15 +## 05/19/2021 + +1. [](#improved) + * Allow optional start date in page collections [#3350](https://github.com/getgrav/grav/pull/3350) + * Added `page` and `output` properties to `onOutputGenerated` and `onOutputRendered` events +1. [](#bugfix) + * Fixed twig deprecated TwigFilter messages [#3348](https://github.com/getgrav/grav/issues/3348) + * Fixed fatal error with some markdown links [getgrav/grav-premium-issues#95](https://github.com/getgrav/grav-premium-issues/issues/95) + * Fixed markdown media operations not working when using `image://` stream [#3333](https://github.com/getgrav/grav/issues/3333) [#3349](https://github.com/getgrav/grav/issues/3349) + * Fixed copying page without changing the slug [getgrav/grav-plugin-admin#2135](https://github.com/getgrav/grav-plugin-admin/issues/2139) + * Fixed missing and commonly used methods when using `system.twig.undefined_functions = false` [getgrav/grav-plugin-admin#2138](https://github.com/getgrav/grav-plugin-admin/issues/2138) + * Fixed uploading images into Flex Object if field destination is not set + +# v1.7.14 +## 04/29/2021 + +1. [](#new) + * Added `MediaUploadTrait::checkFileMetadata()` method +1. [](#improved) + * Updating a theme should always keep the custom files [getgrav/grav-plugin-admin#2135](https://github.com/getgrav/grav-plugin-admin/issues/2135) +1. [](#bugfix) + * Fixed broken numeric language codes in Flex Pages [#3332](https://github.com/getgrav/grav/issues/3332) + * Fixed broken `exif_imagetype()` twig function + +# v1.7.13 +## 04/23/2021 + +1. [](#new) + * Added support for getting translated collection of Flex Pages using `$collection->withTranslated('de')` +1. [](#improved) + * Moved `gregwar/Image` and `gregwar/Cache` in-house to official `getgrav/Image` and `getgrav/Cache` packagist packages. This will help environments with very strict proxy setups that don't allow VCS setup. [#3289](https://github.com/getgrav/grav/issues/3289) + * Improved XSS Invalid Protocol detection regex [#3298](https://github.com/getgrav/grav/issues/3298) + * Added support for user provided folder in Flex `$page->copy()` +1. [](#bugfix) + * Fixed `The "Grav/Common/Twig/TwigExtension" extension is not enabled` when using markdown twig tag [#3317](https://github.com/getgrav/grav/issues/3317) + * Fixed text field maxlength validation newline issue [#3324](https://github.com/getgrav/grav/issues/3324) + * Fixed a bug in Flex Object `refresh()` method + +# v1.7.12 +## 04/15/2021 + +1. [](#improved) + * Improve JSON support for the request +1. [](#bugfix) + * Fixed absolute path support for Windows [#3297](https://github.com/getgrav/grav/issues/3297) + * Fixed adding tags in admin after upgrading Grav [#3315](https://github.com/getgrav/grav/issues/3315) + +# v1.7.11 +## 04/13/2021 + +1. [](#new) + * Added configuration options to allow PHP methods to be used in Twig functions (`system.twig.safe_functions`) and filters (`system.twig.safe_filters`) + * Deprecated using PHP methods in Twig without them being in the safe lists + * Prevent dangerous PHP methods from being used as Twig functions and filters + * Restrict filesystem Twig functions to accept only local filesystem and grav streams 1. [](#improved) * Better GPM detection of unauthorized installations 1. [](#bugfix) + * **IMPORTANT** Fixed security vulnerability with Twig allowing dangerous PHP functions by default [GHSA-g8r4-p96j-xfxc](https://github.com/getgrav/grav/security/advisories/GHSA-g8r4-p96j-xfxc) * Fixed nxinx appending repeating `?_url=` in some redirects * Fixed deleting page with language code not removing the folder if it was the last language [#3305](https://github.com/getgrav/grav/issues/3305) * Fixed fatal error when using markdown links with `image://` stream [#3285](https://github.com/getgrav/grav/issues/3285) diff --git a/README.md b/README.md index 562349f91..31a4a7e4b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav [![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan) -[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad) [![Discord](https://img.shields.io/discord/501836936584101899.svg?logo=discord&colorB=728ADA&label=Discord%20Chat)](https://chat.getgrav.org) [![PHP Tests](https://github.com/getgrav/grav/workflows/PHP%20Tests/badge.svg?branch=develop)](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors) diff --git a/composer.json b/composer.json index 34d1e7e09..e86c6aa06 100644 --- a/composer.json +++ b/composer.json @@ -44,8 +44,8 @@ "filp/whoops": "~2.9", "matthiasmullie/minify": "^1.3", "monolog/monolog": "~1.25", - "gregwar/image": "dev-php8", - "gregwar/cache": "dev-php8", + "getgrav/image": "^3.0", + "getgrav/cache": "^2.0", "donatj/phpuseragentparser": "~1.1", "pimple/pimple": "~3.3.0", "rockettheme/toolbox": "~1.5", @@ -55,19 +55,18 @@ "miljar/php-exif": "^0.6", "composer/ca-bundle": "^1.2", "dragonmantank/cron-expression": "^1.2", - "phive/twig-extensions-deferred": "^1.0", "willdurand/negotiation": "^3.0", "itsgoingd/clockwork": "^5.0", - "enshrined/svg-sanitize": "~0.13", "symfony/http-client": "^4.4", - "composer/semver": "^1.4" + "composer/semver": "^1.4", + "rhukster/dom-sanitizer": "^1.0" }, "require-dev": { "codeception/codeception": "^4.1", "phpstan/phpstan": "^0.12", "phpstan/phpstan-deprecation-rules": "^0.12", "phpunit/php-code-coverage": "~9.2", - "victorjonsson/markdowndocs": "dev-master", + "getgrav/markdowndocs": "^2.0", "codeception/module-asserts": "^1.3", "codeception/module-phpbrowser": "^1.0", "symfony/service-contracts": "*" @@ -91,23 +90,10 @@ "php": "7.3.6" } }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator" - }, - { - "type": "vcs", - "url": "https://github.com/getgrav/Cache" - }, - { - "type": "vcs", - "url": "https://github.com/getgrav/Image" - } - ], "autoload": { "psr-4": { - "Grav\\": "system/src/Grav" + "Grav\\": "system/src/Grav", + "Twig\\": "system/src/Twig" }, "files": [ "system/defines.php" @@ -121,8 +107,8 @@ "scripts": { "api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md", "post-create-project-cmd": "bin/grav install", - "phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src", - "phpstan-framework": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer", + "phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=520M system/src", + "phpstan-framework": "vendor/bin/phpstan analyse -l 3 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer", "phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=400M user/plugins", "test": "vendor/bin/codecept run unit", "test-windows": "vendor\\bin\\codecept run unit" diff --git a/composer.lock b/composer.lock index 350b627b3..df02b8d5a 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": "4ae6fc7274c018b1bb34bb1b80bd62c5", + "content-hash": "23dd68cea2a3f2d963e57638131f1122", "packages": [ { "name": "antoligy/dom-string-iterators", @@ -56,16 +56,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.9", + "version": "1.2.11", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5" + "reference": "0b072d51c5a9c6f3412f7ea3ab043d6603cb2582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/78a0e288fdcebf92aa2318a8d3656168da6ac1a5", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0b072d51c5a9c6f3412f7ea3ab043d6603cb2582", + "reference": "0b072d51c5a9c6f3412f7ea3ab043d6603cb2582", "shasum": "" }, "require": { @@ -77,7 +77,7 @@ "phpstan/phpstan": "^0.12.55", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" }, "type": "library", "extra": { @@ -112,7 +112,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.9" + "source": "https://github.com/composer/ca-bundle/tree/1.2.11" }, "funding": [ { @@ -128,7 +128,7 @@ "type": "tidelift" } ], - "time": "2021-01-12T12:10:35+00:00" + "time": "2021-09-25T20:32:43+00:00" }, { "name": "composer/semver", @@ -212,16 +212,16 @@ }, { "name": "doctrine/cache", - "version": "1.10.2", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "13e3381b25847283a91948d04640543941309727" + "reference": "4cf401d14df219fa6f38b671f5493449151c9ad8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727", - "reference": "13e3381b25847283a91948d04640543941309727", + "url": "https://api.github.com/repos/doctrine/cache/zipball/4cf401d14df219fa6f38b671f5493449151c9ad8", + "reference": "4cf401d14df219fa6f38b671f5493449151c9ad8", "shasum": "" }, "require": { @@ -232,20 +232,19 @@ }, "require-dev": { "alcaeus/mongo-php-adapter": "^1.1", - "doctrine/coding-standard": "^6.0", + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^8.0", "mongodb/mongodb": "^1.1", - "phpunit/phpunit": "^7.0", - "predis/predis": "~1.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "predis/predis": "~1.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.2 || ^6.0@dev", + "symfony/var-exporter": "^4.4 || ^5.2 || ^6.0@dev" }, "suggest": { "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.9.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" @@ -292,7 +291,7 @@ ], "support": { "issues": "https://github.com/doctrine/cache/issues", - "source": "https://github.com/doctrine/cache/tree/1.10.x" + "source": "https://github.com/doctrine/cache/tree/1.12.1" }, "funding": [ { @@ -308,30 +307,30 @@ "type": "tidelift" } ], - "time": "2020-07-07T18:54:01+00:00" + "time": "2021-07-17T14:39:21+00:00" }, { "name": "doctrine/collections", - "version": "1.6.7", + "version": "1.6.8", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a" + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/55f8b799269a1a472457bd1a41b4f379d4cfba4a", - "reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af", + "reference": "1958a744696c6bb3bb0d28db2611dc11610e78af", "shasum": "" }, "require": { "php": "^7.1.3 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan-shim": "^0.9.2", - "phpunit/phpunit": "^7.0", - "vimeo/psalm": "^3.8.1" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5", + "vimeo/psalm": "^4.2.1" }, "type": "library", "autoload": { @@ -375,26 +374,26 @@ ], "support": { "issues": "https://github.com/doctrine/collections/issues", - "source": "https://github.com/doctrine/collections/tree/1.6.7" + "source": "https://github.com/doctrine/collections/tree/1.6.8" }, - "time": "2020-07-27T17:53:49+00:00" + "time": "2021-08-10T18:51:53+00:00" }, { "name": "donatj/phpuseragentparser", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/donatj/PhpUserAgent.git", - "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0" + "reference": "cc9d872cddfc180c52d084d0dff1e4aad653d37f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/246c1cf0a44f07168c702203bf30d5f48f17bab0", - "reference": "246c1cf0a44f07168c702203bf30d5f48f17bab0", + "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/cc9d872cddfc180c52d084d0dff1e4aad653d37f", + "reference": "cc9d872cddfc180c52d084d0dff1e4aad653d37f", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=5.4.0" }, "require-dev": { "camspiers/json-pretty": "~1.0", @@ -433,7 +432,7 @@ ], "support": { "issues": "https://github.com/donatj/PhpUserAgent/issues", - "source": "https://github.com/donatj/PhpUserAgent/tree/v1.4.0" + "source": "https://github.com/donatj/PhpUserAgent/tree/v1.5.0" }, "funding": [ { @@ -445,7 +444,7 @@ "type": "github" } ], - "time": "2021-03-16T16:25:14+00:00" + "time": "2021-09-16T17:05:03+00:00" }, { "name": "dragonmantank/cron-expression", @@ -494,51 +493,6 @@ }, "time": "2017-01-23T04:29:33+00:00" }, - { - "name": "enshrined/svg-sanitize", - "version": "0.14.0", - "source": { - "type": "git", - "url": "https://github.com/darylldoyle/svg-sanitizer.git", - "reference": "beff89576a72540ee99476aeb9cfe98222e76fb8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/beff89576a72540ee99476aeb9cfe98222e76fb8", - "reference": "beff89576a72540ee99476aeb9cfe98222e76fb8", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*" - }, - "require-dev": { - "codeclimate/php-test-reporter": "^0.1.2", - "phpunit/phpunit": "^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "enshrined\\svgSanitize\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "Daryll Doyle", - "email": "daryll@enshrined.co.uk" - } - ], - "description": "An SVG sanitizer for PHP", - "support": { - "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", - "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.14.0" - }, - "time": "2021-01-21T10:13:20+00:00" - }, { "name": "erusev/parsedown", "version": "1.7.4", @@ -642,21 +596,21 @@ }, { "name": "filp/whoops", - "version": "2.12.0", + "version": "2.14.4", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "d501fd2658d55491a2295ff600ae5978eaad7403" + "reference": "f056f1fe935d9ed86e698905a957334029899895" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/d501fd2658d55491a2295ff600ae5978eaad7403", - "reference": "d501fd2658d55491a2295ff600ae5978eaad7403", + "url": "https://api.github.com/repos/filp/whoops/zipball/f056f1fe935d9ed86e698905a957334029899895", + "reference": "f056f1fe935d9ed86e698905a957334029899895", "shasum": "" }, "require": { "php": "^5.5.9 || ^7.0 || ^8.0", - "psr/log": "^1.0.1" + "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { "mockery/mockery": "^0.9 || ^1.0", @@ -701,7 +655,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.12.0" + "source": "https://github.com/filp/whoops/tree/2.14.4" }, "funding": [ { @@ -709,21 +663,21 @@ "type": "github" } ], - "time": "2021-03-30T12:00:00+00:00" + "time": "2021-10-03T12:00:00+00:00" }, { - "name": "gregwar/cache", - "version": "dev-php8", + "name": "getgrav/cache", + "version": "v2.0.0", "target-dir": "Gregwar/Cache", "source": { "type": "git", "url": "https://github.com/getgrav/Cache.git", - "reference": "49ccdf9ae760b009a192bc3c7b417980c8a8cc2e" + "reference": "56fd63f752779928fcd1074ab7d12f406dde8861" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getgrav/Cache/zipball/49ccdf9ae760b009a192bc3c7b417980c8a8cc2e", - "reference": "49ccdf9ae760b009a192bc3c7b417980c8a8cc2e", + "url": "https://api.github.com/repos/getgrav/Cache/zipball/56fd63f752779928fcd1074ab7d12f406dde8861", + "reference": "56fd63f752779928fcd1074ab7d12f406dde8861", "shasum": "" }, "require": { @@ -735,6 +689,7 @@ "Gregwar\\Cache": "" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -742,6 +697,11 @@ { "name": "Gregwar", "email": "g.passault@gmail.com" + }, + { + "name": "Grav CMS", + "email": "hello@getgrav.org", + "homepage": "https://getgrav.org" } ], "description": "A lightweight file-system cache system", @@ -752,28 +712,28 @@ "system" ], "support": { - "source": "https://github.com/getgrav/Cache/tree/php8" + "source": "https://github.com/getgrav/Cache/tree/v2.0.0" }, - "time": "2020-12-02T10:54:35+00:00" + "time": "2021-04-20T05:48:00+00:00" }, { - "name": "gregwar/image", - "version": "dev-php8", + "name": "getgrav/image", + "version": "v3.0.0", "target-dir": "Gregwar/Image", "source": { "type": "git", "url": "https://github.com/getgrav/Image.git", - "reference": "ea23859700f32447a85e79d96f331e3d6c8897a8" + "reference": "02c1bb2c179dd894c4f6610c9c49da364ee7d264" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getgrav/Image/zipball/ea23859700f32447a85e79d96f331e3d6c8897a8", - "reference": "ea23859700f32447a85e79d96f331e3d6c8897a8", + "url": "https://api.github.com/repos/getgrav/Image/zipball/02c1bb2c179dd894c4f6610c9c49da364ee7d264", + "reference": "02c1bb2c179dd894c4f6610c9c49da364ee7d264", "shasum": "" }, "require": { "ext-gd": "*", - "gregwar/cache": "dev-php8", + "getgrav/cache": "^2.0", "php": "^5.6 || ^7.0 || ^8.0" }, "require-dev": { @@ -789,6 +749,7 @@ "Gregwar\\Image": "" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -797,6 +758,11 @@ "name": "Grégoire Passault", "email": "g.passault@gmail.com", "homepage": "http://www.gregwar.com/" + }, + { + "name": "Grav CMS", + "email": "hello@getgrav.org", + "homepage": "https://getgrav.org" } ], "description": "Image handling", @@ -806,22 +772,22 @@ "image" ], "support": { - "source": "https://github.com/getgrav/Image/tree/php8" + "source": "https://github.com/getgrav/Image/tree/v3.0.0" }, - "time": "2021-03-15T17:03:52+00:00" + "time": "2021-04-20T05:50:18+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.8.1", + "version": "1.8.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1" + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", "shasum": "" }, "require": { @@ -858,13 +824,34 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" } ], @@ -881,22 +868,36 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.1" + "source": "https://github.com/guzzle/psr7/tree/1.8.3" }, - "time": "2021-03-21T16:25:00+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2021-10-05T13:56:00+00:00" }, { "name": "itsgoingd/clockwork", - "version": "v5.0.7", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/itsgoingd/clockwork.git", - "reference": "e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4" + "reference": "b963dee47429a49c9669981cfa9a8362ce209278" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4", - "reference": "e41ee368ff4dcc30d3f4563fe8bd80ed72b293b4", + "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/b963dee47429a49c9669981cfa9a8362ce209278", + "reference": "b963dee47429a49c9669981cfa9a8362ce209278", "shasum": "" }, "require": { @@ -944,7 +945,7 @@ ], "support": { "issues": "https://github.com/itsgoingd/clockwork/issues", - "source": "https://github.com/itsgoingd/clockwork/tree/v5.0.7" + "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.0" }, "funding": [ { @@ -952,7 +953,7 @@ "type": "github" } ], - "time": "2021-03-14T16:29:40+00:00" + "time": "2021-08-07T23:04:17+00:00" }, { "name": "league/climate", @@ -1152,21 +1153,21 @@ }, { "name": "maximebf/debugbar", - "version": "v1.16.5", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "6d51ee9e94cff14412783785e79a4e7ef97b9d62" + "reference": "3541f09f09c003c4a9ff7ddb0eb3361a7f14d418" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/6d51ee9e94cff14412783785e79a4e7ef97b9d62", - "reference": "6d51ee9e94cff14412783785e79a4e7ef97b9d62", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/3541f09f09c003c4a9ff7ddb0eb3361a7f14d418", + "reference": "3541f09f09c003c4a9ff7ddb0eb3361a7f14d418", "shasum": "" }, "require": { "php": "^7.1|^8", - "psr/log": "^1.0", + "psr/log": "^1|^2|^3", "symfony/var-dumper": "^2.6|^3|^4|^5" }, "require-dev": { @@ -1180,7 +1181,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.16-dev" + "dev-master": "1.17-dev" } }, "autoload": { @@ -1211,9 +1212,9 @@ ], "support": { "issues": "https://github.com/maximebf/php-debugbar/issues", - "source": "https://github.com/maximebf/php-debugbar/tree/v1.16.5" + "source": "https://github.com/maximebf/php-debugbar/tree/v1.17.2" }, - "time": "2020-12-07T11:07:24+00:00" + "time": "2021-10-18T09:39:00+00:00" }, { "name": "miljar/php-exif", @@ -1277,16 +1278,16 @@ }, { "name": "monolog/monolog", - "version": "1.26.0", + "version": "1.26.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33" + "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/2209ddd84e7ef1256b7af205d0717fb62cfc9c33", - "reference": "2209ddd84e7ef1256b7af205d0717fb62cfc9c33", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5", + "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5", "shasum": "" }, "require": { @@ -1347,7 +1348,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/1.26.0" + "source": "https://github.com/Seldaek/monolog/tree/1.26.1" }, "funding": [ { @@ -1359,20 +1360,20 @@ "type": "tidelift" } ], - "time": "2020-12-14T12:56:38+00:00" + "time": "2021-05-28T08:32:12+00:00" }, { "name": "nyholm/psr7", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7.git", - "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b" + "reference": "2212385b47153ea71b1c1b1374f8cb5e4f7892ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7/zipball/23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", - "reference": "23ae1f00fbc6a886cbe3062ca682391b9cc7c37b", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/2212385b47153ea71b1c1b1374f8cb5e4f7892ec", + "reference": "2212385b47153ea71b1c1b1374f8cb5e4f7892ec", "shasum": "" }, "require": { @@ -1386,7 +1387,7 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "http-interop/http-factory-tests": "^0.8", + "http-interop/http-factory-tests": "^0.9", "php-http/psr7-integration-tests": "^1.0", "phpunit/phpunit": "^7.5 || 8.5 || 9.4", "symfony/error-handler": "^4.4" @@ -1424,7 +1425,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7/issues", - "source": "https://github.com/Nyholm/psr7/tree/1.4.0" + "source": "https://github.com/Nyholm/psr7/tree/1.4.1" }, "funding": [ { @@ -1436,20 +1437,20 @@ "type": "github" } ], - "time": "2021-02-18T15:41:32+00:00" + "time": "2021-07-02T08:32:20+00:00" }, { "name": "nyholm/psr7-server", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/Nyholm/psr7-server.git", - "reference": "5c134aeb5dd6521c7978798663470dabf0528c96" + "reference": "b846a689844cef114e8079d8c80f0afd96745ae3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/5c134aeb5dd6521c7978798663470dabf0528c96", - "reference": "5c134aeb5dd6521c7978798663470dabf0528c96", + "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/b846a689844cef114e8079d8c80f0afd96745ae3", + "reference": "b846a689844cef114e8079d8c80f0afd96745ae3", "shasum": "" }, "require": { @@ -1490,7 +1491,7 @@ ], "support": { "issues": "https://github.com/Nyholm/psr7-server/issues", - "source": "https://github.com/Nyholm/psr7-server/tree/1.0.1" + "source": "https://github.com/Nyholm/psr7-server/tree/1.0.2" }, "funding": [ { @@ -1502,60 +1503,7 @@ "type": "github" } ], - "time": "2020-11-15T15:26:20+00:00" - }, - { - "name": "phive/twig-extensions-deferred", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/rybakit/twig-extensions-deferred-legacy.git", - "reference": "5a2426d622afa74034e754ca5ea1d1ff7887627f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rybakit/twig-extensions-deferred-legacy/zipball/5a2426d622afa74034e754ca5ea1d1ff7887627f", - "reference": "5a2426d622afa74034e754ca5ea1d1ff7887627f", - "shasum": "" - }, - "require": { - "twig/twig": "~1.18" - }, - "type": "library", - "autoload": { - "psr-4": { - "Phive\\Twig\\Extensions\\Deferred\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eugene Leonovich", - "email": "gen.work@gmail.com" - } - ], - "description": "An extension for Twig that allows to defer block rendering", - "homepage": "https://github.com/rybakit/twig-extensions-deferred", - "keywords": [ - "defer", - "extension", - "lazy", - "twig" - ], - "support": { - "issues": "https://github.com/rybakit/twig-extensions-deferred-legacy/issues", - "source": "https://github.com/rybakit/twig-extensions-deferred-legacy/tree/v1.0.2" - }, - "funding": [ - { - "url": "https://github.com/rybakit", - "type": "github" - } - ], - "time": "2017-03-17T21:39:21+00:00" + "time": "2021-05-12T11:11:27+00:00" }, { "name": "php-http/message-factory", @@ -1990,16 +1938,16 @@ }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -2023,7 +1971,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -2034,9 +1982,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.3" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "time": "2020-03-23T09:12:05+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { "name": "psr/simple-cache", @@ -2134,17 +2082,62 @@ "time": "2019-03-08T08:55:37+00:00" }, { - "name": "rockettheme/toolbox", - "version": "1.5.7", + "name": "rhukster/dom-sanitizer", + "version": "1.0.6", "source": { "type": "git", - "url": "https://github.com/rockettheme/toolbox.git", - "reference": "8d3ebc4d982595d6eac90e851f2b4d5c0cec0399" + "url": "https://github.com/rhukster/dom-sanitizer.git", + "reference": "4db3ef1ac3d5505d044c5eb12aa106ba745bf129" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/8d3ebc4d982595d6eac90e851f2b4d5c0cec0399", - "reference": "8d3ebc4d982595d6eac90e851f2b4d5c0cec0399", + "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/4db3ef1ac3d5505d044c5eb12aa106ba745bf129", + "reference": "4db3ef1ac3d5505d044c5eb12aa106ba745bf129", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Rhukster\\DomSanitizer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andy Miller", + "email": "rhuk@rhuk.net" + } + ], + "description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+", + "support": { + "issues": "https://github.com/rhukster/dom-sanitizer/issues", + "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.6" + }, + "time": "2021-09-30T15:41:33+00:00" + }, + { + "name": "rockettheme/toolbox", + "version": "1.5.10", + "source": { + "type": "git", + "url": "https://github.com/rockettheme/toolbox.git", + "reference": "d9738de013fa12df77754a0f11dded220b246efb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/d9738de013fa12df77754a0f11dded220b246efb", + "reference": "d9738de013fa12df77754a0f11dded220b246efb", "shasum": "" }, "require": { @@ -2185,9 +2178,9 @@ ], "support": { "issues": "https://github.com/rockettheme/toolbox/issues", - "source": "https://github.com/rockettheme/toolbox/tree/1.5.7" + "source": "https://github.com/rockettheme/toolbox/tree/1.5.10" }, - "time": "2021-02-17T17:58:36+00:00" + "time": "2021-09-29T16:50:13+00:00" }, { "name": "seld/cli-prompt", @@ -2246,36 +2239,37 @@ }, { "name": "symfony/console", - "version": "v4.4.21", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23" + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", + "url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22", + "reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22", "shasum": "" }, "require": { "php": ">=7.1.3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", + "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2" }, "conflict": { + "psr/log": ">=3", "symfony/dependency-injection": "<3.4", "symfony/event-dispatcher": "<4.3|>=5", "symfony/lock": "<4.4", "symfony/process": "<3.3" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", + "psr/log": "^1|^2", "symfony/config": "^3.4|^4.0|^5.0", "symfony/dependency-injection": "^3.4|^4.0|^5.0", "symfony/event-dispatcher": "^4.3", @@ -2315,7 +2309,7 @@ "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.21" + "source": "https://github.com/symfony/console/tree/v4.4.30" }, "funding": [ { @@ -2331,7 +2325,7 @@ "type": "tidelift" } ], - "time": "2021-03-26T09:23:24+00:00" + "time": "2021-08-25T19:27:26+00:00" }, { "name": "symfony/contracts", @@ -2429,21 +2423,22 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.4.20", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c" + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c352647244bd376bf7d31efbd5401f13f50dad0c", - "reference": "c352647244bd376bf7d31efbd5401f13f50dad0c", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac", + "reference": "2fe81680070043c4c80e7cedceb797e34f377bac", "shasum": "" }, "require": { "php": ">=7.1.3", - "symfony/event-dispatcher-contracts": "^1.1" + "symfony/event-dispatcher-contracts": "^1.1", + "symfony/polyfill-php80": "^1.16" }, "conflict": { "symfony/dependency-injection": "<3.4" @@ -2453,7 +2448,7 @@ "symfony/event-dispatcher-implementation": "1.1" }, "require-dev": { - "psr/log": "~1.0", + "psr/log": "^1|^2|^3", "symfony/config": "^3.4|^4.0|^5.0", "symfony/dependency-injection": "^3.4|^4.0|^5.0", "symfony/error-handler": "~3.4|~4.4", @@ -2492,7 +2487,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.20" + "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30" }, "funding": [ { @@ -2508,27 +2503,28 @@ "type": "tidelift" } ], - "time": "2021-01-27T09:09:26+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/http-client", - "version": "v4.4.21", + "version": "v4.4.31", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "911177e186b82e5b9a9f41c13af53699b6745657" + "reference": "6b900ffa399e25203f30f79f6f4a56b89eee14c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/911177e186b82e5b9a9f41c13af53699b6745657", - "reference": "911177e186b82e5b9a9f41c13af53699b6745657", + "url": "https://api.github.com/repos/symfony/http-client/zipball/6b900ffa399e25203f30f79f6f4a56b89eee14c2", + "reference": "6b900ffa399e25203f30f79f6f4a56b89eee14c2", "shasum": "" }, "require": { "php": ">=7.1.3", - "psr/log": "^1.0", + "psr/log": "^1|^2|^3", "symfony/http-client-contracts": "^1.1.10|^2", "symfony/polyfill-php73": "^1.11", + "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.0|^2" }, "provide": { @@ -2572,7 +2568,7 @@ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-client/tree/v4.4.21" + "source": "https://github.com/symfony/http-client/tree/v4.4.31" }, "funding": [ { @@ -2588,20 +2584,20 @@ "type": "tidelift" } ], - "time": "2021-03-25T17:52:07+00:00" + "time": "2021-09-06T10:00:00+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", "shasum": "" }, "require": { @@ -2613,7 +2609,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2651,7 +2647,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" }, "funding": [ { @@ -2667,20 +2663,20 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "06fb361659649bcfd6a208a0f1fcaf4e827ad342" + "reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/06fb361659649bcfd6a208a0f1fcaf4e827ad342", - "reference": "06fb361659649bcfd6a208a0f1fcaf4e827ad342", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/63b5bb7db83e5673936d6e3b8b3e022ff6474933", + "reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933", "shasum": "" }, "require": { @@ -2692,7 +2688,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2731,7 +2727,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.23.0" }, "funding": [ { @@ -2747,20 +2743,20 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2021-05-27T09:27:20+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.22.1", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", + "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", "shasum": "" }, "require": { @@ -2772,7 +2768,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2811,7 +2807,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" }, "funding": [ { @@ -2827,20 +2823,20 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2021-05-27T12:26:48+00:00" }, { "name": "symfony/polyfill-php74", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php74.git", - "reference": "577e147350331efeb816897e004d85e6e765daaf" + "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/577e147350331efeb816897e004d85e6e765daaf", - "reference": "577e147350331efeb816897e004d85e6e765daaf", + "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/a5d80cdf049bd3b0af6da91184a2cd37533c0fd8", + "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8", "shasum": "" }, "require": { @@ -2849,7 +2845,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2891,7 +2887,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php74/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-php74/tree/v1.23.0" }, "funding": [ { @@ -2907,20 +2903,20 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.22.1", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", + "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", "shasum": "" }, "require": { @@ -2929,7 +2925,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2974,7 +2970,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1" }, "funding": [ { @@ -2990,24 +2986,25 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-07-28T13:41:28+00:00" }, { "name": "symfony/process", - "version": "v4.4.20", + "version": "v4.4.30", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a" + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7e950b6366d4da90292c2e7fa820b3c1842b965a", - "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a", + "url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", + "reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d", "shasum": "" }, "require": { - "php": ">=7.1.3" + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -3035,7 +3032,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v4.4.20" + "source": "https://github.com/symfony/process/tree/v4.4.30" }, "funding": [ { @@ -3051,27 +3048,27 @@ "type": "tidelift" } ], - "time": "2021-01-27T09:09:26+00:00" + "time": "2021-08-04T20:31:23+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.4.21", + "version": "v4.4.31", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0da0e174f728996f5d5072d6a9f0a42259dbc806" + "reference": "1f12cc0c2e880a5f39575c19af81438464717839" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0da0e174f728996f5d5072d6a9f0a42259dbc806", - "reference": "0da0e174f728996f5d5072d6a9f0a42259dbc806", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1f12cc0c2e880a5f39575c19af81438464717839", + "reference": "1f12cc0c2e880a5f39575c19af81438464717839", "shasum": "" }, "require": { "php": ">=7.1.3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php72": "~1.5", - "symfony/polyfill-php80": "^1.15" + "symfony/polyfill-php80": "^1.16" }, "conflict": { "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", @@ -3124,7 +3121,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v4.4.21" + "source": "https://github.com/symfony/var-dumper/tree/v4.4.31" }, "funding": [ { @@ -3140,20 +3137,20 @@ "type": "tidelift" } ], - "time": "2021-03-27T19:49:03+00:00" + "time": "2021-09-24T15:30:11+00:00" }, { "name": "symfony/yaml", - "version": "v4.4.21", + "version": "v4.4.29", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "3871c720871029f008928244e56cf43497da7e9d" + "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/3871c720871029f008928244e56cf43497da7e9d", - "reference": "3871c720871029f008928244e56cf43497da7e9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/3abcc4db06d4e776825eaa3ed8ad924d5bc7432a", + "reference": "3abcc4db06d4e776825eaa3ed8ad924d5bc7432a", "shasum": "" }, "require": { @@ -3195,7 +3192,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v4.4.21" + "source": "https://github.com/symfony/yaml/tree/v4.4.29" }, "funding": [ { @@ -3211,20 +3208,20 @@ "type": "tidelift" } ], - "time": "2021-03-05T17:58:50+00:00" + "time": "2021-07-27T16:19:30+00:00" }, { "name": "twig/twig", - "version": "v1.44.2", + "version": "v1.44.5", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "138c493c5b8ee7cff3821f80b8896d371366b5fe" + "reference": "dd4353357c5a116322e92a00d16043a31881a81e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/138c493c5b8ee7cff3821f80b8896d371366b5fe", - "reference": "138c493c5b8ee7cff3821f80b8896d371366b5fe", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/dd4353357c5a116322e92a00d16043a31881a81e", + "reference": "dd4353357c5a116322e92a00d16043a31881a81e", "shasum": "" }, "require": { @@ -3277,7 +3274,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v1.44.2" + "source": "https://github.com/twigphp/Twig/tree/v1.44.5" }, "funding": [ { @@ -3289,7 +3286,7 @@ "type": "tidelift" } ], - "time": "2021-01-05T10:10:05+00:00" + "time": "2021-09-17T08:35:19+00:00" }, { "name": "willdurand/negotiation", @@ -3351,25 +3348,24 @@ "packages-dev": [ { "name": "behat/gherkin", - "version": "v4.8.0", + "version": "v4.9.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "2391482cd003dfdc36b679b27e9f5326bd656acd" + "reference": "0bc8d1e30e96183e4f36db9dc79caead300beff4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/2391482cd003dfdc36b679b27e9f5326bd656acd", - "reference": "2391482cd003dfdc36b679b27e9f5326bd656acd", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/0bc8d1e30e96183e4f36db9dc79caead300beff4", + "reference": "0bc8d1e30e96183e4f36db9dc79caead300beff4", "shasum": "" }, "require": { "php": "~7.2|~8.0" }, "require-dev": { - "cucumber/cucumber": "dev-gherkin-16.0.0", + "cucumber/cucumber": "dev-gherkin-22.0.0", "phpunit/phpunit": "~8|~9", - "symfony/phpunit-bridge": "~3|~4|~5", "symfony/yaml": "~3|~4|~5" }, "suggest": { @@ -3378,7 +3374,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "4.x-dev" } }, "autoload": { @@ -3409,22 +3405,22 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.8.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.9.0" }, - "time": "2021-02-04T12:44:21+00:00" + "time": "2021-10-12T13:05:09+00:00" }, { "name": "codeception/codeception", - "version": "4.1.19", + "version": "4.1.22", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "138dc9345a81ec994dcd6b9680c501a752a37b00" + "reference": "9777ec3690ceedc4bce2ed13af7af4ca4ee3088f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/138dc9345a81ec994dcd6b9680c501a752a37b00", - "reference": "138dc9345a81ec994dcd6b9680c501a752a37b00", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/9777ec3690ceedc4bce2ed13af7af4ca4ee3088f", + "reference": "9777ec3690ceedc4bce2ed13af7af4ca4ee3088f", "shasum": "" }, "require": { @@ -3435,7 +3431,7 @@ "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "guzzlehttp/psr7": "~1.4", + "guzzlehttp/psr7": "^1.4 | ^2.0", "php": ">=5.6.0 <9.0", "symfony/console": ">=2.7 <6.0", "symfony/css-selector": ">=2.7 <6.0", @@ -3444,11 +3440,11 @@ "symfony/yaml": ">=2.7 <6.0" }, "require-dev": { - "codeception/module-asserts": "*@dev", - "codeception/module-cli": "*@dev", - "codeception/module-db": "*@dev", - "codeception/module-filesystem": "*@dev", - "codeception/module-phpbrowser": "*@dev", + "codeception/module-asserts": "1.*@dev", + "codeception/module-cli": "1.*@dev", + "codeception/module-db": "1.*@dev", + "codeception/module-filesystem": "1.*@dev", + "codeception/module-phpbrowser": "1.*@dev", "codeception/specify": "~0.3", "codeception/util-universalframework": "*@dev", "monolog/monolog": "~1.8", @@ -3498,7 +3494,7 @@ ], "support": { "issues": "https://github.com/Codeception/Codeception/issues", - "source": "https://github.com/Codeception/Codeception/tree/4.1.19" + "source": "https://github.com/Codeception/Codeception/tree/4.1.22" }, "funding": [ { @@ -3506,7 +3502,7 @@ "type": "open_collective" } ], - "time": "2021-03-28T13:26:08+00:00" + "time": "2021-08-06T17:15:34+00:00" }, { "name": "codeception/lib-asserts", @@ -3564,20 +3560,20 @@ }, { "name": "codeception/lib-innerbrowser", - "version": "1.4.1", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "693e116f81ef98eae98c43ef785a726faf87394e" + "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/693e116f81ef98eae98c43ef785a726faf87394e", - "reference": "693e116f81ef98eae98c43ef785a726faf87394e", + "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", + "reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2", "shasum": "" }, "require": { - "codeception/codeception": "*@dev", + "codeception/codeception": "4.*@dev", "ext-dom": "*", "ext-json": "*", "ext-mbstring": "*", @@ -3618,9 +3614,9 @@ ], "support": { "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.4.1" + "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.1" }, - "time": "2021-03-02T08:01:54+00:00" + "time": "2021-08-30T15:21:42+00:00" }, { "name": "codeception/module-asserts", @@ -3892,25 +3888,78 @@ "time": "2020-11-10T18:47:58+00:00" }, { - "name": "guzzlehttp/guzzle", - "version": "7.3.0", + "name": "getgrav/markdowndocs", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "7008573787b430c1c1f650e3722d9bba59967628" + "url": "https://github.com/getgrav/PHP-Markdown-Documentation-Generator.git", + "reference": "4a24d1b64a88da17e8f1696dc64969f5ca769064" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628", - "reference": "7008573787b430c1c1f650e3722d9bba59967628", + "url": "https://api.github.com/repos/getgrav/PHP-Markdown-Documentation-Generator/zipball/4a24d1b64a88da17e8f1696dc64969f5ca769064", + "reference": "4a24d1b64a88da17e8f1696dc64969f5ca769064", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "symfony/console": ">=2.6" + }, + "require-dev": { + "phpunit/phpunit": "3.7.23" + }, + "bin": [ + "bin/phpdoc-md" + ], + "type": "library", + "autoload": { + "psr-0": { + "PHPDocsMD": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Victor Jonsson", + "email": "kontakt@victorjonsson.se" + }, + { + "name": "Grav CMS", + "email": "hello@getgrav.org", + "homepage": "https://getgrav.org" + } + ], + "description": "Command line tool for generating markdown-formatted class documentation", + "homepage": "https://github.com/victorjonsson/PHP-Markdown-Documentation-Generator", + "support": { + "source": "https://github.com/getgrav/PHP-Markdown-Documentation-Generator/tree/2.0.1" + }, + "time": "2021-04-20T06:04:42+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.4.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "868b3571a039f0ebc11ac8f344f4080babe2cb94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/868b3571a039f0ebc11ac8f344f4080babe2cb94", + "reference": "868b3571a039f0ebc11ac8f344f4080babe2cb94", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.4", - "guzzlehttp/psr7": "^1.7 || ^2.0", + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^1.8.3 || ^2.1", "php": "^7.2.5 || ^8.0", - "psr/http-client": "^1.0" + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2" }, "provide": { "psr/http-client-implementation": "1.0" @@ -3920,7 +3969,7 @@ "ext-curl": "*", "php-http/client-integration-tests": "^3.0", "phpunit/phpunit": "^8.5.5 || ^9.3.5", - "psr/log": "^1.1" + "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { "ext-curl": "Required for CURL handler support", @@ -3930,7 +3979,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "7.3-dev" + "dev-master": "7.4-dev" } }, "autoload": { @@ -3946,19 +3995,43 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, { "name": "Márk Sági-Kazár", "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", "keywords": [ "client", "curl", @@ -3972,7 +4045,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.3.0" + "source": "https://github.com/guzzle/guzzle/tree/7.4.0" }, "funding": [ { @@ -3984,28 +4057,24 @@ "type": "github" }, { - "url": "https://github.com/alexeyshockov", - "type": "github" - }, - { - "url": "https://github.com/gmponos", - "type": "github" + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" } ], - "time": "2021-03-23T11:33:13+00:00" + "time": "2021-10-18T09:52:00+00:00" }, { "name": "guzzlehttp/promises", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + "reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", - "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "url": "https://api.github.com/repos/guzzle/promises/zipball/136a635e2b4a49b9d79e9c8fee267ffb257fdba0", + "reference": "136a635e2b4a49b9d79e9c8fee267ffb257fdba0", "shasum": "" }, "require": { @@ -4017,7 +4086,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -4033,10 +4102,25 @@ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle promises library", @@ -4045,9 +4129,23 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.1" + "source": "https://github.com/guzzle/promises/tree/1.5.0" }, - "time": "2021-03-07T09:25:29+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-07T13:05:22+00:00" }, { "name": "myclabs/deep-copy", @@ -4109,16 +4207,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.10.4", + "version": "v4.13.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" + "reference": "50953a2691a922aa1769461637869a0a2faa3f53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", - "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/50953a2691a922aa1769461637869a0a2faa3f53", + "reference": "50953a2691a922aa1769461637869a0a2faa3f53", "shasum": "" }, "require": { @@ -4159,22 +4257,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.0" }, - "time": "2020-12-20T10:01:03+00:00" + "time": "2021-09-20T12:20:58+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", "shasum": "" }, "require": { @@ -4219,9 +4317,9 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.3" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2021-07-20T11:28:43+00:00" }, { "name": "phar-io/version", @@ -4329,16 +4427,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", "shasum": "" }, "require": { @@ -4349,7 +4447,8 @@ "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2" + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" }, "type": "library", "extra": { @@ -4379,22 +4478,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" }, - "time": "2020-09-03T19:13:55+00:00" + "time": "2021-10-19T17:43:47+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/a12f7e301eb7258bb68acd89d4aefa05c2906cae", + "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae", "shasum": "" }, "require": { @@ -4402,7 +4501,8 @@ "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "*" + "ext-tokenizer": "*", + "psalm/phar": "^4.8" }, "type": "library", "extra": { @@ -4428,39 +4528,39 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.1" }, - "time": "2020-09-17T18:55:26+00:00" + "time": "2021-10-02T14:08:47+00:00" }, { "name": "phpspec/prophecy", - "version": "1.13.0", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e", + "reference": "d86dfc2e2a3cd366cee475e52c6bb3bbc371aa0e", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", + "php": "^7.2 || ~8.0, <8.2", "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { - "phpspec/phpspec": "^6.0", + "phpspec/phpspec": "^6.0 || ^7.0", "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -4495,22 +4595,22 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "source": "https://github.com/phpspec/prophecy/tree/1.14.0" }, - "time": "2021-03-17T13:42:18+00:00" + "time": "2021-09-10T09:02:12+00:00" }, { "name": "phpstan/phpstan", - "version": "0.12.82", + "version": "0.12.99", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11" + "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3920f0fb0aff39263d3a4cb0bca120a67a1a6a11", - "reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b4d40f1d759942f523be267a1bab6884f46ca3f7", + "reference": "b4d40f1d759942f523be267a1bab6884f46ca3f7", "shasum": "" }, "require": { @@ -4541,13 +4641,17 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.82" + "source": "https://github.com/phpstan/phpstan/tree/0.12.99" }, "funding": [ { "url": "https://github.com/ondrejmirtes", "type": "github" }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, { "url": "https://www.patreon.com/phpstan", "type": "patreon" @@ -4557,7 +4661,7 @@ "type": "tidelift" } ], - "time": "2021-03-19T06:08:17+00:00" + "time": "2021-09-12T20:09:55+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -4612,23 +4716,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "version": "9.2.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d4c798ed8d51506800b441f7a13ecb0f76f12218", + "reference": "d4c798ed8d51506800b441f7a13ecb0f76f12218", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", + "nikic/php-parser": "^4.12.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -4677,7 +4781,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.7" }, "funding": [ { @@ -4685,7 +4789,7 @@ "type": "github" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2021-09-17T05:39:03+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4930,16 +5034,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.4", + "version": "9.5.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741" + "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c73c6737305e779771147af66c96ca6a7ed8a741", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c814a05837f2edb0d1471d6e3f4ab3501ca3899a", + "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a", "shasum": "" }, "require": { @@ -4951,11 +5055,11 @@ "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", + "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", + "phpunit/php-code-coverage": "^9.2.7", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -4969,7 +5073,7 @@ "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3", + "sebastian/type": "^2.3.4", "sebastian/version": "^3.0.2" }, "require-dev": { @@ -5017,7 +5121,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.10" }, "funding": [ { @@ -5029,7 +5133,7 @@ "type": "github" } ], - "time": "2021-03-23T07:16:29+00:00" + "time": "2021-09-25T07:38:51+00:00" }, { "name": "psr/http-client", @@ -5589,16 +5693,16 @@ }, { "name": "sebastian/global-state", - "version": "5.0.2", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23bd5951f7ff26f12d4e3242864df3e08dec4e49", + "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49", "shasum": "" }, "require": { @@ -5641,7 +5745,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.3" }, "funding": [ { @@ -5649,7 +5753,7 @@ "type": "github" } ], - "time": "2020-10-26T15:55:19+00:00" + "time": "2021-06-11T13:31:12+00:00" }, { "name": "sebastian/lines-of-code", @@ -5936,20 +6040,21 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { "name": "sebastian/type", - "version": "2.3.1", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", "shasum": "" }, "require": { @@ -5984,7 +6089,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" }, "funding": [ { @@ -5992,7 +6097,7 @@ "type": "github" } ], - "time": "2020-10-26T13:18:59+00:00" + "time": "2021-06-15T12:49:02+00:00" }, { "name": "sebastian/version", @@ -6049,21 +6154,22 @@ }, { "name": "symfony/browser-kit", - "version": "v5.2.4", + "version": "v5.3.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93" + "reference": "c1e3f64fcc631c96e2c5843b666db66679ced11c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3ca3a57ce9860318b20a924fec5daf5c6db44d93", - "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c1e3f64fcc631c96e2c5843b666db66679ced11c", + "reference": "c1e3f64fcc631c96e2c5843b666db66679ced11c", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/dom-crawler": "^4.4|^5.0" + "symfony/dom-crawler": "^4.4|^5.0", + "symfony/polyfill-php80": "^1.16" }, "require-dev": { "symfony/css-selector": "^4.4|^5.0", @@ -6100,7 +6206,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.2.4" + "source": "https://github.com/symfony/browser-kit/tree/v5.3.4" }, "funding": [ { @@ -6116,24 +6222,25 @@ "type": "tidelift" } ], - "time": "2021-02-22T06:48:33+00:00" + "time": "2021-07-21T12:40:44+00:00" }, { "name": "symfony/css-selector", - "version": "v5.2.4", + "version": "v5.3.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "f65f217b3314504a1ec99c2d6ef69016bb13490f" + "reference": "7fb120adc7f600a59027775b224c13a33530dd90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/f65f217b3314504a1ec99c2d6ef69016bb13490f", - "reference": "f65f217b3314504a1ec99c2d6ef69016bb13490f", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/7fb120adc7f600a59027775b224c13a33530dd90", + "reference": "7fb120adc7f600a59027775b224c13a33530dd90", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -6165,7 +6272,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.2.4" + "source": "https://github.com/symfony/css-selector/tree/v5.3.4" }, "funding": [ { @@ -6181,27 +6288,95 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:01:46+00:00" + "time": "2021-07-21T12:38:00+00:00" }, { - "name": "symfony/dom-crawler", - "version": "v5.2.4", + "name": "symfony/deprecation-contracts", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "400e265163f65aceee7e904ef532e15228de674b" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/400e265163f65aceee7e904ef532e15228de674b", - "reference": "400e265163f65aceee7e904ef532e15228de674b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-03-23T23:28:01+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v5.3.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", + "reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.15" + "symfony/polyfill-php80": "^1.16" }, "conflict": { "masterminds/html5": "<2.6" @@ -6239,7 +6414,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.2.4" + "source": "https://github.com/symfony/dom-crawler/tree/v5.3.7" }, "funding": [ { @@ -6255,24 +6430,25 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2021-08-29T19:32:13+00:00" }, { "name": "symfony/finder", - "version": "v5.2.4", + "version": "v5.3.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0d639a0943822626290d169965804f79400e6a04" + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", - "reference": "0d639a0943822626290d169965804f79400e6a04", + "url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93", + "reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -6300,7 +6476,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.4" + "source": "https://github.com/symfony/finder/tree/v5.3.7" }, "funding": [ { @@ -6316,20 +6492,20 @@ "type": "tidelift" } ], - "time": "2021-02-15T18:55:04+00:00" + "time": "2021-08-04T21:20:46+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", "shasum": "" }, "require": { @@ -6358,7 +6534,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" }, "funding": [ { @@ -6366,54 +6542,7 @@ "type": "github" } ], - "time": "2020-07-12T23:59:07+00:00" - }, - { - "name": "victorjonsson/markdowndocs", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator.git", - "reference": "c9fa153b28a79f5da89ec32aa501be92db212aed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/trilbymedia/PHP-Markdown-Documentation-Generator/zipball/c9fa153b28a79f5da89ec32aa501be92db212aed", - "reference": "c9fa153b28a79f5da89ec32aa501be92db212aed", - "shasum": "" - }, - "require": { - "php": ">=5.5.0", - "symfony/console": ">=2.6" - }, - "require-dev": { - "phpunit/phpunit": "3.7.23" - }, - "default-branch": true, - "bin": [ - "bin/phpdoc-md" - ], - "type": "library", - "autoload": { - "psr-0": { - "PHPDocsMD": "src/" - } - }, - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Victor Jonsson", - "email": "kontakt@victorjonsson.se" - } - ], - "description": "Command line tool for generating markdown-formatted class documentation", - "homepage": "https://github.com/victorjonsson/PHP-Markdown-Documentation-Generator", - "support": { - "source": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator/tree/master" - }, - "time": "2017-09-20T13:29:22+00:00" + "time": "2021-07-28T10:34:58+00:00" }, { "name": "webmozart/assert", @@ -6476,11 +6605,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "gregwar/image": 20, - "gregwar/cache": 20, - "victorjonsson/markdowndocs": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/index.php b/index.php index 9842952d5..091e9a824 100644 --- a/index.php +++ b/index.php @@ -17,8 +17,8 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) { } if (PHP_SAPI === 'cli-server') { - $symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'symfony -') !== false; + $symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false; + if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) { die("PHP webserver requires a router to run Grav, please use:
php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php
"); } diff --git a/system/blueprints/config/scheduler.yaml b/system/blueprints/config/scheduler.yaml index caa771167..a8dce314b 100644 --- a/system/blueprints/config/scheduler.yaml +++ b/system/blueprints/config/scheduler.yaml @@ -47,7 +47,8 @@ form: label: PLUGIN_ADMIN.EXTRA_ARGUMENTS placeholder: '-lah' .at: - type: cron + type: text + wrapper_classes: cron-selector label: PLUGIN_ADMIN.SCHEDULER_RUNAT help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP placeholder: '* * * * *' diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 84c1dcc05..1aeaf2bf2 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -646,7 +646,7 @@ form: type: toggle label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP - highlight: 1 + highlight: 0 options: 1: PLUGIN_ADMIN.YES 0: PLUGIN_ADMIN.NO @@ -1446,6 +1446,10 @@ form: title: PLUGIN_ADMIN.ADVANCED underline: true + gpm_section: + type: section + title: PLUGIN_ADMIN.GPM_SECTION + gpm.releases: type: toggle label: PLUGIN_ADMIN.GPM_RELEASES @@ -1455,23 +1459,6 @@ form: stable: PLUGIN_ADMIN.STABLE testing: PLUGIN_ADMIN.TESTING - gpm.proxy_url: - type: text - size: medium - placeholder: "e.g. 127.0.0.1:3128" - label: PLUGIN_ADMIN.PROXY_URL - help: PLUGIN_ADMIN.PROXY_URL_HELP - - gpm.method: - type: toggle - label: PLUGIN_ADMIN.GPM_METHOD - highlight: auto - help: PLUGIN_ADMIN.GPM_METHOD_HELP - options: - auto: PLUGIN_ADMIN.AUTO - fopen: PLUGIN_ADMIN.FOPEN - curl: PLUGIN_ADMIN.CURL - gpm.official_gpm_only: type: toggle label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY @@ -1484,17 +1471,80 @@ form: validate: type: bool - gpm.verify_peer: + http_section: + type: section + title: PLUGIN_ADMIN.HTTP_SECTION + + http.method: type: toggle - label: PLUGIN_ADMIN.GPM_VERIFY_PEER + label: PLUGIN_ADMIN.GPM_METHOD + highlight: auto + help: PLUGIN_ADMIN.GPM_METHOD_HELP + options: + auto: PLUGIN_ADMIN.AUTO + fopen: PLUGIN_ADMIN.FOPEN + curl: PLUGIN_ADMIN.CURL + + http.enable_proxy: + type: toggle + label: PLUGIN_ADMIN.SSL_ENABLE_PROXY highlight: 1 - help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: false + validate: + type: bool + + http.proxy_url: + type: text + size: medium + placeholder: "e.g. 127.0.0.1:3128" + label: PLUGIN_ADMIN.PROXY_URL + help: PLUGIN_ADMIN.PROXY_URL_HELP + + http.proxy_cert_path: + type: text + size: medium + placeholder: "e.g. /Users/bob/certs/" + label: PLUGIN_ADMIN.PROXY_CERT + help: PLUGIN_ADMIN.PROXY_CERT_HELP + + http.verify_peer: + type: toggle + label: PLUGIN_ADMIN.SSL_VERIFY_PEER + highlight: 1 + help: PLUGIN_ADMIN.SSL_VERIFY_PEER_HELP options: 1: PLUGIN_ADMIN.YES 0: PLUGIN_ADMIN.NO validate: type: bool + http.verify_host: + type: toggle + label: PLUGIN_ADMIN.SSL_VERIFY_HOST + highlight: 1 + help: PLUGIN_ADMIN.SSL_VERIFY_HOST_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + http.concurrent_connections: + type: number + size: x-small + label: PLUGIN_ADMIN.HTTP_CONNECTIONS + help: PLUGIN_ADMIN.HTTP_CONNECTIONS_HELP + validate: + min: 1 + max: 20 + + misc_section: + type: section + title: PLUGIN_ADMIN.MISC_SECTION + reverse_proxy_setup: type: toggle label: PLUGIN_ADMIN.REVERSE_PROXY diff --git a/system/blueprints/flex/pages.yaml b/system/blueprints/flex/pages.yaml index ee2e7e5fa..5c6ed8eb5 100644 --- a/system/blueprints/flex/pages.yaml +++ b/system/blueprints/flex/pages.yaml @@ -184,9 +184,9 @@ config: # Fields to be searched fields: - key + - slug - menu - title - - name blueprints: configure: diff --git a/system/blueprints/flex/user-groups.yaml b/system/blueprints/flex/user-groups.yaml index a09a821b5..05ba87b73 100644 --- a/system/blueprints/flex/user-groups.yaml +++ b/system/blueprints/flex/user-groups.yaml @@ -18,6 +18,7 @@ config: configure: path: '/accounts/configure' redirects: + '/groups': '/accounts/groups' '/accounts': '/accounts/groups' # Permissions diff --git a/system/blueprints/pages/default.yaml b/system/blueprints/pages/default.yaml index 0e7b8c374..f3ef29f5e 100644 --- a/system/blueprints/pages/default.yaml +++ b/system/blueprints/pages/default.yaml @@ -121,7 +121,7 @@ form: underline: true folder: - type: text + type: folder-slug label: PLUGIN_ADMIN.FOLDER_NAME validate: rule: slug diff --git a/system/config/media.yaml b/system/config/media.yaml index e5439d6d8..b118a210e 100644 --- a/system/config/media.yaml +++ b/system/config/media.yaml @@ -28,6 +28,10 @@ types: type: image thumb: media/thumb-webp.png mime: image/webp + avif: + type: image + thumb: media/thumb.png + mime: image/avif gif: type: animated thumb: media/thumb-gif.png @@ -91,7 +95,7 @@ types: aif: type: audio thumb: media/thumb-aif.png - mime: audio/aif + mime: audio/aiff txt: type: file thumb: media/thumb-txt.png @@ -207,7 +211,7 @@ types: js: type: file thumb: media/thumb-js.png - mime: application/javascript + mime: text/javascript json: type: file thumb: media/thumb-json.png diff --git a/system/config/mime.yaml b/system/config/mime.yaml new file mode 100644 index 000000000..3143c6733 --- /dev/null +++ b/system/config/mime.yaml @@ -0,0 +1,1986 @@ +types: + '123': + - application/vnd.lotus-1-2-3 + wof: + - application/font-woff + php: + - application/php + - application/x-httpd-php + - application/x-httpd-php-source + - application/x-php + - text/php + - text/x-php + otf: + - application/x-font-otf + - font/otf + ttf: + - application/x-font-ttf + - font/ttf + ttc: + - application/x-font-ttf + - font/collection + zip: + - application/x-gzip + - application/zip + - application/x-zip-compressed + amr: + - audio/amr + mp3: + - audio/mpeg + mpga: + - audio/mpeg + mp2: + - audio/mpeg + mp2a: + - audio/mpeg + m2a: + - audio/mpeg + m3a: + - audio/mpeg + jpg: + - image/jpeg + jpeg: + - image/jpeg + jpe: + - image/jpeg + bmp: + - image/x-ms-bmp + - image/bmp + ez: + - application/andrew-inset + aw: + - application/applixware + atom: + - application/atom+xml + atomcat: + - application/atomcat+xml + atomsvc: + - application/atomsvc+xml + ccxml: + - application/ccxml+xml + cdmia: + - application/cdmi-capability + cdmic: + - application/cdmi-container + cdmid: + - application/cdmi-domain + cdmio: + - application/cdmi-object + cdmiq: + - application/cdmi-queue + cu: + - application/cu-seeme + davmount: + - application/davmount+xml + dbk: + - application/docbook+xml + dssc: + - application/dssc+der + xdssc: + - application/dssc+xml + ecma: + - application/ecmascript + emma: + - application/emma+xml + epub: + - application/epub+zip + exi: + - application/exi + pfr: + - application/font-tdpfr + gml: + - application/gml+xml + gpx: + - application/gpx+xml + gxf: + - application/gxf + stk: + - application/hyperstudio + ink: + - application/inkml+xml + inkml: + - application/inkml+xml + ipfix: + - application/ipfix + jar: + - application/java-archive + ser: + - application/java-serialized-object + class: + - application/java-vm + js: + - application/javascript + json: + - application/json + jsonml: + - application/jsonml+json + lostxml: + - application/lost+xml + hqx: + - application/mac-binhex40 + cpt: + - application/mac-compactpro + mads: + - application/mads+xml + mrc: + - application/marc + mrcx: + - application/marcxml+xml + ma: + - application/mathematica + nb: + - application/mathematica + mb: + - application/mathematica + mathml: + - application/mathml+xml + mbox: + - application/mbox + mscml: + - application/mediaservercontrol+xml + metalink: + - application/metalink+xml + meta4: + - application/metalink4+xml + mets: + - application/mets+xml + mods: + - application/mods+xml + m21: + - application/mp21 + mp21: + - application/mp21 + mp4s: + - application/mp4 + doc: + - application/msword + dot: + - application/msword + mxf: + - application/mxf + bin: + - application/octet-stream + dms: + - application/octet-stream + lrf: + - application/octet-stream + mar: + - application/octet-stream + so: + - application/octet-stream + dist: + - application/octet-stream + distz: + - application/octet-stream + pkg: + - application/octet-stream + bpk: + - application/octet-stream + dump: + - application/octet-stream + elc: + - application/octet-stream + deploy: + - application/octet-stream + oda: + - application/oda + opf: + - application/oebps-package+xml + ogx: + - application/ogg + omdoc: + - application/omdoc+xml + onetoc: + - application/onenote + onetoc2: + - application/onenote + onetmp: + - application/onenote + onepkg: + - application/onenote + oxps: + - application/oxps + xer: + - application/patch-ops-error+xml + pdf: + - application/pdf + pgp: + - application/pgp-encrypted + asc: + - application/pgp-signature + sig: + - application/pgp-signature + prf: + - application/pics-rules + p10: + - application/pkcs10 + p7m: + - application/pkcs7-mime + p7c: + - application/pkcs7-mime + p7s: + - application/pkcs7-signature + p8: + - application/pkcs8 + ac: + - application/pkix-attr-cert + cer: + - application/pkix-cert + crl: + - application/pkix-crl + pkipath: + - application/pkix-pkipath + pki: + - application/pkixcmp + pls: + - application/pls+xml + ai: + - application/postscript + eps: + - application/postscript + ps: + - application/postscript + cww: + - application/prs.cww + pskcxml: + - application/pskc+xml + rdf: + - application/rdf+xml + rif: + - application/reginfo+xml + rnc: + - application/relax-ng-compact-syntax + rl: + - application/resource-lists+xml + rld: + - application/resource-lists-diff+xml + rs: + - application/rls-services+xml + gbr: + - application/rpki-ghostbusters + mft: + - application/rpki-manifest + roa: + - application/rpki-roa + rsd: + - application/rsd+xml + rss: + - application/rss+xml + rtf: + - application/rtf + sbml: + - application/sbml+xml + scq: + - application/scvp-cv-request + scs: + - application/scvp-cv-response + spq: + - application/scvp-vp-request + spp: + - application/scvp-vp-response + sdp: + - application/sdp + setpay: + - application/set-payment-initiation + setreg: + - application/set-registration-initiation + shf: + - application/shf+xml + smi: + - application/smil+xml + smil: + - application/smil+xml + rq: + - application/sparql-query + srx: + - application/sparql-results+xml + gram: + - application/srgs + grxml: + - application/srgs+xml + sru: + - application/sru+xml + ssdl: + - application/ssdl+xml + ssml: + - application/ssml+xml + tei: + - application/tei+xml + teicorpus: + - application/tei+xml + tfi: + - application/thraud+xml + tsd: + - application/timestamped-data + plb: + - application/vnd.3gpp.pic-bw-large + psb: + - application/vnd.3gpp.pic-bw-small + pvb: + - application/vnd.3gpp.pic-bw-var + tcap: + - application/vnd.3gpp2.tcap + pwn: + - application/vnd.3m.post-it-notes + aso: + - application/vnd.accpac.simply.aso + imp: + - application/vnd.accpac.simply.imp + acu: + - application/vnd.acucobol + atc: + - application/vnd.acucorp + acutc: + - application/vnd.acucorp + air: + - application/vnd.adobe.air-application-installer-package+zip + fcdt: + - application/vnd.adobe.formscentral.fcdt + fxp: + - application/vnd.adobe.fxp + fxpl: + - application/vnd.adobe.fxp + xdp: + - application/vnd.adobe.xdp+xml + xfdf: + - application/vnd.adobe.xfdf + ahead: + - application/vnd.ahead.space + azf: + - application/vnd.airzip.filesecure.azf + azs: + - application/vnd.airzip.filesecure.azs + azw: + - application/vnd.amazon.ebook + acc: + - application/vnd.americandynamics.acc + ami: + - application/vnd.amiga.ami + apk: + - application/vnd.android.package-archive + cii: + - application/vnd.anser-web-certificate-issue-initiation + fti: + - application/vnd.anser-web-funds-transfer-initiation + atx: + - application/vnd.antix.game-component + mpkg: + - application/vnd.apple.installer+xml + m3u8: + - application/vnd.apple.mpegurl + swi: + - application/vnd.aristanetworks.swi + iota: + - application/vnd.astraea-software.iota + aep: + - application/vnd.audiograph + mpm: + - application/vnd.blueice.multipass + bmi: + - application/vnd.bmi + rep: + - application/vnd.businessobjects + cdxml: + - application/vnd.chemdraw+xml + mmd: + - application/vnd.chipnuts.karaoke-mmd + cdy: + - application/vnd.cinderella + cla: + - application/vnd.claymore + rp9: + - application/vnd.cloanto.rp9 + c4g: + - application/vnd.clonk.c4group + c4d: + - application/vnd.clonk.c4group + c4f: + - application/vnd.clonk.c4group + c4p: + - application/vnd.clonk.c4group + c4u: + - application/vnd.clonk.c4group + c11amc: + - application/vnd.cluetrust.cartomobile-config + c11amz: + - application/vnd.cluetrust.cartomobile-config-pkg + csp: + - application/vnd.commonspace + cdbcmsg: + - application/vnd.contact.cmsg + cmc: + - application/vnd.cosmocaller + clkx: + - application/vnd.crick.clicker + clkk: + - application/vnd.crick.clicker.keyboard + clkp: + - application/vnd.crick.clicker.palette + clkt: + - application/vnd.crick.clicker.template + clkw: + - application/vnd.crick.clicker.wordbank + wbs: + - application/vnd.criticaltools.wbs+xml + pml: + - application/vnd.ctc-posml + ppd: + - application/vnd.cups-ppd + car: + - application/vnd.curl.car + pcurl: + - application/vnd.curl.pcurl + dart: + - application/vnd.dart + rdz: + - application/vnd.data-vision.rdz + uvf: + - application/vnd.dece.data + uvvf: + - application/vnd.dece.data + uvd: + - application/vnd.dece.data + uvvd: + - application/vnd.dece.data + uvt: + - application/vnd.dece.ttml+xml + uvvt: + - application/vnd.dece.ttml+xml + uvx: + - application/vnd.dece.unspecified + uvvx: + - application/vnd.dece.unspecified + uvz: + - application/vnd.dece.zip + uvvz: + - application/vnd.dece.zip + fe_launch: + - application/vnd.denovo.fcselayout-link + dna: + - application/vnd.dna + mlp: + - application/vnd.dolby.mlp + dpg: + - application/vnd.dpgraph + dfac: + - application/vnd.dreamfactory + kpxx: + - application/vnd.ds-keypoint + ait: + - application/vnd.dvb.ait + svc: + - application/vnd.dvb.service + geo: + - application/vnd.dynageo + mag: + - application/vnd.ecowin.chart + nml: + - application/vnd.enliven + esf: + - application/vnd.epson.esf + msf: + - application/vnd.epson.msf + qam: + - application/vnd.epson.quickanime + slt: + - application/vnd.epson.salt + ssf: + - application/vnd.epson.ssf + es3: + - application/vnd.eszigno3+xml + et3: + - application/vnd.eszigno3+xml + ez2: + - application/vnd.ezpix-album + ez3: + - application/vnd.ezpix-package + fdf: + - application/vnd.fdf + mseed: + - application/vnd.fdsn.mseed + seed: + - application/vnd.fdsn.seed + dataless: + - application/vnd.fdsn.seed + gph: + - application/vnd.flographit + ftc: + - application/vnd.fluxtime.clip + fm: + - application/vnd.framemaker + frame: + - application/vnd.framemaker + maker: + - application/vnd.framemaker + book: + - application/vnd.framemaker + fnc: + - application/vnd.frogans.fnc + ltf: + - application/vnd.frogans.ltf + fsc: + - application/vnd.fsc.weblaunch + oas: + - application/vnd.fujitsu.oasys + oa2: + - application/vnd.fujitsu.oasys2 + oa3: + - application/vnd.fujitsu.oasys3 + fg5: + - application/vnd.fujitsu.oasysgp + bh2: + - application/vnd.fujitsu.oasysprs + ddd: + - application/vnd.fujixerox.ddd + xdw: + - application/vnd.fujixerox.docuworks + xbd: + - application/vnd.fujixerox.docuworks.binder + fzs: + - application/vnd.fuzzysheet + txd: + - application/vnd.genomatix.tuxedo + ggb: + - application/vnd.geogebra.file + ggt: + - application/vnd.geogebra.tool + gex: + - application/vnd.geometry-explorer + gre: + - application/vnd.geometry-explorer + gxt: + - application/vnd.geonext + g2w: + - application/vnd.geoplan + g3w: + - application/vnd.geospace + gmx: + - application/vnd.gmx + kml: + - application/vnd.google-earth.kml+xml + kmz: + - application/vnd.google-earth.kmz + gqf: + - application/vnd.grafeq + gqs: + - application/vnd.grafeq + gac: + - application/vnd.groove-account + ghf: + - application/vnd.groove-help + gim: + - application/vnd.groove-identity-message + grv: + - application/vnd.groove-injector + gtm: + - application/vnd.groove-tool-message + tpl: + - application/vnd.groove-tool-template + vcg: + - application/vnd.groove-vcard + hal: + - application/vnd.hal+xml + zmm: + - application/vnd.handheld-entertainment+xml + hbci: + - application/vnd.hbci + les: + - application/vnd.hhe.lesson-player + hpgl: + - application/vnd.hp-hpgl + hpid: + - application/vnd.hp-hpid + hps: + - application/vnd.hp-hps + jlt: + - application/vnd.hp-jlyt + pcl: + - application/vnd.hp-pcl + pclxl: + - application/vnd.hp-pclxl + sfd-hdstx: + - application/vnd.hydrostatix.sof-data + mpy: + - application/vnd.ibm.minipay + afp: + - application/vnd.ibm.modcap + listafp: + - application/vnd.ibm.modcap + list3820: + - application/vnd.ibm.modcap + irm: + - application/vnd.ibm.rights-management + sc: + - application/vnd.ibm.secure-container + icc: + - application/vnd.iccprofile + icm: + - application/vnd.iccprofile + igl: + - application/vnd.igloader + ivp: + - application/vnd.immervision-ivp + ivu: + - application/vnd.immervision-ivu + igm: + - application/vnd.insors.igm + xpw: + - application/vnd.intercon.formnet + xpx: + - application/vnd.intercon.formnet + i2g: + - application/vnd.intergeo + qbo: + - application/vnd.intu.qbo + qfx: + - application/vnd.intu.qfx + rcprofile: + - application/vnd.ipunplugged.rcprofile + irp: + - application/vnd.irepository.package+xml + xpr: + - application/vnd.is-xpr + fcs: + - application/vnd.isac.fcs + jam: + - application/vnd.jam + rms: + - application/vnd.jcp.javame.midlet-rms + jisp: + - application/vnd.jisp + joda: + - application/vnd.joost.joda-archive + ktz: + - application/vnd.kahootz + ktr: + - application/vnd.kahootz + karbon: + - application/vnd.kde.karbon + chrt: + - application/vnd.kde.kchart + kfo: + - application/vnd.kde.kformula + flw: + - application/vnd.kde.kivio + kon: + - application/vnd.kde.kontour + kpr: + - application/vnd.kde.kpresenter + kpt: + - application/vnd.kde.kpresenter + ksp: + - application/vnd.kde.kspread + kwd: + - application/vnd.kde.kword + kwt: + - application/vnd.kde.kword + htke: + - application/vnd.kenameaapp + kia: + - application/vnd.kidspiration + kne: + - application/vnd.kinar + knp: + - application/vnd.kinar + skp: + - application/vnd.koan + skd: + - application/vnd.koan + skt: + - application/vnd.koan + skm: + - application/vnd.koan + sse: + - application/vnd.kodak-descriptor + lasxml: + - application/vnd.las.las+xml + lbd: + - application/vnd.llamagraphics.life-balance.desktop + lbe: + - application/vnd.llamagraphics.life-balance.exchange+xml + apr: + - application/vnd.lotus-approach + pre: + - application/vnd.lotus-freelance + nsf: + - application/vnd.lotus-notes + org: + - application/vnd.lotus-organizer + scm: + - application/vnd.lotus-screencam + lwp: + - application/vnd.lotus-wordpro + portpkg: + - application/vnd.macports.portpkg + mcd: + - application/vnd.mcd + mc1: + - application/vnd.medcalcdata + cdkey: + - application/vnd.mediastation.cdkey + mwf: + - application/vnd.mfer + mfm: + - application/vnd.mfmp + flo: + - application/vnd.micrografx.flo + igx: + - application/vnd.micrografx.igx + mif: + - application/vnd.mif + daf: + - application/vnd.mobius.daf + dis: + - application/vnd.mobius.dis + mbk: + - application/vnd.mobius.mbk + mqy: + - application/vnd.mobius.mqy + msl: + - application/vnd.mobius.msl + plc: + - application/vnd.mobius.plc + txf: + - application/vnd.mobius.txf + mpn: + - application/vnd.mophun.application + mpc: + - application/vnd.mophun.certificate + xul: + - application/vnd.mozilla.xul+xml + cil: + - application/vnd.ms-artgalry + cab: + - application/vnd.ms-cab-compressed + xls: + - application/vnd.ms-excel + xlm: + - application/vnd.ms-excel + xla: + - application/vnd.ms-excel + xlc: + - application/vnd.ms-excel + xlt: + - application/vnd.ms-excel + xlw: + - application/vnd.ms-excel + xlam: + - application/vnd.ms-excel.addin.macroenabled.12 + xlsb: + - application/vnd.ms-excel.sheet.binary.macroenabled.12 + xlsm: + - application/vnd.ms-excel.sheet.macroenabled.12 + xltm: + - application/vnd.ms-excel.template.macroenabled.12 + eot: + - application/vnd.ms-fontobject + chm: + - application/vnd.ms-htmlhelp + ims: + - application/vnd.ms-ims + lrm: + - application/vnd.ms-lrm + thmx: + - application/vnd.ms-officetheme + cat: + - application/vnd.ms-pki.seccat + stl: + - application/vnd.ms-pki.stl + ppt: + - application/vnd.ms-powerpoint + pps: + - application/vnd.ms-powerpoint + pot: + - application/vnd.ms-powerpoint + ppam: + - application/vnd.ms-powerpoint.addin.macroenabled.12 + pptm: + - application/vnd.ms-powerpoint.presentation.macroenabled.12 + sldm: + - application/vnd.ms-powerpoint.slide.macroenabled.12 + ppsm: + - application/vnd.ms-powerpoint.slideshow.macroenabled.12 + potm: + - application/vnd.ms-powerpoint.template.macroenabled.12 + mpp: + - application/vnd.ms-project + mpt: + - application/vnd.ms-project + docm: + - application/vnd.ms-word.document.macroenabled.12 + dotm: + - application/vnd.ms-word.template.macroenabled.12 + wps: + - application/vnd.ms-works + wks: + - application/vnd.ms-works + wcm: + - application/vnd.ms-works + wdb: + - application/vnd.ms-works + wpl: + - application/vnd.ms-wpl + xps: + - application/vnd.ms-xpsdocument + mseq: + - application/vnd.mseq + mus: + - application/vnd.musician + msty: + - application/vnd.muvee.style + taglet: + - application/vnd.mynfc + nlu: + - application/vnd.neurolanguage.nlu + ntf: + - application/vnd.nitf + nitf: + - application/vnd.nitf + nnd: + - application/vnd.noblenet-directory + nns: + - application/vnd.noblenet-sealer + nnw: + - application/vnd.noblenet-web + ngdat: + - application/vnd.nokia.n-gage.data + n-gage: + - application/vnd.nokia.n-gage.symbian.install + rpst: + - application/vnd.nokia.radio-preset + rpss: + - application/vnd.nokia.radio-presets + edm: + - application/vnd.novadigm.edm + edx: + - application/vnd.novadigm.edx + ext: + - application/vnd.novadigm.ext + odc: + - application/vnd.oasis.opendocument.chart + otc: + - application/vnd.oasis.opendocument.chart-template + odb: + - application/vnd.oasis.opendocument.database + odf: + - application/vnd.oasis.opendocument.formula + odft: + - application/vnd.oasis.opendocument.formula-template + odg: + - application/vnd.oasis.opendocument.graphics + otg: + - application/vnd.oasis.opendocument.graphics-template + odi: + - application/vnd.oasis.opendocument.image + oti: + - application/vnd.oasis.opendocument.image-template + odp: + - application/vnd.oasis.opendocument.presentation + otp: + - application/vnd.oasis.opendocument.presentation-template + ods: + - application/vnd.oasis.opendocument.spreadsheet + ots: + - application/vnd.oasis.opendocument.spreadsheet-template + odt: + - application/vnd.oasis.opendocument.text + odm: + - application/vnd.oasis.opendocument.text-master + ott: + - application/vnd.oasis.opendocument.text-template + oth: + - application/vnd.oasis.opendocument.text-web + xo: + - application/vnd.olpc-sugar + dd2: + - application/vnd.oma.dd2+xml + oxt: + - application/vnd.openofficeorg.extension + pptx: + - application/vnd.openxmlformats-officedocument.presentationml.presentation + sldx: + - application/vnd.openxmlformats-officedocument.presentationml.slide + ppsx: + - application/vnd.openxmlformats-officedocument.presentationml.slideshow + potx: + - application/vnd.openxmlformats-officedocument.presentationml.template + xlsx: + - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xltx: + - application/vnd.openxmlformats-officedocument.spreadsheetml.template + docx: + - application/vnd.openxmlformats-officedocument.wordprocessingml.document + dotx: + - application/vnd.openxmlformats-officedocument.wordprocessingml.template + mgp: + - application/vnd.osgeo.mapguide.package + dp: + - application/vnd.osgi.dp + esa: + - application/vnd.osgi.subsystem + pdb: + - application/vnd.palm + pqa: + - application/vnd.palm + oprc: + - application/vnd.palm + paw: + - application/vnd.pawaafile + str: + - application/vnd.pg.format + ei6: + - application/vnd.pg.osasli + efif: + - application/vnd.picsel + wg: + - application/vnd.pmi.widget + plf: + - application/vnd.pocketlearn + pbd: + - application/vnd.powerbuilder6 + box: + - application/vnd.previewsystems.box + mgz: + - application/vnd.proteus.magazine + qps: + - application/vnd.publishare-delta-tree + ptid: + - application/vnd.pvi.ptid1 + qxd: + - application/vnd.quark.quarkxpress + qxt: + - application/vnd.quark.quarkxpress + qwd: + - application/vnd.quark.quarkxpress + qwt: + - application/vnd.quark.quarkxpress + qxl: + - application/vnd.quark.quarkxpress + qxb: + - application/vnd.quark.quarkxpress + bed: + - application/vnd.realvnc.bed + mxl: + - application/vnd.recordare.musicxml + musicxml: + - application/vnd.recordare.musicxml+xml + cryptonote: + - application/vnd.rig.cryptonote + cod: + - application/vnd.rim.cod + rm: + - application/vnd.rn-realmedia + rmvb: + - application/vnd.rn-realmedia-vbr + link66: + - application/vnd.route66.link66+xml + st: + - application/vnd.sailingtracker.track + see: + - application/vnd.seemail + sema: + - application/vnd.sema + semd: + - application/vnd.semd + semf: + - application/vnd.semf + ifm: + - application/vnd.shana.informed.formdata + itp: + - application/vnd.shana.informed.formtemplate + iif: + - application/vnd.shana.informed.interchange + ipk: + - application/vnd.shana.informed.package + twd: + - application/vnd.simtech-mindmapper + twds: + - application/vnd.simtech-mindmapper + mmf: + - application/vnd.smaf + teacher: + - application/vnd.smart.teacher + sdkm: + - application/vnd.solent.sdkm+xml + sdkd: + - application/vnd.solent.sdkm+xml + dxp: + - application/vnd.spotfire.dxp + sfs: + - application/vnd.spotfire.sfs + sdc: + - application/vnd.stardivision.calc + sda: + - application/vnd.stardivision.draw + sdd: + - application/vnd.stardivision.impress + smf: + - application/vnd.stardivision.math + sdw: + - application/vnd.stardivision.writer + vor: + - application/vnd.stardivision.writer + sgl: + - application/vnd.stardivision.writer-global + smzip: + - application/vnd.stepmania.package + sm: + - application/vnd.stepmania.stepchart + sxc: + - application/vnd.sun.xml.calc + stc: + - application/vnd.sun.xml.calc.template + sxd: + - application/vnd.sun.xml.draw + std: + - application/vnd.sun.xml.draw.template + sxi: + - application/vnd.sun.xml.impress + sti: + - application/vnd.sun.xml.impress.template + sxm: + - application/vnd.sun.xml.math + sxw: + - application/vnd.sun.xml.writer + sxg: + - application/vnd.sun.xml.writer.global + stw: + - application/vnd.sun.xml.writer.template + sus: + - application/vnd.sus-calendar + susp: + - application/vnd.sus-calendar + svd: + - application/vnd.svd + sis: + - application/vnd.symbian.install + sisx: + - application/vnd.symbian.install + xsm: + - application/vnd.syncml+xml + bdm: + - application/vnd.syncml.dm+wbxml + xdm: + - application/vnd.syncml.dm+xml + tao: + - application/vnd.tao.intent-module-archive + pcap: + - application/vnd.tcpdump.pcap + cap: + - application/vnd.tcpdump.pcap + dmp: + - application/vnd.tcpdump.pcap + tmo: + - application/vnd.tmobile-livetv + tpt: + - application/vnd.trid.tpt + mxs: + - application/vnd.triscape.mxs + tra: + - application/vnd.trueapp + ufd: + - application/vnd.ufdl + ufdl: + - application/vnd.ufdl + utz: + - application/vnd.uiq.theme + umj: + - application/vnd.umajin + unityweb: + - application/vnd.unity + uoml: + - application/vnd.uoml+xml + vcx: + - application/vnd.vcx + vsd: + - application/vnd.visio + vst: + - application/vnd.visio + vss: + - application/vnd.visio + vsw: + - application/vnd.visio + vis: + - application/vnd.visionary + vsf: + - application/vnd.vsf + wbxml: + - application/vnd.wap.wbxml + wmlc: + - application/vnd.wap.wmlc + wmlsc: + - application/vnd.wap.wmlscriptc + wtb: + - application/vnd.webturbo + nbp: + - application/vnd.wolfram.player + wpd: + - application/vnd.wordperfect + wqd: + - application/vnd.wqd + stf: + - application/vnd.wt.stf + xar: + - application/vnd.xara + xfdl: + - application/vnd.xfdl + hvd: + - application/vnd.yamaha.hv-dic + hvs: + - application/vnd.yamaha.hv-script + hvp: + - application/vnd.yamaha.hv-voice + osf: + - application/vnd.yamaha.openscoreformat + osfpvg: + - application/vnd.yamaha.openscoreformat.osfpvg+xml + saf: + - application/vnd.yamaha.smaf-audio + spf: + - application/vnd.yamaha.smaf-phrase + cmp: + - application/vnd.yellowriver-custom-menu + zir: + - application/vnd.zul + zirz: + - application/vnd.zul + zaz: + - application/vnd.zzazz.deck+xml + vxml: + - application/voicexml+xml + wgt: + - application/widget + hlp: + - application/winhlp + wsdl: + - application/wsdl+xml + wspolicy: + - application/wspolicy+xml + 7z: + - application/x-7z-compressed + abw: + - application/x-abiword + ace: + - application/x-ace-compressed + dmg: + - application/x-apple-diskimage + aab: + - application/x-authorware-bin + x32: + - application/x-authorware-bin + u32: + - application/x-authorware-bin + vox: + - application/x-authorware-bin + aam: + - application/x-authorware-map + aas: + - application/x-authorware-seg + bcpio: + - application/x-bcpio + torrent: + - application/x-bittorrent + blb: + - application/x-blorb + blorb: + - application/x-blorb + bz: + - application/x-bzip + bz2: + - application/x-bzip2 + boz: + - application/x-bzip2 + cbr: + - application/x-cbr + cba: + - application/x-cbr + cbt: + - application/x-cbr + cbz: + - application/x-cbr + cb7: + - application/x-cbr + vcd: + - application/x-cdlink + cfs: + - application/x-cfs-compressed + chat: + - application/x-chat + pgn: + - application/x-chess-pgn + nsc: + - application/x-conference + cpio: + - application/x-cpio + csh: + - application/x-csh + deb: + - application/x-debian-package + udeb: + - application/x-debian-package + dgc: + - application/x-dgc-compressed + dir: + - application/x-director + dcr: + - application/x-director + dxr: + - application/x-director + cst: + - application/x-director + cct: + - application/x-director + cxt: + - application/x-director + w3d: + - application/x-director + fgd: + - application/x-director + swa: + - application/x-director + wad: + - application/x-doom + ncx: + - application/x-dtbncx+xml + dtb: + - application/x-dtbook+xml + res: + - application/x-dtbresource+xml + dvi: + - application/x-dvi + evy: + - application/x-envoy + eva: + - application/x-eva + bdf: + - application/x-font-bdf + gsf: + - application/x-font-ghostscript + psf: + - application/x-font-linux-psf + pcf: + - application/x-font-pcf + snf: + - application/x-font-snf + pfa: + - application/x-font-type1 + pfb: + - application/x-font-type1 + pfm: + - application/x-font-type1 + afm: + - application/x-font-type1 + arc: + - application/x-freearc + spl: + - application/x-futuresplash + gca: + - application/x-gca-compressed + ulx: + - application/x-glulx + gnumeric: + - application/x-gnumeric + gramps: + - application/x-gramps-xml + gtar: + - application/x-gtar + hdf: + - application/x-hdf + install: + - application/x-install-instructions + iso: + - application/x-iso9660-image + jnlp: + - application/x-java-jnlp-file + latex: + - application/x-latex + lzh: + - application/x-lzh-compressed + lha: + - application/x-lzh-compressed + mie: + - application/x-mie + prc: + - application/x-mobipocket-ebook + mobi: + - application/x-mobipocket-ebook + application: + - application/x-ms-application + lnk: + - application/x-ms-shortcut + wmd: + - application/x-ms-wmd + wmz: + - application/x-ms-wmz + - application/x-msmetafile + xbap: + - application/x-ms-xbap + mdb: + - application/x-msaccess + obd: + - application/x-msbinder + crd: + - application/x-mscardfile + clp: + - application/x-msclip + exe: + - application/x-msdownload + dll: + - application/x-msdownload + com: + - application/x-msdownload + bat: + - application/x-msdownload + msi: + - application/x-msdownload + mvb: + - application/x-msmediaview + m13: + - application/x-msmediaview + m14: + - application/x-msmediaview + wmf: + - application/x-msmetafile + emf: + - application/x-msmetafile + emz: + - application/x-msmetafile + mny: + - application/x-msmoney + pub: + - application/x-mspublisher + scd: + - application/x-msschedule + trm: + - application/x-msterminal + wri: + - application/x-mswrite + nc: + - application/x-netcdf + cdf: + - application/x-netcdf + nzb: + - application/x-nzb + p12: + - application/x-pkcs12 + pfx: + - application/x-pkcs12 + p7b: + - application/x-pkcs7-certificates + spc: + - application/x-pkcs7-certificates + p7r: + - application/x-pkcs7-certreqresp + rar: + - application/x-rar-compressed + ris: + - application/x-research-info-systems + sh: + - application/x-sh + shar: + - application/x-shar + swf: + - application/x-shockwave-flash + xap: + - application/x-silverlight-app + sql: + - application/x-sql + sit: + - application/x-stuffit + sitx: + - application/x-stuffitx + srt: + - application/x-subrip + sv4cpio: + - application/x-sv4cpio + sv4crc: + - application/x-sv4crc + t3: + - application/x-t3vm-image + gam: + - application/x-tads + tar: + - application/x-tar + tcl: + - application/x-tcl + tex: + - application/x-tex + tfm: + - application/x-tex-tfm + texinfo: + - application/x-texinfo + texi: + - application/x-texinfo + obj: + - application/x-tgif + ustar: + - application/x-ustar + src: + - application/x-wais-source + der: + - application/x-x509-ca-cert + crt: + - application/x-x509-ca-cert + fig: + - application/x-xfig + xlf: + - application/x-xliff+xml + xpi: + - application/x-xpinstall + xz: + - application/x-xz + z1: + - application/x-zmachine + z2: + - application/x-zmachine + z3: + - application/x-zmachine + z4: + - application/x-zmachine + z5: + - application/x-zmachine + z6: + - application/x-zmachine + z7: + - application/x-zmachine + z8: + - application/x-zmachine + xaml: + - application/xaml+xml + xdf: + - application/xcap-diff+xml + xenc: + - application/xenc+xml + xhtml: + - application/xhtml+xml + xht: + - application/xhtml+xml + xml: + - application/xml + xsl: + - application/xml + dtd: + - application/xml-dtd + xop: + - application/xop+xml + xpl: + - application/xproc+xml + xslt: + - application/xslt+xml + xspf: + - application/xspf+xml + mxml: + - application/xv+xml + xhvml: + - application/xv+xml + xvml: + - application/xv+xml + xvm: + - application/xv+xml + yang: + - application/yang + yin: + - application/yin+xml + adp: + - audio/adpcm + au: + - audio/basic + snd: + - audio/basic + mid: + - audio/midi + midi: + - audio/midi + kar: + - audio/midi + rmi: + - audio/midi + m4a: + - audio/mp4 + mp4a: + - audio/mp4 + oga: + - audio/ogg + ogg: + - audio/ogg + spx: + - audio/ogg + s3m: + - audio/s3m + sil: + - audio/silk + uva: + - audio/vnd.dece.audio + uvva: + - audio/vnd.dece.audio + eol: + - audio/vnd.digital-winds + dra: + - audio/vnd.dra + dts: + - audio/vnd.dts + dtshd: + - audio/vnd.dts.hd + lvp: + - audio/vnd.lucent.voice + pya: + - audio/vnd.ms-playready.media.pya + ecelp4800: + - audio/vnd.nuera.ecelp4800 + ecelp7470: + - audio/vnd.nuera.ecelp7470 + ecelp9600: + - audio/vnd.nuera.ecelp9600 + rip: + - audio/vnd.rip + weba: + - audio/webm + aac: + - audio/x-aac + aif: + - audio/x-aiff + aiff: + - audio/x-aiff + aifc: + - audio/x-aiff + caf: + - audio/x-caf + flac: + - audio/x-flac + mka: + - audio/x-matroska + m3u: + - audio/x-mpegurl + wax: + - audio/x-ms-wax + wma: + - audio/x-ms-wma + ram: + - audio/x-pn-realaudio + ra: + - audio/x-pn-realaudio + rmp: + - audio/x-pn-realaudio-plugin + wav: + - audio/x-wav + xm: + - audio/xm + cdx: + - chemical/x-cdx + cif: + - chemical/x-cif + cmdf: + - chemical/x-cmdf + cml: + - chemical/x-cml + csml: + - chemical/x-csml + xyz: + - chemical/x-xyz + woff: + - font/woff + woff2: + - font/woff2 + cgm: + - image/cgm + g3: + - image/g3fax + gif: + - image/gif + ief: + - image/ief + ktx: + - image/ktx + png: + - image/png + btif: + - image/prs.btif + sgi: + - image/sgi + svg: + - image/svg+xml + svgz: + - image/svg+xml + tiff: + - image/tiff + tif: + - image/tiff + psd: + - image/vnd.adobe.photoshop + uvi: + - image/vnd.dece.graphic + uvvi: + - image/vnd.dece.graphic + uvg: + - image/vnd.dece.graphic + uvvg: + - image/vnd.dece.graphic + djvu: + - image/vnd.djvu + djv: + - image/vnd.djvu + sub: + - image/vnd.dvb.subtitle + - text/vnd.dvb.subtitle + dwg: + - image/vnd.dwg + dxf: + - image/vnd.dxf + fbs: + - image/vnd.fastbidsheet + fpx: + - image/vnd.fpx + fst: + - image/vnd.fst + mmr: + - image/vnd.fujixerox.edmics-mmr + rlc: + - image/vnd.fujixerox.edmics-rlc + mdi: + - image/vnd.ms-modi + wdp: + - image/vnd.ms-photo + npx: + - image/vnd.net-fpx + wbmp: + - image/vnd.wap.wbmp + xif: + - image/vnd.xiff + webp: + - image/webp + 3ds: + - image/x-3ds + ras: + - image/x-cmu-raster + cmx: + - image/x-cmx + fh: + - image/x-freehand + fhc: + - image/x-freehand + fh4: + - image/x-freehand + fh5: + - image/x-freehand + fh7: + - image/x-freehand + ico: + - image/x-icon + sid: + - image/x-mrsid-image + pcx: + - image/x-pcx + pic: + - image/x-pict + pct: + - image/x-pict + pnm: + - image/x-portable-anymap + pbm: + - image/x-portable-bitmap + pgm: + - image/x-portable-graymap + ppm: + - image/x-portable-pixmap + rgb: + - image/x-rgb + tga: + - image/x-tga + xbm: + - image/x-xbitmap + xpm: + - image/x-xpixmap + xwd: + - image/x-xwindowdump + eml: + - message/rfc822 + mime: + - message/rfc822 + igs: + - model/iges + iges: + - model/iges + msh: + - model/mesh + mesh: + - model/mesh + silo: + - model/mesh + dae: + - model/vnd.collada+xml + dwf: + - model/vnd.dwf + gdl: + - model/vnd.gdl + gtw: + - model/vnd.gtw + mts: + - model/vnd.mts + vtu: + - model/vnd.vtu + wrl: + - model/vrml + vrml: + - model/vrml + x3db: + - model/x3d+binary + x3dbz: + - model/x3d+binary + x3dv: + - model/x3d+vrml + x3dvz: + - model/x3d+vrml + x3d: + - model/x3d+xml + x3dz: + - model/x3d+xml + appcache: + - text/cache-manifest + ics: + - text/calendar + ifb: + - text/calendar + css: + - text/css + csv: + - text/csv + html: + - text/html + htm: + - text/html + n3: + - text/n3 + txt: + - text/plain + text: + - text/plain + conf: + - text/plain + def: + - text/plain + list: + - text/plain + log: + - text/plain + in: + - text/plain + dsc: + - text/prs.lines.tag + rtx: + - text/richtext + sgml: + - text/sgml + sgm: + - text/sgml + tsv: + - text/tab-separated-values + t: + - text/troff + tr: + - text/troff + roff: + - text/troff + man: + - text/troff + me: + - text/troff + ms: + - text/troff + ttl: + - text/turtle + uri: + - text/uri-list + uris: + - text/uri-list + urls: + - text/uri-list + vcard: + - text/vcard + curl: + - text/vnd.curl + dcurl: + - text/vnd.curl.dcurl + mcurl: + - text/vnd.curl.mcurl + scurl: + - text/vnd.curl.scurl + fly: + - text/vnd.fly + flx: + - text/vnd.fmi.flexstor + gv: + - text/vnd.graphviz + 3dml: + - text/vnd.in3d.3dml + spot: + - text/vnd.in3d.spot + jad: + - text/vnd.sun.j2me.app-descriptor + wml: + - text/vnd.wap.wml + wmls: + - text/vnd.wap.wmlscript + s: + - text/x-asm + asm: + - text/x-asm + c: + - text/x-c + cc: + - text/x-c + cxx: + - text/x-c + cpp: + - text/x-c + h: + - text/x-c + hh: + - text/x-c + dic: + - text/x-c + f: + - text/x-fortran + for: + - text/x-fortran + f77: + - text/x-fortran + f90: + - text/x-fortran + java: + - text/x-java-source + nfo: + - text/x-nfo + opml: + - text/x-opml + p: + - text/x-pascal + pas: + - text/x-pascal + etx: + - text/x-setext + sfv: + - text/x-sfv + uu: + - text/x-uuencode + vcs: + - text/x-vcalendar + vcf: + - text/x-vcard + 3gp: + - video/3gpp + 3g2: + - video/3gpp2 + h261: + - video/h261 + h263: + - video/h263 + h264: + - video/h264 + jpgv: + - video/jpeg + jpm: + - video/jpm + jpgm: + - video/jpm + mj2: + - video/mj2 + mjp2: + - video/mj2 + mp4: + - video/mp4 + mp4v: + - video/mp4 + mpg4: + - video/mp4 + mpeg: + - video/mpeg + mpg: + - video/mpeg + mpe: + - video/mpeg + m1v: + - video/mpeg + m2v: + - video/mpeg + ogv: + - video/ogg + qt: + - video/quicktime + mov: + - video/quicktime + uvh: + - video/vnd.dece.hd + uvvh: + - video/vnd.dece.hd + uvm: + - video/vnd.dece.mobile + uvvm: + - video/vnd.dece.mobile + uvp: + - video/vnd.dece.pd + uvvp: + - video/vnd.dece.pd + uvs: + - video/vnd.dece.sd + uvvs: + - video/vnd.dece.sd + uvv: + - video/vnd.dece.video + uvvv: + - video/vnd.dece.video + dvb: + - video/vnd.dvb.file + fvt: + - video/vnd.fvt + mxu: + - video/vnd.mpegurl + m4u: + - video/vnd.mpegurl + pyv: + - video/vnd.ms-playready.media.pyv + uvu: + - video/vnd.uvvu.mp4 + uvvu: + - video/vnd.uvvu.mp4 + viv: + - video/vnd.vivo + webm: + - video/webm + f4v: + - video/x-f4v + fli: + - video/x-fli + flv: + - video/x-flv + m4v: + - video/x-m4v + mkv: + - video/x-matroska + mk3d: + - video/x-matroska + mks: + - video/x-matroska + mng: + - video/x-mng + asf: + - video/x-ms-asf + asx: + - video/x-ms-asf + vob: + - video/x-ms-vob + wm: + - video/x-ms-wm + wmv: + - video/x-ms-wmv + wmx: + - video/x-ms-wmx + wvx: + - video/x-ms-wvx + avi: + - video/x-msvideo + movie: + - video/x-sgi-movie + smv: + - video/x-smv + ice: + - x-conference/x-cooltalk diff --git a/system/config/system.yaml b/system/config/system.yaml index 3d02b2ea3..3d0983c31 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -96,7 +96,7 @@ cache: purge_at: '0 4 * * *' # How often to purge old file cache (using new scheduler) clear_at: '0 3 * * *' # How often to clear cache (using new scheduler) clear_job_type: 'standard' # Type to clear when processing the scheduled clear job `standard`|`all` - clear_images_by_default: true # By default grav will include processed images in cache clear, this can be disabled + clear_images_by_default: false # By default grav does not include processed images in cache clear, this can be enabled cli_compatibility: false # Ensures only non-volatile drivers are used (file, redis, memcache, etc.) lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite) gzip: false # GZip compress the page output @@ -113,6 +113,8 @@ twig: autoescape: true # Autoescape Twig vars (DEPRECATED, always enabled in strict mode) undefined_functions: true # Allow undefined functions undefined_filters: true # Allow undefined filters + safe_functions: [] # List of PHP functions which are allowed to be used as Twig functions + safe_filters: [] # List of PHP functions which are allowed to be used as Twig filters umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775 assets: # Configuration for Assets Manager (JS, CSS) @@ -129,7 +131,7 @@ assets: # Configuration for Assets Mana enable_asset_timestamp: false # Enable asset timestamps enable_asset_sri: false # Enable asset SRI collections: - jquery: system://assets/jquery/jquery-2.x.min.js + jquery: system://assets/jquery/jquery-3.x.min.js errors: display: 0 # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error @@ -188,11 +190,17 @@ session: gpm: releases: stable # Set to either 'stable' or 'testing' - proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128) - method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL - verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help. official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security +http: + method: auto # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL + enable_proxy: true # Enable proxy server configuration + proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128) + proxy_cert_path: # Local path to proxy certificate folder containing pem files + concurrent_connections: 5 # Concurrent HTTP connections when multiplexing + verify_peer: true # Enable/Disable SSL verification of peer certificates + verify_host: true # Enable/Disable SSL verification of host certificates + accounts: type: regular # EXPERIMENTAL: Account type: regular or flex storage: file # EXPERIMENTAL: Flex storage type: file or folder diff --git a/system/defines.php b/system/defines.php index 51e2bc1f2..e3cbb369f 100644 --- a/system/defines.php +++ b/system/defines.php @@ -1,4 +1,5 @@ فشل التحقق من صحة:' INVALID_INPUT: 'إدخال غير صحيح في' MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:' + XSS_ISSUES: "مشاكل XSS محتملة تم اكتشافها في حقل '%s' '" MONTHS_OF_THE_YEAR: - 'كانون الثاني' - 'شباط' @@ -72,6 +73,8 @@ GRAV: - 'الجمعة' - 'السبت' - 'الأحد' + YES: "نعم" + NO: "لا" CRON: EVERY: كل EVERY_HOUR: كل ساعة @@ -80,3 +83,11 @@ GRAV: EVERY_DAY_OF_MONTH: كل يوم في الشهر EVERY_MONTH: ' كل شهر' TEXT_PERIOD: كل + TEXT_MINS: ' في دقيقة(دقائق) بعد الساعة' + TEXT_TIME: ' في :' + TEXT_DOW: ' في ' + TEXT_MONTH: ' من ' + TEXT_DOM: ' في ' + ERROR1: الوسم %s غير مدعوم! + ERROR2: عدد عناصر غير صالح. + ERROR4: تعبير غير معروف diff --git a/system/languages/ca.yaml b/system/languages/ca.yaml index 7795aef8b..f5a194017 100644 --- a/system/languages/ca.yaml +++ b/system/languages/ca.yaml @@ -15,6 +15,7 @@ GRAV: BAD_DATE: Data invàlida AGO: abans FROM_NOW: des d'ara + JUST_NOW: Ara mateix SECOND: segon MINUTE: minut HOUR: hora @@ -48,6 +49,7 @@ GRAV: VALIDATION_FAIL: 'Ha fallat la validació:' INVALID_INPUT: 'Entrada no vàlida a' MISSING_REQUIRED_FIELD: 'Falta camp obligatori:' + XSS_ISSUES: "Detectats potencials problemes XSS al camp '%s'" MONTHS_OF_THE_YEAR: - 'Gener' - 'Febrer' @@ -69,3 +71,17 @@ GRAV: - 'Divendres' - 'Dissabte' - 'Diumenge' + YES: "Sí" + NO: "No" + CRON: + EVERY: cada + EVERY_HOUR: cada hora + EVERY_MINUTE: cada minut + EVERY_DAY_OF_WEEK: cada dia de la setmana + EVERY_DAY_OF_MONTH: cada dia del mes + EVERY_MONTH: cada mes + TEXT_PERIOD: Cada + ERROR1: L'etiqueta %s no està suportada! + ERROR2: Nombre d'elements incorrecte + ERROR3: El jquery_element s'ha d'establir a la configuració de jqCron + ERROR4: Expressió no reconeguda diff --git a/system/languages/fr.yaml b/system/languages/fr.yaml index d9fa177ea..6c17e6aa5 100644 --- a/system/languages/fr.yaml +++ b/system/languages/fr.yaml @@ -24,6 +24,7 @@ GRAV: '/(quiz)zes$/i': '\1' '/(alias|status)es$/i': '\1' '/([octop|vir])i$/i': '\1us' + '/(n)ews$/i': '\1ouvelles' INFLECTOR_UNCOUNTABLE: - 'équipement' - 'information' @@ -58,10 +59,10 @@ GRAV: MONTH: mois YEAR: année DECADE: décennie - SEC: s - MIN: m - HR: h - WK: sem + SEC: sec. + MIN: min. + HR: hr. + WK: sem. MO: m YR: an DEC: déc @@ -84,6 +85,7 @@ GRAV: VALIDATION_FAIL: 'La validation a échoué :' INVALID_INPUT: 'Saisie non valide' MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :' + XSS_ISSUES: "Erreurs XSS probablement détectées dans le champ '%s'" MONTHS_OF_THE_YEAR: - 'janvier' - 'février' @@ -105,6 +107,8 @@ GRAV: - 'vendredi' - 'samedi' - 'dimanche' + YES: "Oui" + NO: "Non" CRON: EVERY: chaque EVERY_HOUR: toutes les heures @@ -118,7 +122,7 @@ GRAV: TEXT_DOW: ' sur ' TEXT_MONTH: ' de ' TEXT_DOM: ' sur ' - ERROR1: La balise %s n'est pas supportée! + ERROR1: La balise %s n'est pas prise en charge ! ERROR2: Nombre invalide d'éléments ERROR3: L'élément jquery_element doit être défini dans les paramètres jqCron ERROR4: Expression non reconnue diff --git a/system/languages/gl.yaml b/system/languages/gl.yaml index b016c59c3..b9e581f91 100644 --- a/system/languages/gl.yaml +++ b/system/languages/gl.yaml @@ -104,6 +104,7 @@ GRAV: VALIDATION_FAIL: 'Fallou a validación:' INVALID_INPUT: 'Entrada incorrecta en' MISSING_REQUIRED_FIELD: 'Falta un campo requirido:' + XSS_ISSUES: "Detectáronse posibles problemas XSS no campo '% s'" MONTHS_OF_THE_YEAR: - 'xaneiro' - 'febreiro' @@ -125,6 +126,8 @@ GRAV: - 'venres' - 'sábado' - 'domingo' + YES: "Si" + NO: "Non" CRON: EVERY: cada EVERY_HOUR: Cada hora diff --git a/system/languages/id.yaml b/system/languages/id.yaml index 690959d85..810723595 100644 --- a/system/languages/id.yaml +++ b/system/languages/id.yaml @@ -3,26 +3,72 @@ GRAV: FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Frontmatter tidak valid\n\nLokasi: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" INFLECTOR_PLURALS: '/(quiz)$/i': '\1zes' + '/^(ox)$/i': '\1en' + '/([m|l])ouse$/i': '\1ice' + '/(matr|vert|ind)ix|ex$/i': '\1ices' + '/(x|ch|ss|sh)$/i': '\1es' + '/([^aeiouy]|qu)ies$/i': '\1y' + '/([^aeiouy]|qu)y$/i': '\1ies' + '/(hive)$/i': '\1s' + '/(?:([^f])fe|([lr])f)$/i': '\1\2ves' + '/sis$/i': 'ses' + '/([ti])um$/i': '\1a' + '/(buffal|tomat)o$/i': '\1oes' + '/(bu)s$/i': '\1ses' + '/(alias|status)/i': '\1es' + '/(octop|vir)us$/i': '\1i' + '/(ax|test)is$/i': '\1es' + '/s$/i': 's' + '/$/': 's' + INFLECTOR_SINGULAR: + '/(quiz)zes$/i': '\1' + '/(matr)ices$/i': '\1ix' + '/(vert|ind)ices$/i': '\1ex' + '/^(ox)en/i': '\1' + '/(alias|status)es$/i': '\1' + '/([octop|vir])i$/i': '\1us' + '/(cris|ax|test)es$/i': '\1is' + '/(shoe)s$/i': '\1' + '/(o)es$/i': '\1' + '/(bus)es$/i': '\1' + '/([m|l])ice$/i': '\1ouse' + '/(x|ch|ss|sh)es$/i': '\1' + '/(m)ovies$/i': '\1ovie' + '/(s)eries$/i': '\1eries' + '/([^aeiouy]|qu)ies$/i': '\1y' + '/([lr])ves$/i': '\1f' + '/(tive)s$/i': '\1' + '/(hive)s$/i': '\1' + '/([^f])ves$/i': '\1fe' + '/(^analy)ses$/i': '\1sis' + '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis' + '/([ti])a$/i': '\1um' + '/(n)ews$/i': '\1ews' INFLECTOR_UNCOUNTABLE: - - 'peralatan' - - 'informasi' - - 'nasi' - - 'uang' - - 'spesies' - - 'rangkaian' - - 'ikan' - - 'domba' + - 'Peralatan' + - 'Informasi ' + - 'Nasi' + - 'Uang' + - 'Jenis' + - 'Seri' + - 'Ikan' + - 'Domba' INFLECTOR_IRREGULAR: - 'person': 'orang-orang' - 'man': 'laki-laki' - 'child': 'anak-anak' - 'sex': 'jenis kelamin' + 'person': 'Orang-orang' + 'man': 'Pria' + 'child': 'Balita' + 'sex': 'Jenis Kelamin' 'move': 'pindahkan' + INFLECTOR_ORDINALS: + 'default': 'ke' + 'first': 'pertama' + 'second': 'nd' + 'third': 'rd' NICETIME: - NO_DATE_PROVIDED: Tanggal tidak tersedia + NO_DATE_PROVIDED: Tidak ada tanggal yang disediakan BAD_DATE: Format tanggal salah AGO: yang lalu - FROM_NOW: dari saat ini + FROM_NOW: dari sekarang JUST_NOW: baru saja SECOND: detik MINUTE: menit @@ -32,12 +78,12 @@ GRAV: MONTH: bulan YEAR: tahun DECADE: dekade - SEC: dtk - MIN: mnt - HR: j - WK: mng - MO: bln - YR: thn + SEC: detik + MIN: menit + HR: ' jam' + WK: minggu + MO: bulan + YR: tahun DEC: desimal SECOND_PLURAL: detik MINUTE_PLURAL: menit @@ -47,17 +93,18 @@ GRAV: MONTH_PLURAL: bulan YEAR_PLURAL: tahun DECADE_PLURAL: dekade - SEC_PLURAL: dtk - MIN_PLURAL: mnt - HR_PLURAL: j - WK_PLURAL: mgg - MO_PLURAL: bln - YR_PLURAL: thn + SEC_PLURAL: detik + MIN_PLURAL: menit + HR_PLURAL: jam + WK_PLURAL: minggu + MO_PLURAL: bulan + YR_PLURAL: tahun DEC_PLURAL: dekade FORM: VALIDATION_FAIL: 'Validasi gagal:' INVALID_INPUT: 'Input tidak valid di' MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:' + XSS_ISSUES: "Isu berpotensial XSS terdeteksi dalam baris %s" MONTHS_OF_THE_YEAR: - 'Januari' - 'Februari' @@ -76,22 +123,25 @@ GRAV: - 'Selasa' - 'Rabu' - 'Kamis' - - 'Jumat' + - 'Jum''at' - 'Sabtu' - 'Minggu' + YES: "Ya" + NO: "Tidak" CRON: EVERY: Setiap EVERY_HOUR: Setiap jam EVERY_MINUTE: Setiap menit EVERY_DAY_OF_WEEK: Setiap hari selama seminggu - EVERY_DAY_OF_MONTH: pada tanggal setiap bulannya + EVERY_DAY_OF_MONTH: Setiap hari dalam sebulan EVERY_MONTH: setiap bulan TEXT_PERIOD: Setiap + TEXT_MINS: 'dalam menit setelah jam yang lalu' TEXT_TIME: ' pada :' TEXT_DOW: ' pada ' TEXT_MONTH: ' pada ' TEXT_DOM: ' pada ' ERROR1: Tag %s tidak didukung! - ERROR2: Jumlah elemen tidak valid - ERROR3: jquery_element harus ditetapkan ke pengaturan jqCron - ERROR4: Ekspresi tidak dikenali + ERROR2: Jumlah elemen yang buruk + ERROR3: jquery_element harus diatur ke dalam pengaturan jqCron + ERROR4: Ekspresi tidak dikenal diff --git a/system/languages/mn.yaml b/system/languages/mn.yaml new file mode 100644 index 000000000..73cda0ee3 --- /dev/null +++ b/system/languages/mn.yaml @@ -0,0 +1,147 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nГарчиг: %1$s\n---\n\n# Алдаа: Буруу Формат\n\nЗам: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + INFLECTOR_PLURALS: + '/(quiz)$/i': '\1зүүд' + '/^(ox)$/i': '\1ууд' + '/([m|l])ouse$/i': '\1ууд' + '/(matr|vert|ind)ix|ex$/i': '\1иксүүд' + '/(x|ch|ss|sh)$/i': '\1үүд' + '/([^aeiouy]|qu)ies$/i': '\1үүд' + '/([^aeiouy]|qu)y$/i': '\1үүд' + '/(hive)$/i': '\1үүд' + '/(?:([^f])fe|([lr])f)$/i': '\1\2үүд' + '/sis$/i': 'үүд' + '/([ti])um$/i': '\1үүд' + '/(buffal|tomat)o$/i': '\1үүд' + '/(bu)s$/i': '\1үүд' + '/(alias|status)/i': '\1үүд' + '/(octop|vir)us$/i': '\1үүд' + '/(ax|test)is$/i': '\1үүд' + '/s$/i': 'үүд' + '/$/': 'үүд' + INFLECTOR_SINGULAR: + '/(quiz)zes$/i': '\1' + '/(matr)ices$/i': '\1икс' + '/(vert|ind)ices$/i': '\1икс' + '/^(ox)en/i': '\1' + '/(alias|status)es$/i': '\1' + '/([octop|vir])i$/i': '\1' + '/(cris|ax|test)es$/i': '\1' + '/(shoe)s$/i': '\1' + '/(o)es$/i': '\1' + '/(bus)es$/i': '\1' + '/([m|l])ice$/i': '\1' + '/(x|ch|ss|sh)es$/i': '\1' + '/(m)ovies$/i': '\1' + '/(s)eries$/i': '\1' + '/([^aeiouy]|qu)ies$/i': '\1үүд' + '/([lr])ves$/i': '\1' + '/(tive)s$/i': '\1' + '/(hive)s$/i': '\1' + '/([^f])ves$/i': '\1' + '/(^analy)ses$/i': '\1' + '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2үүд' + '/([ti])a$/i': '\1' + '/(n)ews$/i': '\1' + INFLECTOR_UNCOUNTABLE: + - 'тоног төхөөрөмж' + - 'Мэдээлэл' + - 'будаа' + - 'мөнгө' + - 'төрөл зүйл' + - 'цуврал' + - 'загас' + - 'хонь' + INFLECTOR_IRREGULAR: + 'person': 'хүмүүс' + 'man': 'эрчүүд' + 'child': 'хүүхэд' + 'sex': 'хүйс' + 'move': 'хөдөлгөөн' + INFLECTOR_ORDINALS: + 'default': 'th' + 'first': 'st' + 'second': 'nd' + 'third': 'rd' + NICETIME: + NO_DATE_PROVIDED: Огноо алга + BAD_DATE: Буруу огноо + AGO: өмнө + FROM_NOW: одооноос + JUST_NOW: дөнгөж сая + SECOND: секунд + MINUTE: минут + HOUR: цаг + DAY: өдөр + WEEK: долоо хоног + MONTH: сар + YEAR: он + DECADE: арван жил + SEC: сек + MIN: мин + HR: цаг + WK: д.х. + MO: сар + YR: он + DEC: арван жил + SECOND_PLURAL: секунд + MINUTE_PLURAL: минут + HOUR_PLURAL: цаг + DAY_PLURAL: өдрүүд + WEEK_PLURAL: долоо хоногууд + MONTH_PLURAL: сарууд + YEAR_PLURAL: онууд + DECADE_PLURAL: арван жилүүд + SEC_PLURAL: сек.-үүд + MIN_PLURAL: мин.-ууд + HR_PLURAL: цагууд + WK_PLURAL: д.х.-ууд + MO_PLURAL: сарууд + YR_PLURAL: жилүүд + DEC_PLURAL: арван жилүүд + FORM: + VALIDATION_FAIL: 'Баталгаажуулалт амжилтгүй боллоо:' + INVALID_INPUT: 'Буруу өгөгдөл дараахид' + MISSING_REQUIRED_FIELD: 'Шаардлагатай талбар дутуу байна:' + XSS_ISSUES: "'%s' талбарт XSS -ийн болзошгүй асуудлууд илэрсэн" + MONTHS_OF_THE_YEAR: + - '1-р сар' + - '2-р сар' + - '3-р сар' + - '4-р сар' + - '5 сар' + - '6 сар' + - '7 сар' + - '8 сар' + - '9 сар' + - '10 сар' + - '11 сар' + - '12 сар' + DAYS_OF_THE_WEEK: + - 'Даваа гараг' + - 'Мягмар гараг' + - 'Лхагва гараг' + - 'Пүрэв гараг' + - 'Баасан гараг' + - 'Бямба гараг' + - 'Ням гараг' + YES: "Тийм" + NO: "Үгүй" + CRON: + EVERY: бүрийн + EVERY_HOUR: цаг бүрийн + EVERY_MINUTE: минут бүрийн + EVERY_DAY_OF_WEEK: долоо хоногийн өдөр болгонд + EVERY_DAY_OF_MONTH: сарын өдөр болгонд + EVERY_MONTH: сар болгон + TEXT_PERIOD: Бүрийн + TEXT_MINS: ' энэ сүүлийн цагийн минутад' + TEXT_TIME: ' : -д' + TEXT_DOW: ' -д' + TEXT_MONTH: ' -ын' + TEXT_DOM: ' -т' + ERROR1: '%s -н утга нь дэмжигддэггүй!' + ERROR2: Элементүүдийн тоо хэмжээ буруу + ERROR3: jquery_element нь jqCron тохиргоонд хийгдсэн байх ёстой + ERROR4: Танигдаагүй илэрхийлэл diff --git a/system/languages/pt.yaml b/system/languages/pt.yaml index 2da6944d1..daaa616d0 100644 --- a/system/languages/pt.yaml +++ b/system/languages/pt.yaml @@ -104,6 +104,7 @@ GRAV: VALIDATION_FAIL: 'Falha na validação:' INVALID_INPUT: 'Dados inseridos são inválidos em' MISSING_REQUIRED_FIELD: 'Campo obrigatório em falta:' + XSS_ISSUES: "Potenciais problemas de XSS detectados no campo '%s'" MONTHS_OF_THE_YEAR: - 'Janeiro' - 'Fevereiro' @@ -125,6 +126,8 @@ GRAV: - 'Sexta-feira' - 'Sábado' - 'Domingo' + YES: "Sim" + NO: "Não" CRON: EVERY: cada EVERY_HOUR: cada hora diff --git a/system/languages/si.yaml b/system/languages/si.yaml new file mode 100644 index 000000000..18850a4e0 --- /dev/null +++ b/system/languages/si.yaml @@ -0,0 +1,9 @@ +--- +GRAV: + INFLECTOR_SINGULAR: + '/(quiz)zes$/i': '\1' + '/^(ox)en/i': '\1' + '/(alias|status)es$/i': '\1' + '/(o)es$/i': '\1' + '/(bus)es$/i': '\1' + '/(x|ch|ss|sh)es$/i': '\1' diff --git a/system/languages/tr.yaml b/system/languages/tr.yaml index 783674d58..47f32dbe8 100644 --- a/system/languages/tr.yaml +++ b/system/languages/tr.yaml @@ -82,6 +82,8 @@ GRAV: - 'Cuma' - 'Cumartesi' - 'Pazar' + YES: "Evet" + NO: "Hayır" CRON: EVERY: her EVERY_HOUR: saatte bir diff --git a/system/languages/zh-tw.yaml b/system/languages/zh-tw.yaml index 6cb39c9ad..fefbc3317 100644 --- a/system/languages/zh-tw.yaml +++ b/system/languages/zh-tw.yaml @@ -38,7 +38,9 @@ GRAV: YR_PLURAL: 年 DEC_PLURAL: 十年 FORM: - MISSING_REQUIRED_FIELD: 遺漏必填欄位: + VALIDATION_FAIL: '確驗證失敗:' + INVALID_INPUT: '無效輸入:' + MISSING_REQUIRED_FIELD: '遺漏必填欄位:' MONTHS_OF_THE_YEAR: - '一月' - '二月' @@ -60,3 +62,16 @@ GRAV: - '星期五' - '星期六' - '星期日' + CRON: + EVERY: 每 + EVERY_HOUR: 每小時 + EVERY_MINUTE: 每分鐘 + EVERY_DAY_OF_WEEK: 每一天 + EVERY_DAY_OF_MONTH: 每一天 + EVERY_MONTH: 每個月 + TEXT_PERIOD: 每 + TEXT_MINS: ' 的 分' + TEXT_TIME: ' :' + TEXT_DOW: ' 的 ' + TEXT_MONTH: ' 的 ' + TEXT_DOM: ' 的 ' diff --git a/system/src/Grav/Common/Assets.php b/system/src/Grav/Common/Assets.php index 51a2531e4..a9b930b13 100644 --- a/system/src/Grav/Common/Assets.php +++ b/system/src/Grav/Common/Assets.php @@ -110,7 +110,7 @@ class Assets extends PropertyObject /** @var UniformResourceLocator $locator */ $locator = $grav['locator']; - $this->assets_dir = $locator->findResource('asset://') . DS; + $this->assets_dir = $locator->findResource('asset://'); $this->assets_url = $locator->findResource('asset://', false); $this->config($asset_config); @@ -164,10 +164,19 @@ class Assets extends PropertyObject // More than one asset if (is_array($asset)) { - foreach ($asset as $a) { - array_shift($args); - $args = array_merge([$a], $args); - call_user_func_array([$this, 'add'], $args); + foreach ($asset as $index => $location) { + $params = array_slice($args, 1); + if (is_array($location)) { + $params = array_shift($params); + if (is_numeric($params)) { + $params = [ 'priority' => $params ]; + } + $params = [array_replace_recursive([], $location, $params)]; + $location = $index; + } + + $params = array_merge([$location], $params); + call_user_func_array([$this, 'add'], $params); } } elseif (isset($this->collections[$asset])) { array_shift($args); @@ -201,8 +210,13 @@ class Assets extends PropertyObject protected function addType($collection, $type, $asset, $options) { if (is_array($asset)) { - foreach ($asset as $a) { - $this->addType($collection, $type, $a, $options); + foreach ($asset as $index => $location) { + $assetOptions = $options; + if (is_array($location)) { + $assetOptions = array_replace_recursive([], $options, $location); + $location = $index; + } + $this->addType($collection, $type, $location, $assetOptions); } return $this; diff --git a/system/src/Grav/Common/Assets/BaseAsset.php b/system/src/Grav/Common/Assets/BaseAsset.php index 3e079746b..a115ce9ec 100644 --- a/system/src/Grav/Common/Assets/BaseAsset.php +++ b/system/src/Grav/Common/Assets/BaseAsset.php @@ -15,6 +15,7 @@ use Grav\Common\Grav; use Grav\Common\Uri; use Grav\Common\Utils; use Grav\Framework\Object\PropertyObject; +use RocketTheme\Toolbox\File\File; use SplFileInfo; /** @@ -91,6 +92,10 @@ abstract class BaseAsset extends PropertyObject */ public function init($asset, $options) { + if (!$asset) { + return false; + } + $config = Grav::instance()['config']; $uri = Grav::instance()['uri']; @@ -182,16 +187,21 @@ abstract class BaseAsset extends PropertyObject public static function integrityHash($input) { $grav = Grav::instance(); + $uri = $grav['uri']; $assetsConfig = $grav['config']->get('system.assets'); - if ( !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri'] ) - { - $dataToHash = file_get_contents( GRAV_WEBROOT . $input); + if (!self::isRemoteLink($input) && !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri']) { + $input = preg_replace('#^' . $uri->rootUrl() . '#', '', $input); + $asset = File::instance(GRAV_WEBROOT . $input); - $hash = hash('sha256', $dataToHash, true); - $hash_base64 = base64_encode($hash); - return ' integrity="sha256-' . $hash_base64 . '"'; + if ($asset->exists()) { + $dataToHash = $asset->content(); + $hash = hash('sha256', $dataToHash, true); + $hash_base64 = base64_encode($hash); + + return ' integrity="sha256-' . $hash_base64 . '"'; + } } return ''; @@ -253,6 +263,6 @@ abstract class BaseAsset extends PropertyObject */ protected function cssRewrite($file, $dir, $local) { - return; + return ''; } } diff --git a/system/src/Grav/Common/Assets/Pipeline.php b/system/src/Grav/Common/Assets/Pipeline.php index 7aef0e145..948104ab9 100644 --- a/system/src/Grav/Common/Assets/Pipeline.php +++ b/system/src/Grav/Common/Assets/Pipeline.php @@ -9,9 +9,9 @@ namespace Grav\Common\Assets; -use Grav\Common\Assets\BaseAsset; use Grav\Common\Assets\Traits\AssetUtilsTrait; use Grav\Common\Config\Config; +use Grav\Common\Filesystem\Folder; use Grav\Common\Grav; use Grav\Common\Uri; use Grav\Common\Utils; @@ -88,7 +88,14 @@ class Pipeline extends PropertyObject $uri = Grav::instance()['uri']; $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/'; - $this->assets_dir = $locator->findResource('asset://') . DS; + $this->assets_dir = $locator->findResource('asset://'); + if (!$this->assets_dir) { + // Attempt to create assets folder if it doesn't exist yet. + $this->assets_dir = $locator->findResource('asset://', true, true); + Folder::mkdir($this->assets_dir); + $locator->clearCache(); + } + $this->assets_url = $locator->findResource('asset://', false); } @@ -119,10 +126,9 @@ class Pipeline extends PropertyObject $file = $uid . '.css'; $relative_path = "{$this->base_url}{$this->assets_url}/{$file}"; - $buffer = null; - - if (file_exists($this->assets_dir . $file)) { - $buffer = file_get_contents($this->assets_dir . $file) . "\n"; + $filepath = "{$this->assets_dir}/{$file}"; + if (file_exists($filepath)) { + $buffer = file_get_contents($filepath) . "\n"; } else { //if nothing found get out of here! if (empty($assets)) { @@ -141,7 +147,7 @@ class Pipeline extends PropertyObject // Write file if (trim($buffer) !== '') { - file_put_contents($this->assets_dir . $file, $buffer); + file_put_contents($filepath, $buffer); } } @@ -182,10 +188,9 @@ class Pipeline extends PropertyObject $file = $uid . '.js'; $relative_path = "{$this->base_url}{$this->assets_url}/{$file}"; - $buffer = null; - - if (file_exists($this->assets_dir . $file)) { - $buffer = file_get_contents($this->assets_dir . $file) . "\n"; + $filepath = "{$this->assets_dir}/{$file}"; + if (file_exists($filepath)) { + $buffer = file_get_contents($filepath) . "\n"; } else { //if nothing found get out of here! if (empty($assets)) { @@ -204,7 +209,7 @@ class Pipeline extends PropertyObject // Write file if (trim($buffer) !== '') { - file_put_contents($this->assets_dir . $file, $buffer); + file_put_contents($filepath, $buffer); } } @@ -249,7 +254,7 @@ class Pipeline extends PropertyObject $old_url = ltrim($old_url, '/'); } - $new_url = ($local ? $this->base_url: '') . $old_url; + $new_url = ($local ? $this->base_url : '') . $old_url; return str_replace($matches[2], $new_url, $matches[0]); }, $file); diff --git a/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php index a2dad4faa..3f5a690fb 100644 --- a/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php +++ b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php @@ -68,8 +68,6 @@ trait AssetUtilsTrait protected function gatherLinks(array $assets, $css = true) { $buffer = ''; - - foreach ($assets as $id => $asset) { $local = true; @@ -90,7 +88,7 @@ trait AssetUtilsTrait } $relative_dir = dirname($relative_path); - $link = ROOT_DIR . $relative_path; + $link = GRAV_ROOT . '/' . $relative_path; } // TODO: looks like this is not being used. @@ -135,7 +133,7 @@ trait AssetUtilsTrait $imports = []; - $file = (string)preg_replace_callback($regex, function ($matches) use (&$imports) { + $file = (string)preg_replace_callback($regex, static function ($matches) use (&$imports) { $imports[] = $matches[0]; return ''; @@ -156,6 +154,10 @@ trait AssetUtilsTrait $no_key = ['loading']; foreach ($this->attributes as $key => $value) { + if ($value === null) { + continue; + } + if (is_numeric($key)) { $key = $value; } @@ -196,7 +198,7 @@ trait AssetUtilsTrait } if ($this->timestamp) { - if (Utils::contains($asset, '?') || $querystring) { + if ($querystring || Utils::contains($asset, '?')) { $querystring .= '&' . $this->timestamp; } else { $querystring .= '?' . $this->timestamp; diff --git a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php index cb8de18ae..15dc00e3d 100644 --- a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php +++ b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php @@ -252,7 +252,7 @@ trait TestingAssetsTrait */ public function addDir($directory, $pattern = self::DEFAULT_REGEX) { - $root_dir = rtrim(ROOT_DIR, '/'); + $root_dir = GRAV_ROOT; // Check if $directory is a stream. if (strpos($directory, '://')) { diff --git a/system/src/Grav/Common/Backup/Backups.php b/system/src/Grav/Common/Backup/Backups.php index 9483b15f7..b70def96f 100644 --- a/system/src/Grav/Common/Backup/Backups.php +++ b/system/src/Grav/Common/Backup/Backups.php @@ -222,7 +222,7 @@ class Backups $backup_root = rtrim(GRAV_ROOT . $backup_root, '/'); } - if (!file_exists($backup_root)) { + if (!$backup_root || !file_exists($backup_root)) { throw new RuntimeException("Backup location: {$backup_root} does not exist..."); } diff --git a/system/src/Grav/Common/Cache.php b/system/src/Grav/Common/Cache.php index a961904de..a50351836 100644 --- a/system/src/Grav/Common/Cache.php +++ b/system/src/Grav/Common/Cache.php @@ -141,7 +141,7 @@ class Cache extends Getters $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8); // Cache key allows us to invalidate all cache on configuration changes. - $this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness; + $this->key = ($prefix ?: 'g') . '-' . $uniqueness; $this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true); $this->driver_setting = $this->config->get('system.cache.driver'); $this->driver = $this->getCacheDriver(); @@ -618,11 +618,7 @@ class Cache extends Getters */ public function isVolatileDriver($setting) { - if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) { - return true; - } - - return false; + return in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'], true); } /** diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php index 5693a921f..6ff7372d0 100644 --- a/system/src/Grav/Common/Config/Setup.php +++ b/system/src/Grav/Common/Config/Setup.php @@ -41,6 +41,9 @@ class Setup extends Data */ public static $environment; + /** @var string */ + public static $securityFile = 'config://security.yaml'; + /** @var array */ protected $streams = [ 'user' => [ @@ -390,12 +393,19 @@ class Setup extends Data if (!$locator->findResource('environment://config', true)) { // If environment does not have its own directory, remove it from the lookup. - $this->set('streams.schemes.environment.prefixes', ['config' => []]); + $prefixes = $this->get('streams.schemes.environment.prefixes'); + $prefixes['config'] = []; + + $this->set('streams.schemes.environment.prefixes', $prefixes); $this->initializeLocator($locator); } - // Create security.yaml if it doesn't exist. - $filename = $locator->findResource('config://security.yaml', true, true); + // Create security.yaml salt if it doesn't exist into existing configuration environment if possible. + $securityFile = basename(static::$securityFile); + $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile)); + $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true); + $filename = "{$securityFolder}/{$securityFile}"; + $security_file = CompiledYamlFile::instance($filename); $security_content = (array)$security_file->content(); diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 29a26369c..287819939 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -37,7 +37,7 @@ class Blueprint extends BlueprintForm /** @var string|null */ protected $scope; - /** @var BlueprintSchema */ + /** @var BlueprintSchema|null */ protected $blueprintSchema; /** @var object|null */ @@ -54,7 +54,7 @@ class Blueprint extends BlueprintForm */ public function __clone() { - if ($this->blueprintSchema) { + if (null !== $this->blueprintSchema) { $this->blueprintSchema = clone $this->blueprintSchema; } } diff --git a/system/src/Grav/Common/Data/BlueprintSchema.php b/system/src/Grav/Common/Data/BlueprintSchema.php index a50bf2310..7990bec29 100644 --- a/system/src/Grav/Common/Data/BlueprintSchema.php +++ b/system/src/Grav/Common/Data/BlueprintSchema.php @@ -56,6 +56,15 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface return $this->types[$name] ?? []; } + /** + * @param string $name + * @return array|null + */ + public function getNestedRules(string $name) + { + return $this->getNested($name); + } + /** * Validate data against blueprints. * @@ -317,6 +326,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface $toggle = []; } // Recursively fetch the items. + $childData = $data[$key] ?? null; + if (null !== $childData && !is_array($childData)) { + throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData))); + } $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value); } else { $field = $this->get($value); diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php index 67fb0a831..4de437dcb 100644 --- a/system/src/Grav/Common/Data/Data.php +++ b/system/src/Grav/Common/Data/Data.php @@ -264,7 +264,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable, */ public function blueprints() { - if (!$this->blueprints) { + if (null === $this->blueprints) { $this->blueprints = new Blueprint(); } elseif (is_callable($this->blueprints)) { // Lazy load blueprints. diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 4cba620af..bfb40578d 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -238,6 +238,7 @@ class Validation $value = trim($value); } + $value = preg_replace("/\r\n|\r/um", "\n", $value); $len = mb_strlen($value); $min = (int)($params['min'] ?? 0); @@ -280,7 +281,7 @@ class Validation $value = trim($value); } - return $value; + return preg_replace("/\r\n|\r/um", "\n", $value); } /** @@ -518,17 +519,32 @@ class Validation return false; } - if (isset($params['min']) && $value < $params['min']) { - return false; + $value = (float)$value; + + $min = 0; + if (isset($params['min'])) { + $min = (float)$params['min']; + if ($value < $min) { + return false; + } } - if (isset($params['max']) && $value > $params['max']) { - return false; + if (isset($params['max'])) { + $max = (float)$params['max']; + if ($value > $max) { + return false; + } } - $min = $params['min'] ?? 0; + if (isset($params['step'])) { + $step = (float)$params['step']; + // Count of how many steps we are above/below the minimum value. + $pos = ($value - $min) / $step; - return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0); + return is_int(static::filterNumber($pos, $params, $field)); + } + + return true; } /** @@ -592,7 +608,7 @@ class Validation */ public static function typeColor($value, array $params, array $field) { - return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value); + return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value); } /** @@ -765,14 +781,22 @@ class Validation } // If creating new values is allowed, no further checks are needed. - if (!empty($field['selectize']['create'])) { + $validateOptions = $field['validate']['options'] ?? null; + if (!empty($field['selectize']['create']) || $validateOptions === 'ignore') { return true; } $options = $field['options'] ?? []; $use = $field['use'] ?? 'values'; - if (empty($field['selectize']) || empty($field['multiple'])) { + if ($validateOptions) { + // Use custom options structure. + foreach ($options as &$option) { + $option = $option[$validateOptions] ?? null; + } + unset($option); + $options = array_values($options); + } elseif (empty($field['selectize']) || empty($field['multiple'])) { $options = array_keys($options); } if ($use === 'keys') { @@ -1173,7 +1197,7 @@ class Validation */ public static function filterItem_List($value, $params) { - return array_values(array_filter($value, function ($v) { + return array_values(array_filter($value, static function ($v) { return !empty($v); })); } diff --git a/system/src/Grav/Common/Debugger.php b/system/src/Grav/Common/Debugger.php index 6a1e256b9..b8143f68b 100644 --- a/system/src/Grav/Common/Debugger.php +++ b/system/src/Grav/Common/Debugger.php @@ -332,7 +332,7 @@ class Debugger return new Response(404, $headers, json_encode($response)); } - $data = is_array($data) ? array_map(function ($item) { + $data = is_array($data) ? array_map(static function ($item) { return $item->toArray(); }, $data) : $data->toArray(); diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index 6a3783b91..ecbc6ceac 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -197,7 +197,7 @@ abstract class Folder * Shift first directory out of the path. * * @param string $path - * @return string + * @return string|null */ public static function shift(&$path) { @@ -371,7 +371,7 @@ abstract class Folder return; } - if (strpos($target, $source) === 0) { + if (strpos($target, $source . '/') === 0) { throw new RuntimeException('Cannot move folder to itself'); } @@ -417,7 +417,8 @@ abstract class Folder if (!$success) { $error = error_get_last(); - throw new RuntimeException($error['message']); + + throw new RuntimeException($error['message'] ?? 'Unknown error'); } // Make sure that the change will be detected when caching. diff --git a/system/src/Grav/Common/Filesystem/ZipArchiver.php b/system/src/Grav/Common/Filesystem/ZipArchiver.php index 450a58126..6e53e70a3 100644 --- a/system/src/Grav/Common/Filesystem/ZipArchiver.php +++ b/system/src/Grav/Common/Filesystem/ZipArchiver.php @@ -57,7 +57,9 @@ class ZipArchiver extends Archiver throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...'); } - if (!file_exists($source)) { + // Get real path for our folder + $rootPath = realpath($source); + if (!$rootPath) { throw new InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...'); } @@ -66,9 +68,6 @@ class ZipArchiver extends Archiver throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...'); } - // Get real path for our folder - $rootPath = realpath($source); - $files = $this->getArchiveFiles($rootPath); $status && $status([ diff --git a/system/src/Grav/Common/Flex/FlexObject.php b/system/src/Grav/Common/Flex/FlexObject.php index 2a43eaa44..b64aea1df 100644 --- a/system/src/Grav/Common/Flex/FlexObject.php +++ b/system/src/Grav/Common/Flex/FlexObject.php @@ -13,6 +13,7 @@ namespace Grav\Common\Flex; use Grav\Common\Flex\Traits\FlexGravTrait; use Grav\Common\Flex\Traits\FlexObjectTrait; +use Grav\Common\Media\Interfaces\MediaInterface; use Grav\Framework\Flex\Traits\FlexMediaTrait; use function is_array; @@ -21,7 +22,7 @@ use function is_array; * * @package Grav\Common\Flex */ -abstract class FlexObject extends \Grav\Framework\Flex\FlexObject +abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface { use FlexGravTrait; use FlexObjectTrait; @@ -42,7 +43,7 @@ abstract class FlexObject extends \Grav\Framework\Flex\FlexObject // Handle media fields. $settings = $this->getFieldSettings($name); - if ($settings['media_field'] ?? false === true) { + if (($settings['media_field'] ?? false) === true) { return $this->parseFileProperty($value, $settings); } diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageCollection.php b/system/src/Grav/Common/Flex/Types/Pages/PageCollection.php index c75c4ce27..7503653b8 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageCollection.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageCollection.php @@ -19,7 +19,6 @@ use Grav\Common\Page\Header; use Grav\Common\Page\Interfaces\PageCollectionInterface; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Utils; -use Grav\Framework\Flex\Interfaces\FlexObjectInterface; use Grav\Framework\Flex\Pages\FlexPageCollection; use Collator; use InvalidArgumentException; @@ -159,7 +158,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa */ public function addPage(PageInterface $page) { - if (!$page instanceof FlexObjectInterface) { + if (!$page instanceof PageObject) { throw new InvalidArgumentException('$page is not a flex page.'); } @@ -192,6 +191,14 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa throw new RuntimeException(__METHOD__ . '(): Not Implemented'); } + /** + * Set current page. + */ + public function setCurrent(string $path): void + { + throw new RuntimeException(__METHOD__ . '(): Not Implemented'); + } + /** * Return previous item. * @@ -392,8 +399,8 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa $i = count($manual); $new_list = []; foreach ($list as $key => $dummy) { - $child = $this[$key]; - $order = array_search($child->slug, $manual, true); + $child = $this[$key] ?? null; + $order = $child ? array_search($child->slug, $manual, true) : false; if ($order === false) { $order = $i++; } @@ -426,20 +433,20 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa /** * Returns the items between a set of date ranges of either the page date field (default) or - * an arbitrary datetime page field where end date is optional - * Dates can be passed in as text that strtotime() can process + * an arbitrary datetime page field where start date and end date are optional + * Dates must be passed in as text that strtotime() can process * http://php.net/manual/en/function.strtotime.php * - * @param string $startDate - * @param string|false $endDate + * @param string|null $startDate + * @param string|null $endDate * @param string|null $field * @return static * @throws Exception */ - public function dateRange($startDate, $endDate = false, $field = null) + public function dateRange($startDate = null, $endDate = null, $field = null) { - $start = Utils::date2timestamp($startDate); - $end = $endDate ? Utils::date2timestamp($endDate) : false; + $start = $startDate ? Utils::date2timestamp($startDate) : null; + $end = $endDate ? Utils::date2timestamp($endDate) : null; $entries = []; foreach ($this as $key => $object) { @@ -449,7 +456,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa $date = $field ? strtotime($object->getNestedProperty($field)) : $object->date(); - if ($date >= $start && (!$end || $date <= $end)) { + if ((!$start || $date >= $start) && (!$end || $date <= $end)) { $entries[$key] = $object; } } @@ -746,6 +753,16 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa return $bool ? $this->select($list) : $this->unselect($list); } + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return PageIndex + */ + public function withTranslated(string $languageCode = null, bool $fallback = null) + { + return $this->getIndex()->withTranslated($languageCode, $fallback); + } + /** * Filter pages by given filters. * diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php index b43b88602..f02a64866 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageIndex.php @@ -18,6 +18,7 @@ use Grav\Common\File\CompiledYamlFile; use Grav\Common\Flex\Traits\FlexGravTrait; use Grav\Common\Flex\Traits\FlexIndexTrait; use Grav\Common\Grav; +use Grav\Common\Language\Language; use Grav\Common\Page\Header; use Grav\Common\Page\Interfaces\PageCollectionInterface; use Grav\Common\Page\Interfaces\PageInterface; @@ -108,6 +109,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface } $element = parent::get($key); + if (null === $element) { + return null; + } + if (isset($params)) { $element = $element->getTranslation(ltrim($params, '.')); } @@ -164,6 +169,31 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface return $root; } + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return PageIndex + */ + public function withTranslated(string $languageCode = null, bool $fallback = null) + { + if (null === $languageCode) { + return $this; + } + + $entries = $this->translateEntries($this->getEntries(), $languageCode, $fallback); + $params = ['language' => $languageCode, 'language_fallback' => $fallback] + $this->getParams(); + + return $this->createFrom($entries)->setParams($params); + } + + /** + * @return string|null + */ + public function getLanguage(): ?string + { + return $this->_params['language'] ?? null; + } + /** * Get the collection params * @@ -174,6 +204,17 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface return $this->_params ?? []; } + /** + * Get the collection param + * + * @param string $name + * @return mixed + */ + public function getParam(string $name) + { + return $this->_params[$name] ?? null; + } + /** * Set parameters to the Collection * @@ -187,6 +228,20 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface return $this; } + /** + * Set a parameter to the Collection + * + * @param string $name + * @param mixed $value + * @return $this + */ + public function setParam(string $name, $value) + { + $this->_params[$name] = $value; + + return $this; + } + /** * Get the collection params * @@ -197,6 +252,15 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface return $this->getParams(); } + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getCacheKey() + */ + public function getCacheKey(): string + { + return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->getKeyField() . $this->getLanguage()); + } + /** * Filter pages by given filters. * @@ -271,7 +335,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface */ protected function filterByParent(array $filters) { - return parent::filterBy($filters); + /** @var static $index */ + $index = parent::filterBy($filters); + + return $index; } /** @@ -345,6 +412,96 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface return $index; } + /** + * @param array $entries + * @param string $lang + * @param bool|null $fallback + * @return array + */ + protected function translateEntries(array $entries, string $lang, bool $fallback = null): array + { + $languages = $this->getFallbackLanguages($lang, $fallback); + foreach ($entries as $key => &$entry) { + // Find out which version of the page we should load. + $translations = $this->getLanguageTemplates((string)$key); + if (!$translations) { + // No translations found, is this a folder? + continue; + } + + // Find a translation. + $template = null; + foreach ($languages as $code) { + if (isset($translations[$code])) { + $template = $translations[$code]; + break; + } + } + + // We couldn't find a translation, remove entry from the list. + if (!isset($code, $template)) { + unset($entries['key']); + continue; + } + + // Get the main key without template and langauge. + [$main_key,] = explode('|', $entry['storage_key'] . '|', 2); + + // Update storage key and language. + $entry['storage_key'] = $main_key . '|' . $template . '.' . $code; + $entry['lang'] = $code; + } + unset($entry); + + return $entries; + } + + /** + * @return array + */ + protected function getLanguageTemplates(string $key): array + { + $meta = $this->getMetaData($key); + $template = $meta['template'] ?? 'folder'; + $translations = $meta['markdown'] ?? []; + $list = []; + foreach ($translations as $code => $search) { + if (isset($search[$template])) { + // Use main template if possible. + $list[$code] = $template; + } elseif (!empty($search)) { + // Fall back to first matching template. + $list[$code] = key($search); + } + } + + return $list; + } + + /** + * @param string|null $languageCode + * @param bool|null $fallback + * @return array + */ + protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array + { + $fallback = $fallback ?? true; + if (!$fallback && null !== $languageCode) { + return [$languageCode]; + } + + $grav = Grav::instance(); + + /** @var Language $language */ + $language = $grav['language']; + $languageCode = $languageCode ?? ''; + if ($languageCode === '' && $fallback) { + return $language->getFallbackLanguages(null, true); + } + + return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode]; + } + /** * @param array $options * @return array @@ -523,13 +680,14 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface $child_count = $tmp->count(); $count = $filters ? $tmp->filterBy($filters, true)->count() : null; $route = $child->getRoute(); + $route = $route ? ($route->toString(false) ?: '/') : ''; $payload = [ - 'item-key' => basename($child->rawRoute() ?? $child->getKey()), + 'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())), 'icon' => $icon, 'title' => htmlspecialchars($child->menu()), 'route' => [ - 'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '', - 'raw' => $child->rawRoute(), + 'display' => htmlspecialchars($route) ?: null, + 'raw' => htmlspecialchars($child->rawRoute()), ], 'modified' => $this->jsDate($child->modified()), 'child_count' => $child_count ?: null, @@ -684,12 +842,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface /** * Remove item from the list. * - * @param PageInterface|string|null $key - * - * @return $this + * @param string $key + * @return PageObject|null * @throws InvalidArgumentException */ - public function remove($key = null) + public function remove($key) { return $this->getCollection()->remove($key); } @@ -799,17 +956,17 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface /** * Returns the items between a set of date ranges of either the page date field (default) or - * an arbitrary datetime page field where end date is optional - * Dates can be passed in as text that strtotime() can process + * an arbitrary datetime page field where start date and end date are optional + * Dates must be passed in as text that strtotime() can process * http://php.net/manual/en/function.strtotime.php * - * @param string $startDate - * @param bool $endDate + * @param string|null $startDate + * @param string|null $endDate * @param string|null $field * @return static * @throws Exception */ - public function dateRange($startDate, $endDate = false, $field = null) + public function dateRange($startDate = null, $endDate = null, $field = null) { $collection = $this->__call('dateRange', [$startDate, $endDate, $field]); diff --git a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php index 933a00a32..4c83f959b 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/PageObject.php +++ b/system/src/Grav/Common/Flex/Types/Pages/PageObject.php @@ -104,12 +104,12 @@ class PageObject extends FlexPageObject */ public function getRoute($query = []): ?Route { - $route = $this->route(); - if (null === $route) { + $path = $this->route(); + if (null === $path) { return null; } - $route = RouteFactory::createFromString($route); + $route = RouteFactory::createFromString($path); if ($lang = $route->getLanguage()) { $grav = Grav::instance(); if (!$grav['config']->get('system.languages.include_default_lang')) { @@ -262,6 +262,24 @@ class PageObject extends FlexPageObject $this->getFlexDirectory()->reloadIndex(); } + /** + * @param UserInterface|null $user + */ + public function check(UserInterface $user = null): void + { + parent::check($user); + + if ($user && $this->isMoved()) { + $parentKey = $this->getProperty('parent_key'); + + /** @var PageObject|null $parent */ + $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key'); + if (!$parent || !$parent->isAuthorized('create', null, $user)) { + throw new \RuntimeException('Forbidden', 403); + } + } + } + /** * @param array|bool $reorder * @return FlexObject|FlexObjectInterface @@ -293,7 +311,7 @@ class PageObject extends FlexPageObject } // Reset original after save events have all been called. - $this->_original = null; + $this->_originalObject = null; return $instance; } @@ -357,6 +375,19 @@ class PageObject extends FlexPageObject return parent::isAuthorizedOverride($user, $action, $scope, $isMe); } + /** + * @return bool + */ + protected function isMoved(): bool + { + $storageKey = $this->getMasterKey(); + $filesystem = Filesystem::getInstance(false); + $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/'); + $newParentKey = $this->getProperty('parent_key'); + + return $this->exists() && $oldParentKey !== $newParentKey; + } + /** * @param array $ordering * @return PageCollection|null @@ -364,10 +395,7 @@ class PageObject extends FlexPageObject protected function reorderSiblings(array $ordering) { $storageKey = $this->getMasterKey(); - $filesystem = Filesystem::getInstance(false); - $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/'); - $newParentKey = $this->getProperty('parent_key'); - $isMoved = $oldParentKey !== $newParentKey; + $isMoved = $this->isMoved(); $order = !$isMoved ? $this->order() : false; if ($order !== false) { $order = (int)$order; @@ -385,10 +413,12 @@ class PageObject extends FlexPageObject // Handle special case where ordering isn't given. if ($ordering === []) { if ($order >= 999999) { - // Set ordering to point to be the last item. + // Set ordering to point to be the last item, ignoring the object itself. $order = 0; foreach ($siblings as $sibling) { - $order = max($order, (int)$sibling->order()); + if ($sibling->getKey() !== $this->getKey()) { + $order = max($order, (int)$sibling->order()); + } } $this->order($order + 1); } @@ -411,7 +441,8 @@ class PageObject extends FlexPageObject // Add missing siblings into the end of the list, keeping the previous ordering between them. foreach ($siblings as $sibling) { - $basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder')); + $folder = (string)$sibling->getProperty('folder'); + $basename = preg_replace('|^\d+\.|', '', $folder); if (!in_array($basename, $ordering, true)) { $ordering[] = $basename; } @@ -421,7 +452,8 @@ class PageObject extends FlexPageObject $ordering = array_flip(array_values($ordering)); $count = count($ordering); foreach ($siblings as $sibling) { - $basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder')); + $folder = (string)$sibling->getProperty('folder'); + $basename = preg_replace('|^\d+\.|', '', $folder); $newOrder = $ordering[$basename] ?? null; $newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count; $sibling->order($newOrder); @@ -500,6 +532,8 @@ class PageObject extends FlexPageObject if ($isNew === true && $name === '') { // Support onBlueprintCreated event just like in Pages::blueprints($template) $blueprint->set('initialized', true); + $blueprint->setFilename($template); + Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template])); } diff --git a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php index d8193df7c..589ea881a 100644 --- a/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php +++ b/system/src/Grav/Common/Flex/Types/Pages/Traits/PageLegacyTrait.php @@ -103,7 +103,10 @@ trait PageLegacyTrait $parent = $this->parent(); $collection = $parent ? $parent->collection('content', false) : null; if (null !== $path && $collection instanceof PageCollectionInterface) { - return $collection->adjacentSibling($path, $direction); + $child = $collection->adjacentSibling($path, $direction); + if ($child instanceof PageInterface) { + return $child; + } } return false; diff --git a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php index fb69eab68..ea68fa1bf 100644 --- a/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php +++ b/system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php @@ -41,6 +41,14 @@ class UserGroupObject extends FlexObject implements UserGroupInterface ] + parent::getCachedMethods(); } + /** + * @return string + */ + public function getTitle(): string + { + return $this->getProperty('readableName'); + } + /** * Checks user authorization to the action. * diff --git a/system/src/Grav/Common/Flex/Types/Users/UserCollection.php b/system/src/Grav/Common/Flex/Types/Users/UserCollection.php index e17525a9c..9b6490a8a 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserCollection.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserCollection.php @@ -92,7 +92,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface } else { $user = parent::find($query, $field); } - if ($user) { + if ($user instanceof UserObject) { return $user; } } @@ -123,7 +123,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface * @param string $key * @return string */ - protected function filterUsername(string $key) + protected function filterUsername(string $key): string { $storage = $this->getFlexDirectory()->getStorage(); if (method_exists($storage, 'normalizeKey')) { diff --git a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php index 4a3f58773..6e0bc65fe 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserIndex.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserIndex.php @@ -62,7 +62,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface * @param FlexStorageInterface $storage * @return void */ - public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage) + public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage): void { // Username can also be number and stored as such. $key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']); @@ -187,7 +187,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface * @param array $updated * @param array $removed */ - protected static function onChanges(array $entries, array $added, array $updated, array $removed) + protected static function onChanges(array $entries, array $added, array $updated, array $removed): void { $message = sprintf('Flex: User index updated, %d objects (%d added, %d updated, %d removed).', count($entries), count($added), count($updated), count($removed)); diff --git a/system/src/Grav/Common/Flex/Types/Users/UserObject.php b/system/src/Grav/Common/Flex/Types/Users/UserObject.php index 4109eec34..02caee4aa 100644 --- a/system/src/Grav/Common/Flex/Types/Users/UserObject.php +++ b/system/src/Grav/Common/Flex/Types/Users/UserObject.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Grav\Common\Flex\Types\Users; +use Closure; use Countable; use Grav\Common\Config\Config; use Grav\Common\Data\Blueprint; @@ -22,7 +23,6 @@ use Grav\Common\Grav; use Grav\Common\Media\Interfaces\MediaCollectionInterface; use Grav\Common\Media\Interfaces\MediaUploadInterface; use Grav\Common\Page\Media; -use Grav\Common\Page\Medium\Medium; use Grav\Common\Page\Medium\MediumFactory; use Grav\Common\User\Access; use Grav\Common\User\Authentication; @@ -32,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface; use Grav\Common\User\Traits\UserTrait; use Grav\Framework\File\Formatter\JsonFormatter; use Grav\Framework\File\Formatter\YamlFormatter; +use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Flex\Flex; use Grav\Framework\Flex\FlexDirectory; use Grav\Framework\Flex\Storage\FileStorage; @@ -76,18 +77,17 @@ class UserObject extends FlexObject implements UserInterface, Countable use UserTrait; use UserObjectLegacyTrait; + /** @var Closure|null */ + static public $authorizeCallable; + /** @var array|null */ protected $_uploads_original; - /** @var FileInterface|null */ protected $_storage; - /** @var UserGroupIndex */ protected $_groups; - /** @var Access */ protected $_access; - /** @var array|null */ protected $access; @@ -230,6 +230,16 @@ class UserObject extends FlexObject implements UserInterface, Countable return $this; } + /** + * @return bool + */ + public function isMyself(): bool + { + $me = $this->getActiveUser(); + + return $me && $me->authenticated && $this->username === $me->username; + } + /** * Checks user authorization to the action. * @@ -264,6 +274,15 @@ class UserObject extends FlexObject implements UserInterface, Countable } } + $authorizeCallable = static::$authorizeCallable; + if ($authorizeCallable instanceof Closure) { + $authorizeCallable->bindTo($this); + $authorized = $authorizeCallable($action, $scope); + if (is_bool($authorized)) { + return $authorized; + } + } + // Check user access. $access = $this->getAccess(); $authorized = $access->authorize($action, $scope); @@ -297,6 +316,14 @@ class UserObject extends FlexObject implements UserInterface, Countable return $value; } + /** + * @return UserGroupIndex + */ + public function getRoles(): UserGroupIndex + { + return $this->getGroups(); + } + /** * Convert object into an array. * @@ -694,6 +721,7 @@ class UserObject extends FlexObject implements UserInterface, Countable /** * @param array $files + * @return void */ protected function setUpdatedMedia(array $files): void { @@ -701,10 +729,16 @@ class UserObject extends FlexObject implements UserInterface, Countable $locator = Grav::instance()['locator']; $media = $this->getMedia(); + if (!$media instanceof MediaUploadInterface) { + return; + } + + $filesystem = Filesystem::getInstance(false); $list = []; $list_original = []; foreach ($files as $field => $group) { + // Ignore files without a field. if ($field === '') { continue; } @@ -712,7 +746,6 @@ class UserObject extends FlexObject implements UserInterface, Countable // Load settings for the field. $settings = $this->getMediaFieldSettings($field); - foreach ($group as $filename => $file) { if ($file) { // File upload. @@ -727,8 +760,8 @@ class UserObject extends FlexObject implements UserInterface, Countable } if ($file) { - // Check file upload against media limits. - $filename = $media->checkUploadedFile($file, $filename, $settings); + // Check file upload against media limits (except for max size). + $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings); } $self = $settings['self']; @@ -751,19 +784,25 @@ class UserObject extends FlexObject implements UserInterface, Countable continue; } + // Calculate path without the retina scaling factor. + $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath)); + $list[$filename] = [$file, $settings]; + $path = str_replace('.', "\n", $field); if (null !== $data) { $data['name'] = $filename; $data['path'] = $filepath; - $this->setNestedProperty("{$field}\n{$filepath}", $data, "\n"); + $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n"); } else { - $this->unsetNestedProperty("{$field}\n{$filepath}", "\n"); + $this->unsetNestedProperty("{$path}\n{$realpath}", "\n"); } } } + $this->clearMediaCache(); + $this->_uploads = $list; $this->_uploads_original = $list_original; } diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index 1b15e5de8..450569632 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -35,7 +35,11 @@ class GPM extends Iterator /** @var Remote\Packages|null Remote available Packages */ private $repository; /** @var Remote\GravCore|null Remove Grav Packages */ - public $grav; + private $grav; + /** @var bool */ + private $refresh; + /** @var callable|null */ + private $callback; /** @var array Internal cache */ protected $cache; @@ -55,13 +59,45 @@ class GPM extends Iterator public function __construct($refresh = false, $callback = null) { parent::__construct(); + + Folder::create(CACHE_DIR . '/gpm'); + $this->cache = []; $this->installed = new Local\Packages(); - try { - $this->repository = new Remote\Packages($refresh, $callback); - $this->grav = new Remote\GravCore($refresh, $callback); - } catch (Exception $e) { + $this->refresh = $refresh; + $this->callback = $callback; + } + + /** + * Magic getter method + * + * @param string $offset Asset name value + * @return mixed Asset value + */ + public function __get($offset) + { + switch ($offset) { + case 'grav': + return $this->getGrav(); } + + return parent::__get($offset); + } + + /** + * Magic method to determine if the attribute is set + * + * @param string $offset Asset name value + * @return bool True if the value is set + */ + public function __isset($offset) + { + switch ($offset) { + case 'grav': + return $this->getGrav() !== null; + } + + return parent::__isset($offset); } /** @@ -266,11 +302,12 @@ class GPM extends Iterator { $items = []; - if (null === $this->repository) { + $repository = $this->getRepository(); + if (null === $repository) { return $items; } - $repository = $this->repository['plugins']; + $plugins = $repository['plugins']; // local cache to speed things up if (isset($this->cache[__METHOD__])) { @@ -278,18 +315,18 @@ class GPM extends Iterator } foreach ($this->installed['plugins'] as $slug => $plugin) { - if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) { + if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) { continue; } $local_version = $plugin->version ?? 'Unknown'; - $remote_version = $repository[$slug]->version; + $remote_version = $plugins[$slug]->version; if (version_compare($local_version, $remote_version) < 0) { - $repository[$slug]->available = $remote_version; - $repository[$slug]->version = $local_version; - $repository[$slug]->type = $repository[$slug]->release_type; - $items[$slug] = $repository[$slug]; + $plugins[$slug]->available = $remote_version; + $plugins[$slug]->version = $local_version; + $plugins[$slug]->type = $plugins[$slug]->release_type; + $items[$slug] = $plugins[$slug]; } } @@ -306,19 +343,20 @@ class GPM extends Iterator */ public function getLatestVersionOfPackage($package_name) { - if (null === $this->repository) { + $repository = $this->getRepository(); + if (null === $repository) { return null; } - $repository = $this->repository['plugins']; - if (isset($repository[$package_name])) { - return $repository[$package_name]->available ?: $repository[$package_name]->version; + $plugins = $repository['plugins']; + if (isset($plugins[$package_name])) { + return $plugins[$package_name]->available ?: $plugins[$package_name]->version; } //Not a plugin, it's a theme? - $repository = $this->repository['themes']; - if (isset($repository[$package_name])) { - return $repository[$package_name]->available ?: $repository[$package_name]->version; + $themes = $repository['themes']; + if (isset($themes[$package_name])) { + return $themes[$package_name]->available ?: $themes[$package_name]->version; } return null; @@ -356,11 +394,12 @@ class GPM extends Iterator { $items = []; - if (null === $this->repository) { + $repository = $this->getRepository(); + if (null === $repository) { return $items; } - $repository = $this->repository['themes']; + $themes = $repository['themes']; // local cache to speed things up if (isset($this->cache[__METHOD__])) { @@ -368,18 +407,18 @@ class GPM extends Iterator } foreach ($this->installed['themes'] as $slug => $plugin) { - if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) { + if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) { continue; } $local_version = $plugin->version ?? 'Unknown'; - $remote_version = $repository[$slug]->version; + $remote_version = $themes[$slug]->version; if (version_compare($local_version, $remote_version) < 0) { - $repository[$slug]->available = $remote_version; - $repository[$slug]->version = $local_version; - $repository[$slug]->type = $repository[$slug]->release_type; - $items[$slug] = $repository[$slug]; + $themes[$slug]->available = $remote_version; + $themes[$slug]->version = $local_version; + $themes[$slug]->type = $themes[$slug]->release_type; + $items[$slug] = $themes[$slug]; } } @@ -407,19 +446,20 @@ class GPM extends Iterator */ public function getReleaseType($package_name) { - if (null === $this->repository) { + $repository = $this->getRepository(); + if (null === $repository) { return null; } - $repository = $this->repository['plugins']; - if (isset($repository[$package_name])) { - return $repository[$package_name]->release_type; + $plugins = $repository['plugins']; + if (isset($plugins[$package_name])) { + return $plugins[$package_name]->release_type; } //Not a plugin, it's a theme? - $repository = $this->repository['themes']; - if (isset($repository[$package_name])) { - return $repository[$package_name]->release_type; + $themes = $repository['themes']; + if (isset($themes[$package_name])) { + return $themes[$package_name]->release_type; } return null; @@ -470,7 +510,7 @@ class GPM extends Iterator */ public function getRepositoryPlugins() { - return $this->repository['plugins'] ?? null; + return $this->getRepository()['plugins'] ?? null; } /** @@ -493,7 +533,7 @@ class GPM extends Iterator */ public function getRepositoryThemes() { - return $this->repository['themes'] ?? null; + return $this->getRepository()['themes'] ?? null; } /** @@ -504,9 +544,31 @@ class GPM extends Iterator */ public function getRepository() { + if (null === $this->repository) { + try { + $this->repository = new Remote\Packages($this->refresh, $this->callback); + } catch (Exception $e) {} + } + return $this->repository; } + /** + * Returns Grav version available in the repository + * + * @return Remote\GravCore|null + */ + public function getGrav() + { + if (null === $this->grav) { + try { + $this->grav = new Remote\GravCore($this->refresh, $this->callback); + } catch (Exception $e) {} + } + + return $this->grav; + } + /** * Searches for a Package in the repository * @@ -527,7 +589,7 @@ class GPM extends Iterator $plugins = $this->getRepositoryPlugins(); if (null === $themes || null === $plugins) { - if (!is_writable(ROOT_DIR . '/cache/gpm')) { + if (!is_writable(GRAV_ROOT . '/cache/gpm')) { throw new RuntimeException('The cache/gpm folder is not writable. Please check the folder permissions.'); } diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php index 9cdea52aa..639240be6 100644 --- a/system/src/Grav/Common/GPM/Installer.php +++ b/system/src/Grav/Common/GPM/Installer.php @@ -135,7 +135,10 @@ class Installer } if (!$options['sophisticated']) { - if ($options['theme']) { + $isTheme = $options['theme'] ?? false; + // Make sure that themes are always being copied, even if option was not set! + $isTheme = $isTheme || preg_match('|/themes/[^/]+|ui', $install_path); + if ($isTheme) { self::copyInstall($extracted, $install_path); } else { self::moveInstall($extracted, $install_path); diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php index 47cef7c09..98654b6a1 100644 --- a/system/src/Grav/Common/GPM/Response.php +++ b/system/src/Grav/Common/GPM/Response.php @@ -1,143 +1,3 @@ 'Grav CMS' - ]; - - /** - * Makes a request to the URL by using the preferred method - * - * @param string $uri URL to call - * @param array $overrides An array of parameters for both `curl` and `fopen` - * @param callable|null $callback Either a function or callback in array notation - * @return string The response of the request - * @throws TransportExceptionInterface - */ - public static function get($uri = '', $overrides = [], $callback = null) - { - if (empty($uri)) { - throw new TransportException('missing URI'); - } - - // check if this function is available, if so use it to stop any timeouts - try { - if (Utils::functionExists('set_time_limit')) { - @set_time_limit(0); - } - } catch (Exception $e) { - } - - $config = Grav::instance()['config']; - $referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true); - $options = new HttpOptions(); - - // Set default Headers - $options->setHeaders(array_merge([ 'Referer' => $referer ], self::$headers)); - - // Disable verify Peer if required - $verify_peer = $config->get('system.gpm.verify_peer', true); - if ($verify_peer !== true) { - $options->verifyPeer($verify_peer); - } - - // Set proxy url if provided - $proxy_url = $config->get('system.gpm.proxy_url', false); - if ($proxy_url) { - $options->setProxy($proxy_url); - } - - // Use callback if provided - if ($callback) { - self::$callback = $callback; - $options->setOnProgress([Response::class, 'progress']); - } - - $preferred_method = $config->get('system.gpm.method', 'auto'); - - $settings = array_merge_recursive($options->toArray(), $overrides); - - switch ($preferred_method) { - case 'curl': - $client = new CurlHttpClient($settings); - break; - case 'fopen': - case 'native': - $client = new NativeHttpClient($settings); - break; - default: - $client = HttpClient::create($settings); - } - - $response = $client->request('GET', $uri); - - return $response->getContent(); - } - - - /** - * Is this a remote file or not - * - * @param string $file - * @return bool - */ - public static function isRemote($file) - { - return (bool) filter_var($file, FILTER_VALIDATE_URL); - } - - /** - * Progress normalized for cURL and Fopen - * Accepts a variable length of arguments passed in by stream method - * - * @return void - */ - public static function progress(int $bytes_transferred, int $filesize, array $info) - { - - if ($bytes_transferred > 0) { - $percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize); - - $progress = [ - 'code' => $info['http_code'], - 'filesize' => $filesize, - 'transferred' => $bytes_transferred, - 'percent' => $percent < 100 ? $percent : 100 - ]; - - if (self::$callback !== null) { - call_user_func(self::$callback, $progress); - } - } - } -} +// Create alias for the deprecated class. +class_alias(\Grav\Common\HTTP\Response::class, \Grav\Common\GPM\Response::class); diff --git a/system/src/Grav/Common/Getters.php b/system/src/Grav/Common/Getters.php index 8f3a73a2b..916d5249a 100644 --- a/system/src/Grav/Common/Getters.php +++ b/system/src/Grav/Common/Getters.php @@ -69,6 +69,7 @@ abstract class Getters implements ArrayAccess, Countable * @param int|string $offset * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { if ($this->gettersVariable) { @@ -84,6 +85,7 @@ abstract class Getters implements ArrayAccess, Countable * @param int|string $offset * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { if ($this->gettersVariable) { @@ -99,6 +101,7 @@ abstract class Getters implements ArrayAccess, Countable * @param int|string $offset * @param mixed $value */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { if ($this->gettersVariable) { @@ -112,6 +115,7 @@ abstract class Getters implements ArrayAccess, Countable /** * @param int|string $offset */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { if ($this->gettersVariable) { diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index c9097eea8..f5919613b 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -9,6 +9,7 @@ namespace Grav\Common; +use Composer\Autoload\ClassLoader; use Grav\Common\Config\Config; use Grav\Common\Config\Setup; use Grav\Common\Helpers\Exif; @@ -134,7 +135,7 @@ class Grav extends Container * * @return void */ - public static function resetInstance() + public static function resetInstance(): void { if (self::$instance) { // @phpstan-ignore-next-line @@ -152,6 +153,13 @@ class Grav extends Container { if (null === self::$instance) { self::$instance = static::load($values); + + /** @var ClassLoader|null $loader */ + $loader = self::$instance['loader'] ?? null; + if ($loader) { + // Load fix for Deferred Twig Extension + $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true); + } } elseif ($values) { $instance = self::$instance; foreach ($values as $key => $value) { @@ -234,7 +242,7 @@ class Grav extends Container * * @return void */ - public function process() + public function process(): void { if (isset($this->initialized['process'])) { return; @@ -466,7 +474,7 @@ class Grav extends Container * @param int $code Redirection code (30x) * @return void */ - public function redirectLangSafe($route, $code = null) + public function redirectLangSafe($route, $code = null): void { if (!$this['uri']->isExternal($route)) { $this->redirect($this['pages']->route($route), $code); @@ -481,7 +489,7 @@ class Grav extends Container * @param ResponseInterface|null $response * @return void */ - public function header(ResponseInterface $response = null) + public function header(ResponseInterface $response = null): void { if (null === $response) { /** @var PageInterface $page */ @@ -506,7 +514,7 @@ class Grav extends Container * * @return void */ - public function setLocale() + public function setLocale(): void { // Initialize Locale if set and configured. if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) { @@ -567,7 +575,7 @@ class Grav extends Container * * @return void */ - public function shutdown() + public function shutdown(): void { // Prevent user abort allowing onShutdown event to run without interruptions. if (function_exists('ignore_user_abort')) { @@ -686,7 +694,7 @@ class Grav extends Container * * @return void */ - protected function registerServices() + protected function registerServices(): void { foreach (self::$diMap as $serviceKey => $serviceClass) { if (is_int($serviceKey)) { @@ -753,12 +761,10 @@ class Grav extends Container // unsupported media type, try to download it... if ($uri_extension) { $extension = $uri_extension; + } elseif (isset($path_parts['extension'])) { + $extension = $path_parts['extension']; } else { - if (isset($path_parts['extension'])) { - $extension = $path_parts['extension']; - } else { - $extension = null; - } + $extension = null; } if ($extension) { @@ -773,6 +779,6 @@ class Grav extends Container return false; } - return $page; + return $page ?? false; } } diff --git a/system/src/Grav/Common/HTTP/Client.php b/system/src/Grav/Common/HTTP/Client.php new file mode 100644 index 000000000..cd4f8dbb2 --- /dev/null +++ b/system/src/Grav/Common/HTTP/Client.php @@ -0,0 +1,130 @@ + 'Grav CMS' + ]; + + public static function getClient(array $overrides = [], int $connections = 6, callable $callback = null): HttpClientInterface + { + $config = Grav::instance()['config']; + $options = static::getOptions(); + + // Use callback if provided + if ($callback) { + self::$callback = $callback; + $options->setOnProgress([Client::class, 'progress']); + } + + $settings = array_merge($options->toArray(), $overrides); + $preferred_method = $config->get('system.http.method'); + // Try old GPM setting if value is the same as system default + if ($preferred_method === 'auto') { + $preferred_method = $config->get('system.gpm.method', 'auto'); + } + + switch ($preferred_method) { + case 'curl': + $client = new CurlHttpClient($settings, $connections); + break; + case 'fopen': + case 'native': + $client = new NativeHttpClient($settings, $connections); + break; + default: + $client = HttpClient::create($settings, $connections); + } + + return $client; + } + + /** + * Get HTTP Options + * + * @return HttpOptions + */ + public static function getOptions(): HttpOptions + { + $config = Grav::instance()['config']; + $referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true); + + $options = new HttpOptions(); + + // Set default Headers + $options->setHeaders(array_merge([ 'Referer' => $referer ], self::$headers)); + + // Disable verify Peer if required + $verify_peer = $config->get('system.http.verify_peer'); + // Try old GPM setting if value is default + if ($verify_peer === true) { + $verify_peer = $config->get('system.gpm.verify_peer', null) ?? $verify_peer; + } + $options->verifyPeer($verify_peer); + + // Set verify Host + $verify_host = $config->get('system.http.verify_host', true); + $options->verifyHost($verify_host); + + // New setting and must be enabled for Proxy to work + if ($config->get('system.http.enable_proxy', true)) { + // Set proxy url if provided + $proxy_url = $config->get('system.http.proxy_url', $config->get('system.gpm.proxy_url', null)); + if ($proxy_url !== null) { + $options->setProxy($proxy_url); + } + + // Certificate + $proxy_cert = $config->get('system.http.proxy_cert_path', null); + if ($proxy_cert !== null) { + $options->setCaPath($proxy_cert); + } + } + + return $options; + } + + /** + * Progress normalized for cURL and Fopen + * Accepts a variable length of arguments passed in by stream method + * + * @return void + */ + public static function progress(int $bytes_transferred, int $filesize, array $info) + { + + if ($bytes_transferred > 0) { + $percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize); + + $progress = [ + 'code' => $info['http_code'], + 'filesize' => $filesize, + 'transferred' => $bytes_transferred, + 'percent' => $percent < 100 ? $percent : 100 + ]; + + if (self::$callback !== null) { + call_user_func(self::$callback, $progress); + } + } + } +} diff --git a/system/src/Grav/Common/HTTP/Response.php b/system/src/Grav/Common/HTTP/Response.php new file mode 100644 index 000000000..4a0513667 --- /dev/null +++ b/system/src/Grav/Common/HTTP/Response.php @@ -0,0 +1,96 @@ +getContent(); + } + + + /** + * Makes a request to the URL by using the preferred method + * + * @param string $method method to call such as GET, PUT, etc + * @param string $uri URL to call + * @param array $overrides An array of parameters for both `curl` and `fopen` + * @param callable|null $callback Either a function or callback in array notation + * @return ResponseInterface + * @throws TransportExceptionInterface + */ + public static function request(string $method, string $uri, array $overrides = [], callable $callback = null): ResponseInterface + { + if (empty($method)) { + throw new TransportException('missing method (GET, PUT, etc.)'); + } + + if (empty($uri)) { + throw new TransportException('missing URI'); + } + + // check if this function is available, if so use it to stop any timeouts + try { + if (Utils::functionExists('set_time_limit')) { + @set_time_limit(0); + } + } catch (Exception $e) {} + + $client = Client::getClient($overrides, 6, $callback); + + return $client->request($method, $uri); + } + + + /** + * Is this a remote file or not + * + * @param string $file + * @return bool + */ + public static function isRemote($file): bool + { + return (bool) filter_var($file, FILTER_VALIDATE_URL); + } + + +} diff --git a/system/src/Grav/Common/Helpers/Excerpts.php b/system/src/Grav/Common/Helpers/Excerpts.php index cd285cf87..173850a53 100644 --- a/system/src/Grav/Common/Helpers/Excerpts.php +++ b/system/src/Grav/Common/Helpers/Excerpts.php @@ -33,6 +33,9 @@ class Excerpts public static function processImageHtml($html, PageInterface $page = null) { $excerpt = static::getExcerptFromHtml($html, 'img'); + if (null === $excerpt) { + return ''; + } $original_src = $excerpt['element']['attributes']['src']; $excerpt['element']['attributes']['href'] = $original_src; @@ -61,6 +64,9 @@ class Excerpts public static function processLinkHtml($html, PageInterface $page = null) { $excerpt = static::getExcerptFromHtml($html, 'a'); + if (null === $excerpt) { + return ''; + } $original_href = $excerpt['element']['attributes']['href']; $excerpt = static::processLinkExcerpt($excerpt, $page, 'link'); @@ -89,7 +95,6 @@ class Excerpts $excerpt = null; $inner = []; - /** @var DOMElement $element */ foreach ($elements as $element) { $attributes = []; foreach ($element->attributes as $name => $value) { diff --git a/system/src/Grav/Common/Helpers/LogViewer.php b/system/src/Grav/Common/Helpers/LogViewer.php index a03fde810..187828ae3 100644 --- a/system/src/Grav/Common/Helpers/LogViewer.php +++ b/system/src/Grav/Common/Helpers/LogViewer.php @@ -53,7 +53,6 @@ class LogViewer */ public function tail($filepath, $lines = 1) { - $f = $filepath ? @fopen($filepath, 'rb') : false; if ($f === false) { return false; @@ -62,13 +61,12 @@ class LogViewer $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096)); fseek($f, -1, SEEK_END); - if (fread($f, 1) != "\n") { - $lines -= 1; + if (fread($f, 1) !== "\n") { + --$lines; } // Start reading $output = ''; - $chunk = ''; // While we would like more while (ftell($f) > 0 && $lines >= 0) { // Figure out how far back we should jump @@ -76,7 +74,11 @@ class LogViewer // Do the jump (backwards, relative to where we are) fseek($f, -$seek, SEEK_CUR); // Read a chunk and prepend it to our output - $output = ($chunk = fread($f, $seek)) . $output; + $chunk = fread($f, $seek); + if ($chunk === false) { + throw new \RuntimeException('Cannot read file'); + } + $output = $chunk . $output; // Jump back to where we started reading fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); // Decrease our line counter @@ -123,13 +125,13 @@ class LogViewer */ public function parse($line) { - if (!is_string($line) || strlen($line) === 0) { - return array(); + if (!is_string($line) || $line === '') { + return []; } preg_match($this->pattern, $line, $data); if (!isset($data['date'])) { - return array(); + return []; } preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches); @@ -138,7 +140,7 @@ class LogViewer $data['trace'] = trim($matches[2]); } - return array( + return [ 'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']), 'logger' => $data['logger'], 'level' => $data['level'], @@ -146,7 +148,7 @@ class LogViewer 'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null, 'context' => json_decode($data['context'], true), 'extra' => json_decode($data['extra'], true) - ); + ]; } /** diff --git a/system/src/Grav/Common/Iterator.php b/system/src/Grav/Common/Iterator.php index cc7cf9286..6e80b947e 100644 --- a/system/src/Grav/Common/Iterator.php +++ b/system/src/Grav/Common/Iterator.php @@ -230,9 +230,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable public function filter(callable $callback = null) { foreach ($this->items as $key => $value) { - if ((!$callback && !(bool)$value) || - ($callback && !$callback($value, $key)) - ) { + if ((!$callback && !(bool)$value) || ($callback && !$callback($value, $key))) { unset($this->items[$key]); } } diff --git a/system/src/Grav/Common/Language/LanguageCodes.php b/system/src/Grav/Common/Language/LanguageCodes.php index 9282070d6..e637bddaf 100644 --- a/system/src/Grav/Common/Language/LanguageCodes.php +++ b/system/src/Grav/Common/Language/LanguageCodes.php @@ -86,12 +86,14 @@ class LanguageCodes 'ja-JP' => [ 'name' => 'Japanese', 'nativeName' => '日本語' ], // not iso-639-1 'ka' => [ 'name' => 'Georgian', 'nativeName' => 'ქართული' ], 'kk' => [ 'name' => 'Kazakh', 'nativeName' => 'Қазақ' ], + 'km' => [ 'name' => 'Khmer', 'nativeName' => 'Khmer' ], 'kn' => [ 'name' => 'Kannada', 'nativeName' => 'ಕನ್ನಡ' ], 'ko' => [ 'name' => 'Korean', 'nativeName' => '한국어' ], 'ku' => [ 'name' => 'Kurdish', 'nativeName' => 'Kurdî' ], 'la' => [ 'name' => 'Latin', 'nativeName' => 'Latina' ], 'lb' => [ 'name' => 'Luxembourgish', 'nativeName' => 'Lëtzebuergesch' ], 'lg' => [ 'name' => 'Luganda', 'nativeName' => 'Luganda' ], + 'lo' => [ 'name' => 'Lao', 'nativeName' => 'Lao' ], 'lt' => [ 'name' => 'Lithuanian', 'nativeName' => 'Lietuvių' ], 'lv' => [ 'name' => 'Latvian', 'nativeName' => 'Latviešu' ], 'mai' => [ 'name' => 'Maithili', 'nativeName' => 'मैथिली মৈথিলী' ], @@ -101,6 +103,7 @@ class LanguageCodes 'ml' => [ 'name' => 'Malayalam', 'nativeName' => 'മലയാളം' ], 'mn' => [ 'name' => 'Mongolian', 'nativeName' => 'Монгол' ], 'mr' => [ 'name' => 'Marathi', 'nativeName' => 'मराठी' ], + 'my' => [ 'name' => 'Myanmar (Burmese)', 'nativeName' => 'ဗမာी' ], 'no' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ], 'nb' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ], 'nb-NO' => [ 'name' => 'Norwegian (Bokmål)', 'nativeName' => 'Norsk bokmål' ], @@ -132,6 +135,7 @@ class LanguageCodes 'st' => [ 'name' => 'Southern Sotho', 'nativeName' => 'Sesotho' ], 'sv' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ], 'sv-SE' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ], + 'sw' => [ 'name' => 'Swahili', 'nativeName' => 'Swahili' ], 'ta' => [ 'name' => 'Tamil', 'nativeName' => 'தமிழ்' ], 'ta-IN' => [ 'name' => 'Tamil (India)', 'nativeName' => 'தமிழ் (இந்தியா)' ], 'ta-LK' => [ 'name' => 'Tamil (Sri Lanka)', 'nativeName' => 'தமிழ் (இலங்கை)' ], @@ -187,12 +191,7 @@ class LanguageCodes */ public static function getOrientation($code) { - if (isset(static::$codes[$code])) { - if (isset(static::$codes[$code]['orientation'])) { - return static::get($code, 'orientation'); - } - } - return 'ltr'; + return static::$codes[$code]['orientation'] ?? 'ltr'; } /** @@ -226,11 +225,7 @@ class LanguageCodes */ public static function get($code, $type) { - if (isset(static::$codes[$code][$type])) { - return static::$codes[$code][$type]; - } - - return false; + return static::$codes[$code][$type] ?? false; } /** diff --git a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php index 0f5c5ed26..f6e22224e 100644 --- a/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php +++ b/system/src/Grav/Common/Media/Traits/ImageMediaTrait.php @@ -58,7 +58,7 @@ trait ImageMediaTrait 'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop', 'negate', 'brightness', 'contrast', 'grayscale', 'emboss', 'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive', - 'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format', 'merge' + 'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format', 'create', 'fill', 'merge' ]; /** @var array */ diff --git a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php index 3a0181a32..7a1f55d6e 100644 --- a/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php +++ b/system/src/Grav/Common/Media/Traits/MediaUploadTrait.php @@ -20,11 +20,13 @@ use Grav\Common\Security; use Grav\Common\Utils; use Grav\Framework\Filesystem\Filesystem; use Grav\Framework\Form\FormFlashFile; +use Grav\Framework\Mime\MimeTypes; use Psr\Http\Message\UploadedFileInterface; use RocketTheme\Toolbox\File\YamlFile; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RuntimeException; use function dirname; +use function in_array; /** * Implements media upload and delete functionality. @@ -71,15 +73,6 @@ trait MediaUploadTrait */ public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string { - // Add the defaults to the settings. - $settings = $this->getUploadSettings($settings); - - // Destination is always needed (but it can be set in defaults). - $self = $settings['self'] ?? false; - if (!isset($settings['destination']) && $self === false) { - throw new RuntimeException($this->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED'), 400); - } - // Check if there is an upload error. switch ($uploadedFile->getError()) { case UPLOAD_ERR_OK: @@ -101,10 +94,38 @@ trait MediaUploadTrait throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400); } + $metadata = [ + 'filename' => $uploadedFile->getClientFilename(), + 'mime' => $uploadedFile->getClientMediaType(), + 'size' => $uploadedFile->getSize(), + ]; + + return $this->checkFileMetadata($metadata, $filename, $settings); + } + + /** + * Checks that file metadata meets the requirements. Returns new filename. + * + * @param array $metadata + * @param array|null $settings + * @return string + * @throws RuntimeException + */ + public function checkFileMetadata(array $metadata, string $filename = null, array $settings = null): string + { + // Add the defaults to the settings. + $settings = $this->getUploadSettings($settings); + + // Destination is always needed (but it can be set in defaults). + $self = $settings['self'] ?? false; + if (!isset($settings['destination']) && $self === false) { + throw new RuntimeException($this->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED'), 400); + } + if (null === $filename) { // If no filename is given, use the filename from the uploaded file (path is not allowed). $folder = ''; - $filename = $uploadedFile->getClientFilename() ?? ''; + $filename = $metadata['filename'] ?? ''; } else { // If caller sets the filename, we will accept any custom path. $folder = dirname($filename); @@ -128,7 +149,7 @@ trait MediaUploadTrait $filename = date('YmdHis') . '-' . $filename; } } - $filepath = $folder !== '' ? $folder . $filename : $filename; + $filepath = $folder . $filename; // Check if the filename is allowed. if (!Utils::checkFilename($filename)) { @@ -148,23 +169,32 @@ trait MediaUploadTrait $filesize = $settings['filesize']; if ($filesize) { $max_filesize = $filesize * 1048576; - if ($uploadedFile->getSize() > $max_filesize) { + if ($metadata['size'] > $max_filesize) { // TODO: use own language string throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400); } } elseif (null === $filesize) { // Check size against the Grav upload limit. $grav_limit = Utils::getUploadLimit(); - if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) { + if ($grav_limit > 0 && $metadata['size'] > $grav_limit) { throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400); } } + $grav = Grav::instance(); + /** @var MimeTypes $mimeChecker */ + $mimeChecker = $grav['mime']; + // Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg) + // Do not trust mime type sent by the browser. + $mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension); + $validExtensions = $mimeChecker->getExtensions($mime); + if (!in_array($extension, $validExtensions, true)) { + throw new RuntimeException('The mime type does not match to file extension', 400); + } + $accepted = false; $errors = []; - // Do not trust mime type sent by the browser. - $mime = Utils::getMimeByFilename($filename); foreach ((array)$settings['accept'] as $type) { // Force acceptance of any file when star notation if ($type === '*') { @@ -394,6 +424,17 @@ trait MediaUploadTrait $uploadedFile->moveTo($filepath); } + /** + * Get upload settings. + * + * @param array|null $settings Form field specific settings (override). + * @return array + */ + public function getUploadSettings(?array $settings = null): array + { + return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults; + } + /** * Internal logic to copy file. * @@ -580,17 +621,6 @@ trait MediaUploadTrait } } - /** - * Get upload settings. - * - * @param array|null $settings Form field specific settings (override). - * @return array - */ - protected function getUploadSettings(?array $settings = null): array - { - return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults; - } - /** * @param string $filename * @param string $path diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php index e67ff047b..930aeda2b 100644 --- a/system/src/Grav/Common/Page/Collection.php +++ b/system/src/Grav/Common/Page/Collection.php @@ -47,7 +47,7 @@ class Collection extends Iterator implements PageCollectionInterface parent::__construct($items); $this->params = $params; - $this->pages = $pages ? $pages : Grav::instance()->offsetGet('pages'); + $this->pages = $pages ?: Grav::instance()->offsetGet('pages'); } /** @@ -145,6 +145,18 @@ class Collection extends Iterator implements PageCollectionInterface return $this; } + /** + * Set current page. + */ + public function setCurrent(string $path): void + { + reset($this->items); + + while (($key = key($this->items)) !== null && $key !== $path) { + next($this->items); + } + } + /** * Returns current page. * @@ -175,6 +187,7 @@ class Collection extends Iterator implements PageCollectionInterface * @param string $offset * @return PageInterface|null */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->pages->get($offset) ?: null; @@ -319,30 +332,32 @@ class Collection extends Iterator implements PageCollectionInterface /** * Returns the items between a set of date ranges of either the page date field (default) or - * an arbitrary datetime page field where end date is optional - * Dates can be passed in as text that strtotime() can process + * an arbitrary datetime page field where start date and end date are optional + * Dates must be passed in as text that strtotime() can process * http://php.net/manual/en/function.strtotime.php * - * @param string $startDate - * @param bool $endDate + * @param string|null $startDate + * @param string|null $endDate * @param string|null $field * @return $this * @throws Exception */ - public function dateRange($startDate, $endDate = false, $field = null) + public function dateRange($startDate = null, $endDate = null, $field = null) { - $start = Utils::date2timestamp($startDate); - $end = $endDate ? Utils::date2timestamp($endDate) : false; + $start = $startDate ? Utils::date2timestamp($startDate) : null; + $end = $endDate ? Utils::date2timestamp($endDate) : null; $date_range = []; foreach ($this->items as $path => $slug) { $page = $this->pages->get($path); - if ($page !== null) { - $date = $field ? strtotime($page->value($field)) : $page->date(); + if (!$page) { + continue; + } - if ($date >= $start && (!$end || $date <= $end)) { - $date_range[$path] = $slug; - } + $date = $field ? strtotime($page->value($field)) : $page->date(); + + if ((!$start || $date >= $start) && (!$end || $date <= $end)) { + $date_range[$path] = $slug; } } diff --git a/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php b/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php index 67c5b1e96..50029118f 100644 --- a/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php +++ b/system/src/Grav/Common/Page/Interfaces/PageCollectionInterface.php @@ -158,17 +158,17 @@ interface PageCollectionInterface extends Traversable, ArrayAccess, Countable, S /** * Returns the items between a set of date ranges of either the page date field (default) or - * an arbitrary datetime page field where end date is optional - * Dates can be passed in as text that strtotime() can process + * an arbitrary datetime page field where start date and end date are optional + * Dates must be passed in as text that strtotime() can process * http://php.net/manual/en/function.strtotime.php * - * @param string $startDate - * @param bool $endDate + * @param string|null $startDate + * @param string|null $endDate * @param string|null $field * @return PageCollectionInterface * @throws Exception */ - public function dateRange($startDate, $endDate = false, $field = null); + public function dateRange($startDate = null, $endDate = null, $field = null); /** * Creates new collection with only visible pages diff --git a/system/src/Grav/Common/Page/Markdown/Excerpts.php b/system/src/Grav/Common/Page/Markdown/Excerpts.php index 0c687e7f1..9b0b9963c 100644 --- a/system/src/Grav/Common/Page/Markdown/Excerpts.php +++ b/system/src/Grav/Common/Page/Markdown/Excerpts.php @@ -114,7 +114,7 @@ class Excerpts ); // Valid attributes supported. - $valid_attributes = $grav['config']->get('system.pages.markdown.valid_link_attributes'); + $valid_attributes = $grav['config']->get('system.pages.markdown.valid_link_attributes') ?? []; $skip = []; // Unless told to not process, go through actions. @@ -157,7 +157,7 @@ class Excerpts // Handle custom streams. /** @var UniformResourceLocator $locator */ $locator = $grav['locator']; - if ($locator->isStream($url)) { + if ($type === 'link' && $locator->isStream($url)) { $path = $locator->findResource($url, false) ?: $locator->findResource($url, false, true); $url_parts['path'] = $grav['base_url_relative'] . '/' . $path; unset($url_parts['stream'], $url_parts['scheme']); diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php index 9201e66f8..e18201420 100644 --- a/system/src/Grav/Common/Page/Media.php +++ b/system/src/Grav/Common/Page/Media.php @@ -63,6 +63,7 @@ class Media extends AbstractMedia * @param string $offset * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { return parent::offsetExists($offset) ?: isset(static::$global[$offset]); @@ -72,6 +73,7 @@ class Media extends AbstractMedia * @param string $offset * @return MediaObjectInterface|null */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return parent::offsetGet($offset) ?: static::$global[$offset]; @@ -102,12 +104,13 @@ class Media extends AbstractMedia foreach ($iterator as $file => $info) { // Ignore folders and Markdown files. - if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) { + $filename = $info->getFilename(); + if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) { continue; } // Find out what type we're dealing with - [$basename, $ext, $type, $extra] = $this->getFileParts($info->getFilename()); + [$basename, $ext, $type, $extra] = $this->getFileParts($filename); if (!in_array(strtolower($ext), $media_types, true)) { continue; diff --git a/system/src/Grav/Common/Page/Medium/GlobalMedia.php b/system/src/Grav/Common/Page/Medium/GlobalMedia.php index 4097c8f58..50d69c747 100644 --- a/system/src/Grav/Common/Page/Medium/GlobalMedia.php +++ b/system/src/Grav/Common/Page/Medium/GlobalMedia.php @@ -46,6 +46,7 @@ class GlobalMedia extends AbstractMedia * @param string $offset * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { return parent::offsetExists($offset) ?: !empty($this->resolveStream($offset)); @@ -55,6 +56,7 @@ class GlobalMedia extends AbstractMedia * @param string $offset * @return MediaObjectInterface|null */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return parent::offsetGet($offset) ?: $this->addMedium($offset); diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index d35c0a241..46c832ee9 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -399,6 +399,37 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate return $this; } + /** + * Add a frame to image + * + * @return $this + */ + public function addFrame(int $border = 10, string $color = '0x000000') + { + if(is_int(intval($border)) && $border>0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????). + $image = ImageFile::open($this->path()); + } + else { + return $this; + } + + $dst_width = $image->width()+2*$border; + $dst_height = $image->height()+2*$border; + + $frame = ImageFile::create($dst_width, $dst_height); + + $frame->__call('fill', [$color]); + + $this->image = $frame; + + $this->__call('merge', [$image, $border, $border]); + + $this->saveImage(); + + return $this; + + } + /** * Forward the call to the image processing method. * @@ -406,6 +437,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate * @param mixed $args * @return $this|mixed */ + public function __call($method, $args) { if (!in_array($method, static::$magic_actions, true)) { diff --git a/system/src/Grav/Common/Page/Medium/Link.php b/system/src/Grav/Common/Page/Medium/Link.php index 2bb857ba1..d57687573 100644 --- a/system/src/Grav/Common/Page/Medium/Link.php +++ b/system/src/Grav/Common/Page/Medium/Link.php @@ -41,9 +41,7 @@ class Link implements RenderableInterface, MediaLinkInterface $this->attributes = $attributes; $source = $medium->reset()->thumbnail('auto')->display('thumbnail'); - - // FIXME: Thumbnail can be null, maybe we should not allow that? - if (null === $source) { + if (!$source instanceof MediaObjectInterface) { throw new RuntimeException('Media has no thumbnail set'); } @@ -89,10 +87,15 @@ class Link implements RenderableInterface, MediaLinkInterface throw new BadMethodCallException(get_class($object) . '::' . $method . '() not found.'); } - $this->source = call_user_func_array($callable, $args); + $object = call_user_func_array($callable, $args); + if (!$object instanceof MediaLinkInterface) { + // Don't start nesting links, if user has multiple link calls in his + // actions, we will drop the previous links. + return $this; + } - // Don't start nesting links, if user has multiple link calls in his - // actions, we will drop the previous links. - return $this->source instanceof MediaLinkInterface ? $this->source : $this; + $this->source = $object; + + return $object; } } diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 5c92cba9a..2099be212 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -2095,7 +2095,7 @@ class Page implements PageInterface */ public function filePathClean() { - return str_replace(ROOT_DIR, '', $this->filePath()); + return str_replace(GRAV_ROOT . DS, '', $this->filePath()); } /** @@ -2274,11 +2274,11 @@ class Page implements PageInterface { if ($var !== null) { // make sure first level are arrays - array_walk($var, function (&$value) { + array_walk($var, static function (&$value) { $value = (array) $value; }); // make sure all values are strings - array_walk_recursive($var, function (&$value) { + array_walk_recursive($var, static function (&$value) { $value = (string) $value; }); $this->taxonomy = $var; diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index 3b3ce8ef3..941c9a7b3 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -196,6 +196,58 @@ class Pages return $this->baseRoute($lang) . $route; } + /** + * Get relative referrer route and language code. Returns null if the route isn't within the current base, language (if set) and route. + * + * @example `$langCode = null; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within /admin and updates $langCode + * @example `$langCode = 'en'; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within the /en/admin + * + * @param string|null $langCode Variable to store the language code. If already set, check only against that language. + * @param string $route Optional route within the site. + * @return string|null + * @since 1.7.23 + */ + public function referrerRoute(?string &$langCode, string $route = '/'): ?string + { + $referrer = $_SERVER['HTTP_REFERER'] ?? null; + + // Start by checking that referrer came from our site. + $root = $this->grav['base_url_absolute']; + if (!is_string($referrer) || !str_starts_with($referrer, $root)) { + return null; + } + + /** @var Language $language */ + $language = $this->grav['language']; + + // Get all language codes and append no language. + if (null === $langCode) { + $languages = $language->enabled() ? $language->getLanguages() : []; + $languages[] = ''; + } else { + $languages[] = $langCode; + } + + $path_base = rtrim($this->base(), '/'); + $path_route = rtrim($route, '/'); + + // Try to figure out the language code. + foreach ($languages as $code) { + $path_lang = $code ? "/{$code}" : ''; + + $base = $path_base . $path_lang . $path_route; + if ($referrer === $base || str_starts_with($referrer, "{$base}/")) { + if (null === $langCode) { + $langCode = $code; + } + + return substr($referrer, \strlen($base)); + } + } + + return null; + } + /** * * Get base URL for Grav pages. @@ -274,7 +326,7 @@ class Pages * * @return void */ - public function reset() + public function reset(): void { $this->initialized = false; @@ -540,9 +592,9 @@ class Pages } if (isset($params['dateRange'])) { - $start = $params['dateRange']['start'] ?? 0; - $end = $params['dateRange']['end'] ?? false; - $field = $params['dateRange']['field'] ?? false; + $start = $params['dateRange']['start'] ?? null; + $end = $params['dateRange']['end'] ?? null; + $field = $params['dateRange']['field'] ?? null; $collection = $collection->dateRange($start, $end, $field); } @@ -554,7 +606,7 @@ class Pages if (is_array($sort_flags)) { $sort_flags = array_map('constant', $sort_flags); //transform strings to constant value - $sort_flags = array_reduce($sort_flags, function ($a, $b) { + $sort_flags = array_reduce($sort_flags, static function ($a, $b) { return $a | $b; }, 0); //merge constant values using bit or } @@ -663,29 +715,39 @@ class Pages switch ($type) { case 'all': - return $page->children(); + $collection = $page->children(); + break; case 'modules': case 'modular': - return $page->children()->modules(); + $collection = $page->children()->modules(); + break; case 'pages': case 'children': - return $page->children()->pages(); + $collection = $page->children()->pages(); + break; case 'page': case 'self': - return !$page->root() ? (new Collection())->addPage($page) : new Collection(); + $collection = !$page->root() ? (new Collection())->addPage($page) : new Collection(); + break; case 'parent': $parent = $page->parent(); $collection = new Collection(); - return $parent ? $collection->addPage($parent) : $collection; + $collection = $parent ? $collection->addPage($parent) : $collection; + break; case 'siblings': $parent = $page->parent(); - return $parent ? $parent->children()->remove($page->path()) : new Collection(); + $collection = $parent ? $parent->children()->remove($page->path()) : new Collection(); + break; case 'descendants': - return $this->all($page)->remove($page->path())->pages(); + $collection = $this->all($page)->remove($page->path())->pages(); + break; default: // Unknown type; return empty collection. - return new Collection(); + $collection = new Collection(); + break; } + + return $collection; } /** @@ -1761,7 +1823,7 @@ class Pages // Build regular expression for all the allowed page extensions. $page_extensions = $language->getFallbackPageExtensions(); $regex = '/^[^\.]*(' . implode('|', array_map( - function ($str) { + static function ($str) { return preg_quote($str, '/'); }, $page_extensions diff --git a/system/src/Grav/Common/Plugin.php b/system/src/Grav/Common/Plugin.php index 65b11dba1..3b0d3fb14 100644 --- a/system/src/Grav/Common/Plugin.php +++ b/system/src/Grav/Common/Plugin.php @@ -10,6 +10,7 @@ namespace Grav\Common; use ArrayAccess; +use Composer\Autoload\ClassLoader; use Grav\Common\Data\Blueprint; use Grav\Common\Data\Data; use Grav\Common\Page\Interfaces\PageInterface; @@ -42,6 +43,8 @@ class Plugin implements EventSubscriberInterface, ArrayAccess protected $active = true; /** @var Blueprint|null */ protected $blueprint; + /** @var ClassLoader|null */ + protected $loader; /** * By default assign all methods as listeners using the default priority. @@ -79,6 +82,24 @@ class Plugin implements EventSubscriberInterface, ArrayAccess } } + /** + * @return ClassLoader|null + * @internal + */ + final public function getAutoloader(): ?ClassLoader + { + return $this->loader; + } + + /** + * @param ClassLoader|null $loader + * @internal + */ + final public function setAutoloader(?ClassLoader $loader): void + { + $this->loader = $loader; + } + /** * @param Config $config * @return $this @@ -206,6 +227,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess * @param string $offset An offset to check for. * @return bool Returns TRUE on success or FALSE on failure. */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { if ($offset === 'title') { @@ -223,6 +245,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess * @param string $offset The offset to retrieve. * @return mixed Can return all value types. */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { if ($offset === 'title') { @@ -241,6 +264,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess * @param mixed $value The value to set. * @throws LogicException */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { throw new LogicException(__CLASS__ . ' blueprints cannot be modified.'); @@ -252,6 +276,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess * @param string $offset The offset to unset. * @throws LogicException */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { throw new LogicException(__CLASS__ . ' blueprints cannot be modified.'); diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php index be8f43bc2..88b5e524e 100644 --- a/system/src/Grav/Common/Plugins.php +++ b/system/src/Grav/Common/Plugins.php @@ -143,7 +143,7 @@ class Plugins extends Iterator $instance->setConfig($config); // Register autoloader. if (method_exists($instance, 'autoload')) { - $instance->autoload(); + $instance->setAutoloader($instance->autoload()); } // Register event listeners. $events->addSubscriber($instance); @@ -287,33 +287,42 @@ class Plugins extends Iterator { // NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM! $grav = Grav::instance(); + /** @var UniformResourceLocator $locator */ $locator = $grav['locator']; - $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); + $class = null; + // Start by attempting to load the plugin_name.php file. + $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); if (is_file($file)) { // Local variables available in the file: $grav, $name, $file $class = include_once $file; + if (!is_object($class) || !is_subclass_of($class, Plugin::class, true)) { + $class = null; + } + } - if (!$class || !is_subclass_of($class, Plugin::class, true)) { - $className = Inflector::camelize($name); - $pluginClassFormat = [ - 'Grav\\Plugin\\' . ucfirst($name). 'Plugin', - 'Grav\\Plugin\\' . $className . 'Plugin', - 'Grav\\Plugin\\' . $className - ]; + // If the class hasn't been initialized yet, guess the class name and create a new instance. + if (null === $class) { + $className = Inflector::camelize($name); + $pluginClassFormat = [ + 'Grav\\Plugin\\' . ucfirst($name). 'Plugin', + 'Grav\\Plugin\\' . $className . 'Plugin', + 'Grav\\Plugin\\' . $className + ]; - foreach ($pluginClassFormat as $pluginClass) { - if (is_subclass_of($pluginClass, Plugin::class, true)) { - $class = new $pluginClass($name, $grav); - break; - } + foreach ($pluginClassFormat as $pluginClass) { + if (is_subclass_of($pluginClass, Plugin::class, true)) { + $class = new $pluginClass($name, $grav); + break; } } - } else { + } + + // Log a warning if plugin cannot be found. + if (null === $class) { $grav['log']->addWarning( - sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name) + sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name) ); - return null; } return $class; diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php index cc8e4f260..55cba033c 100644 --- a/system/src/Grav/Common/Processors/InitializeProcessor.php +++ b/system/src/Grav/Common/Processors/InitializeProcessor.php @@ -105,7 +105,7 @@ class InitializeProcessor extends ProcessorBase // TODO: remove in 2.0. $this->container['accounts']; - // Initialize session. + // Initialize session (used by URI, see issue #3269). $this->initializeSession($config); // Initialize URI (uses session, see issue #3269). diff --git a/system/src/Grav/Common/Processors/PagesProcessor.php b/system/src/Grav/Common/Processors/PagesProcessor.php index 33b483fbd..470ca907b 100644 --- a/system/src/Grav/Common/Processors/PagesProcessor.php +++ b/system/src/Grav/Common/Processors/PagesProcessor.php @@ -10,6 +10,8 @@ namespace Grav\Common\Processors; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Framework\RequestHandler\Exception\RequestException; +use Grav\Plugin\Form\Forms; use RocketTheme\Toolbox\Event\Event; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -47,8 +49,17 @@ class PagesProcessor extends ProcessorBase $page = $this->container['page']; if (!$page->routable()) { + $exception = new RequestException($request, 'Page Not Found', 404); + $route = $this->container['route']; // If no page found, fire event - $event = new Event(['page' => $page]); + $event = new Event([ + 'page' => $page, + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'exception' => $exception, + 'route' => $route, + 'request' => $request + ]); $event->page = null; $event = $this->container->fireEvent('onPageNotFound', $event); @@ -65,12 +76,18 @@ class PagesProcessor extends ProcessorBase $task = $this->container['task']; $action = $this->container['action']; + + /** @var Forms $forms */ + $forms = $this->container['forms'] ?? null; + $form = $forms ? $forms->getActiveForm() : null; + + $options = ['page' => $page, 'form' => $form, 'request' => $request]; if ($task) { - $event = new Event(['task' => $task, 'page' => $page]); + $event = new Event(['task' => $task] + $options); $this->container->fireEvent('onPageTask', $event); $this->container->fireEvent('onPageTask.' . $task, $event); } elseif ($action) { - $event = new Event(['action' => $action, 'page' => $page]); + $event = new Event(['action' => $action] + $options); $this->container->fireEvent('onPageAction', $event); $this->container->fireEvent('onPageAction.' . $action, $event); } diff --git a/system/src/Grav/Common/Processors/RenderProcessor.php b/system/src/Grav/Common/Processors/RenderProcessor.php index 32f48e7ce..329da22a3 100644 --- a/system/src/Grav/Common/Processors/RenderProcessor.php +++ b/system/src/Grav/Common/Processors/RenderProcessor.php @@ -14,6 +14,7 @@ use Grav\Framework\Psr7\Response; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use RocketTheme\Toolbox\Event\Event; /** * Class RenderProcessor @@ -42,23 +43,27 @@ class RenderProcessor extends ProcessorBase return $output; } - ob_start(); + /** @var PageInterface $page */ + $page = $this->container['page']; // Use internal Grav output. $container->output = $output; - $container->fireEvent('onOutputGenerated'); + + ob_start(); + + $event = new Event(['page' => $page, 'output' => &$container->output]); + $container->fireEvent('onOutputGenerated', $event); echo $container->output; + $html = ob_get_clean(); + // remove any output $container->output = ''; - $this->container->fireEvent('onOutputRendered'); + $event = new Event(['page' => $page, 'output' => $html]); + $this->container->fireEvent('onOutputRendered', $event); - $html = ob_get_clean(); - - /** @var PageInterface $page */ - $page = $this->container['page']; $this->stopTimer(); return new Response($page->httpResponseCode(), $page->httpHeaders(), $html); diff --git a/system/src/Grav/Common/Scheduler/Job.php b/system/src/Grav/Common/Scheduler/Job.php index f21c26def..13059a572 100644 --- a/system/src/Grav/Common/Scheduler/Job.php +++ b/system/src/Grav/Common/Scheduler/Job.php @@ -271,7 +271,7 @@ class Job if ($whenOverlapping) { $this->whenOverlapping = $whenOverlapping; } else { - $this->whenOverlapping = function () { + $this->whenOverlapping = static function () { return false; }; } @@ -390,7 +390,9 @@ class Job if (count($this->outputTo) > 0) { foreach ($this->outputTo as $file) { $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX; - file_put_contents($file, $this->output, $output_mode); + $timestamp = (new DateTime('now'))->format('c'); + $output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output; + file_put_contents($file, $output, $output_mode); } } diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php index 21d059e23..55aa54593 100644 --- a/system/src/Grav/Common/Security.php +++ b/system/src/Grav/Common/Security.php @@ -9,10 +9,11 @@ namespace Grav\Common; -use enshrined\svgSanitize\Sanitizer; use Exception; use Grav\Common\Config\Config; +use Grav\Common\Filesystem\Folder; use Grav\Common\Page\Pages; +use Rhukster\DomSanitizer\DOMSanitizer; use function chr; use function count; use function is_array; @@ -33,7 +34,7 @@ class Security public static function sanitizeSvgString(string $svg): string { if (Grav::instance()['config']->get('security.sanitize_svg')) { - $sanitizer = new Sanitizer(); + $sanitizer = new DOMSanitizer(DOMSanitizer::SVG); $sanitized = $sanitizer->sanitize($svg); if (is_string($sanitized)) { $svg = $sanitized; @@ -52,13 +53,20 @@ class Security public static function sanitizeSVG(string $file): void { if (file_exists($file) && Grav::instance()['config']->get('security.sanitize_svg')) { - $sanitizer = new Sanitizer(); + $sanitizer = new DOMSanitizer(DOMSanitizer::SVG); $original_svg = file_get_contents($file); $clean_svg = $sanitizer->sanitize($original_svg); - // TODO: what to do with bad SVG files which return false? - if ($clean_svg !== false && $clean_svg !== $original_svg) { + // Quarantine bad SVG files and throw exception + if ($clean_svg !== false ) { file_put_contents($file, $clean_svg); + } else { + $quarantine_file = basename($file); + $quarantine_dir = 'log://quarantine'; + Folder::mkdir($quarantine_dir); + file_put_contents("$quarantine_dir/$quarantine_file", $original_svg); + unlink($file); + throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder'); } } } @@ -99,7 +107,7 @@ class Security $content = $page->value('content'); $data = ['header' => $header, 'content' => $content]; - $results = Security::detectXssFromArray($data); + $results = static::detectXssFromArray($data); if (!empty($results)) { if ($route) { @@ -130,7 +138,7 @@ class Security $options = static::getXssDefaults(); } - $list = []; + $list = [[]]; foreach ($array as $key => $value) { if (is_array($value)) { $list[] = static::detectXssFromArray($value, $prefix . $key . '.', $options); @@ -140,11 +148,7 @@ class Security } } - if (!empty($list)) { - return array_merge(...$list); - } - - return $list; + return array_merge(...$list); } /** @@ -191,7 +195,7 @@ class Security $string = urldecode($string); // Convert Hexadecimals - $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function ($m) { + $string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', static function ($m) { return chr(hexdec($m[2])); }, $string); @@ -210,7 +214,7 @@ class Security 'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])([\s\/]on|\sxmlns)[a-z].*=>?#iUu', // Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols - 'invalid_protocols' => '#(' . implode('|', array_map('preg_quote', $invalid_protocols, ['#'])) . '):.*?#iUu', + 'invalid_protocols' => '#(' . implode('|', array_map('preg_quote', $invalid_protocols, ['#'])) . '):\S.*?#iUu', // Match -moz-bindings 'moz_binding' => '#-moz-binding[a-z\x00-\x20]*:#u', @@ -231,7 +235,7 @@ class Security } } - return false; + return null; } public static function getXssDefaults(): array diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php index a423f6e1d..b56f62fde 100644 --- a/system/src/Grav/Common/Service/ConfigServiceProvider.php +++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php @@ -17,6 +17,7 @@ use Grav\Common\Config\Config; use Grav\Common\Config\ConfigFileFinder; use Grav\Common\Config\Setup; use Grav\Common\Language\Language; +use Grav\Framework\Mime\MimeTypes; use Pimple\Container; use Pimple\ServiceProviderInterface; use RocketTheme\Toolbox\File\YamlFile; @@ -56,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface return $config; }; + $container['mime'] = function ($c) { + /** @var Config $config */ + $config = $c['config']; + $mimes = $config->get('mime.types', []); + foreach ($config->get('media.types', []) as $ext => $media) { + if (!empty($media['mime'])) { + $mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? [])); + } + } + + return MimeTypes::createFromMimes($mimes); + }; + $container['languages'] = function ($c) { return static::languages($c); }; diff --git a/system/src/Grav/Common/Service/RequestServiceProvider.php b/system/src/Grav/Common/Service/RequestServiceProvider.php index 271571351..17b3149e5 100644 --- a/system/src/Grav/Common/Service/RequestServiceProvider.php +++ b/system/src/Grav/Common/Service/RequestServiceProvider.php @@ -10,6 +10,7 @@ namespace Grav\Common\Service; use Grav\Common\Uri; +use JsonException; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7Server\ServerRequestCreator; use Pimple\Container; @@ -18,6 +19,7 @@ use function explode; use function fopen; use function function_exists; use function in_array; +use function is_array; use function strtolower; use function trim; @@ -51,18 +53,30 @@ class RequestServiceProvider implements ServiceProviderInterface $headers = function_exists('getallheaders') ? getallheaders() : $creator::getHeadersFromServer($_SERVER); $post = null; - if ('POST' === $method) { + if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) { foreach ($headers as $headerName => $headerValue) { if ('content-type' !== strtolower($headerName)) { continue; } - if (in_array( - strtolower(trim(explode(';', $headerValue, 2)[0])), - ['application/x-www-form-urlencoded', 'multipart/form-data'] - )) { - $post = $_POST; - break; + $contentType = strtolower(trim(explode(';', $headerValue, 2)[0])); + switch ($contentType) { + case 'application/x-www-form-urlencoded': + case 'multipart/form-data': + $post = $_POST; + break 2; + case 'application/json': + case 'application/vnd.api+json': + try { + $json = file_get_contents('php://input'); + $post = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + if (!is_array($post)) { + $post = null; + } + } catch (JsonException $e) { + $post = null; + } + break 2; } } } diff --git a/system/src/Grav/Common/Service/TaskServiceProvider.php b/system/src/Grav/Common/Service/TaskServiceProvider.php index 9afab59a4..49ce147e9 100644 --- a/system/src/Grav/Common/Service/TaskServiceProvider.php +++ b/system/src/Grav/Common/Service/TaskServiceProvider.php @@ -12,6 +12,7 @@ namespace Grav\Common\Service; use Grav\Common\Grav; use Pimple\Container; use Pimple\ServiceProviderInterface; +use Psr\Http\Message\ServerRequestInterface; /** * Class TaskServiceProvider @@ -26,7 +27,11 @@ class TaskServiceProvider implements ServiceProviderInterface public function register(Container $container) { $container['task'] = function (Grav $c) { - $task = $_POST['task'] ?? $c['uri']->param('task'); + /** @var ServerRequestInterface $request */ + $request = $c['request']; + $body = $request->getParsedBody(); + + $task = $body['task'] ?? $c['uri']->param('task'); if (null !== $task) { $task = filter_var($task, FILTER_SANITIZE_STRING); } @@ -35,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface }; $container['action'] = function (Grav $c) { - $action = $_POST['action'] ?? $c['uri']->param('action'); + /** @var ServerRequestInterface $request */ + $request = $c['request']; + $body = $request->getParsedBody(); + + $action = $body['action'] ?? $c['uri']->param('action'); if (null !== $action) { $action = filter_var($action, FILTER_SANITIZE_STRING); } diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php index 8856e7a0a..322162705 100644 --- a/system/src/Grav/Common/Session.php +++ b/system/src/Grav/Common/Session.php @@ -12,6 +12,7 @@ namespace Grav\Common; use Grav\Common\Form\FormFlash; use Grav\Events\SessionStartEvent; use Grav\Plugin\Form\Forms; +use JsonException; use function is_string; /** @@ -128,12 +129,12 @@ class Session extends \Grav\Framework\Session\Session /** @var Uri $uri */ $uri = $grav['uri']; /** @var Forms|null $form */ - $form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line + $form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line (form plugin) $sessionField = base64_encode($uri->url); /** @var FormFlash|null $flash */ - $flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line + $flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line (form plugin) $object = $flash && method_exists($flash, 'getLegacyFiles') ? [$sessionField => $flash->getLegacyFiles()] : null; } } @@ -148,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session * @param mixed $object * @param int $time * @return $this + * @throws JsonException */ public function setFlashCookieObject($name, $object, $time = 60) { - setcookie($name, json_encode($object), time() + $time, '/'); + setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time)); return $this; } @@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session * * @param string $name * @return mixed|null + * @throws JsonException */ public function getFlashCookieObject($name) { if (isset($_COOKIE[$name])) { - $object = json_decode($_COOKIE[$name], false); - setcookie($name, '', time() - 3600, '/'); - return $object; + $cookie = $_COOKIE[$name]; + setcookie($name, '', $this->getCookieOptions(-42000)); + + return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR); } return null; diff --git a/system/src/Grav/Common/Taxonomy.php b/system/src/Grav/Common/Taxonomy.php index de3c4526c..0d0a450b0 100644 --- a/system/src/Grav/Common/Taxonomy.php +++ b/system/src/Grav/Common/Taxonomy.php @@ -105,7 +105,7 @@ class Taxonomy } } elseif (is_string($value)) { if (!empty($key)) { - $taxonomy = $taxonomy . $key; + $taxonomy .= $key; } $this->taxonomy_map[$taxonomy][(string) $value][$page->path()] = ['slug' => $page->slug()]; } diff --git a/system/src/Grav/Common/Themes.php b/system/src/Grav/Common/Themes.php index bf155ede5..6126841cf 100644 --- a/system/src/Grav/Common/Themes.php +++ b/system/src/Grav/Common/Themes.php @@ -224,28 +224,18 @@ class Themes extends Iterator $grav = $this->grav; $config = $this->config; $name = $this->current(); + $class = null; /** @var UniformResourceLocator $locator */ $locator = $grav['locator']; - $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php"); + // Start by attempting to load the theme.php file. + $file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php"); if ($file) { // Local variables available in the file: $grav, $config, $name, $file $class = include $file; - - if (!$class || !is_subclass_of($class, Plugin::class, true)) { - $className = Inflector::camelize($name); - $themeClassFormat = [ - 'Grav\\Theme\\' . $className, - 'Grav\\Theme\\' . ucfirst($name) - ]; - - foreach ($themeClassFormat as $themeClass) { - if (is_subclass_of($themeClass, Theme::class, true)) { - $class = new $themeClass($grav, $config, $name); - break; - } - } + if (!\is_object($class) || !is_subclass_of($class, Theme::class, true)) { + $class = null; } } elseif (!$locator('theme://') && !defined('GRAV_CLI')) { $response = new Response(500, [], "Theme '$name' does not exist, unable to display page."); @@ -253,12 +243,28 @@ class Themes extends Iterator $grav->close($response); } - $this->config->set('theme', $config->get('themes.' . $name)); + // If the class hasn't been initialized yet, guess the class name and create a new instance. + if (null === $class) { + $themeClassFormat = [ + 'Grav\\Theme\\' . Inflector::camelize($name), + 'Grav\\Theme\\' . ucfirst($name) + ]; - if (empty($class)) { + foreach ($themeClassFormat as $themeClass) { + if (is_subclass_of($themeClass, Theme::class, true)) { + $class = new $themeClass($grav, $config, $name); + break; + } + } + } + + // Finally if everything else fails, just create a new instance from the default Theme class. + if (null === $class) { $class = new Theme($grav, $config, $name); } + $this->config->set('theme', $config->get('themes.' . $name)); + return $class; } diff --git a/system/src/Grav/Common/Twig/Exception/TwigException.php b/system/src/Grav/Common/Twig/Exception/TwigException.php new file mode 100644 index 000000000..8f543dcc1 --- /dev/null +++ b/system/src/Grav/Common/Twig/Exception/TwigException.php @@ -0,0 +1,19 @@ +locator = Grav::instance()['locator']; + } + + /** + * @return TwigFilter[] + */ + public function getFilters() + { + return [ + new TwigFilter('file_exists', [$this, 'file_exists']), + new TwigFilter('fileatime', [$this, 'fileatime']), + new TwigFilter('filectime', [$this, 'filectime']), + new TwigFilter('filemtime', [$this, 'filemtime']), + new TwigFilter('filesize', [$this, 'filesize']), + new TwigFilter('filetype', [$this, 'filetype']), + new TwigFilter('is_dir', [$this, 'is_dir']), + new TwigFilter('is_file', [$this, 'is_file']), + new TwigFilter('is_link', [$this, 'is_link']), + new TwigFilter('is_readable', [$this, 'is_readable']), + new TwigFilter('is_writable', [$this, 'is_writable']), + new TwigFilter('is_writeable', [$this, 'is_writable']), + new TwigFilter('lstat', [$this, 'lstat']), + new TwigFilter('getimagesize', [$this, 'getimagesize']), + new TwigFilter('exif_read_data', [$this, 'exif_read_data']), + new TwigFilter('read_exif_data', [$this, 'exif_read_data']), + new TwigFilter('exif_imagetype', [$this, 'exif_imagetype']), + new TwigFilter('hash_file', [$this, 'hash_file']), + new TwigFilter('hash_hmac_file', [$this, 'hash_hmac_file']), + new TwigFilter('md5_file', [$this, 'md5_file']), + new TwigFilter('sha1_file', [$this, 'sha1_file']), + new TwigFilter('get_meta_tags', [$this, 'get_meta_tags']), + new TwigFilter('pathinfo', [$this, 'pathinfo']), + ]; + } + + /** + * Return a list of all functions. + * + * @return TwigFunction[] + */ + public function getFunctions() + { + return [ + new TwigFunction('file_exists', [$this, 'file_exists']), + new TwigFunction('fileatime', [$this, 'fileatime']), + new TwigFunction('filectime', [$this, 'filectime']), + new TwigFunction('filemtime', [$this, 'filemtime']), + new TwigFunction('filesize', [$this, 'filesize']), + new TwigFunction('filetype', [$this, 'filetype']), + new TwigFunction('is_dir', [$this, 'is_dir']), + new TwigFunction('is_file', [$this, 'is_file']), + new TwigFunction('is_link', [$this, 'is_link']), + new TwigFunction('is_readable', [$this, 'is_readable']), + new TwigFunction('is_writable', [$this, 'is_writable']), + new TwigFunction('is_writeable', [$this, 'is_writable']), + new TwigFunction('lstat', [$this, 'lstat']), + new TwigFunction('getimagesize', [$this, 'getimagesize']), + new TwigFunction('exif_read_data', [$this, 'exif_read_data']), + new TwigFunction('read_exif_data', [$this, 'exif_read_data']), + new TwigFunction('exif_imagetype', [$this, 'exif_imagetype']), + new TwigFunction('hash_file', [$this, 'hash_file']), + new TwigFunction('hash_hmac_file', [$this, 'hash_hmac_file']), + new TwigFunction('md5_file', [$this, 'md5_file']), + new TwigFunction('sha1_file', [$this, 'sha1_file']), + new TwigFunction('get_meta_tags', [$this, 'get_meta_tags']), + new TwigFunction('pathinfo', [$this, 'pathinfo']), + ]; + } + + /** + * @param string $filename + * @return bool + */ + public function file_exists($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return file_exists($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function fileatime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return fileatime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filectime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filectime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filemtime($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filemtime($filename); + } + + /** + * @param string $filename + * @return int|false + */ + public function filesize($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filesize($filename); + } + + /** + * @param string $filename + * @return string|false + */ + public function filetype($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return filetype($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_dir($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_dir($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_file($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_file($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_link($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_link($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_readable($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_readable($filename); + } + + /** + * @param string $filename + * @return bool + */ + public function is_writable($filename): bool + { + if (!$this->checkFilename($filename)) { + return false; + } + + return is_writable($filename); + } + + /** + * @param string $filename + * @return array|false + */ + public function lstat($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return lstat($filename); + } + + /** + * @param string $filename + * @return array|false + */ + public function getimagesize($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return getimagesize($filename); + } + + /** + * @param string $file + * @param string|null $required_sections + * @param bool $as_arrays + * @param bool $read_thumbnail + * @return array|false + */ + public function exif_read_data($file, ?string $required_sections, bool $as_arrays = false, bool $read_thumbnail = false) + { + if (!Utils::functionExists('exif_read_data') || !$this->checkFilename($file)) { + return false; + } + + return exif_read_data($file, $required_sections, $as_arrays, $read_thumbnail); + } + + /** + * @param string $filename + * @return string|false + */ + public function exif_imagetype($filename) + { + if (!Utils::functionExists('exif_imagetype') || !$this->checkFilename($filename)) { + return false; + } + + return @exif_imagetype($filename); + } + + /** + * @param string $algo + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function hash_file(string $algo, string $filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return hash_file($algo, $filename, $binary); + } + + /** + * @param string $algo + * @param string $data + * @param string $key + * @param bool $binary + * @return string|false + */ + public function hash_hmac_file(string $algo, string $data, string $key, bool $binary = false) + { + if (!$this->checkFilename($data)) { + return false; + } + + return hash_hmac_file($algo, $data, $key, $binary); + } + + /** + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function md5_file($filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return md5_file($filename, $binary); + } + + /** + * @param string $filename + * @param bool $binary + * @return string|false + */ + public function sha1_file($filename, bool $binary = false) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return sha1_file($filename, $binary); + } + + /** + * @param string $filename + * @return array|false + */ + public function get_meta_tags($filename) + { + if (!$this->checkFilename($filename)) { + return false; + } + + return get_meta_tags($filename); + } + + /** + * @param string $path + * @param int|null $flags + * @return string|string[] + */ + public function pathinfo($path, $flags = null) + { + if (null !== $flags) { + return pathinfo($path, (int)$flags); + } + + return pathinfo($path); + } + + /** + * @param string $filename + * @return bool + */ + private function checkFilename($filename): bool + { + return is_string($filename) && (!str_contains($filename, '://') || $this->locator->isStream($filename)); + } +} diff --git a/system/src/Grav/Common/Twig/Extension/GravExtension.php b/system/src/Grav/Common/Twig/Extension/GravExtension.php new file mode 100644 index 000000000..638617c4d --- /dev/null +++ b/system/src/Grav/Common/Twig/Extension/GravExtension.php @@ -0,0 +1,1635 @@ +grav = Grav::instance(); + $this->debugger = $this->grav['debugger'] ?? null; + $this->config = $this->grav['config']; + } + + /** + * Register some standard globals + * + * @return array + */ + public function getGlobals(): array + { + return [ + 'grav' => $this->grav, + ]; + } + + /** + * Return a list of all filters. + * + * @return array + */ + public function getFilters(): array + { + return [ + new TwigFilter('*ize', [$this, 'inflectorFilter']), + new TwigFilter('absolute_url', [$this, 'absoluteUrlFilter']), + new TwigFilter('contains', [$this, 'containsFilter']), + new TwigFilter('chunk_split', [$this, 'chunkSplitFilter']), + new TwigFilter('nicenumber', [$this, 'niceNumberFunc']), + new TwigFilter('nicefilesize', [$this, 'niceFilesizeFunc']), + new TwigFilter('nicetime', [$this, 'nicetimeFunc']), + new TwigFilter('defined', [$this, 'definedDefaultFilter']), + new TwigFilter('ends_with', [$this, 'endsWithFilter']), + new TwigFilter('fieldName', [$this, 'fieldNameFilter']), + new TwigFilter('ksort', [$this, 'ksortFilter']), + new TwigFilter('ltrim', [$this, 'ltrimFilter']), + new TwigFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]), + new TwigFilter('md5', [$this, 'md5Filter']), + new TwigFilter('base32_encode', [$this, 'base32EncodeFilter']), + new TwigFilter('base32_decode', [$this, 'base32DecodeFilter']), + new TwigFilter('base64_encode', [$this, 'base64EncodeFilter']), + new TwigFilter('base64_decode', [$this, 'base64DecodeFilter']), + new TwigFilter('randomize', [$this, 'randomizeFilter']), + new TwigFilter('modulus', [$this, 'modulusFilter']), + new TwigFilter('rtrim', [$this, 'rtrimFilter']), + new TwigFilter('pad', [$this, 'padFilter']), + new TwigFilter('regex_replace', [$this, 'regexReplace']), + new TwigFilter('safe_email', [$this, 'safeEmailFilter'], ['is_safe' => ['html']]), + new TwigFilter('safe_truncate', [Utils::class, 'safeTruncate']), + new TwigFilter('safe_truncate_html', [Utils::class, 'safeTruncateHTML']), + new TwigFilter('sort_by_key', [$this, 'sortByKeyFilter']), + new TwigFilter('starts_with', [$this, 'startsWithFilter']), + new TwigFilter('truncate', [Utils::class, 'truncate']), + new TwigFilter('truncate_html', [Utils::class, 'truncateHTML']), + new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']), + new TwigFilter('array_unique', 'array_unique'), + new TwigFilter('basename', 'basename'), + new TwigFilter('dirname', 'dirname'), + new TwigFilter('print_r', [$this, 'print_r']), + new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']), + new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']), + new TwigFilter('nicecron', [$this, 'niceCronFilter']), + + // Translations + new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]), + new TwigFilter('tl', [$this, 'translateLanguage']), + new TwigFilter('ta', [$this, 'translateArray']), + + // Casting values + new TwigFilter('string', [$this, 'stringFilter']), + new TwigFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]), + new TwigFilter('bool', [$this, 'boolFilter']), + new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]), + new TwigFilter('array', [$this, 'arrayFilter']), + new TwigFilter('yaml', [$this, 'yamlFilter']), + + // Object Types + new TwigFilter('get_type', [$this, 'getTypeFunc']), + new TwigFilter('of_type', [$this, 'ofTypeFunc']), + + // PHP methods + new TwigFilter('count', 'count'), + new TwigFilter('array_diff', 'array_diff'), + ]; + } + + /** + * Return a list of all functions. + * + * @return array + */ + public function getFunctions(): array + { + return [ + new TwigFunction('array', [$this, 'arrayFilter']), + new TwigFunction('array_key_value', [$this, 'arrayKeyValueFunc']), + new TwigFunction('array_key_exists', 'array_key_exists'), + new TwigFunction('array_unique', 'array_unique'), + new TwigFunction('array_intersect', [$this, 'arrayIntersectFunc']), + new TwigFunction('array_diff', 'array_diff'), + new TwigFunction('authorize', [$this, 'authorize']), + new TwigFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), + new TwigFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), + new TwigFunction('vardump', [$this, 'vardumpFunc']), + new TwigFunction('print_r', [$this, 'print_r']), + new TwigFunction('http_response_code', 'http_response_code'), + new TwigFunction('evaluate', [$this, 'evaluateStringFunc'], ['needs_context' => true]), + new TwigFunction('evaluate_twig', [$this, 'evaluateTwigFunc'], ['needs_context' => true]), + new TwigFunction('gist', [$this, 'gistFunc']), + new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']), + new TwigFunction('pathinfo', 'pathinfo'), + new TwigFunction('random_string', [$this, 'randomStringFunc']), + new TwigFunction('repeat', [$this, 'repeatFunc']), + new TwigFunction('regex_replace', [$this, 'regexReplace']), + new TwigFunction('regex_filter', [$this, 'regexFilter']), + new TwigFunction('regex_match', [$this, 'regexMatch']), + new TwigFunction('regex_split', [$this, 'regexSplit']), + new TwigFunction('string', [$this, 'stringFilter']), + new TwigFunction('url', [$this, 'urlFunc']), + new TwigFunction('json_decode', [$this, 'jsonDecodeFilter']), + new TwigFunction('get_cookie', [$this, 'getCookie']), + new TwigFunction('redirect_me', [$this, 'redirectFunc']), + new TwigFunction('range', [$this, 'rangeFunc']), + new TwigFunction('isajaxrequest', [$this, 'isAjaxFunc']), + new TwigFunction('exif', [$this, 'exifFunc']), + new TwigFunction('media_directory', [$this, 'mediaDirFunc']), + new TwigFunction('body_class', [$this, 'bodyClassFunc'], ['needs_context' => true]), + new TwigFunction('theme_var', [$this, 'themeVarFunc'], ['needs_context' => true]), + new TwigFunction('header_var', [$this, 'pageHeaderVarFunc'], ['needs_context' => true]), + new TwigFunction('read_file', [$this, 'readFileFunc']), + new TwigFunction('nicenumber', [$this, 'niceNumberFunc']), + new TwigFunction('nicefilesize', [$this, 'niceFilesizeFunc']), + new TwigFunction('nicetime', [$this, 'nicetimeFunc']), + new TwigFunction('cron', [$this, 'cronFunc']), + new TwigFunction('svg_image', [$this, 'svgImageFunction']), + new TwigFunction('xss', [$this, 'xssFunc']), + new TwigFunction('unique_id', [$this, 'uniqueId']), + + // Translations + new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]), + new TwigFunction('tl', [$this, 'translateLanguage']), + new TwigFunction('ta', [$this, 'translateArray']), + + // Object Types + new TwigFunction('get_type', [$this, 'getTypeFunc']), + new TwigFunction('of_type', [$this, 'ofTypeFunc']), + + // PHP methods + new TwigFunction('is_numeric', 'is_numeric'), + new TwigFunction('is_iterable', 'is_iterable'), + new TwigFunction('is_countable', 'is_countable'), + new TwigFunction('is_null', 'is_null'), + new TwigFunction('is_string', 'is_string'), + new TwigFunction('is_array', 'is_array'), + new TwigFunction('is_object', 'is_object'), + new TwigFunction('count', 'count'), + new TwigFunction('array_diff', 'array_diff'), + ]; + } + + /** + * @return array + */ + public function getTokenParsers(): array + { + return [ + new TwigTokenParserRender(), + new TwigTokenParserThrow(), + new TwigTokenParserTryCatch(), + new TwigTokenParserScript(), + new TwigTokenParserStyle(), + new TwigTokenParserMarkdown(), + new TwigTokenParserSwitch(), + new TwigTokenParserCache(), + ]; + } + + public function print_r($var) + { + return print_r($var, true); + } + + /** + * Filters field name by changing dot notation into array notation. + * + * @param string $str + * @return string + */ + public function fieldNameFilter($str) + { + $path = explode('.', rtrim($str, '.')); + + return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : ''); + } + + /** + * Protects email address. + * + * @param string $str + * @return string + */ + public function safeEmailFilter($str) + { + static $list = [ + '"' => '"', + "'" => ''', + '&' => '&', + '<' => '<', + '>' => '>', + '@' => '@' + ]; + + $characters = mb_str_split($str, 1, 'UTF-8'); + + $encoded = ''; + foreach ($characters as $chr) { + $encoded .= $list[$chr] ?? (random_int(0, 1) ? '&#' . mb_ord($chr) . ';' : $chr); + } + + return $encoded; + } + + /** + * Returns array in a random order. + * + * @param array|Traversable $original + * @param int $offset Can be used to return only slice of the array. + * @return array + */ + public function randomizeFilter($original, $offset = 0) + { + if ($original instanceof Traversable) { + $original = iterator_to_array($original, false); + } + + if (!is_array($original)) { + return $original; + } + + $sorted = []; + $random = array_slice($original, $offset); + shuffle($random); + + $sizeOf = count($original); + for ($x = 0; $x < $sizeOf; $x++) { + if ($x < $offset) { + $sorted[] = $original[$x]; + } else { + $sorted[] = array_shift($random); + } + } + + return $sorted; + } + + /** + * Returns the modulus of an integer + * + * @param string|int $number + * @param int $divider + * @param array|null $items array of items to select from to return + * @return int + */ + public function modulusFilter($number, $divider, $items = null) + { + if (is_string($number)) { + $number = strlen($number); + } + + $remainder = $number % $divider; + + if (is_array($items)) { + return $items[$remainder] ?? $items[0]; + } + + return $remainder; + } + + /** + * Inflector supports following notations: + * + * `{{ 'person'|pluralize }} => people` + * `{{ 'shoes'|singularize }} => shoe` + * `{{ 'welcome page'|titleize }} => "Welcome Page"` + * `{{ 'send_email'|camelize }} => SendEmail` + * `{{ 'CamelCased'|underscorize }} => camel_cased` + * `{{ 'Something Text'|hyphenize }} => something-text` + * `{{ 'something_text_to_read'|humanize }} => "Something text to read"` + * `{{ '181'|monthize }} => 5` + * `{{ '10'|ordinalize }} => 10th` + * + * @param string $action + * @param string $data + * @param int|null $count + * @return string + */ + public function inflectorFilter($action, $data, $count = null) + { + $action .= 'ize'; + + /** @var Inflector $inflector */ + $inflector = $this->grav['inflector']; + + if (in_array( + $action, + ['titleize', 'camelize', 'underscorize', 'hyphenize', 'humanize', 'ordinalize', 'monthize'], + true + )) { + return $inflector->{$action}($data); + } + + if (in_array($action, ['pluralize', 'singularize'], true)) { + return $count ? $inflector->{$action}($data, $count) : $inflector->{$action}($data); + } + + return $data; + } + + /** + * Return MD5 hash from the input. + * + * @param string $str + * @return string + */ + public function md5Filter($str) + { + return md5($str); + } + + /** + * Return Base32 encoded string + * + * @param string $str + * @return string + */ + public function base32EncodeFilter($str) + { + return Base32::encode($str); + } + + /** + * Return Base32 decoded string + * + * @param string $str + * @return string + */ + public function base32DecodeFilter($str) + { + return Base32::decode($str); + } + + /** + * Return Base64 encoded string + * + * @param string $str + * @return string + */ + public function base64EncodeFilter($str) + { + return base64_encode($str); + } + + /** + * Return Base64 decoded string + * + * @param string $str + * @return string|false + */ + public function base64DecodeFilter($str) + { + return base64_decode($str); + } + + /** + * Sorts a collection by key + * + * @param array $input + * @param string $filter + * @param int $direction + * @param int $sort_flags + * @return array + */ + public function sortByKeyFilter($input, $filter, $direction = SORT_ASC, $sort_flags = SORT_REGULAR) + { + return Utils::sortArrayByKey($input, $filter, $direction, $sort_flags); + } + + /** + * Return ksorted collection. + * + * @param array|null $array + * @return array + */ + public function ksortFilter($array) + { + if (null === $array) { + $array = []; + } + ksort($array); + + return $array; + } + + /** + * Wrapper for chunk_split() function + * + * @param string $value + * @param int $chars + * @param string $split + * @return string + */ + public function chunkSplitFilter($value, $chars, $split = '-') + { + return chunk_split($value, $chars, $split); + } + + /** + * determine if a string contains another + * + * @param string $haystack + * @param string $needle + * @return string|bool + * @todo returning $haystack here doesn't make much sense + */ + public function containsFilter($haystack, $needle) + { + if (empty($needle)) { + return $haystack; + } + + return (strpos($haystack, (string) $needle) !== false); + } + + /** + * Gets a human readable output for cron syntax + * + * @param string $at + * @return string + */ + public function niceCronFilter($at) + { + $cron = new Cron($at); + return $cron->getText('en'); + } + + /** + * Get Cron object for a crontab 'at' format + * + * @param string $at + * @return CronExpression + */ + public function cronFunc($at) + { + return CronExpression::factory($at); + } + + /** + * displays a facebook style 'time ago' formatted date/time + * + * @param string $date + * @param bool $long_strings + * @param bool $show_tense + * @return string + */ + public function nicetimeFunc($date, $long_strings = true, $show_tense = true) + { + if (empty($date)) { + return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED'); + } + + if ($long_strings) { + $periods = [ + 'NICETIME.SECOND', + 'NICETIME.MINUTE', + 'NICETIME.HOUR', + 'NICETIME.DAY', + 'NICETIME.WEEK', + 'NICETIME.MONTH', + 'NICETIME.YEAR', + 'NICETIME.DECADE' + ]; + } else { + $periods = [ + 'NICETIME.SEC', + 'NICETIME.MIN', + 'NICETIME.HR', + 'NICETIME.DAY', + 'NICETIME.WK', + 'NICETIME.MO', + 'NICETIME.YR', + 'NICETIME.DEC' + ]; + } + + $lengths = ['60', '60', '24', '7', '4.35', '12', '10']; + + $now = time(); + + // check if unix timestamp + if ((string)(int)$date === (string)$date) { + $unix_date = $date; + } else { + $unix_date = strtotime($date); + } + + // check validity of date + if (empty($unix_date)) { + return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE'); + } + + // is it future date or past date + if ($now > $unix_date) { + $difference = $now - $unix_date; + $tense = $this->grav['language']->translate('GRAV.NICETIME.AGO'); + } elseif ($now == $unix_date) { + $difference = $now - $unix_date; + $tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW'); + } else { + $difference = $unix_date - $now; + $tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW'); + } + + for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) { + $difference /= $lengths[$j]; + } + + $difference = round($difference); + + if ($difference != 1) { + $periods[$j] .= '_PLURAL'; + } + + if ($this->grav['language']->getTranslation( + $this->grav['language']->getLanguage(), + $periods[$j] . '_MORE_THAN_TWO' + ) + ) { + if ($difference > 2) { + $periods[$j] .= '_MORE_THAN_TWO'; + } + } + + $periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j]); + + if ($now == $unix_date) { + return $tense; + } + + $time = "{$difference} {$periods[$j]}"; + $time .= $show_tense ? " {$tense}" : ''; + + return $time; + } + + /** + * Allow quick check of a string for XSS Vulnerabilities + * + * @param string|array $data + * @return bool|string|array + */ + public function xssFunc($data) + { + if (!is_array($data)) { + return Security::detectXss($data); + } + + $results = Security::detectXssFromArray($data); + $results_parts = array_map(static function ($value, $key) { + return $key.': \''.$value . '\''; + }, array_values($results), array_keys($results)); + + return implode(', ', $results_parts); + } + + /** + * Generates a random string with configurable length, prefix and suffix. + * Unlike the built-in `uniqid()`, this string is non-conflicting and safe + * + * @param int $length + * @param array $options + * @return string + * @throws \Exception + */ + public function uniqueId(int $length = 9, array $options = ['prefix' => '', 'suffix' => '']): string + { + return Utils::uniqueId($length, $options); + } + + /** + * @param string $string + * @return string + */ + public function absoluteUrlFilter($string) + { + $url = $this->grav['uri']->base(); + $string = preg_replace('/((?:href|src) *= *[\'"](?!(http|ftp)))/i', "$1$url", $string); + + return $string; + } + + /** + * @param array $context + * @param string $string + * @param bool $block Block or Line processing + * @return string + */ + public function markdownFunction($context, $string, $block = true) + { + $page = $context['page'] ?? null; + return Utils::processMarkdown($string, $block, $page); + } + + /** + * @param string $haystack + * @param string $needle + * @return bool + */ + public function startsWithFilter($haystack, $needle) + { + return Utils::startsWith($haystack, $needle); + } + + /** + * @param string $haystack + * @param string $needle + * @return bool + */ + public function endsWithFilter($haystack, $needle) + { + return Utils::endsWith($haystack, $needle); + } + + /** + * @param mixed $value + * @param null $default + * @return mixed|null + */ + public function definedDefaultFilter($value, $default = null) + { + return $value ?? $default; + } + + /** + * @param string $value + * @param string|null $chars + * @return string + */ + public function rtrimFilter($value, $chars = null) + { + return null !== $chars ? rtrim($value, $chars) : rtrim($value); + } + + /** + * @param string $value + * @param string|null $chars + * @return string + */ + public function ltrimFilter($value, $chars = null) + { + return null !== $chars ? ltrim($value, $chars) : ltrim($value); + } + + /** + * Returns a string from a value. If the value is array, return it json encoded + * + * @param mixed $value + * @return string + */ + public function stringFilter($value) + { + // Format the array as a string + if (is_array($value)) { + return json_encode($value); + } + + // Boolean becomes '1' or '0' + if (is_bool($value)) { + $value = (int)$value; + } + + // Cast the other values to string. + return (string)$value; + } + + /** + * Casts input to int. + * + * @param mixed $input + * @return int + */ + public function intFilter($input) + { + return (int) $input; + } + + /** + * Casts input to bool. + * + * @param mixed $input + * @return bool + */ + public function boolFilter($input) + { + return (bool) $input; + } + + /** + * Casts input to float. + * + * @param mixed $input + * @return float + */ + public function floatFilter($input) + { + return (float) $input; + } + + /** + * Casts input to array. + * + * @param mixed $input + * @return array + */ + public function arrayFilter($input) + { + if (is_array($input)) { + return $input; + } + + if (is_object($input)) { + if (method_exists($input, 'toArray')) { + return $input->toArray(); + } + + if ($input instanceof Iterator) { + return iterator_to_array($input); + } + } + + return (array)$input; + } + + /** + * @param array|object $value + * @param int|null $inline + * @param int|null $indent + * @return string + */ + public function yamlFilter($value, $inline = null, $indent = null): string + { + return Yaml::dump($value, $inline, $indent); + } + + /** + * @param Environment $twig + * @return string + */ + public function translate(Environment $twig, ...$args) + { + // If admin and tu filter provided, use it + if (isset($this->grav['admin'])) { + $numargs = count($args); + $lang = null; + + if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) { + $lang = array_pop($args); + } elseif ($numargs === 2 && is_array($args[1])) { + $subs = array_pop($args); + $args = array_merge($args, $subs); + } + + return $this->grav['admin']->translate($args, $lang); + } + + // else use the default grav translate functionality + return $this->grav['language']->translate($args); + } + + /** + * Translate Strings + * + * @param string|array $args + * @param array|null $languages + * @param bool $array_support + * @param bool $html_out + * @return string + */ + public function translateLanguage($args, array $languages = null, $array_support = false, $html_out = false) + { + /** @var Language $language */ + $language = $this->grav['language']; + + return $language->translate($args, $languages, $array_support, $html_out); + } + + /** + * @param string $key + * @param string $index + * @param array|null $lang + * @return string + */ + public function translateArray($key, $index, $lang = null) + { + /** @var Language $language */ + $language = $this->grav['language']; + + return $language->translateArray($key, $index, $lang); + } + + /** + * Repeat given string x times. + * + * @param string $input + * @param int $multiplier + * + * @return string + */ + public function repeatFunc($input, $multiplier) + { + return str_repeat($input, $multiplier); + } + + /** + * Return URL to the resource. + * + * @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }} + * + * @param string $input Resource to be located. + * @param bool $domain True to include domain name. + * @param bool $failGracefully If true, return URL even if the file does not exist. + * @return string|false Returns url to the resource or null if resource was not found. + */ + public function urlFunc($input, $domain = false, $failGracefully = false) + { + return Utils::url($input, $domain, $failGracefully); + } + + /** + * This function will evaluate Twig $twig through the $environment, and return its results. + * + * @param array $context + * @param string $twig + * @return mixed + */ + public function evaluateTwigFunc($context, $twig) + { + + $loader = new FilesystemLoader('.'); + $env = new Environment($loader); + $env->addExtension($this); + + $template = $env->createTemplate($twig); + + return $template->render($context); + } + + /** + * This function will evaluate a $string through the $environment, and return its results. + * + * @param array $context + * @param string $string + * @return mixed + */ + public function evaluateStringFunc($context, $string) + { + return $this->evaluateTwigFunc($context, "{{ $string }}"); + } + + /** + * Based on Twig\Extension\Debug / twig_var_dump + * (c) 2011 Fabien Potencier + * + * @param Environment $env + * @param array $context + */ + public function dump(Environment $env, $context) + { + if (!$env->isDebug() || !$this->debugger) { + return; + } + + $count = func_num_args(); + if (2 === $count) { + $data = []; + foreach ($context as $key => $value) { + if (is_object($value)) { + if (method_exists($value, 'toArray')) { + $data[$key] = $value->toArray(); + } else { + $data[$key] = 'Object (' . get_class($value) . ')'; + } + } else { + $data[$key] = $value; + } + } + $this->debugger->addMessage($data, 'debug'); + } else { + for ($i = 2; $i < $count; $i++) { + $var = func_get_arg($i); + $this->debugger->addMessage($var, 'debug'); + } + } + } + + /** + * Output a Gist + * + * @param string $id + * @param string|false $file + * @return string + */ + public function gistFunc($id, $file = false) + { + $url = 'https://gist.github.com/' . $id . '.js'; + if ($file) { + $url .= '?file=' . $file; + } + return ''; + } + + /** + * Generate a random string + * + * @param int $count + * @return string + */ + public function randomStringFunc($count = 5) + { + return Utils::generateRandomString($count); + } + + /** + * Pad a string to a certain length with another string + * + * @param string $input + * @param int $pad_length + * @param string $pad_string + * @param int $pad_type + * @return string + */ + public static function padFilter($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT) + { + return str_pad($input, (int)$pad_length, $pad_string, $pad_type); + } + + /** + * Workaround for twig associative array initialization + * Returns a key => val array + * + * @param string $key key of item + * @param string $val value of item + * @param array|null $current_array optional array to add to + * @return array + */ + public function arrayKeyValueFunc($key, $val, $current_array = null) + { + if (empty($current_array)) { + return array($key => $val); + } + + $current_array[$key] = $val; + + return $current_array; + } + + /** + * Wrapper for array_intersect() method + * + * @param array|Collection $array1 + * @param array|Collection $array2 + * @return array|Collection + */ + public function arrayIntersectFunc($array1, $array2) + { + if ($array1 instanceof Collection && $array2 instanceof Collection) { + return $array1->intersect($array2)->toArray(); + } + + return array_intersect($array1, $array2); + } + + /** + * Translate a string + * + * @return string + */ + public function translateFunc() + { + return $this->grav['language']->translate(func_get_args()); + } + + /** + * Authorize an action. Returns true if the user is logged in and + * has the right to execute $action. + * + * @param string|array $action An action or a list of actions. Each + * entry can be a string like 'group.action' + * or without dot notation an associative + * array. + * @return bool Returns TRUE if the user is authorized to + * perform the action, FALSE otherwise. + */ + public function authorize($action) + { + // Admin can use Flex users even if the site does not; make sure we use the right version of the user. + $admin = $this->grav['admin'] ?? null; + if ($admin) { + $user = $admin->user; + } else { + /** @var UserInterface|null $user */ + $user = $this->grav['user'] ?? null; + } + + if (!$user) { + return false; + } + + if (is_array($action)) { + if (Utils::isAssoc($action)) { + // Handle nested access structure. + $actions = Utils::arrayFlattenDotNotation($action); + } else { + // Handle simple access list. + $actions = array_combine($action, array_fill(0, count($action), true)); + } + } else { + // Handle single action. + $actions = [(string)$action => true]; + } + + $count = count($actions); + foreach ($actions as $act => $authenticated) { + // Ignore 'admin.super' if it's not the only value to be checked. + if ($act === 'admin.super' && $count > 1 && $user instanceof FlexObjectInterface) { + continue; + } + + $auth = $user->authorize($act) ?? false; + if (is_bool($auth) && $auth === Utils::isPositive($authenticated)) { + return true; + } + } + + return false; + } + + /** + * Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action. + * + * For maximum protection, ensure that the string representing the action is as specific as possible + * + * @param string $action the action + * @param string $nonceParamName a custom nonce param name + * @return string the nonce input field + */ + public function nonceFieldFunc($action, $nonceParamName = 'nonce') + { + $string = ''; + + return $string; + } + + /** + * Decodes string from JSON. + * + * @param string $str + * @param bool $assoc + * @param int $depth + * @param int $options + * @return array + */ + public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0) + { + return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options); + } + + /** + * Used to retrieve a cookie value + * + * @param string $key The cookie name to retrieve + * @return string + */ + public function getCookie($key) + { + return filter_input(INPUT_COOKIE, $key, FILTER_SANITIZE_STRING); + } + + /** + * Twig wrapper for PHP's preg_replace method + * + * @param string|string[] $subject the content to perform the replacement on + * @param string|string[] $pattern the regex pattern to use for matches + * @param string|string[] $replace the replacement value either as a string or an array of replacements + * @param int $limit the maximum possible replacements for each pattern in each subject + * @return string|string[]|null the resulting content + */ + public function regexReplace($subject, $pattern, $replace, $limit = -1) + { + return preg_replace($pattern, $replace, $subject, $limit); + } + + /** + * Twig wrapper for PHP's preg_grep method + * + * @param array $array + * @param string $regex + * @param int $flags + * @return array + */ + public function regexFilter($array, $regex, $flags = 0) + { + return preg_grep($regex, $array, $flags); + } + + /** + * Twig wrapper for PHP's preg_match method + * + * @param string $subject the content to perform the match on + * @param string $pattern the regex pattern to use for match + * @param int $flags + * @param int $offset + * @return array|false returns the matches if there is at least one match in the subject for a given pattern or null if not. + */ + public function regexMatch($subject, $pattern, $flags = 0, $offset = 0) + { + if (preg_match($pattern, $subject, $matches, $flags, $offset) === false) { + return false; + } + + return $matches; + } + + /** + * Twig wrapper for PHP's preg_split method + * + * @param string $subject the content to perform the split on + * @param string $pattern the regex pattern to use for split + * @param int $limit the maximum possible splits for the given pattern + * @param int $flags + * @return array|false the resulting array after performing the split operation + */ + public function regexSplit($subject, $pattern, $limit = -1, $flags = 0) + { + return preg_split($pattern, $subject, $limit, $flags); + } + + /** + * redirect browser from twig + * + * @param string $url the url to redirect to + * @param int $statusCode statusCode, default 303 + * @return void + */ + public function redirectFunc($url, $statusCode = 303) + { + $response = new Response($statusCode, ['location' => $url]); + + $this->grav->close($response); + } + + /** + * Generates an array containing a range of elements, optionally stepped + * + * @param int $start Minimum number, default 0 + * @param int $end Maximum number, default `getrandmax()` + * @param int $step Increment between elements in the sequence, default 1 + * @return array + */ + public function rangeFunc($start = 0, $end = 100, $step = 1) + { + return range($start, $end, $step); + } + + /** + * Check if HTTP_X_REQUESTED_WITH has been set to xmlhttprequest, + * in which case we may unsafely assume ajax. Non critical use only. + * + * @return bool True if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest + */ + public function isAjaxFunc() + { + return ( + !empty($_SERVER['HTTP_X_REQUESTED_WITH']) + && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); + } + + /** + * Get the Exif data for a file + * + * @param string $image + * @param bool $raw + * @return mixed + */ + public function exifFunc($image, $raw = false) + { + if (isset($this->grav['exif'])) { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + if ($locator->isStream($image)) { + $image = $locator->findResource($image); + } + + $exif_reader = $this->grav['exif']->getReader(); + + if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { + $exif_data = $exif_reader->read($image); + + if ($exif_data) { + if ($raw) { + return $exif_data->getRawData(); + } + + return $exif_data->getData(); + } + } + } + + return null; + } + + /** + * Simple function to read a file based on a filepath and output it + * + * @param string $filepath + * @return bool|string + */ + public function readFileFunc($filepath) + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + if ($locator->isStream($filepath)) { + $filepath = $locator->findResource($filepath); + } + + if ($filepath && file_exists($filepath)) { + return file_get_contents($filepath); + } + + return false; + } + + /** + * Process a folder as Media and return a media object + * + * @param string $media_dir + * @return Media|null + */ + public function mediaDirFunc($media_dir) + { + /** @var UniformResourceLocator $locator */ + $locator = $this->grav['locator']; + + if ($locator->isStream($media_dir)) { + $media_dir = $locator->findResource($media_dir); + } + + if ($media_dir && file_exists($media_dir)) { + return new Media($media_dir); + } + + return null; + } + + /** + * Dump a variable to the browser + * + * @param mixed $var + * @return void + */ + public function vardumpFunc($var) + { + var_dump($var); + } + + /** + * Returns a nicer more readable filesize based on bytes + * + * @param int $bytes + * @return string + */ + public function niceFilesizeFunc($bytes) + { + return Utils::prettySize($bytes); + } + + /** + * Returns a nicer more readable number + * + * @param int|float|string $n + * @return string|bool + */ + public function niceNumberFunc($n) + { + if (!is_float($n) && !is_int($n)) { + if (!is_string($n) || $n === '') { + return false; + } + + // Strip any thousand formatting and find the first number. + $list = array_filter(preg_split("/\D+/", str_replace(',', '', $n))); + $n = reset($list); + + if (!is_numeric($n)) { + return false; + } + + $n = (float)$n; + } + + // now filter it; + if ($n > 1000000000000) { + return round($n/1000000000000, 2).' t'; + } + if ($n > 1000000000) { + return round($n/1000000000, 2).' b'; + } + if ($n > 1000000) { + return round($n/1000000, 2).' m'; + } + if ($n > 1000) { + return round($n/1000, 2).' k'; + } + + return number_format($n); + } + + /** + * Get a theme variable + * Will try to get the variable for the current page, if not found, it tries it's parent page on up to root. + * If still not found, will use the theme's configuration value, + * If still not found, will use the $default value passed in + * + * @param array $context Twig Context + * @param string $var variable to be found (using dot notation) + * @param null $default the default value to be used as last resort + * @param null $page an optional page to use for the current page + * @param bool $exists toggle to simply return the page where the variable is set, else null + * @return mixed + */ + public function themeVarFunc($context, $var, $default = null, $page = null, $exists = false) + { + $page = $page ?? $context['page'] ?? Grav::instance()['page'] ?? null; + + // Try to find var in the page headers + if ($page instanceof PageInterface && $page->exists()) { + // Loop over pages and look for header vars + while ($page && !$page->root()) { + $header = new Data((array)$page->header()); + $value = $header->get($var); + if (isset($value)) { + if ($exists) { + return $page; + } + + return $value; + } + $page = $page->parent(); + } + } + + if ($exists) { + return false; + } + + return Grav::instance()['config']->get('theme.' . $var, $default); + } + + /** + * Look for a page header variable in an array of pages working its way through until a value is found + * + * @param array $context + * @param string $var the variable to look for in the page header + * @param string|string[]|null $pages array of pages to check (current page upwards if not null) + * @return mixed + * @deprecated 1.7 Use themeVarFunc() instead + */ + public function pageHeaderVarFunc($context, $var, $pages = null) + { + if (is_array($pages)) { + $page = array_shift($pages); + } else { + $page = null; + } + return $this->themeVarFunc($context, $var, null, $page); + } + + /** + * takes an array of classes, and if they are not set on body_classes + * look to see if they are set in theme config + * + * @param array $context + * @param string|string[] $classes + * @return string + */ + public function bodyClassFunc($context, $classes) + { + + $header = $context['page']->header(); + $body_classes = $header->body_classes ?? ''; + + foreach ((array)$classes as $class) { + if (!empty($body_classes) && Utils::contains($body_classes, $class)) { + continue; + } + + $val = $this->config->get('theme.' . $class, false) ? $class : false; + $body_classes .= $val ? ' ' . $val : ''; + } + + return $body_classes; + } + + /** + * Returns the content of an SVG image and adds extra classes as needed + * + * @param string $path + * @param string|null $classes + * @return string|string[]|null + */ + public static function svgImageFunction($path, $classes = null, $strip_style = false) + { + $path = Utils::fullPath($path); + + $classes = $classes ?: ''; + + if (file_exists($path) && !is_dir($path)) { + $svg = file_get_contents($path); + $classes = " inline-block $classes"; + $matched = false; + + //Remove xml tag if it exists + $svg = preg_replace('/^<\?xml.*\?>/','', $svg); + + //Strip style if needed + if ($strip_style) { + $svg = preg_replace('//s', '', $svg); + } + + //Look for existing class + $svg = preg_replace_callback('/^]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) { + if (isset($matches[2])) { + $new_classes = $matches[2] . $classes; + $matched = true; + return str_replace($matches[1], "class=\"$new_classes\"", $matches[0]); + } + return $matches[0]; + }, $svg + ); + + // no matches found just add the class + if (!$matched) { + $classes = trim($classes); + $svg = str_replace('jsonSerialize(); + } elseif (method_exists($data, 'toArray')) { + $data = $data->toArray(); + } else { + $data = json_decode(json_encode($data), true); + } + } + + return Yaml::dump($data, $inline); + } + + /** + * Decode/Parse data from YAML format + * + * @param string $data + * @return array + */ + public function yamlDecodeFilter($data) + { + return Yaml::parse($data); + } + + /** + * Function/Filter to return the type of variable + * + * @param mixed $var + * @return string + */ + public function getTypeFunc($var) + { + return gettype($var); + } + + /** + * Function/Filter to test type of variable + * + * @param mixed $var + * @param string|null $typeTest + * @param string|null $className + * @return bool + */ + public function ofTypeFunc($var, $typeTest = null, $className = null) + { + + switch ($typeTest) { + default: + return false; + + case 'array': + return is_array($var); + + case 'bool': + return is_bool($var); + + case 'class': + return is_object($var) === true && get_class($var) === $className; + + case 'float': + return is_float($var); + + case 'int': + return is_int($var); + + case 'numeric': + return is_numeric($var); + + case 'object': + return is_object($var); + + case 'scalar': + return is_scalar($var); + + case 'string': + return is_string($var); + } + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php index 38194e669..81cecaeaf 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php @@ -47,6 +47,6 @@ class TwigNodeMarkdown extends Node implements NodeOutputInterface ->write('$lines = explode("\n", $content);' . PHP_EOL) ->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL) ->write('$content = join("\n", $content);' . PHP_EOL) - ->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($context, $content);' . PHP_EOL); + ->write('echo $this->env->getExtension(\'Grav\Common\Twig\Extension\GravExtension\')->markdownFunction($context, $content);' . PHP_EOL); } } diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php index 1a32a91a3..3bfe6124e 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php @@ -43,7 +43,7 @@ class TwigNodeThrow extends Node $compiler->addDebugInfo($this); $compiler - ->write('throw new \RuntimeException(') + ->write('throw new \Grav\Common\Twig\Exception\TwigException(') ->subcompile($this->getNode('message')) ->write(', ') ->write($this->getAttribute('code') ?: 500) diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php index da95a1d98..40e9240f3 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php @@ -49,16 +49,15 @@ class TwigNodeTryCatch extends Node $compiler ->indent() - ->subcompile($this->getNode('try')); + ->subcompile($this->getNode('try')) + ->outdent() + ->write('} catch (\Exception $e) {' . "\n") + ->indent() + ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n") + ->write('$context[\'e\'] = $e;' . "\n"); if ($this->hasNode('catch')) { - $compiler - ->outdent() - ->write('} catch (\Exception $e) {' . "\n") - ->indent() - ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n") - ->write('$context[\'e\'] = $e;' . "\n") - ->subcompile($this->getNode('catch')); + $compiler->subcompile($this->getNode('catch')); } $compiler diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index d4f710be3..19bad01b1 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -16,24 +16,30 @@ use Grav\Common\Language\Language; use Grav\Common\Language\LanguageCodes; use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Pages; +use Grav\Common\Twig\Exception\TwigException; +use Grav\Common\Twig\Extension\FilesystemExtension; +use Grav\Common\Twig\Extension\GravExtension; +use Grav\Common\Utils; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\Event\Event; -use Phive\Twig\Extensions\Deferred\DeferredExtension; use RuntimeException; use Twig\Cache\FilesystemCache; +use Twig\DeferredExtension\DeferredExtension; use Twig\Environment; use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; use Twig\Extension\CoreExtension; use Twig\Extension\DebugExtension; use Twig\Extension\StringLoaderExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; -use Twig\Loader\ExistsLoaderInterface; use Twig\Loader\FilesystemLoader; use Twig\Profiler\Profile; use Twig\TwigFilter; use Twig\TwigFunction; use function function_exists; +use function in_array; +use function is_array; /** * Class Twig @@ -154,27 +160,53 @@ class Twig $this->twig = new TwigEnvironment($loader_chain, $params); - if ($config->get('system.twig.undefined_functions')) { - $this->twig->registerUndefinedFunctionCallback(function ($name) { + $this->twig->registerUndefinedFunctionCallback(function ($name) use ($config) { + $allowed = $config->get('system.twig.safe_functions'); + if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) { + return new TwigFunction($name, $name); + } + if ($config->get('system.twig.undefined_functions')) { if (function_exists($name)) { - return new TwigFunction($name, $name); + if (!Utils::isDangerousFunction($name)) { + user_error("PHP function {$name}() was used as Twig function. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_functions`", E_USER_DEPRECATED); + + return new TwigFunction($name, $name); + } + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig function. If you really want to use it, please add it to system configuration: `system.twig.safe_functions`")); } - return new TwigFunction($name, static function () { - }); - }); - } + return new TwigFunction($name, static function () {}); + } - if ($config->get('system.twig.undefined_filters')) { - $this->twig->registerUndefinedFilterCallback(function ($name) { + return false; + }); + + $this->twig->registerUndefinedFilterCallback(function ($name) use ($config) { + $allowed = $config->get('system.twig.safe_filters'); + if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) { + return new TwigFilter($name, $name); + } + if ($config->get('system.twig.undefined_filters')) { if (function_exists($name)) { - return new TwigFilter($name, $name); + if (!Utils::isDangerousFunction($name)) { + user_error("PHP function {$name}() used as Twig filter. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_filters`", E_USER_DEPRECATED); + + return new TwigFilter($name, $name); + } + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig filter. If you really want to use it, please add it to system configuration: `system.twig.safe_filters`")); } - return new TwigFilter($name, static function () { - }); - }); - } + return new TwigFilter($name, static function () {}); + } + + return false; + }); $this->grav->fireEvent('onTwigInitialized'); @@ -188,7 +220,8 @@ class Twig if ($config->get('system.twig.debug')) { $this->twig->addExtension(new DebugExtension()); } - $this->twig->addExtension(new TwigExtension()); + $this->twig->addExtension(new GravExtension()); + $this->twig->addExtension(new FilesystemExtension()); $this->twig->addExtension(new DeferredExtension()); $this->twig->addExtension(new StringLoaderExtension()); @@ -211,7 +244,7 @@ class Twig 'assets' => $this->grav['assets'], 'taxonomy' => $this->grav['taxonomy'], 'browser' => $this->grav['browser'], - 'base_dir' => rtrim(ROOT_DIR, '/'), + 'base_dir' => GRAV_ROOT, 'home_url' => $pages->homeUrl($active_language), 'base_url' => $pages->baseUrl($active_language), 'base_url_absolute' => $pages->baseUrl($active_language, true), @@ -372,38 +405,63 @@ class Twig */ public function processSite($format = null, array $vars = []) { - // set the page now its been processed - $this->grav->fireEvent('onTwigSiteVariables'); - /** @var Pages $pages */ - $pages = $this->grav['pages']; - /** @var PageInterface $page */ - $page = $this->grav['page']; - $content = $page->content(); - - $twig_vars = $this->twig_vars; - - $twig_vars['theme'] = $this->grav['config']->get('theme'); - $twig_vars['pages'] = $pages->root(); - $twig_vars['page'] = $page; - $twig_vars['header'] = $page->header(); - $twig_vars['media'] = $page->media(); - $twig_vars['content'] = $content; - - // determine if params are set, if so disable twig cache - $params = $this->grav['uri']->params(null, true); - if (!empty($params)) { - $this->twig->setCache(false); - } - - // Get Twig template layout - $template = $this->getPageTwigTemplate($page, $format); - $page->templateFormat($format); - try { + $grav = $this->grav; + + // set the page now its been processed + $grav->fireEvent('onTwigSiteVariables'); + + /** @var Pages $pages */ + $pages = $grav['pages']; + + /** @var PageInterface $page */ + $page = $grav['page']; + + $twig_vars = $this->twig_vars; + $twig_vars['theme'] = $grav['config']->get('theme'); + $twig_vars['pages'] = $pages->root(); + $twig_vars['page'] = $page; + $twig_vars['header'] = $page->header(); + $twig_vars['media'] = $page->media(); + $twig_vars['content'] = $page->content(); + + // determine if params are set, if so disable twig cache + $params = $grav['uri']->params(null, true); + if (!empty($params)) { + $this->twig->setCache(false); + } + + // Get Twig template layout + $template = $this->getPageTwigTemplate($page, $format); + $page->templateFormat($format); + $output = $this->twig->render($template, $vars + $twig_vars); } catch (LoaderError $e) { - $error_msg = $e->getMessage(); - throw new RuntimeException($error_msg, 400, $e); + throw new RuntimeException($e->getMessage(), 400, $e); + } catch (RuntimeError $e) { + $prev = $e->getPrevious(); + if ($prev instanceof TwigException) { + $code = $prev->getCode() ?: 500; + // Fire onPageNotFound event. + $event = new Event([ + 'page' => $page, + 'code' => $code, + 'message' => $prev->getMessage(), + 'exception' => $prev, + 'route' => $grav['route'], + 'request' => $grav['request'] + ]); + $event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event); + $newPage = $event['page']; + if ($newPage && $newPage !== $page) { + unset($grav['page']); + $grav['page'] = $newPage; + + return $this->processSite($newPage->templateFormat(), $vars); + } + } + + throw $e; } return $output; @@ -456,25 +514,21 @@ class Twig $twig_extension = $extension ? '.'. $extension .TWIG_EXT : TEMPLATE_EXT; $template_file = $this->template($page->template() . $twig_extension); - $page_template = null; - $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface) { - if ($loader->exists($template_file)) { - // template.xxx.twig - $page_template = $template_file; - } elseif ($twig_extension !== TEMPLATE_EXT && $loader->exists($template . TEMPLATE_EXT)) { - // template.html.twig - $page_template = $template . TEMPLATE_EXT; - $format = 'html'; - } elseif ($loader->exists($default . $twig_extension)) { - // default.xxx.twig - $page_template = $default . $twig_extension; - } else { - // default.html.twig - $page_template = $default . TEMPLATE_EXT; - $format = 'html'; - } + if ($loader->exists($template_file)) { + // template.xxx.twig + $page_template = $template_file; + } elseif ($twig_extension !== TEMPLATE_EXT && $loader->exists($template . TEMPLATE_EXT)) { + // template.html.twig + $page_template = $template . TEMPLATE_EXT; + $format = 'html'; + } elseif ($loader->exists($default . $twig_extension)) { + // default.xxx.twig + $page_template = $default . $twig_extension; + } else { + // default.html.twig + $page_template = $default . TEMPLATE_EXT; + $format = 'html'; } return $page_template; diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index 4c6fa9e00..698858b56 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -9,1591 +9,13 @@ namespace Grav\Common\Twig; -use Cron\CronExpression; -use Grav\Common\Config\Config; -use Grav\Common\Data\Data; -use Grav\Common\Debugger; -use Grav\Common\Grav; -use Grav\Common\Inflector; -use Grav\Common\Language\Language; -use Grav\Common\Page\Collection; -use Grav\Common\Page\Interfaces\PageInterface; -use Grav\Common\Page\Media; -use Grav\Common\Scheduler\Cron; -use Grav\Common\Security; -use Grav\Common\Twig\TokenParser\TwigTokenParserCache; -use Grav\Common\Twig\TokenParser\TwigTokenParserRender; -use Grav\Common\Twig\TokenParser\TwigTokenParserScript; -use Grav\Common\Twig\TokenParser\TwigTokenParserStyle; -use Grav\Common\Twig\TokenParser\TwigTokenParserSwitch; -use Grav\Common\Twig\TokenParser\TwigTokenParserThrow; -use Grav\Common\Twig\TokenParser\TwigTokenParserTryCatch; -use Grav\Common\Twig\TokenParser\TwigTokenParserMarkdown; -use Grav\Common\User\Interfaces\UserInterface; -use Grav\Common\Utils; -use Grav\Common\Yaml; -use Grav\Common\Helpers\Base32; -use Grav\Framework\Flex\Interfaces\FlexObjectInterface; -use Grav\Framework\Psr7\Response; -use Iterator; -use JsonSerializable; -use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; -use Traversable; -use Twig\Environment; -use Twig\Extension\AbstractExtension; -use Twig\Extension\GlobalsInterface; -use Twig\Loader\FilesystemLoader; -use Twig\TwigFilter; -use Twig\TwigFunction; -use function array_slice; -use function count; -use function func_get_args; -use function func_num_args; -use function get_class; -use function gettype; -use function in_array; -use function is_array; -use function is_bool; -use function is_float; -use function is_int; -use function is_numeric; -use function is_object; -use function is_scalar; -use function is_string; -use function ord; -use function strlen; +use Grav\Common\Twig\Extension\GravExtension; /** * Class TwigExtension * @package Grav\Common\Twig + * @deprecated 1.7 Use GravExtension instead */ -class TwigExtension extends AbstractExtension implements GlobalsInterface +class TwigExtension extends GravExtension { - /** @var Grav */ - protected $grav; - /** @var Debugger|null */ - protected $debugger; - /** @var Config */ - protected $config; - - /** - * TwigExtension constructor. - */ - public function __construct() - { - $this->grav = Grav::instance(); - $this->debugger = $this->grav['debugger'] ?? null; - $this->config = $this->grav['config']; - } - - /** - * Register some standard globals - * - * @return array - */ - public function getGlobals() - { - return [ - 'grav' => $this->grav, - ]; - } - - /** - * Return a list of all filters. - * - * @return array - */ - public function getFilters() - { - return [ - new TwigFilter('*ize', [$this, 'inflectorFilter']), - new TwigFilter('absolute_url', [$this, 'absoluteUrlFilter']), - new TwigFilter('contains', [$this, 'containsFilter']), - new TwigFilter('chunk_split', [$this, 'chunkSplitFilter']), - new TwigFilter('nicenumber', [$this, 'niceNumberFunc']), - new TwigFilter('nicefilesize', [$this, 'niceFilesizeFunc']), - new TwigFilter('nicetime', [$this, 'nicetimeFunc']), - new TwigFilter('defined', [$this, 'definedDefaultFilter']), - new TwigFilter('ends_with', [$this, 'endsWithFilter']), - new TwigFilter('fieldName', [$this, 'fieldNameFilter']), - new TwigFilter('ksort', [$this, 'ksortFilter']), - new TwigFilter('ltrim', [$this, 'ltrimFilter']), - new TwigFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]), - new TwigFilter('md5', [$this, 'md5Filter']), - new TwigFilter('base32_encode', [$this, 'base32EncodeFilter']), - new TwigFilter('base32_decode', [$this, 'base32DecodeFilter']), - new TwigFilter('base64_encode', [$this, 'base64EncodeFilter']), - new TwigFilter('base64_decode', [$this, 'base64DecodeFilter']), - new TwigFilter('randomize', [$this, 'randomizeFilter']), - new TwigFilter('modulus', [$this, 'modulusFilter']), - new TwigFilter('rtrim', [$this, 'rtrimFilter']), - new TwigFilter('pad', [$this, 'padFilter']), - new TwigFilter('regex_replace', [$this, 'regexReplace']), - new TwigFilter('safe_email', [$this, 'safeEmailFilter'], ['is_safe' => ['html']]), - new TwigFilter('safe_truncate', [Utils::class, 'safeTruncate']), - new TwigFilter('safe_truncate_html', [Utils::class, 'safeTruncateHTML']), - new TwigFilter('sort_by_key', [$this, 'sortByKeyFilter']), - new TwigFilter('starts_with', [$this, 'startsWithFilter']), - new TwigFilter('truncate', [Utils::class, 'truncate']), - new TwigFilter('truncate_html', [Utils::class, 'truncateHTML']), - new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']), - new TwigFilter('array_unique', 'array_unique'), - new TwigFilter('basename', 'basename'), - new TwigFilter('dirname', 'dirname'), - new TwigFilter('print_r', [$this, 'print_r']), - new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']), - new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']), - new TwigFilter('nicecron', [$this, 'niceCronFilter']), - - // Translations - new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]), - new TwigFilter('tl', [$this, 'translateLanguage']), - new TwigFilter('ta', [$this, 'translateArray']), - - // Casting values - new TwigFilter('string', [$this, 'stringFilter']), - new TwigFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]), - new TwigFilter('bool', [$this, 'boolFilter']), - new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]), - new TwigFilter('array', [$this, 'arrayFilter']), - - // Object Types - new TwigFilter('get_type', [$this, 'getTypeFunc']), - new TwigFilter('of_type', [$this, 'ofTypeFunc']) - ]; - } - - /** - * Return a list of all functions. - * - * @return array - */ - public function getFunctions() - { - return [ - new TwigFunction('array', [$this, 'arrayFilter']), - new TwigFunction('array_key_value', [$this, 'arrayKeyValueFunc']), - new TwigFunction('array_key_exists', 'array_key_exists'), - new TwigFunction('array_unique', 'array_unique'), - new TwigFunction('array_intersect', [$this, 'arrayIntersectFunc']), - new TwigFunction('array_diff', 'array_diff'), - new TwigFunction('authorize', [$this, 'authorize']), - new TwigFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), - new TwigFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), - new TwigFunction('vardump', [$this, 'vardumpFunc']), - new TwigFunction('print_r', [$this, 'print_r']), - new TwigFunction('http_response_code', 'http_response_code'), - new TwigFunction('evaluate', [$this, 'evaluateStringFunc'], ['needs_context' => true]), - new TwigFunction('evaluate_twig', [$this, 'evaluateTwigFunc'], ['needs_context' => true]), - new TwigFunction('gist', [$this, 'gistFunc']), - new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']), - new TwigFunction('pathinfo', 'pathinfo'), - new TwigFunction('random_string', [$this, 'randomStringFunc']), - new TwigFunction('repeat', [$this, 'repeatFunc']), - new TwigFunction('regex_replace', [$this, 'regexReplace']), - new TwigFunction('regex_filter', [$this, 'regexFilter']), - new TwigFunction('regex_match', [$this, 'regexMatch']), - new TwigFunction('regex_split', [$this, 'regexSplit']), - new TwigFunction('string', [$this, 'stringFilter']), - new TwigFunction('url', [$this, 'urlFunc']), - new TwigFunction('json_decode', [$this, 'jsonDecodeFilter']), - new TwigFunction('get_cookie', [$this, 'getCookie']), - new TwigFunction('redirect_me', [$this, 'redirectFunc']), - new TwigFunction('range', [$this, 'rangeFunc']), - new TwigFunction('isajaxrequest', [$this, 'isAjaxFunc']), - new TwigFunction('exif', [$this, 'exifFunc']), - new TwigFunction('media_directory', [$this, 'mediaDirFunc']), - new TwigFunction('body_class', [$this, 'bodyClassFunc'], ['needs_context' => true]), - new TwigFunction('theme_var', [$this, 'themeVarFunc'], ['needs_context' => true]), - new TwigFunction('header_var', [$this, 'pageHeaderVarFunc'], ['needs_context' => true]), - new TwigFunction('read_file', [$this, 'readFileFunc']), - new TwigFunction('nicenumber', [$this, 'niceNumberFunc']), - new TwigFunction('nicefilesize', [$this, 'niceFilesizeFunc']), - new TwigFunction('nicetime', [$this, 'nicetimeFunc']), - new TwigFunction('cron', [$this, 'cronFunc']), - new TwigFunction('svg_image', [$this, 'svgImageFunction']), - new TwigFunction('xss', [$this, 'xssFunc']), - - - // Translations - new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]), - new TwigFunction('tl', [$this, 'translateLanguage']), - new TwigFunction('ta', [$this, 'translateArray']), - - // Object Types - new TwigFunction('get_type', [$this, 'getTypeFunc']), - new TwigFunction('of_type', [$this, 'ofTypeFunc']) - ]; - } - - /** - * @return array - */ - public function getTokenParsers() - { - return [ - new TwigTokenParserRender(), - new TwigTokenParserThrow(), - new TwigTokenParserTryCatch(), - new TwigTokenParserScript(), - new TwigTokenParserStyle(), - new TwigTokenParserMarkdown(), - new TwigTokenParserSwitch(), - new TwigTokenParserCache(), - ]; - } - - public function print_r($var) - { - return print_r($var, true); - } - - /** - * Filters field name by changing dot notation into array notation. - * - * @param string $str - * @return string - */ - public function fieldNameFilter($str) - { - $path = explode('.', rtrim($str, '.')); - - return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : ''); - } - - /** - * Protects email address. - * - * @param string $str - * @return string - */ - public function safeEmailFilter($str) - { - static $list = [ - '"' => '"', - "'" => ''', - '&' => '&', - '<' => '<', - '>' => '>', - '@' => '@' - ]; - - $characters = mb_str_split($str, 1, 'UTF-8'); - - $encoded = ''; - foreach ($characters as $chr) { - $encoded .= $list[$chr] ?? (random_int(0, 1) ? '&#' . mb_ord($chr) . ';' : $chr); - } - - return $encoded; - } - - /** - * Returns array in a random order. - * - * @param array|Traversable $original - * @param int $offset Can be used to return only slice of the array. - * @return array - */ - public function randomizeFilter($original, $offset = 0) - { - if ($original instanceof Traversable) { - $original = iterator_to_array($original, false); - } - - if (!is_array($original)) { - return $original; - } - - $sorted = []; - $random = array_slice($original, $offset); - shuffle($random); - - $sizeOf = count($original); - for ($x = 0; $x < $sizeOf; $x++) { - if ($x < $offset) { - $sorted[] = $original[$x]; - } else { - $sorted[] = array_shift($random); - } - } - - return $sorted; - } - - /** - * Returns the modulus of an integer - * - * @param string|int $number - * @param int $divider - * @param array|null $items array of items to select from to return - * @return int - */ - public function modulusFilter($number, $divider, $items = null) - { - if (is_string($number)) { - $number = strlen($number); - } - - $remainder = $number % $divider; - - if (is_array($items)) { - return $items[$remainder] ?? $items[0]; - } - - return $remainder; - } - - /** - * Inflector supports following notations: - * - * `{{ 'person'|pluralize }} => people` - * `{{ 'shoes'|singularize }} => shoe` - * `{{ 'welcome page'|titleize }} => "Welcome Page"` - * `{{ 'send_email'|camelize }} => SendEmail` - * `{{ 'CamelCased'|underscorize }} => camel_cased` - * `{{ 'Something Text'|hyphenize }} => something-text` - * `{{ 'something_text_to_read'|humanize }} => "Something text to read"` - * `{{ '181'|monthize }} => 5` - * `{{ '10'|ordinalize }} => 10th` - * - * @param string $action - * @param string $data - * @param int|null $count - * @return string - */ - public function inflectorFilter($action, $data, $count = null) - { - $action .= 'ize'; - - /** @var Inflector $inflector */ - $inflector = $this->grav['inflector']; - - if (in_array( - $action, - ['titleize', 'camelize', 'underscorize', 'hyphenize', 'humanize', 'ordinalize', 'monthize'], - true - )) { - return $inflector->{$action}($data); - } - - if (in_array($action, ['pluralize', 'singularize'], true)) { - return $count ? $inflector->{$action}($data, $count) : $inflector->{$action}($data); - } - - return $data; - } - - /** - * Return MD5 hash from the input. - * - * @param string $str - * @return string - */ - public function md5Filter($str) - { - return md5($str); - } - - /** - * Return Base32 encoded string - * - * @param string $str - * @return string - */ - public function base32EncodeFilter($str) - { - return Base32::encode($str); - } - - /** - * Return Base32 decoded string - * - * @param string $str - * @return string - */ - public function base32DecodeFilter($str) - { - return Base32::decode($str); - } - - /** - * Return Base64 encoded string - * - * @param string $str - * @return string - */ - public function base64EncodeFilter($str) - { - return base64_encode($str); - } - - /** - * Return Base64 decoded string - * - * @param string $str - * @return string|false - */ - public function base64DecodeFilter($str) - { - return base64_decode($str); - } - - /** - * Sorts a collection by key - * - * @param array $input - * @param string $filter - * @param int $direction - * @param int $sort_flags - * @return array - */ - public function sortByKeyFilter($input, $filter, $direction = SORT_ASC, $sort_flags = SORT_REGULAR) - { - return Utils::sortArrayByKey($input, $filter, $direction, $sort_flags); - } - - /** - * Return ksorted collection. - * - * @param array|null $array - * @return array - */ - public function ksortFilter($array) - { - if (null === $array) { - $array = []; - } - ksort($array); - - return $array; - } - - /** - * Wrapper for chunk_split() function - * - * @param string $value - * @param int $chars - * @param string $split - * @return string - */ - public function chunkSplitFilter($value, $chars, $split = '-') - { - return chunk_split($value, $chars, $split); - } - - /** - * determine if a string contains another - * - * @param string $haystack - * @param string $needle - * @return string|bool - * @todo returning $haystack here doesn't make much sense - */ - public function containsFilter($haystack, $needle) - { - if (empty($needle)) { - return $haystack; - } - - return (strpos($haystack, (string) $needle) !== false); - } - - /** - * Gets a human readable output for cron syntax - * - * @param string $at - * @return string - */ - public function niceCronFilter($at) - { - $cron = new Cron($at); - return $cron->getText('en'); - } - - /** - * Get Cron object for a crontab 'at' format - * - * @param string $at - * @return CronExpression - */ - public function cronFunc($at) - { - return CronExpression::factory($at); - } - - /** - * displays a facebook style 'time ago' formatted date/time - * - * @param string $date - * @param bool $long_strings - * @param bool $show_tense - * @return string - */ - public function nicetimeFunc($date, $long_strings = true, $show_tense = true) - { - if (empty($date)) { - return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED'); - } - - if ($long_strings) { - $periods = [ - 'NICETIME.SECOND', - 'NICETIME.MINUTE', - 'NICETIME.HOUR', - 'NICETIME.DAY', - 'NICETIME.WEEK', - 'NICETIME.MONTH', - 'NICETIME.YEAR', - 'NICETIME.DECADE' - ]; - } else { - $periods = [ - 'NICETIME.SEC', - 'NICETIME.MIN', - 'NICETIME.HR', - 'NICETIME.DAY', - 'NICETIME.WK', - 'NICETIME.MO', - 'NICETIME.YR', - 'NICETIME.DEC' - ]; - } - - $lengths = ['60', '60', '24', '7', '4.35', '12', '10']; - - $now = time(); - - // check if unix timestamp - if ((string)(int)$date === (string)$date) { - $unix_date = $date; - } else { - $unix_date = strtotime($date); - } - - // check validity of date - if (empty($unix_date)) { - return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE'); - } - - // is it future date or past date - if ($now > $unix_date) { - $difference = $now - $unix_date; - $tense = $this->grav['language']->translate('GRAV.NICETIME.AGO'); - } elseif ($now == $unix_date) { - $difference = $now - $unix_date; - $tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW'); - } else { - $difference = $unix_date - $now; - $tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW'); - } - - for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) { - $difference /= $lengths[$j]; - } - - $difference = round($difference); - - if ($difference != 1) { - $periods[$j] .= '_PLURAL'; - } - - if ($this->grav['language']->getTranslation( - $this->grav['language']->getLanguage(), - $periods[$j] . '_MORE_THAN_TWO' - ) - ) { - if ($difference > 2) { - $periods[$j] .= '_MORE_THAN_TWO'; - } - } - - $periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j]); - - if ($now == $unix_date) { - return $tense; - } - - $time = "{$difference} {$periods[$j]}"; - $time .= $show_tense ? " {$tense}" : ''; - - return $time; - } - - /** - * Allow quick check of a string for XSS Vulnerabilities - * - * @param string|array $data - * @return bool|string|array - */ - public function xssFunc($data) - { - if (!is_array($data)) { - return Security::detectXss($data); - } - - $results = Security::detectXssFromArray($data); - $results_parts = array_map(static function ($value, $key) { - return $key.': \''.$value . '\''; - }, array_values($results), array_keys($results)); - - return implode(', ', $results_parts); - } - - /** - * @param string $string - * @return string - */ - public function absoluteUrlFilter($string) - { - $url = $this->grav['uri']->base(); - $string = preg_replace('/((?:href|src) *= *[\'"](?!(http|ftp)))/i', "$1$url", $string); - - return $string; - } - - /** - * @param array $context - * @param string $string - * @param bool $block Block or Line processing - * @return string - */ - public function markdownFunction($context, $string, $block = true) - { - $page = $context['page'] ?? null; - return Utils::processMarkdown($string, $block, $page); - } - - /** - * @param string $haystack - * @param string $needle - * @return bool - */ - public function startsWithFilter($haystack, $needle) - { - return Utils::startsWith($haystack, $needle); - } - - /** - * @param string $haystack - * @param string $needle - * @return bool - */ - public function endsWithFilter($haystack, $needle) - { - return Utils::endsWith($haystack, $needle); - } - - /** - * @param mixed $value - * @param null $default - * @return mixed|null - */ - public function definedDefaultFilter($value, $default = null) - { - return $value ?? $default; - } - - /** - * @param string $value - * @param string|null $chars - * @return string - */ - public function rtrimFilter($value, $chars = null) - { - return null !== $chars ? rtrim($value, $chars) : rtrim($value); - } - - /** - * @param string $value - * @param string|null $chars - * @return string - */ - public function ltrimFilter($value, $chars = null) - { - return null !== $chars ? ltrim($value, $chars) : ltrim($value); - } - - /** - * Returns a string from a value. If the value is array, return it json encoded - * - * @param mixed $value - * @return string - */ - public function stringFilter($value) - { - // Format the array as a string - if (is_array($value)) { - return json_encode($value); - } - - // Boolean becomes '1' or '0' - if (is_bool($value)) { - $value = (int)$value; - } - - // Cast the other values to string. - return (string)$value; - } - - /** - * Casts input to int. - * - * @param mixed $input - * @return int - */ - public function intFilter($input) - { - return (int) $input; - } - - /** - * Casts input to bool. - * - * @param mixed $input - * @return bool - */ - public function boolFilter($input) - { - return (bool) $input; - } - - /** - * Casts input to float. - * - * @param mixed $input - * @return float - */ - public function floatFilter($input) - { - return (float) $input; - } - - /** - * Casts input to array. - * - * @param mixed $input - * @return array - */ - public function arrayFilter($input) - { - if (is_array($input)) { - return $input; - } - - if (is_object($input)) { - if (method_exists($input, 'toArray')) { - return $input->toArray(); - } - - if ($input instanceof Iterator) { - return iterator_to_array($input); - } - } - - return (array)$input; - } - - /** - * @param Environment $twig - * @return string - */ - public function translate(Environment $twig) - { - // shift off the environment - $args = func_get_args(); - array_shift($args); - - // If admin and tu filter provided, use it - if (isset($this->grav['admin'])) { - $numargs = count($args); - $lang = null; - - if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) { - $lang = array_pop($args); - } elseif ($numargs === 2 && is_array($args[1])) { - $subs = array_pop($args); - $args = array_merge($args, $subs); - } - - return $this->grav['admin']->translate($args, $lang); - } - - // else use the default grav translate functionality - return $this->grav['language']->translate($args); - } - - /** - * Translate Strings - * - * @param string|array $args - * @param array|null $languages - * @param bool $array_support - * @param bool $html_out - * @return string - */ - public function translateLanguage($args, array $languages = null, $array_support = false, $html_out = false) - { - /** @var Language $language */ - $language = $this->grav['language']; - - return $language->translate($args, $languages, $array_support, $html_out); - } - - /** - * @param string $key - * @param string $index - * @param array|null $lang - * @return string - */ - public function translateArray($key, $index, $lang = null) - { - /** @var Language $language */ - $language = $this->grav['language']; - - return $language->translateArray($key, $index, $lang); - } - - /** - * Repeat given string x times. - * - * @param string $input - * @param int $multiplier - * - * @return string - */ - public function repeatFunc($input, $multiplier) - { - return str_repeat($input, $multiplier); - } - - /** - * Return URL to the resource. - * - * @example {{ url('theme://images/logo.png')|default('http://www.placehold.it/150x100/f4f4f4') }} - * - * @param string $input Resource to be located. - * @param bool $domain True to include domain name. - * @param bool $failGracefully If true, return URL even if the file does not exist. - * @return string|false Returns url to the resource or null if resource was not found. - */ - public function urlFunc($input, $domain = false, $failGracefully = false) - { - return Utils::url($input, $domain, $failGracefully); - } - - /** - * This function will evaluate Twig $twig through the $environment, and return its results. - * - * @param array $context - * @param string $twig - * @return mixed - */ - public function evaluateTwigFunc($context, $twig) - { - - $loader = new FilesystemLoader('.'); - $env = new Environment($loader); - $env->addExtension($this); - - $template = $env->createTemplate($twig); - - return $template->render($context); - } - - /** - * This function will evaluate a $string through the $environment, and return its results. - * - * @param array $context - * @param string $string - * @return mixed - */ - public function evaluateStringFunc($context, $string) - { - return $this->evaluateTwigFunc($context, "{{ $string }}"); - } - - /** - * Based on Twig\Extension\Debug / twig_var_dump - * (c) 2011 Fabien Potencier - * - * @param Environment $env - * @param array $context - */ - public function dump(Environment $env, $context) - { - if (!$env->isDebug() || !$this->debugger) { - return; - } - - $count = func_num_args(); - if (2 === $count) { - $data = []; - foreach ($context as $key => $value) { - if (is_object($value)) { - if (method_exists($value, 'toArray')) { - $data[$key] = $value->toArray(); - } else { - $data[$key] = 'Object (' . get_class($value) . ')'; - } - } else { - $data[$key] = $value; - } - } - $this->debugger->addMessage($data, 'debug'); - } else { - for ($i = 2; $i < $count; $i++) { - $var = func_get_arg($i); - $this->debugger->addMessage($var, 'debug'); - } - } - } - - /** - * Output a Gist - * - * @param string $id - * @param string|false $file - * @return string - */ - public function gistFunc($id, $file = false) - { - $url = 'https://gist.github.com/' . $id . '.js'; - if ($file) { - $url .= '?file=' . $file; - } - return ''; - } - - /** - * Generate a random string - * - * @param int $count - * @return string - */ - public function randomStringFunc($count = 5) - { - return Utils::generateRandomString($count); - } - - /** - * Pad a string to a certain length with another string - * - * @param string $input - * @param int $pad_length - * @param string $pad_string - * @param int $pad_type - * @return string - */ - public static function padFilter($input, $pad_length, $pad_string = ' ', $pad_type = STR_PAD_RIGHT) - { - return str_pad($input, (int)$pad_length, $pad_string, $pad_type); - } - - /** - * Workaround for twig associative array initialization - * Returns a key => val array - * - * @param string $key key of item - * @param string $val value of item - * @param array|null $current_array optional array to add to - * @return array - */ - public function arrayKeyValueFunc($key, $val, $current_array = null) - { - if (empty($current_array)) { - return array($key => $val); - } - - $current_array[$key] = $val; - - return $current_array; - } - - /** - * Wrapper for array_intersect() method - * - * @param array|Collection $array1 - * @param array|Collection $array2 - * @return array|Collection - */ - public function arrayIntersectFunc($array1, $array2) - { - if ($array1 instanceof Collection && $array2 instanceof Collection) { - return $array1->intersect($array2)->toArray(); - } - - return array_intersect($array1, $array2); - } - - /** - * Translate a string - * - * @return string - */ - public function translateFunc() - { - return $this->grav['language']->translate(func_get_args()); - } - - /** - * Authorize an action. Returns true if the user is logged in and - * has the right to execute $action. - * - * @param string|array $action An action or a list of actions. Each - * entry can be a string like 'group.action' - * or without dot notation an associative - * array. - * @return bool Returns TRUE if the user is authorized to - * perform the action, FALSE otherwise. - */ - public function authorize($action) - { - // Admin can use Flex users even if the site does not; make sure we use the right version of the user. - $admin = $this->grav['admin'] ?? null; - if ($admin) { - $user = $admin->user; - } else { - /** @var UserInterface|null $user */ - $user = $this->grav['user'] ?? null; - } - - if (!$user) { - return false; - } - - if (is_array($action)) { - if (Utils::isAssoc($action)) { - // Handle nested access structure. - $actions = Utils::arrayFlattenDotNotation($action); - } else { - // Handle simple access list. - $actions = array_combine($action, array_fill(0, count($action), true)); - } - } else { - // Handle single action. - $actions = [(string)$action => true]; - } - - $count = count($actions); - foreach ($actions as $act => $authenticated) { - // Ignore 'admin.super' if it's not the only value to be checked. - if ($act === 'admin.super' && $count > 1 && $user instanceof FlexObjectInterface) { - continue; - } - - $auth = $user->authorize($act) ?? false; - if (is_bool($auth) && $auth === Utils::isPositive($authenticated)) { - return true; - } - } - - return false; - } - - /** - * Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action. - * - * For maximum protection, ensure that the string representing the action is as specific as possible - * - * @param string $action the action - * @param string $nonceParamName a custom nonce param name - * @return string the nonce input field - */ - public function nonceFieldFunc($action, $nonceParamName = 'nonce') - { - $string = ''; - - return $string; - } - - /** - * Decodes string from JSON. - * - * @param string $str - * @param bool $assoc - * @param int $depth - * @param int $options - * @return array - */ - public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0) - { - return json_decode(html_entity_decode($str, ENT_COMPAT | ENT_HTML401, 'UTF-8'), $assoc, $depth, $options); - } - - /** - * Used to retrieve a cookie value - * - * @param string $key The cookie name to retrieve - * @return string - */ - public function getCookie($key) - { - return filter_input(INPUT_COOKIE, $key, FILTER_SANITIZE_STRING); - } - - /** - * Twig wrapper for PHP's preg_replace method - * - * @param string|string[] $subject the content to perform the replacement on - * @param string|string[] $pattern the regex pattern to use for matches - * @param string|string[] $replace the replacement value either as a string or an array of replacements - * @param int $limit the maximum possible replacements for each pattern in each subject - * @return string|string[]|null the resulting content - */ - public function regexReplace($subject, $pattern, $replace, $limit = -1) - { - return preg_replace($pattern, $replace, $subject, $limit); - } - - /** - * Twig wrapper for PHP's preg_grep method - * - * @param array $array - * @param string $regex - * @param int $flags - * @return array - */ - public function regexFilter($array, $regex, $flags = 0) - { - return preg_grep($regex, $array, $flags); - } - - /** - * Twig wrapper for PHP's preg_match method - * - * @param string $subject the content to perform the match on - * @param string $pattern the regex pattern to use for match - * @param int $flags - * @param int $offset - * @return array|false returns the matches if there is at least one match in the subject for a given pattern or null if not. - */ - public function regexMatch($subject, $pattern, $flags = 0, $offset = 0) - { - if (preg_match($pattern, $subject, $matches, $flags, $offset) === false) { - return false; - } - - return $matches; - } - - /** - * Twig wrapper for PHP's preg_split method - * - * @param string $subject the content to perform the split on - * @param string $pattern the regex pattern to use for split - * @param int $limit the maximum possible splits for the given pattern - * @param int $flags - * @return array|false the resulting array after performing the split operation - */ - public function regexSplit($subject, $pattern, $limit = -1, $flags = 0) - { - return preg_split($pattern, $subject, $limit, $flags); - } - - /** - * redirect browser from twig - * - * @param string $url the url to redirect to - * @param int $statusCode statusCode, default 303 - * @return void - */ - public function redirectFunc($url, $statusCode = 303) - { - $response = new Response($statusCode, ['location' => $url]); - - $this->grav->close($response); - } - - /** - * Generates an array containing a range of elements, optionally stepped - * - * @param int $start Minimum number, default 0 - * @param int $end Maximum number, default `getrandmax()` - * @param int $step Increment between elements in the sequence, default 1 - * @return array - */ - public function rangeFunc($start = 0, $end = 100, $step = 1) - { - return range($start, $end, $step); - } - - /** - * Check if HTTP_X_REQUESTED_WITH has been set to xmlhttprequest, - * in which case we may unsafely assume ajax. Non critical use only. - * - * @return bool True if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest - */ - public function isAjaxFunc() - { - return ( - !empty($_SERVER['HTTP_X_REQUESTED_WITH']) - && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); - } - - /** - * Get the Exif data for a file - * - * @param string $image - * @param bool $raw - * @return mixed - */ - public function exifFunc($image, $raw = false) - { - if (isset($this->grav['exif'])) { - /** @var UniformResourceLocator $locator */ - $locator = $this->grav['locator']; - - if ($locator->isStream($image)) { - $image = $locator->findResource($image); - } - - $exif_reader = $this->grav['exif']->getReader(); - - if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { - $exif_data = $exif_reader->read($image); - - if ($exif_data) { - if ($raw) { - return $exif_data->getRawData(); - } - - return $exif_data->getData(); - } - } - } - - return null; - } - - /** - * Simple function to read a file based on a filepath and output it - * - * @param string $filepath - * @return bool|string - */ - public function readFileFunc($filepath) - { - /** @var UniformResourceLocator $locator */ - $locator = $this->grav['locator']; - - if ($locator->isStream($filepath)) { - $filepath = $locator->findResource($filepath); - } - - if ($filepath && file_exists($filepath)) { - return file_get_contents($filepath); - } - - return false; - } - - /** - * Process a folder as Media and return a media object - * - * @param string $media_dir - * @return Media|null - */ - public function mediaDirFunc($media_dir) - { - /** @var UniformResourceLocator $locator */ - $locator = $this->grav['locator']; - - if ($locator->isStream($media_dir)) { - $media_dir = $locator->findResource($media_dir); - } - - if ($media_dir && file_exists($media_dir)) { - return new Media($media_dir); - } - - return null; - } - - /** - * Dump a variable to the browser - * - * @param mixed $var - * @return void - */ - public function vardumpFunc($var) - { - var_dump($var); - } - - /** - * Returns a nicer more readable filesize based on bytes - * - * @param int $bytes - * @return string - */ - public function niceFilesizeFunc($bytes) - { - return Utils::prettySize($bytes); - } - - /** - * Returns a nicer more readable number - * - * @param int|float|string $n - * @return string|bool - */ - public function niceNumberFunc($n) - { - if (!is_float($n) && !is_int($n)) { - if (!is_string($n) || $n === '') { - return false; - } - - // Strip any thousand formatting and find the first number. - $list = array_filter(preg_split("/\D+/", str_replace(',', '', $n))); - $n = reset($list); - - if (!is_numeric($n)) { - return false; - } - - $n = (float)$n; - } - - // now filter it; - if ($n > 1000000000000) { - return round($n/1000000000000, 2).' t'; - } - if ($n > 1000000000) { - return round($n/1000000000, 2).' b'; - } - if ($n > 1000000) { - return round($n/1000000, 2).' m'; - } - if ($n > 1000) { - return round($n/1000, 2).' k'; - } - - return number_format($n); - } - - /** - * Get a theme variable - * Will try to get the variable for the current page, if not found, it tries it's parent page on up to root. - * If still not found, will use the theme's configuration value, - * If still not found, will use the $default value passed in - * - * @param array $context Twig Context - * @param string $var variable to be found (using dot notation) - * @param null $default the default value to be used as last resort - * @param null $page an optional page to use for the current page - * @param bool $exists toggle to simply return the page where the variable is set, else null - * @return mixed - */ - public function themeVarFunc($context, $var, $default = null, $page = null, $exists = false) - { - $page = $page ?? $context['page'] ?? Grav::instance()['page'] ?? null; - - // Try to find var in the page headers - if ($page instanceof PageInterface && $page->exists()) { - // Loop over pages and look for header vars - while ($page && !$page->root()) { - $header = new Data((array)$page->header()); - $value = $header->get($var); - if (isset($value)) { - if ($exists) { - return $page; - } - - return $value; - } - $page = $page->parent(); - } - } - - if ($exists) { - return false; - } - - return Grav::instance()['config']->get('theme.' . $var, $default); - } - - /** - * Look for a page header variable in an array of pages working its way through until a value is found - * - * @param array $context - * @param string $var the variable to look for in the page header - * @param string|string[]|null $pages array of pages to check (current page upwards if not null) - * @return mixed - * @deprecated 1.7 Use themeVarFunc() instead - */ - public function pageHeaderVarFunc($context, $var, $pages = null) - { - if (is_array($pages)) { - $page = array_shift($pages); - } else { - $page = null; - } - return $this->themeVarFunc($context, $var, null, $page); - } - - /** - * takes an array of classes, and if they are not set on body_classes - * look to see if they are set in theme config - * - * @param array $context - * @param string|string[] $classes - * @return string - */ - public function bodyClassFunc($context, $classes) - { - - $header = $context['page']->header(); - $body_classes = $header->body_classes ?? ''; - - foreach ((array)$classes as $class) { - if (!empty($body_classes) && Utils::contains($body_classes, $class)) { - continue; - } - - $val = $this->config->get('theme.' . $class, false) ? $class : false; - $body_classes .= $val ? ' ' . $val : ''; - } - - return $body_classes; - } - - /** - * Returns the content of an SVG image and adds extra classes as needed - * - * @param string $path - * @param string|null $classes - * @return string|string[]|null - */ - public static function svgImageFunction($path, $classes = null, $strip_style = false) - { - $path = Utils::fullPath($path); - - $classes = $classes ?: ''; - - if (file_exists($path) && !is_dir($path)) { - $svg = file_get_contents($path); - $classes = " inline-block $classes"; - $matched = false; - - //Remove xml tag if it exists - $svg = preg_replace('/^<\?xml.*\?>/','', $svg); - - //Strip style if needed - if ($strip_style) { - $svg = preg_replace('//s', '', $svg); - } - - //Look for existing class - $svg = preg_replace_callback('/^]*(class=\")([^"]*)(\")[^>]*>/', function($matches) use ($classes, &$matched) { - if (isset($matches[2])) { - $new_classes = $matches[2] . $classes; - $matched = true; - return str_replace($matches[1], "class=\"$new_classes\"", $matches[0]); - } - return $matches[0]; - }, $svg - ); - - // no matches found just add the class - if (!$matched) { - $classes = trim($classes); - $svg = str_replace('jsonSerialize(); - } elseif (method_exists($data, 'toArray')) { - $data = $data->toArray(); - } else { - $data = json_decode(json_encode($data), true); - } - } - - return Yaml::dump($data, $inline); - } - - /** - * Decode/Parse data from YAML format - * - * @param string $data - * @return array - */ - public function yamlDecodeFilter($data) - { - return Yaml::parse($data); - } - - /** - * Function/Filter to return the type of variable - * - * @param mixed $var - * @return string - */ - public function getTypeFunc($var) - { - return gettype($var); - } - - /** - * Function/Filter to test type of variable - * - * @param mixed $var - * @param string|null $typeTest - * @param string|null $className - * @return bool - */ - public function ofTypeFunc($var, $typeTest = null, $className = null) - { - - switch ($typeTest) { - default: - return false; - - case 'array': - return is_array($var); - - case 'bool': - return is_bool($var); - - case 'class': - return is_object($var) === true && get_class($var) === $className; - - case 'float': - return is_float($var); - - case 'int': - return is_int($var); - - case 'numeric': - return is_numeric($var); - - case 'object': - return is_object($var); - - case 'scalar': - return is_scalar($var); - - case 'string': - return is_string($var); - } - } } diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index d0d6ad5e0..7bef09759 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -160,8 +160,8 @@ class Uri $language = $grav['language']; // add the port to the base for non-standard ports - if ($this->port !== null && $config->get('system.reverse_proxy_setup') === false) { - $this->base .= ':' . (string)$this->port; + if ($this->port && $config->get('system.reverse_proxy_setup') === false) { + $this->base .= ':' . $this->port; } // Handle custom base @@ -176,8 +176,8 @@ class Uri if (isset($custom_parts['scheme'])) { $this->base = $custom_parts['scheme'] . '://' . $custom_parts['host']; $this->port = $custom_parts['port'] ?? null; - if ($this->port !== null && $config->get('system.reverse_proxy_setup') === false) { - $this->base .= ':' . (string)$this->port; + if ($this->port && $config->get('system.reverse_proxy_setup') === false) { + $this->base .= ':' . $this->port; } $this->root = $custom_base; } else { @@ -462,8 +462,8 @@ class Uri public function port($raw = false) { $port = $this->port; - // If not in raw mode and port is not set, figure it out from scheme. - if (!$raw && $port === null) { + // If not in raw mode and port is not set or is 0, figure it out from scheme. + if (!$raw && !$port) { if ($this->scheme === 'http') { $this->port = 80; } elseif ($this->scheme === 'https') { @@ -471,7 +471,7 @@ class Uri } } - return $this->port; + return $this->port ?: null; } /** @@ -586,33 +586,38 @@ class Uri /** * Return relative path to the referrer defaulting to current or given page. * + * You should set the third parameter to `true` for redirects as long as you came from the same sub-site and language. + * * @param string|null $default * @param string|null $attributes + * @param bool $withoutBaseRoute * @return string */ - public function referrer($default = null, $attributes = null) + public function referrer($default = null, $attributes = null, bool $withoutBaseRoute = false) { $referrer = $_SERVER['HTTP_REFERER'] ?? null; // Check that referrer came from our site. - $root = $this->rootUrl(true); - if ($referrer) { - // Referrer should always have host set and it should come from the same base address. - if (stripos($referrer, $root) !== 0) { - $referrer = null; - } + if ($withoutBaseRoute) { + /** @var Pages $pages */ + $pages = Grav::instance()['pages']; + $base = $pages->baseUrl(null, true); + } else { + $base = $this->rootUrl(true); } - if (!$referrer) { + // Referrer should always have host set and it should come from the same base address. + if (!is_string($referrer) || !str_starts_with($referrer, $base)) { $referrer = $default ?: $this->route(true, true); } + // Relative path from grav root. + $referrer = substr($referrer, strlen($base)); if ($attributes) { $referrer .= $attributes; } - // Return relative path. - return substr($referrer, strlen($root)); + return $referrer; } /** @@ -648,7 +653,7 @@ class Uri return [ 'scheme' => $this->scheme, 'host' => $this->host, - 'port' => $this->port, + 'port' => $this->port ?: null, 'user' => $this->user, 'pass' => $this->password, 'path' => $path, @@ -665,7 +670,7 @@ class Uri */ public static function paramsRegex() { - return '/\/([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/'; + return '/\/{1,}([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/'; } /** @@ -675,10 +680,15 @@ class Uri */ public static function ip() { + $ip = 'UNKNOWN'; + if (getenv('HTTP_CLIENT_IP')) { $ip = getenv('HTTP_CLIENT_IP'); + } elseif (getenv('HTTP_CF_CONNECTING_IP')) { + $ip = getenv('HTTP_CF_CONNECTING_IP'); } elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) { - $ip = getenv('HTTP_X_FORWARDED_FOR'); + $ips = array_map('trim', explode(',', getenv('HTTP_X_FORWARDED_FOR'))); + $ip = array_shift($ips); } elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) { $ip = getenv('HTTP_X_FORWARDED'); } elseif (getenv('HTTP_FORWARDED_FOR')) { @@ -687,8 +697,6 @@ class Uri $ip = getenv('HTTP_FORWARDED'); } elseif (getenv('REMOTE_ADDR')) { $ip = getenv('REMOTE_ADDR'); - } else { - $ip = 'UNKNOWN'; } return $ip; @@ -1143,11 +1151,8 @@ class Uri public static function isValidUrl($url) { $regex = '/^(?:(https?|ftp|telnet):)?\/\/((?:[a-z0-9@:.-]|%[0-9A-F]{2}){3,})(?::(\d+))?((?:\/(?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\@]|%[0-9A-F]{2})*)*)(?:\?((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?/'; - if (preg_match($regex, $url)) { - return true; - } - return false; + return (bool)preg_match($regex, $url); } /** @@ -1258,7 +1263,7 @@ class Uri $this->port = null; } - if ($this->hasStandardPort()) { + if ($this->port === 0 || $this->hasStandardPort()) { $this->port = null; } @@ -1298,7 +1303,7 @@ class Uri */ protected function hasStandardPort() { - return ($this->port === 80 || $this->port === 443); + return (!$this->port || $this->port === 80 || $this->port === 443); } /** @@ -1311,11 +1316,13 @@ class Uri if ($parts === false) { throw new RuntimeException('Malformed URL: ' . $url); } + $port = (int)($parts['port'] ?? 0); + $this->scheme = $parts['scheme'] ?? null; $this->user = $parts['user'] ?? null; $this->password = $parts['pass'] ?? null; $this->host = $parts['host'] ?? null; - $this->port = isset($parts['port']) ? (int)$parts['port'] : null; + $this->port = $port ?: null; $this->path = $parts['path'] ?? ''; $this->query = $parts['query'] ?? ''; $this->fragment = $parts['fragment'] ?? null; @@ -1498,7 +1505,7 @@ class Uri * @param string $delimiter * @return string */ - private function processParams($uri, $delimiter = ':') + private function processParams(string $uri, string $delimiter = ':'): string { if (strpos($uri, $delimiter) !== false) { preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER); diff --git a/system/src/Grav/Common/User/DataUser/User.php b/system/src/Grav/Common/User/DataUser/User.php index 47353425d..8fcdbbbd6 100644 --- a/system/src/Grav/Common/User/DataUser/User.php +++ b/system/src/Grav/Common/User/DataUser/User.php @@ -57,6 +57,7 @@ class User extends Data implements UserInterface * @param string $offset * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { $value = parent::offsetExists($offset); @@ -73,6 +74,7 @@ class User extends Data implements UserInterface * @param string $offset * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { $value = parent::offsetGet($offset); diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 51eaf42ef..f843a0c66 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -44,7 +44,7 @@ use function strlen; */ abstract class Utils { - /** @var array */ + /** @var array */ protected static $nonces = []; protected const ROOTURL_REGEX = '{^((?:http[s]?:\/\/[^\/]+)|(?:\/\/[^\/]+))(.*)}'; @@ -166,9 +166,9 @@ abstract class Utils if ($locator->isStream($path)) { $path = $locator->findResource($path, true); - } elseif (!Utils::startsWith($path, GRAV_ROOT)) { + } elseif (!static::startsWith($path, GRAV_ROOT)) { $base_url = Grav::instance()['base_url']; - $path = GRAV_ROOT . '/' . ltrim(Utils::replaceFirstOccurrence($base_url, '', $path), '/'); + $path = GRAV_ROOT . '/' . ltrim(static::replaceFirstOccurrence($base_url, '', $path), '/'); } return $path; @@ -178,8 +178,8 @@ abstract class Utils /** * Check if the $haystack string starts with the substring $needle * - * @param string $haystack - * @param string|string[] $needle + * @param string $haystack + * @param string|string[] $needle * @param bool $case_sensitive * @return bool */ @@ -202,8 +202,8 @@ abstract class Utils /** * Check if the $haystack string ends with the substring $needle * - * @param string $haystack - * @param string|string[] $needle + * @param string $haystack + * @param string|string[] $needle * @param bool $case_sensitive * @return bool */ @@ -227,9 +227,9 @@ abstract class Utils /** * Check if the $haystack string contains the substring $needle * - * @param string $haystack - * @param string|string[] $needle - * @param bool $case_sensitive + * @param string $haystack + * @param string|string[] $needle + * @param bool $case_sensitive * @return bool */ public static function contains($haystack, $needle, $case_sensitive = true) @@ -266,19 +266,19 @@ abstract class Utils { $regex = str_replace( array("\*", "\?"), // wildcard chars - array('.*','.'), // regexp chars + array('.*', '.'), // regexp chars preg_quote($wildcard_pattern, '/') ); - return preg_match('/^'.$regex.'$/is', $haystack); + return preg_match('/^' . $regex . '$/is', $haystack); } /** * Render simple template filling up the variables in it. If value is not defined, leave it as it was. * - * @param string $template Template string - * @param array $variables Variables with values - * @param array $brackets Optional array of opening and closing brackets or symbols + * @param string $template Template string + * @param array $variables Variables with values + * @param array $brackets Optional array of opening and closing brackets or symbols * @return string Final string filled with values */ public static function simpleTemplate(string $template, array $variables, array $brackets = ['{', '}']): string @@ -376,8 +376,8 @@ abstract class Utils /** * Merge two objects into one. * - * @param object $obj1 - * @param object $obj2 + * @param object $obj1 + * @param object $obj2 * * @return object */ @@ -415,7 +415,7 @@ abstract class Utils */ public static function arrayRemoveValue(array $search, $value) { - foreach ((array) $value as $val) { + foreach ((array)$value as $val) { $key = array_search($val, $search); if ($key !== false) { unset($search[$key]); @@ -481,8 +481,8 @@ abstract class Utils /** * Array combine but supports different array lengths * - * @param array $arr1 - * @param array $arr2 + * @param array $arr1 + * @param array $arr2 * @return array|false */ public static function arrayCombine($arr1, $arr2) @@ -495,7 +495,7 @@ abstract class Utils /** * Array is associative or not * - * @param array $arr + * @param array $arr * @return bool */ public static function arrayIsAssociative($arr) @@ -517,15 +517,15 @@ abstract class Utils $now = new DateTime(); $date_formats = [ - 'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')', - 'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')', - 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. '.$now->format('m/d/Y h:i a').')', - 'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')', - 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')', - ]; + 'd-m-Y H:i' => 'd-m-Y H:i (e.g. ' . $now->format('d-m-Y H:i') . ')', + 'Y-m-d H:i' => 'Y-m-d H:i (e.g. ' . $now->format('Y-m-d H:i') . ')', + 'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. ' . $now->format('m/d/Y h:i a') . ')', + 'H:i d-m-Y' => 'H:i d-m-Y (e.g. ' . $now->format('H:i d-m-Y') . ')', + 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. ' . $now->format('h:i a m/d/Y') . ')', + ]; $default_format = Grav::instance()['config']->get('system.pages.dateformat.default'); if ($default_format) { - $date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats); + $date_formats = array_merge([$default_format => $default_format . ' (e.g. ' . $now->format($default_format) . ')'], $date_formats); } return $date_formats; @@ -552,11 +552,11 @@ abstract class Utils /** * Truncate text by number of characters but can cut off words. * - * @param string $string - * @param int $limit Max number of characters. - * @param bool $up_to_break truncate up to breakpoint after char count - * @param string $break Break point. - * @param string $pad Appended padding to the end of the string. + * @param string $string + * @param int $limit Max number of characters. + * @param bool $up_to_break truncate up to breakpoint after char count + * @param string $break Break point. + * @param string $pad Appended padding to the end of the string. * @return string */ public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '…') @@ -582,7 +582,7 @@ abstract class Utils * Truncate text by number of characters in a "word-safe" manor. * * @param string $string - * @param int $limit + * @param int $limit * @return string */ public static function safeTruncate($string, $limit = 150) @@ -594,9 +594,9 @@ abstract class Utils /** * Truncate HTML by number of characters. not "word-safe"! * - * @param string $text - * @param int $length in characters - * @param string $ellipsis + * @param string $text + * @param int $length in characters + * @param string $ellipsis * @return string */ public static function truncateHtml($text, $length = 100, $ellipsis = '...') @@ -607,9 +607,9 @@ abstract class Utils /** * Truncate HTML by number of characters in a "word-safe" manor. * - * @param string $text - * @param int $length in words - * @param string $ellipsis + * @param string $text + * @param int $length in words + * @param string $ellipsis * @return string */ public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...') @@ -628,13 +628,30 @@ abstract class Utils return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length); } + /** + * Generates a random string with configurable length, prefix and suffix. + * Unlike the built-in `uniqid()`, this string is non-conflicting and safe + * + * @param int $length + * @param array $options + * @return string + * @throws Exception + */ + public static function uniqueId(int $length = 13, array $options = []): string + { + $options = array_merge(['prefix' => '', 'suffix' => ''], $options); + $bytes = random_bytes(ceil($length / 2)); + + return $options['prefix'] . substr(bin2hex($bytes), 0, $length) . $options['suffix']; + } + /** * Provides the ability to download a file to the browser * * @param string $file the full path to the file to be downloaded * @param bool $force_download as opposed to letting browser choose if to download or render - * @param int $sec Throttling, try 0.1 for some speed throttling of downloads - * @param int $bytes Size of chunks to send in bytes. Default is 1024 + * @param int $sec Throttling, try 0.1 for some speed throttling of downloads + * @param int $bytes Size of chunks to send in bytes. Default is 1024 * @throws Exception */ public static function download($file, $force_download = true, $sec = 0, $bytes = 1024) @@ -645,7 +662,7 @@ abstract class Utils $file_parts = pathinfo($file); $mimetype = static::getMimeByExtension($file_parts['extension']); - $size = filesize($file); // File size + $size = filesize($file); // File size // clean all buffers while (ob_get_level()) { @@ -742,7 +759,7 @@ abstract class Utils // Set from uri extension $uri_extension = $uri->extension(); if (is_string($uri_extension) && $uri->isValidExtension($uri_extension)) { - return($uri_extension); + return ($uri_extension); } // Use content negotiation via the `accept:` header @@ -750,13 +767,13 @@ abstract class Utils if (is_string($http_accept)) { $negotiator = new Negotiator(); - $supported_types = Utils::getSupportPageTypes(['html', 'json']); - $priorities = Utils::getMimeTypes($supported_types); + $supported_types = static::getSupportPageTypes(['html', 'json']); + $priorities = static::getMimeTypes($supported_types); $media_type = $negotiator->getBest($http_accept, $priorities); $mimetype = $media_type instanceof Accept ? $media_type->getValue() : ''; - return Utils::getExtensionByMime($mimetype); + return static::getExtensionByMime($mimetype); } return 'html'; @@ -791,13 +808,7 @@ abstract class Utils $media_types = Grav::instance()['config']->get('media.types'); - if (isset($media_types[$extension])) { - if (isset($media_types[$extension]['mime'])) { - return $media_types[$extension]['mime']; - } - } - - return $default; + return $media_types[$extension]['mime'] ?? $default; } /** @@ -1060,7 +1071,7 @@ abstract class Utils $pretty_offset = "UTC${offset_prefix}${offset_formatted}"; - $timezone_list[$timezone] = "(${pretty_offset}) ".str_replace('_', ' ', $timezone); + $timezone_list[$timezone] = "(${pretty_offset}) " . str_replace('_', ' ', $timezone); } return $timezone_list; @@ -1069,11 +1080,11 @@ abstract class Utils /** * Recursively filter an array, filtering values by processing them through the $fn function argument * - * @param array $source the Array to filter - * @param callable $fn the function to pass through each array item + * @param array $source the Array to filter + * @param callable $fn the function to pass through each array item * @return array */ - public static function arrayFilterRecursive(Array $source, $fn) + public static function arrayFilterRecursive(array $source, $fn) { $result = []; foreach ($source as $key => $value) { @@ -1093,15 +1104,15 @@ abstract class Utils /** * Flatten a multi-dimensional associative array into query params. * - * @param array $array - * @param string $prepend + * @param array $array + * @param string $prepend * @return array */ public static function arrayToQueryParams($array, $prepend = '') { $results = []; foreach ($array as $key => $value) { - $name = $prepend ? $prepend . '[' . $key . ']' : $key; + $name = $prepend ? $prepend . '[' . $key . ']' : $key; if (is_array($value)) { $results = array_merge($results, static::arrayToQueryParams($value, $name)); @@ -1138,8 +1149,8 @@ abstract class Utils /** * Flatten a multi-dimensional associative array into dot notation * - * @param array $array - * @param string $prepend + * @param array $array + * @param string $prepend * @return array */ public static function arrayFlattenDotNotation($array, $prepend = '') @@ -1147,9 +1158,9 @@ abstract class Utils $results = array(); foreach ($array as $key => $value) { if (is_array($value)) { - $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend.$key.'.')); + $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend . $key . '.')); } else { - $results[$prepend.$key] = $value; + $results[$prepend . $key] = $value; } } @@ -1297,7 +1308,7 @@ abstract class Utils * with reverse proxy setups. * * @param string $action - * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) * @return string the nonce string */ private static function generateNonceString($action, $previousTick = false) @@ -1334,8 +1345,8 @@ abstract class Utils * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given * action is the same for 12 hours. * - * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage) - * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) + * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage) + * @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours) * @return string the nonce */ public static function getNonce($action, $previousTick = false) @@ -1353,7 +1364,7 @@ abstract class Utils /** * Verify the passed nonce for the give action * - * @param string|string[] $nonce the nonce to verify + * @param string|string[] $nonce the nonce to verify * @param string $action the action to verify the nonce to * @return boolean verified or not */ @@ -1434,7 +1445,7 @@ abstract class Utils while (count($keys) > 1) { $key = array_shift($keys); - if (! isset($array[$key]) || ! is_array($array[$key])) { + if (!isset($array[$key]) || !is_array($array[$key])) { $array[$key] = array(); } @@ -1725,7 +1736,7 @@ abstract class Utils $size *= 1024 ** stripos('bkmgtpezy', $unit[0]); } - return (int) abs(round($size)); + return (int)abs(round($size)); } /** @@ -1739,7 +1750,7 @@ abstract class Utils { $enc_url = preg_replace_callback( '%[^:/@?&=#]+%usD', - function ($matches) { + static function ($matches) { return urlencode($matches[0]); }, $url @@ -1770,7 +1781,7 @@ abstract class Utils public static function processMarkdown($string, $block = true, $page = null) { $grav = Grav::instance(); - $page = $page ?? $grav['page'] ?? null; + $page = $page ?? $grav['page'] ?? null; $defaults = [ 'markdown' => $grav['config']->get('system.pages.markdown', []), 'images' => $grav['config']->get('system.images', []) @@ -1812,12 +1823,12 @@ abstract class Utils $ip = (string)inet_pton($ip); // Maximum netmask length = same as packed address - $len = 8*strlen($ip); + $len = 8 * strlen($ip); if ($prefix > $len) { $prefix = $len; } - $mask = str_repeat('f', $prefix>>2); + $mask = str_repeat('f', $prefix >> 2); switch ($prefix & 3) { case 3: @@ -1830,7 +1841,7 @@ abstract class Utils $mask .= '8'; break; } - $mask = str_pad($mask, $len>>2, '0'); + $mask = str_pad($mask, $len >> 2, '0'); // Packed representation of netmask $mask = pack('H*', $mask); @@ -1861,4 +1872,244 @@ abstract class Utils return $types; } + + /** + * @param string $name + * @return bool + */ + public static function isDangerousFunction(string $name): bool + { + static $commandExecutionFunctions = [ + 'exec', + 'passthru', + 'system', + 'shell_exec', + 'popen', + 'proc_open', + 'pcntl_exec', + ]; + + static $codeExecutionFunctions = [ + 'assert', + 'preg_replace', + 'create_function', + 'include', + 'include_once', + 'require', + 'require_once' + ]; + + static $callbackFunctions = [ + 'ob_start' => 0, + 'array_diff_uassoc' => -1, + 'array_diff_ukey' => -1, + 'array_filter' => 1, + 'array_intersect_uassoc' => -1, + 'array_intersect_ukey' => -1, + 'array_map' => 0, + 'array_reduce' => 1, + 'array_udiff_assoc' => -1, + 'array_udiff_uassoc' => [-1, -2], + 'array_udiff' => -1, + 'array_uintersect_assoc' => -1, + 'array_uintersect_uassoc' => [-1, -2], + 'array_uintersect' => -1, + 'array_walk_recursive' => 1, + 'array_walk' => 1, + 'assert_options' => 1, + 'uasort' => 1, + 'uksort' => 1, + 'usort' => 1, + 'preg_replace_callback' => 1, + 'spl_autoload_register' => 0, + 'iterator_apply' => 1, + 'call_user_func' => 0, + 'call_user_func_array' => 0, + 'register_shutdown_function' => 0, + 'register_tick_function' => 0, + 'set_error_handler' => 0, + 'set_exception_handler' => 0, + 'session_set_save_handler' => [0, 1, 2, 3, 4, 5], + 'sqlite_create_aggregate' => [2, 3], + 'sqlite_create_function' => 2, + ]; + + static $informationDiscosureFunctions = [ + 'phpinfo', + 'posix_mkfifo', + 'posix_getlogin', + 'posix_ttyname', + 'getenv', + 'get_current_user', + 'proc_get_status', + 'get_cfg_var', + 'disk_free_space', + 'disk_total_space', + 'diskfreespace', + 'getcwd', + 'getlastmo', + 'getmygid', + 'getmyinode', + 'getmypid', + 'getmyuid' + ]; + + static $otherFunctions = [ + 'extract', + 'parse_str', + 'putenv', + 'ini_set', + 'mail', + 'header', + 'proc_nice', + 'proc_terminate', + 'proc_close', + 'pfsockopen', + 'fsockopen', + 'apache_child_terminate', + 'posix_kill', + 'posix_mkfifo', + 'posix_setpgid', + 'posix_setsid', + 'posix_setuid', + ]; + + if (in_array($name, $commandExecutionFunctions)) { + return true; + } + + if (in_array($name, $codeExecutionFunctions)) { + return true; + } + + if (isset($callbackFunctions[$name])) { + return true; + } + + if (in_array($name, $informationDiscosureFunctions)) { + return true; + } + + if (in_array($name, $otherFunctions)) { + return true; + } + + return static::isFilesystemFunction($name); + } + + /** + * @param string $name + * @return bool + */ + public static function isFilesystemFunction(string $name): bool + { + static $fileWriteFunctions = [ + 'fopen', + 'tmpfile', + 'bzopen', + 'gzopen', + // write to filesystem (partially in combination with reading) + 'chgrp', + 'chmod', + 'chown', + 'copy', + 'file_put_contents', + 'lchgrp', + 'lchown', + 'link', + 'mkdir', + 'move_uploaded_file', + 'rename', + 'rmdir', + 'symlink', + 'tempnam', + 'touch', + 'unlink', + 'imagepng', + 'imagewbmp', + 'image2wbmp', + 'imagejpeg', + 'imagexbm', + 'imagegif', + 'imagegd', + 'imagegd2', + 'iptcembed', + 'ftp_get', + 'ftp_nb_get', + ]; + + static $fileContentFunctions = [ + 'file_get_contents', + 'file', + 'filegroup', + 'fileinode', + 'fileowner', + 'fileperms', + 'glob', + 'is_executable', + 'is_uploaded_file', + 'parse_ini_file', + 'readfile', + 'readlink', + 'realpath', + 'gzfile', + 'readgzfile', + 'stat', + 'imagecreatefromgif', + 'imagecreatefromjpeg', + 'imagecreatefrompng', + 'imagecreatefromwbmp', + 'imagecreatefromxbm', + 'imagecreatefromxpm', + 'ftp_put', + 'ftp_nb_put', + 'hash_update_file', + 'highlight_file', + 'show_source', + 'php_strip_whitespace', + ]; + + static $filesystemFunctions = [ + // read from filesystem + 'file_exists', + 'fileatime', + 'filectime', + 'filemtime', + 'filesize', + 'filetype', + 'is_dir', + 'is_file', + 'is_link', + 'is_readable', + 'is_writable', + 'is_writeable', + 'linkinfo', + 'lstat', + //'pathinfo', + 'getimagesize', + 'exif_read_data', + 'read_exif_data', + 'exif_thumbnail', + 'exif_imagetype', + 'hash_file', + 'hash_hmac_file', + 'md5_file', + 'sha1_file', + 'get_meta_tags', + ]; + + if (in_array($name, $fileWriteFunctions)) { + return true; + } + + if (in_array($name, $fileContentFunctions)) { + return true; + } + + if (in_array($name, $filesystemFunctions)) { + return true; + } + + return false; + } } diff --git a/system/src/Grav/Console/Cli/CleanCommand.php b/system/src/Grav/Console/Cli/CleanCommand.php index b0c949900..84b46d6cc 100644 --- a/system/src/Grav/Console/Cli/CleanCommand.php +++ b/system/src/Grav/Console/Cli/CleanCommand.php @@ -102,11 +102,10 @@ class CleanCommand extends Command 'vendor/dragonmantank/cron-expression/composer.json', 'vendor/dragonmantank/cron-expression/tests', 'vendor/dragonmantank/cron-expression/CHANGELOG.md', - 'vendor/enshrined/svg-sanitize/tests', - 'vendor/enshrined/svg-sanitize/.gitignore', - 'vendor/enshrined/svg-sanitize/.travis.yml', - 'vendor/enshrined/svg-sanitize/composer.json', - 'vendor/enshrined/svg-sanitize/phpunit.xml', + 'vendor/rhukster/dom-sanitizer/tests', + 'vendor/rhukster/dom-sanitizer/.gitignore', + 'vendor/rhukster/dom-sanitizer/composer.json', + 'vendor/rhukster/dom-sanitizer/composer.lock', 'vendor/erusev/parsedown/composer.json', 'vendor/erusev/parsedown/phpunit.xml.dist', 'vendor/erusev/parsedown/.travis.yml', diff --git a/system/src/Grav/Console/Cli/InstallCommand.php b/system/src/Grav/Console/Cli/InstallCommand.php index 08bc7144e..22258beb3 100644 --- a/system/src/Grav/Console/Cli/InstallCommand.php +++ b/system/src/Grav/Console/Cli/InstallCommand.php @@ -73,11 +73,11 @@ class InstallCommand extends GravCommand $io = $this->getIO(); $dependencies_file = '.dependencies'; - $this->destination = $input->getArgument('destination') ?: GRAV_ROOT; + $this->destination = $input->getArgument('destination') ?: GRAV_WEBROOT; // fix trailing slash $this->destination = rtrim($this->destination, DS) . DS; - $this->user_path = $this->destination . USER_PATH; + $this->user_path = $this->destination . GRAV_USER_PATH . DS; if ($local_config_file = $this->loadLocalConfig()) { $io->writeln('Read local config from ' . $local_config_file . ''); } diff --git a/system/src/Grav/Console/Gpm/UninstallCommand.php b/system/src/Grav/Console/Gpm/UninstallCommand.php index ff917191e..cb14c6552 100644 --- a/system/src/Grav/Console/Gpm/UninstallCommand.php +++ b/system/src/Grav/Console/Gpm/UninstallCommand.php @@ -112,7 +112,7 @@ class UninstallCommand extends GpmCommand unset($this->data['not_found'], $this->data['total']); - // Plugins need to be initialized in order to make clear-cache to work. + // Plugins need to be initialized in order to make clearcache to work. try { $this->initializePlugins(); } catch (Throwable $e) { diff --git a/system/src/Grav/Framework/Acl/PermissionsReader.php b/system/src/Grav/Framework/Acl/PermissionsReader.php index 2c38afbea..4f4a6a317 100644 --- a/system/src/Grav/Framework/Acl/PermissionsReader.php +++ b/system/src/Grav/Framework/Acl/PermissionsReader.php @@ -131,7 +131,7 @@ class PermissionsReader */ protected static function getDependencies(array $dependencies): array { - $list = []; + $list = [[]]; foreach ($dependencies as $name => $deps) { $current = $deps ? static::getDependencies($deps) : []; $current[] = $name; diff --git a/system/src/Grav/Framework/Collection/AbstractFileCollection.php b/system/src/Grav/Framework/Collection/AbstractFileCollection.php index 50e90515d..f89226747 100644 --- a/system/src/Grav/Framework/Collection/AbstractFileCollection.php +++ b/system/src/Grav/Framework/Collection/AbstractFileCollection.php @@ -24,7 +24,7 @@ use function array_slice; * Collection of objects stored into a filesystem. * * @package Grav\Framework\Collection - * @template TKey + * @template TKey of array-key * @template T * @extends AbstractLazyCollection * @mplements FileCollectionInterface diff --git a/system/src/Grav/Framework/Collection/AbstractIndexCollection.php b/system/src/Grav/Framework/Collection/AbstractIndexCollection.php index 190f42279..38d3ab51e 100644 --- a/system/src/Grav/Framework/Collection/AbstractIndexCollection.php +++ b/system/src/Grav/Framework/Collection/AbstractIndexCollection.php @@ -20,7 +20,7 @@ use function count; /** * Abstract Index Collection. - * @template TKey + * @template TKey of array-key * @template T * @implements CollectionInterface */ @@ -144,6 +144,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * * {@inheritDoc} */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { return $this->containsKey($offset); @@ -154,6 +155,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * * {@inheritDoc} */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); @@ -164,6 +166,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * * {@inheritDoc} */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { if (null === $offset) { @@ -178,6 +181,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * * {@inheritDoc} */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { return $this->remove($offset); @@ -361,6 +365,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * @param int $start * @param int|null $limit * @return static + * @phpstan-return static */ public function limit($start, $limit = null) { @@ -371,6 +376,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * Reverse the order of the items. * * @return static + * @phpstan-return static */ public function reverse() { @@ -381,6 +387,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * Shuffle items. * * @return static + * @phpstan-return static */ public function shuffle() { @@ -397,6 +404,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * * @param array $keys * @return static + * @phpstan-return static */ public function select(array $keys) { @@ -415,6 +423,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * * @param array $keys * @return static + * @phpstan-return static */ public function unselect(array $keys) { @@ -469,6 +478,7 @@ abstract class AbstractIndexCollection implements CollectionInterface * * @param array $entries Elements. * @return static + * @phpstan-return static */ protected function createFrom(array $entries) { @@ -521,6 +531,7 @@ abstract class AbstractIndexCollection implements CollectionInterface /** * @param array|null $entries * @return CollectionInterface + * @phpstan-return T */ abstract protected function loadCollection(array $entries = null): CollectionInterface; diff --git a/system/src/Grav/Framework/Collection/AbstractLazyCollection.php b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php index af7ffe182..9afaab131 100644 --- a/system/src/Grav/Framework/Collection/AbstractLazyCollection.php +++ b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php @@ -15,7 +15,7 @@ use Doctrine\Common\Collections\AbstractLazyCollection as BaseAbstractLazyCollec * General JSON serializable collection. * * @package Grav\Framework\Collection - * @template TKey + * @template TKey of array-key * @template T * @extends BaseAbstractLazyCollection * @implements CollectionInterface diff --git a/system/src/Grav/Framework/Collection/ArrayCollection.php b/system/src/Grav/Framework/Collection/ArrayCollection.php index 474a3fba5..d76aa05dc 100644 --- a/system/src/Grav/Framework/Collection/ArrayCollection.php +++ b/system/src/Grav/Framework/Collection/ArrayCollection.php @@ -15,7 +15,7 @@ use Doctrine\Common\Collections\ArrayCollection as BaseArrayCollection; * General JSON serializable collection. * * @package Grav\Framework\Collection - * @template TKey + * @template TKey of array-key * @template T * @extends BaseArrayCollection * @implements CollectionInterface diff --git a/system/src/Grav/Framework/Collection/CollectionInterface.php b/system/src/Grav/Framework/Collection/CollectionInterface.php index e024366a4..42414cecd 100644 --- a/system/src/Grav/Framework/Collection/CollectionInterface.php +++ b/system/src/Grav/Framework/Collection/CollectionInterface.php @@ -16,7 +16,7 @@ use JsonSerializable; * Collection Interface. * * @package Grav\Framework\Collection - * @template TKey + * @template TKey of array-key * @template T * @extends Collection */ diff --git a/system/src/Grav/Framework/Collection/FileCollection.php b/system/src/Grav/Framework/Collection/FileCollection.php index 5dd8d55b0..59df2210e 100644 --- a/system/src/Grav/Framework/Collection/FileCollection.php +++ b/system/src/Grav/Framework/Collection/FileCollection.php @@ -13,7 +13,7 @@ namespace Grav\Framework\Collection; * Collection of objects stored into a filesystem. * * @package Grav\Framework\Collection - * @template TKey + * @template TKey of array-key * @template T * @extends AbstractFileCollection */ diff --git a/system/src/Grav/Framework/Collection/FileCollectionInterface.php b/system/src/Grav/Framework/Collection/FileCollectionInterface.php index ce6e18f25..45c446c63 100644 --- a/system/src/Grav/Framework/Collection/FileCollectionInterface.php +++ b/system/src/Grav/Framework/Collection/FileCollectionInterface.php @@ -15,7 +15,7 @@ use Doctrine\Common\Collections\Selectable; * Collection of objects stored into a filesystem. * * @package Grav\Framework\Collection - * @template TKey + * @template TKey of array-key * @template T * @extends CollectionInterface * @extends Selectable diff --git a/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php b/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php index bbc8f1a4f..62ed3e103 100644 --- a/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php +++ b/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php @@ -173,6 +173,7 @@ trait ControllerResponseTrait if ($method !== 'GET' && $method !== 'HEAD') { $this->setMessage($message, 'error'); $referer = $request->getHeaderLine('Referer'); + return $this->createRedirectResponse($referer, 303); } diff --git a/system/src/Grav/Framework/Flex/FlexDirectory.php b/system/src/Grav/Framework/Flex/FlexDirectory.php index b9c9d543b..29f1490f8 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectory.php +++ b/system/src/Grav/Framework/Flex/FlexDirectory.php @@ -47,7 +47,7 @@ use function is_callable; * @package Grav\Framework\Flex * @template T */ -class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface +class FlexDirectory implements FlexDirectoryInterface { use FlexAuthorizeTrait; @@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface /** @var UniformResourceLocator $locator */ $locator = $grav['locator']; - $filename = $locator->findResource($this->getDirectoryConfigUri($name), true); + $uri = $this->getDirectoryConfigUri($name); + + // If configuration is found in main configuration, use it. + if (str_starts_with($uri, 'config://')) { + $path = str_replace('/', '.', substr($uri, 9, -5)); + + return (array)$grav['config']->get($path); + } + + // Load the configuration file. + $filename = $locator->findResource($uri, true); if ($filename === false) { return []; } @@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface * @param array $call * @return void */ - protected function dynamicFlexField(array &$field, $property, array $call) + protected function dynamicFlexField(array &$field, $property, array $call): void { $params = (array)$call['params']; $object = $call['object'] ?? null; $method = array_shift($params); + $not = false; + if (str_starts_with($method, '!')) { + $method = substr($method, 1); + $not = true; + } elseif (str_starts_with($method, 'not ')) { + $method = substr($method, 4); + $not = true; + } + $method = trim($method); if ($object && method_exists($object, $method)) { $value = $object->{$method}(...$params); if (is_array($value) && isset($field[$property]) && is_array($field[$property])) { - $field[$property] = array_merge_recursive($field[$property], $value); + $value = $this->mergeArrays($field[$property], $value); + } + $field[$property] = $not ? !$value : $value; + } + } + + /** + * @param array $array1 + * @param array $array2 + * @return array + */ + protected function mergeArrays(array $array1, array $array2): array + { + foreach ($array2 as $key => $value) { + if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) { + $array1[$key] = $this->mergeArrays($array1[$key], $value); } else { - $field[$property] = $value; + $array1[$key] = $value; } } + + return $array1; } /** diff --git a/system/src/Grav/Framework/Flex/FlexDirectoryForm.php b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php index ba26b6319..a2259678b 100644 --- a/system/src/Grav/Framework/Flex/FlexDirectoryForm.php +++ b/system/src/Grav/Framework/Flex/FlexDirectoryForm.php @@ -129,6 +129,17 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable return $this; } + /** + * @param string $uniqueId + * @return void + */ + public function setUniqueId(string $uniqueId): void + { + if ($uniqueId !== '') { + $this->uniqueid = $uniqueId; + } + } + /** * @param string $name * @param mixed $default @@ -318,11 +329,11 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable } /** - * @param string $field - * @param string $filename + * @param string|null $field + * @param string|null $filename * @return Route|null */ - public function getFileDeleteAjaxRoute($field, $filename): ?Route + public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route { return null; } @@ -453,7 +464,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable protected function doSerialize(): array { return $this->doTraitSerialize() + [ + 'form' => $this->form, 'directory' => $this->directory, + 'flexName' => $this->flexName ]; } @@ -465,7 +478,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable { $this->doTraitUnserialize($data); + $this->form = $data['form']; $this->directory = $data['directory']; + $this->flexName = $data['flexName']; } /** diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php index 83c98d6bc..4fb8daf27 100644 --- a/system/src/Grav/Framework/Flex/FlexForm.php +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -103,7 +103,14 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable { $this->name = $name; $this->setObject($object); - $this->setName($object->getFlexType(), $name); + + if (isset($options['form']['name'])) { + // Use custom form name. + $this->flexName = $options['form']['name']; + } else { + // Use standard form name. + $this->setName($object->getFlexType(), $name); + } $this->setId($this->getName()); $uniqueId = $options['unique_id'] ?? null; @@ -119,7 +126,7 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable } $this->setUniqueId($uniqueId); $directory = $object->getFlexDirectory(); - $this->setFlashLookupFolder($directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]'); + $this->setFlashLookupFolder($options['flash_folder'] ?? $directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]'); $this->form = $options['form'] ?? null; if (!empty($options['reset'])) { @@ -165,6 +172,17 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable return $this; } + /** + * @param string $uniqueId + * @return void + */ + public function setUniqueId(string $uniqueId): void + { + if ($uniqueId !== '') { + $this->uniqueid = $uniqueId; + } + } + /** * @param string $name * @param mixed $default @@ -371,22 +389,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable { $object = $this->getObject(); if (!method_exists($object, 'route')) { - return null; + /** @var Route $route */ + $route = Grav::instance()['route']; + + return $route->withExtension('json')->withGravParam('task', 'media.upload'); } return $object->route('/edit.json/task:media.upload'); } /** - * @param string $field - * @param string $filename + * @param string|null $field + * @param string|null $filename * @return Route|null */ - public function getFileDeleteAjaxRoute($field, $filename): ?Route + public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route { $object = $this->getObject(); if (!method_exists($object, 'route')) { - return null; + /** @var Route $route */ + $route = Grav::instance()['route']; + + return $route->withExtension('json')->withGravParam('task', 'media.delete'); } return $object->route('/edit.json/task:media.delete'); @@ -536,7 +560,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable protected function doSerialize(): array { return $this->doTraitSerialize() + [ + 'items' => $this->items, + 'form' => $this->form, 'object' => $this->object, + 'flexName' => $this->flexName, + 'submitMethod' => $this->submitMethod, ]; } @@ -548,7 +576,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable { $this->doTraitUnserialize($data); - $this->object = $data['object']; + $this->items = $data['items'] ?? null; + $this->form = $data['form'] ?? null; + $this->object = $data['object'] ?? null; + $this->flexName = $data['flexName'] ?? null; + $this->submitMethod = $data['submitMethod'] ?? null; } /** diff --git a/system/src/Grav/Framework/Flex/FlexIndex.php b/system/src/Grav/Framework/Flex/FlexIndex.php index 08785c28e..1df5c4d0d 100644 --- a/system/src/Grav/Framework/Flex/FlexIndex.php +++ b/system/src/Grav/Framework/Flex/FlexIndex.php @@ -540,6 +540,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde */ protected function createFrom(array $entries, string $keyField = null) { + /** @phpstan-var static $index */ $index = new static($entries, $this->getFlexDirectory()); $index->setKeyField($keyField ?? $this->_keyField); @@ -630,7 +631,10 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde */ protected function loadCollection(array $entries = null): CollectionInterface { - return $this->getFlexDirectory()->loadCollection($entries ?? $this->getEntries(), $this->_keyField); + /** @var C $collection */ + $collection = $this->getFlexDirectory()->loadCollection($entries ?? $this->getEntries(), $this->_keyField); + + return $collection; } /** diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php index 09c563825..945f5adfe 100644 --- a/system/src/Grav/Framework/Flex/FlexObject.php +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -44,6 +44,7 @@ use function is_array; use function is_object; use function is_scalar; use function is_string; +use function json_encode; /** * Class FlexObject @@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface /** @var array */ private $_meta; /** @var array */ + protected $_original; + /** @var array */ protected $_changes; /** @var string */ protected $storage_key; @@ -196,9 +199,10 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface /** * Refresh object from the storage. * + * @param bool $keepMissing * @return bool True if the object was refreshed */ - public function refresh(): bool + public function refresh(bool $keepMissing = false): bool { $key = $this->getStorageKey(); if ('' === $key) { @@ -216,20 +220,36 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface return false; } + // Get current elements (if requested). + $current = $keepMissing ? $this->getElements() : []; + // Get elements from the filesystem. $elements = $storage->readRows([$key => null])[$key] ?? null; - if (null !== $elements || isset($elements['__ERROR'])) { - $meta = $elements['_META'] ?? $meta; + if (null !== $elements) { + $meta = $elements['__META'] ?? $meta; + unset($elements['__META']); $this->filterElements($elements); $newKey = $meta['key'] ?? $this->getKey(); if ($meta) { $this->setMetaData($meta); } $this->objectConstruct($elements, $newKey); - } - /** @var Debugger $debugger */ - $debugger = Grav::instance()['debugger']; - $debugger->addMessage("Refreshed {$this->getFlexType()} object {$this->getKey()}", 'debug'); + if ($current) { + // Inject back elements which are missing in the filesystem. + $data = $this->getBlueprint()->flattenData($current); + foreach ($data as $property => $value) { + if (strpos($property, '.') === false) { + $this->defProperty($property, $value); + } else { + $this->defNestedProperty($property, $value); + } + } + } + + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addMessage("Refreshed {$this->getFlexType()} object {$this->getKey()}", 'debug'); + } return true; } @@ -281,7 +301,11 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface $weight = 0; foreach ($properties as $property) { - $weight += $this->searchNestedProperty($property, $search, $options); + if (strpos($property, '.')) { + $weight += $this->searchNestedProperty($property, $search, $options); + } else { + $weight += $this->searchProperty($property, $search, $options); + } } return $weight > 0 ? min($weight, 1) : 0; @@ -348,7 +372,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function searchProperty(string $property, string $search, array $options = null): float { - $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); + $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); $value = $this->getProperty($property); return $this->searchValue($property, $value, $search, $options); @@ -362,7 +386,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function searchNestedProperty(string $property, string $search, array $options = null): float { - $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); + $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options'); if ($property === 'key') { $value = $this->getKey(); } else { @@ -419,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface return 0; } + /** + * Get original data before update + * + * @return array + */ + public function getOriginalData(): array + { + return $this->_original ?? []; + } + /** * Get any changes based on data sent to update * @@ -632,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface } // Store the changes - $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements); + $this->_original = $this->getElements(); + $this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements); } if ($files && method_exists($this, 'setUpdatedMedia')) { @@ -670,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface return $this->create($key); } + /** + * @param UserInterface|null $user + */ + public function check(UserInterface $user = null): void + { + // If user has been provided, check if the user has permissions to save this object. + if ($user && !$this->isAuthorized('save', null, $user)) { + throw new \RuntimeException('Forbidden', 403); + } + } + /** * {@inheritdoc} * @see FlexObjectInterface::save() @@ -788,11 +834,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface */ public function getForm(string $name = '', array $options = null) { - if (!isset($this->_forms[$name])) { - $this->_forms[$name] = $this->createFormObject($name, $options); + $hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR)); + if (!isset($this->_forms[$hash])) { + $this->_forms[$hash] = $this->createFormObject($name, $options); } - return $this->_forms[$name]; + return $this->_forms[$hash]; } /** @@ -1042,6 +1089,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface return $action; } + /** + * Method to reset blueprints if the type changes. + * + * @return void + * @since 1.7.18 + */ + protected function resetBlueprints(): void + { + $this->_blueprint = []; + } + // DEPRECATED METHODS /** diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryInterface.php index 752324133..07eab014a 100644 --- a/system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryInterface.php +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryInterface.php @@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface; * Interface FlexDirectoryInterface * @package Grav\Framework\Flex\Interfaces */ -interface FlexDirectoryInterface +interface FlexDirectoryInterface extends FlexAuthorizeInterface { /** * @return bool diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php index 9539a154c..32dab1968 100644 --- a/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php @@ -38,8 +38,8 @@ interface FlexFormInterface extends Serializable, FormInterface /** * Get route for deleting files by AJAX. * - * @param string $field Field where the file is associated into. - * @param string $filename Filename for the file. + * @param string|null $field Field where the file is associated into. + * @param string|null $filename Filename for the file. * @return Route|null Returns Route object or null if file uploads are not enabled. */ public function getFileDeleteAjaxRoute($field, $filename); diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexIndexInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexIndexInterface.php index d751e4b40..71dd72770 100644 --- a/system/src/Grav/Framework/Flex/Interfaces/FlexIndexInterface.php +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexIndexInterface.php @@ -51,6 +51,7 @@ interface FlexIndexInterface extends FlexCollectionInterface * * @param string|null $keyField Switch key field of the collection. * @return static Returns a new Flex Collection with new key field. + * @phpstan-return static * @api */ public function withKeyField(string $keyField = null); diff --git a/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php b/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php index 621dae046..ebc26b45a 100644 --- a/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php +++ b/system/src/Grav/Framework/Flex/Pages/FlexPageObject.php @@ -50,7 +50,7 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI /** @var array|null */ protected $_reorder; /** @var FlexPageObject|null */ - protected $_original; + protected $_originalObject; /** * Clone page. @@ -264,7 +264,7 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI */ public function getOriginal() { - return $this->_original; + return $this->_originalObject; } /** @@ -276,8 +276,8 @@ class FlexPageObject extends FlexObject implements PageInterface, FlexTranslateI */ public function storeOriginal(): void { - if (null === $this->_original) { - $this->_original = clone $this; + if (null === $this->_originalObject) { + $this->_originalObject = clone $this; } } diff --git a/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php b/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php index 1c32bf566..6453bcf5e 100644 --- a/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php +++ b/system/src/Grav/Framework/Flex/Pages/Traits/PageLegacyTrait.php @@ -320,14 +320,22 @@ trait PageLegacyTrait // Find non-existing key. $parentKey = $parent ? $parent->getKey() : ''; - $key = trim($parentKey . '/' . basename($this->getKey()), '/'); - $key = preg_replace('/-\d+$/', '', $key); - $i = 1; - do { - $i++; - $test = "{$key}-{$i}"; - } while ($index->containsKey($test)); - $key = $test; + if ($this instanceof FlexPageObject) { + $key = trim($parentKey . '/' . $this->folder(), '/'); + $key = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $key); + } else { + $key = trim($parentKey . '/' . basename($this->getKey()), '/'); + } + + if ($index->containsKey($key)) { + $key = preg_replace('/\d+$/', '', $key); + $i = 1; + do { + $i++; + $test = "{$key}{$i}"; + } while ($index->containsKey($test)); + $key = $test; + } $folder = basename($key); // Get the folder name. diff --git a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php index 229194d7c..ff2c94d7a 100644 --- a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php @@ -40,6 +40,8 @@ class FolderStorage extends AbstractFilesystemStorage protected $dataFolder; /** @var string Pattern to access an object. */ protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}'; + /** @var string[] */ + protected $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s']; /** @var string Filename for the object. */ protected $dataFile; /** @var string File extension for the object. */ @@ -380,6 +382,12 @@ class FolderStorage extends AbstractFilesystemStorage if (isset($data[0])) { throw new RuntimeException('Broken object file'); } + + // Add key field to the object. + $keyField = $this->keyField; + if ($keyField !== 'storage_key' && !isset($data[$keyField])) { + $data[$keyField] = $key; + } } catch (RuntimeException $e) { $data = ['__ERROR' => $e->getMessage()]; } finally { @@ -626,7 +634,7 @@ class FolderStorage extends AbstractFilesystemStorage $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS; $iterator = new FilesystemIterator($path, $flags); - $list = []; + $list = [[]]; /** @var SplFileInfo $info */ foreach ($iterator as $filename => $info) { if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) { @@ -636,11 +644,7 @@ class FolderStorage extends AbstractFilesystemStorage $list[] = $this->buildIndexFromFilesystem($filename); } - if (!$list) { - return []; - } - - return count($list) > 1 ? array_merge(...$list) : $list[0]; + return array_merge(...$list); } /** @@ -692,9 +696,7 @@ class FolderStorage extends AbstractFilesystemStorage $this->keyLen = (int)($options['key_len'] ?? 32); $this->caseSensitive = (bool)($options['case_sensitive'] ?? true); - $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s']; - $pattern = Utils::simpleTemplate($pattern, $variables); - + $pattern = Utils::simpleTemplate($pattern, $this->variables); if (!$pattern) { throw new RuntimeException('Bad storage folder pattern'); } diff --git a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php index 85c14290e..6faac9d84 100644 --- a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php +++ b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php @@ -455,7 +455,7 @@ class SimpleStorage extends AbstractFilesystemStorage $content = (array) $file->content(); if ($this->prefix) { $data = new Data($content); - $content = $data->get($this->prefix); + $content = $data->get($this->prefix, []); } $file->free(); diff --git a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php index c3b810d1a..dabfe17b7 100644 --- a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php +++ b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php @@ -120,7 +120,7 @@ trait FlexMediaTrait // Load settings for the field. $schema = $this->getBlueprint()->schema(); $settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null; - if (!isset($settings) || !is_array($settings)) { + if (!is_array($settings)) { return null; } @@ -373,7 +373,7 @@ trait FlexMediaTrait if (is_array($upload)) { // Uses new format with [UploadedFileInterface, array]. $settings = $upload[1]; - if ($settings['destination'] === $media->getPath()) { + if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) { $upload = $upload[0]; } else { $upload = false; @@ -383,6 +383,7 @@ trait FlexMediaTrait $medium = $upload ? MediumFactory::fromUploadedFile($upload) : null; $updated = true; if ($medium) { + $medium->uploaded = true; $media->add($filename, $medium); } elseif (is_callable([$media, 'hide'])) { $media->hide($filename); diff --git a/system/src/Grav/Framework/Form/FormFlash.php b/system/src/Grav/Framework/Form/FormFlash.php index 00aab2893..864e992f3 100644 --- a/system/src/Grav/Framework/Form/FormFlash.php +++ b/system/src/Grav/Framework/Form/FormFlash.php @@ -120,7 +120,7 @@ class FormFlash implements FormFlashInterface protected function loadStoredForm(): ?array { $file = $this->getTmpIndex(); - $exists = $file->exists(); + $exists = $file && $file->exists(); $data = null; if ($exists) { @@ -246,8 +246,10 @@ class FormFlash implements FormFlashInterface if ($force || $this->data || $this->files) { // Only save if there is data or files to be saved. $file = $this->getTmpIndex(); - $file->save($this->jsonSerialize()); - $this->exists = true; + if ($file) { + $file->save($this->jsonSerialize()); + $this->exists = true; + } } elseif ($this->exists) { // Delete empty form flash if it exists (it carries no information). return $this->delete(); @@ -476,12 +478,14 @@ class FormFlash implements FormFlashInterface } /** - * @return YamlFile + * @return ?YamlFile */ - protected function getTmpIndex(): YamlFile + protected function getTmpIndex(): ?YamlFile { + $tmpDir = $this->getTmpDir(); + // Do not use CompiledYamlFile as the file can change multiple times per second. - return YamlFile::instance($this->getTmpDir() . '/index.yaml'); + return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null; } /** @@ -503,7 +507,9 @@ class FormFlash implements FormFlashInterface { // Make sure that index file cache gets always cleared. $file = $this->getTmpIndex(); - $file->free(); + if ($file) { + $file->free(); + } $tmpDir = $this->getTmpDir(); if ($tmpDir && file_exists($tmpDir)) { diff --git a/system/src/Grav/Framework/Mime/MimeTypes.php b/system/src/Grav/Framework/Mime/MimeTypes.php new file mode 100644 index 000000000..dadcddf7a --- /dev/null +++ b/system/src/Grav/Framework/Mime/MimeTypes.php @@ -0,0 +1,107 @@ + ['mime/type', 'mime/type2']] + */ + public static function createFromMimes(array $mimes): self + { + $extensions = []; + foreach ($mimes as $ext => $list) { + foreach ($list as $mime) { + $list = $extensions[$mime] ?? []; + if (!in_array($ext, $list, true)) { + $list[] = $ext; + $extensions[$mime] = $list; + } + } + } + + return new static($extensions, $mimes); + } + + /** + * @param string $extension + * @return string|null + */ + public function getMimeType(string $extension): ?string + { + $extension = $this->cleanInput($extension); + + return $this->mimes[$extension][0] ?? null; + } + + /** + * @param string $mime + * @return string|null + */ + public function getExtension(string $mime): ?string + { + $mime = $this->cleanInput($mime); + + return $this->extensions[$mime][0] ?? null; + } + + /** + * @param string $extension + * @return array + */ + public function getMimeTypes(string $extension): array + { + $extension = $this->cleanInput($extension); + + return $this->mimes[$extension] ?? []; + } + + /** + * @param string $mime + * @return array + */ + public function getExtensions(string $mime): array + { + $mime = $this->cleanInput($mime); + + return $this->extensions[$mime] ?? []; + } + + /** + * @param string $input + * @return string + */ + protected function cleanInput(string $input): string + { + return strtolower(trim($input)); + } + + /** + * @param array $extensions + * @param array $mimes + */ + protected function __construct(array $extensions, array $mimes) + { + $this->extensions = $extensions; + $this->mimes = $mimes; + } +} diff --git a/system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php b/system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php index de5cc7aeb..affff03b0 100644 --- a/system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php +++ b/system/src/Grav/Framework/Object/Access/ArrayAccessTrait.php @@ -21,6 +21,7 @@ trait ArrayAccessTrait * @param mixed $offset An offset to check for. * @return bool Returns TRUE on success or FALSE on failure. */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { return $this->hasProperty($offset); @@ -32,6 +33,7 @@ trait ArrayAccessTrait * @param mixed $offset The offset to retrieve. * @return mixed Can return all value types. */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->getProperty($offset); @@ -44,6 +46,7 @@ trait ArrayAccessTrait * @param mixed $value The value to set. * @return void */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { $this->setProperty($offset, $value); @@ -55,6 +58,7 @@ trait ArrayAccessTrait * @param mixed $offset The offset to unset. * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { $this->unsetProperty($offset); diff --git a/system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php b/system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php index e0485655c..83658753d 100644 --- a/system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php +++ b/system/src/Grav/Framework/Object/Access/NestedArrayAccessTrait.php @@ -21,6 +21,7 @@ trait NestedArrayAccessTrait * @param mixed $offset An offset to check for. * @return bool Returns TRUE on success or FALSE on failure. */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { return $this->hasNestedProperty($offset); @@ -32,6 +33,7 @@ trait NestedArrayAccessTrait * @param mixed $offset The offset to retrieve. * @return mixed Can return all value types. */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->getNestedProperty($offset); @@ -44,6 +46,7 @@ trait NestedArrayAccessTrait * @param mixed $value The value to set. * @return void */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { $this->setNestedProperty($offset, $value); @@ -55,6 +58,7 @@ trait NestedArrayAccessTrait * @param mixed $offset The offset to unset. * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { $this->unsetNestedProperty($offset); diff --git a/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php b/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php index fa0920b88..3c172fecd 100644 --- a/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php +++ b/system/src/Grav/Framework/Object/Base/ObjectCollectionTrait.php @@ -207,8 +207,6 @@ trait ObjectCollectionTrait /** * Create a copy from this collection by cloning all objects in the collection. - * - * @return static */ public function copy() { diff --git a/system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php b/system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php index 0e2373eb6..a2431be31 100644 --- a/system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php +++ b/system/src/Grav/Framework/Object/Interfaces/NestedObjectCollectionInterface.php @@ -15,7 +15,7 @@ use RuntimeException; * Common Interface for both Objects and Collections * @package Grav\Framework\Object * - * @template TKey + * @template TKey of array-key * @template T * @extends ObjectCollectionInterface */ diff --git a/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php b/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php index 3a43981b1..8169c246e 100644 --- a/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php +++ b/system/src/Grav/Framework/Object/Interfaces/ObjectCollectionInterface.php @@ -16,7 +16,7 @@ use Serializable; /** * ObjectCollection Interface * @package Grav\Framework\Collection - * @template TKey + * @template TKey of array-key * @template T * @extends CollectionInterface * @extends Selectable @@ -76,6 +76,7 @@ interface ObjectCollectionInterface extends CollectionInterface, Selectable, Ser * Create a copy from this collection by cloning all objects in the collection. * * @return static + * @phpstan-return static */ public function copy(); diff --git a/system/src/Grav/Framework/Object/ObjectCollection.php b/system/src/Grav/Framework/Object/ObjectCollection.php index 0d82434a1..3fdebce91 100644 --- a/system/src/Grav/Framework/Object/ObjectCollection.php +++ b/system/src/Grav/Framework/Object/ObjectCollection.php @@ -9,7 +9,6 @@ namespace Grav\Framework\Object; -use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Grav\Framework\Collection\ArrayCollection; use Grav\Framework\Object\Access\NestedPropertyCollectionTrait; @@ -22,7 +21,7 @@ use function array_slice; /** * Class contains a collection of objects. * - * @template TKey + * @template TKey of array-key * @template T * @extends ArrayCollection * @implements NestedObjectCollectionInterface diff --git a/system/src/Grav/Framework/Object/ObjectIndex.php b/system/src/Grav/Framework/Object/ObjectIndex.php index 2c13765bc..b7b416d0a 100644 --- a/system/src/Grav/Framework/Object/ObjectIndex.php +++ b/system/src/Grav/Framework/Object/ObjectIndex.php @@ -23,7 +23,7 @@ use function is_object; * This is an abstract class and has some protected abstract methods to load objects which you need to implement in * order to use the class. * - * @template TKey + * @template TKey of array-key * @template T * @extends AbstractIndexCollection * @implements NestedObjectCollectionInterface @@ -176,6 +176,7 @@ abstract class ObjectIndex extends AbstractIndexCollection implements NestedObje * Create a copy from this collection by cloning all objects in the collection. * * @return static + * @return static */ public function copy() { diff --git a/system/src/Grav/Framework/Psr7/UploadedFile.php b/system/src/Grav/Framework/Psr7/UploadedFile.php index f13674228..bfa63cdf4 100644 --- a/system/src/Grav/Framework/Psr7/UploadedFile.php +++ b/system/src/Grav/Framework/Psr7/UploadedFile.php @@ -23,6 +23,9 @@ class UploadedFile implements UploadedFileInterface { use UploadedFileDecoratorTrait; + /** @var array */ + private $meta = []; + /** * @param StreamInterface|string|resource $streamOrFile * @param int $size @@ -34,4 +37,34 @@ class UploadedFile implements UploadedFileInterface { $this->uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType); } + + /** + * @param array $meta + * @return $this + */ + public function setMeta(array $meta) + { + $this->meta = $meta; + + return $this; + } + + /** + * @param array $meta + * @return $this + */ + public function addMeta(array $meta) + { + $this->meta = array_merge($this->meta, $meta); + + return $this; + } + + /** + * @return array + */ + public function getMeta(): array + { + return $this->meta; + } } diff --git a/system/src/Grav/Framework/Session/Session.php b/system/src/Grav/Framework/Session/Session.php index ddab08d75..3feae5a2a 100644 --- a/system/src/Grav/Framework/Session/Session.php +++ b/system/src/Grav/Framework/Session/Session.php @@ -338,23 +338,12 @@ class Session implements SessionInterface { $name = $this->getName(); if (null !== $name) { - $params = session_get_cookie_params(); - - $cookie_options = array ( - 'expires' => time() - 42000, - 'path' => $params['path'], - 'domain' => $params['domain'], - 'secure' => $params['secure'], - 'httponly' => $params['httponly'], - 'samesite' => $params['samesite'] - ); - $this->removeCookie(); setcookie( session_name(), '', - $cookie_options + $this->getCookieOptions(-42000) ); } @@ -463,27 +452,36 @@ class Session implements SessionInterface } /** - * @return void + * Store something in cookie temporarily. + * + * @param int|null $lifetime + * @return array */ - protected function setCookie(): void + public function getCookieOptions(int $lifetime = null): array { $params = session_get_cookie_params(); - $cookie_options = array ( - 'expires' => time() + $params['lifetime'], + return [ + 'expires' => time() + ($lifetime ?? $params['lifetime']), 'path' => $params['path'], 'domain' => $params['domain'], 'secure' => $params['secure'], 'httponly' => $params['httponly'], 'samesite' => $params['samesite'] - ); + ]; + } + /** + * @return void + */ + protected function setCookie(): void + { $this->removeCookie(); setcookie( session_name(), session_id(), - $cookie_options + $this->getCookieOptions() ); } diff --git a/system/src/Twig/DeferredExtension/DeferredBlockNode.php b/system/src/Twig/DeferredExtension/DeferredBlockNode.php new file mode 100755 index 000000000..6ae974f88 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredBlockNode.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Compiler; +use Twig\Node\BlockNode; + +final class DeferredBlockNode extends BlockNode +{ + public function compile(Compiler $compiler) : void + { + $name = $this->getAttribute('name'); + + $compiler + ->write("public function block_$name(\$context, array \$blocks = [])\n", "{\n") + ->indent() + ->write("\$this->deferred->defer(\$this, '$name');\n") + ->outdent() + ->write("}\n\n") + ; + + $compiler + ->addDebugInfo($this) + ->write("public function block_{$name}_deferred(\$context, array \$blocks = [])\n", "{\n") + ->indent() + ->subcompile($this->getNode('body')) + ->write("\$this->deferred->resolve(\$this, \$context, \$blocks);\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredExtension.php b/system/src/Twig/DeferredExtension/DeferredExtension.php new file mode 100644 index 000000000..f27c2a36f --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredExtension.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Environment; +use Twig\Extension\AbstractExtension; +use Twig\Template; + +final class DeferredExtension extends AbstractExtension +{ + private $blocks = []; + + public function getTokenParsers() : array + { + return [new DeferredTokenParser()]; + } + + public function getNodeVisitors() : array + { + if (Environment::VERSION_ID < 20000) { + // Twig 1.x support + return [new DeferredNodeVisitorCompat()]; + } + + return [new DeferredNodeVisitor()]; + } + + public function defer(Template $template, string $blockName) : void + { + $templateName = $template->getTemplateName(); + $this->blocks[$templateName][] = $blockName; + $index = \count($this->blocks[$templateName]) - 1; + + \ob_start(function (string $buffer) use ($index, $templateName) { + unset($this->blocks[$templateName][$index]); + + return $buffer; + }); + } + + public function resolve(Template $template, array $context, array $blocks) : void + { + $templateName = $template->getTemplateName(); + if (empty($this->blocks[$templateName])) { + return; + } + + while ($blockName = \array_pop($this->blocks[$templateName])) { + $buffer = \ob_get_clean(); + + $blocks[$blockName] = [$template, 'block_'.$blockName.'_deferred']; + $template->displayBlock($blockName, $context, $blocks); + + echo $buffer; + } + + if ($parent = $template->getParent($context)) { + $this->resolve($parent, $context, $blocks); + } + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredExtensionNode.php b/system/src/Twig/DeferredExtension/DeferredExtensionNode.php new file mode 100644 index 000000000..1b851b4c2 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredExtensionNode.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Compiler; +use Twig\Node\Node; + +final class DeferredExtensionNode extends Node +{ + public function compile(Compiler $compiler) : void + { + $compiler + ->write("\$this->deferred = \$this->env->getExtension('".DeferredExtension::class."');\n") + ; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredNode.php b/system/src/Twig/DeferredExtension/DeferredNode.php new file mode 100755 index 000000000..2ac73bd1f --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredNode.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Compiler; +use Twig\Node\Node; + +final class DeferredNode extends Node +{ + public function compile(Compiler $compiler) : void + { + $compiler + ->write("\$this->deferred->resolve(\$this, \$context, \$blocks);\n") + ; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredNodeVisitor.php b/system/src/Twig/DeferredExtension/DeferredNodeVisitor.php new file mode 100644 index 000000000..aef739943 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredNodeVisitor.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Environment; +use Twig\Node\ModuleNode; +use Twig\Node\Node; +use Twig\NodeVisitor\NodeVisitorInterface; + +final class DeferredNodeVisitor implements NodeVisitorInterface +{ + private $hasDeferred = false; + + public function enterNode(Node $node, Environment $env) : Node + { + if (!$this->hasDeferred && $node instanceof DeferredBlockNode) { + $this->hasDeferred = true; + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env) : ?Node + { + if ($this->hasDeferred && $node instanceof ModuleNode) { + $node->setNode('constructor_end', new Node([new DeferredExtensionNode(), $node->getNode('constructor_end')])); + $node->setNode('display_end', new Node([new DeferredNode(), $node->getNode('display_end')])); + $this->hasDeferred = false; + } + + return $node; + } + + public function getPriority() : int + { + return 0; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php b/system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php new file mode 100644 index 000000000..8c441bb9a --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Environment; +use Twig\Node\ModuleNode; +use Twig\Node\Node; +use Twig\NodeVisitor\NodeVisitorInterface; + +final class DeferredNodeVisitorCompat implements NodeVisitorInterface +{ + private $hasDeferred = false; + + public function enterNode(\Twig_NodeInterface $node, Environment $env) : Node + { + if (!$this->hasDeferred && $node instanceof DeferredBlockNode) { + $this->hasDeferred = true; + } + + return $node; + } + + public function leaveNode(\Twig_NodeInterface $node, Environment $env) : ?Node + { + if ($this->hasDeferred && $node instanceof ModuleNode) { + $node->setNode('constructor_end', new Node([new DeferredExtensionNode(), $node->getNode('constructor_end')])); + $node->setNode('display_end', new Node([new DeferredNode(), $node->getNode('display_end')])); + $this->hasDeferred = false; + } + + return $node; + } + + public function getPriority() : int + { + return 0; + } +} diff --git a/system/src/Twig/DeferredExtension/DeferredTokenParser.php b/system/src/Twig/DeferredExtension/DeferredTokenParser.php new file mode 100644 index 000000000..1870ae0e4 --- /dev/null +++ b/system/src/Twig/DeferredExtension/DeferredTokenParser.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Twig\DeferredExtension; + +use Twig\Node\BlockNode; +use Twig\Node\Node; +use Twig\Parser; +use Twig\Token; +use Twig\TokenParser\AbstractTokenParser; +use Twig\TokenParser\BlockTokenParser; + +final class DeferredTokenParser extends AbstractTokenParser +{ + private $blockTokenParser; + + public function setParser(Parser $parser) : void + { + parent::setParser($parser); + + $this->blockTokenParser = new BlockTokenParser(); + $this->blockTokenParser->setParser($parser); + } + + public function parse(Token $token) : Node + { + $stream = $this->parser->getStream(); + $nameToken = $stream->next(); + $deferredToken = $stream->nextIf(Token::NAME_TYPE, 'deferred'); + $stream->injectTokens([$nameToken]); + + $node = $this->blockTokenParser->parse($token); + + if ($deferredToken) { + $this->replaceBlockNode($nameToken->getValue()); + } + + return $node; + } + + public function getTag() : string + { + return 'block'; + } + + private function replaceBlockNode(string $name) : void + { + $block = $this->parser->getBlock($name)->getNode('0'); + $this->parser->setBlock($name, $this->createDeferredBlockNode($block)); + } + + private function createDeferredBlockNode(BlockNode $block) : DeferredBlockNode + { + $name = $block->getAttribute('name'); + $deferredBlock = new DeferredBlockNode($name, new Node([]), $block->getTemplateLine()); + + foreach ($block as $nodeName => $node) { + $deferredBlock->setNode($nodeName, $node); + } + + if ($sourceContext = $block->getSourceContext()) { + $deferredBlock->setSourceContext($sourceContext); + } + + return $deferredBlock; + } +} diff --git a/tests/phpstan/phpstan.neon b/tests/phpstan/phpstan.neon index ff372d778..c41ae9bfe 100644 --- a/tests/phpstan/phpstan.neon +++ b/tests/phpstan/phpstan.neon @@ -1,4 +1,4 @@ -#phpVersion: 70100 +#phpVersion: 70300 includes: #- '../../vendor/phpstan/phpstan-strict-rules/rules.neon' - '../../vendor/phpstan/phpstan-deprecation-rules/rules.neon' @@ -10,8 +10,11 @@ parameters: - phpstan-bootstrap.php excludes_analyse: - */system/src/Grav/Common/Errors/Resources/layout.html.php + - */system/src/Twig/DeferredExtension/DeferredNodeVisitor.php + inferPrivatePropertyTypeFromConstructor: true reportUnmatchedIgnoredErrors: false + treatPhpDocTypesAsCertain: false # These checks are new in phpstan 0.12, ignore them for now. checkMissingIterableValueType: false @@ -124,8 +127,11 @@ parameters: # Support for deprecated features - - message: '#Instantiation of deprecated class Doctrine\\Common\\Cache\\MemcacheCache#' + message: '#Instantiation of deprecated class Doctrine\\Common\\Cache\\(\w+)Cache#' path: '*/system/src/Grav/Common/Cache.php' + - + message: '#Instantiation of deprecated class Doctrine\\Common\\Cache\\(\w+)Cache#' + path: '*/system/src/Grav/Common/GPM/Remote/*.php' - message: '#Call to deprecated method order#' path: '*/system/src/Grav/Common/Page/Pages.php' @@ -166,3 +172,8 @@ parameters: - message: '#Variable \$this in PHPDoc tag @var does not exist#' path: '*/system/src/Grav/Installer/updates/*' + + # Twig Deferred extension compatibility + - + message: '#typehint with deprecated interface#' + path: '*/system/src/Twig/DeferredExtension/DeferredNodeVisitorCompat.php' diff --git a/tests/phpstan/plugins-bootstrap.php b/tests/phpstan/plugins-bootstrap.php index 1beaaa69f..c28f2ab8a 100644 --- a/tests/phpstan/plugins-bootstrap.php +++ b/tests/phpstan/plugins-bootstrap.php @@ -62,7 +62,7 @@ foreach ($iterator as $directory) { define('GANTRY_DEBUGGER', true); define('GANTRY5_DEBUG', true); define('GANTRY5_PLATFORM', 'grav'); -define('GANTRY5_ROOT', rtrim(ROOT_DIR, '/')); +define('GANTRY5_ROOT', GRAV_ROOT); define('GANTRY5_VERSION', '@version@'); define('GANTRY5_VERSION_DATE', '@versiondate@'); define('GANTRYADMIN_PATH', ''); diff --git a/tests/unit/Grav/Common/AssetsTest.php b/tests/unit/Grav/Common/AssetsTest.php index 747c8ed51..57539a6f3 100644 --- a/tests/unit/Grav/Common/AssetsTest.php +++ b/tests/unit/Grav/Common/AssetsTest.php @@ -340,7 +340,7 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->reset(); $this->assets->addJs('jquery', ['loading' => 'async']); $js = $this->assets->js(); - self::assertSame('' . PHP_EOL, $js); + self::assertSame('' . PHP_EOL, $js); //Test priority too $this->assets->reset(); @@ -348,7 +348,7 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->addJs('test.js', ['loading' => 'async', 'priority' => 2]); $js = $this->assets->js(); self::assertSame('' . PHP_EOL . - '' . PHP_EOL, $js); + '' . PHP_EOL, $js); //Test multiple groups $this->assets->reset(); @@ -357,7 +357,7 @@ class AssetsTest extends \Codeception\TestCase\Test $js = $this->assets->js(); self::assertSame('' . PHP_EOL, $js); $js = $this->assets->js('footer'); - self::assertSame('' . PHP_EOL, $js); + self::assertSame('' . PHP_EOL, $js); //Test adding array of assets //Test priority too @@ -365,7 +365,7 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->addJs(['jquery', 'test.js'], ['loading' => 'async']); $js = $this->assets->js(); - self::assertSame('' . PHP_EOL . + self::assertSame('' . PHP_EOL . '' . PHP_EOL, $js); } @@ -473,6 +473,59 @@ class AssetsTest extends \Codeception\TestCase\Test '' . PHP_EOL, $css); } + public function testAddingAssetPropertiesWithArrayFromCollectionAndParameters(): void + { + $this->assets->registerCollection('collection_multi_params', [ + 'foo.js' => [ 'defer' => true ], + 'bar.js' => [ 'integrity' => 'sha512-abc123' ], + 'foobar.css' => [ 'defer' => null, 'loading' => null ] + ]); + + // # Test adding properties with array + $this->assets->addJs('collection_multi_params', ['loading' => 'async']); + $js = $this->assets->js(); + + // expected output + $expected = [ + '', + '', + '', + ]; + + self::assertCount(count($expected), array_filter(explode("\n", $js))); + self::assertSame(implode("\n", $expected) . PHP_EOL, $js); + + // # Test priority as second argument + render JS should not have any css + $this->assets->reset(); + $this->assets->add('low_priority.js', 1); + $this->assets->add('collection_multi_params', 2); + $js = $this->assets->js(); + + // expected output + $expected = [ + '', + '', + '', + ]; + + self::assertCount(3, array_filter(explode("\n", $js))); + self::assertSame(implode("\n", $expected) . PHP_EOL, $js); + + // # Test rendering CSS, should not have any JS + $this->assets->reset(); + $this->assets->add('collection_multi_params', [ 'class' => '__classname' ]); + $css = $this->assets->css(); + + // expected output + $expected = [ + '', + ]; + + + self::assertCount(1, array_filter(explode("\n", $css))); + self::assertSame(implode("\n", $expected) . PHP_EOL, $css); + } + public function testPriorityOfAssets(): void { $this->assets->reset(); @@ -573,7 +626,7 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->reset(); $this->assets->addAsyncJs('jquery'); $js = $this->assets->js(); - self::assertSame('' . PHP_EOL, $js); + self::assertSame('' . PHP_EOL, $js); } public function testAddDeferJs(): void @@ -581,7 +634,7 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->reset(); $this->assets->addDeferJs('jquery'); $js = $this->assets->js(); - self::assertSame('' . PHP_EOL, $js); + self::assertSame('' . PHP_EOL, $js); } public function testTimestamps(): void @@ -663,7 +716,7 @@ class AssetsTest extends \Codeception\TestCase\Test { self::assertIsArray($this->assets->getCollections()); self::assertContains('jquery', array_keys($this->assets->getCollections())); - self::assertContains('system://assets/jquery/jquery-2.x.min.js', $this->assets->getCollections()); + self::assertContains('system://assets/jquery/jquery-3.x.min.js', $this->assets->getCollections()); } public function testExists(): void @@ -679,6 +732,27 @@ class AssetsTest extends \Codeception\TestCase\Test self::assertContains('debugger', array_keys($this->assets->getCollections())); } + public function testRegisterCollectionWithParameters(): void + { + $this->assets->registerCollection('collection_multi_params', [ + 'foo.js' => [ 'defer' => true ], + 'bar.js' => [ 'integrity' => 'sha512-abc123' ], + 'foobar.css' => [ 'defer' => null ], + ]); + + self::assertTrue($this->assets->exists('collection_multi_params')); + + $collection = $this->assets->getCollections()['collection_multi_params']; + self::assertArrayHasKey('foo.js', $collection); + self::assertArrayHasKey('bar.js', $collection); + self::assertArrayHasKey('foobar.css', $collection); + self::assertArrayHasKey('defer', $collection['foo.js']); + self::assertArrayHasKey('defer', $collection['foobar.css']); + + self::assertNull($collection['foobar.css']['defer']); + self::assertTrue($collection['foo.js']['defer']); + } + public function testReset(): void { $this->assets->addInlineJs('alert("test")'); diff --git a/tests/unit/Grav/Common/Twig/TwigExtensionTest.php b/tests/unit/Grav/Common/Twig/Extensions/GravExtensionTest.php similarity index 96% rename from tests/unit/Grav/Common/Twig/TwigExtensionTest.php rename to tests/unit/Grav/Common/Twig/Extensions/GravExtensionTest.php index 4062675d2..2adb02378 100644 --- a/tests/unit/Grav/Common/Twig/TwigExtensionTest.php +++ b/tests/unit/Grav/Common/Twig/Extensions/GravExtensionTest.php @@ -2,23 +2,23 @@ use Codeception\Util\Fixtures; use Grav\Common\Grav; -use Grav\Common\Twig\TwigExtension; +use Grav\Common\Twig\Extension\GravExtension; /** - * Class TwigExtensionTest + * Class GravExtensionTest */ -class TwigExtensionTest extends \Codeception\TestCase\Test +class GravExtensionTest extends \Codeception\TestCase\Test { /** @var Grav $grav */ protected $grav; - /** @var TwigExtension $twig_ext */ + /** @var GravExtension $twig_ext */ protected $twig_ext; protected function _before(): void { $this->grav = Fixtures::get('grav'); - $this->twig_ext = new TwigExtension(); + $this->twig_ext = new GravExtension(); } public function testInflectorFilter(): void