diff --git a/.editorconfig b/.editorconfig index 685c063..3b95e6d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,5 @@ indent_style = space indent_size = 4 # 2 space indentation -[*.yaml, *.yml] -indent_style = space +[*.{yaml,.yml}] indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e84f52b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: grav +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: # Replace with a single custom sponsorship URL diff --git a/.gitignore b/.gitignore index 366e1fc..12f8319 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ user/data/* !user/data/.* user/plugins/* !user/plugins/.* +user/themes/* +!user/themes/.* user/localhost/config/security.yaml user/config/security.yaml @@ -36,8 +38,9 @@ Thumbs.db # phpstorm .idea/* +# testing stuff tests/_output/* tests/_support/_generated/* tests/cache/* tests/error.log -/system/templates/testing +system/templates/testing/* diff --git a/.htaccess b/.htaccess index 9b61c48..ef79a4b 100644 --- a/.htaccess +++ b/.htaccess @@ -9,7 +9,7 @@ RewriteEngine On # along the lines of: RewriteBase / ## -RewriteBase / +# RewriteBase / ## End - RewriteBase diff --git a/.travis.yml b/.travis.yml index 19df539..ed54c47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: php php: - - '5.6' - - '7.0.21' - '7.1' - '7.2' + - '7.3' branches: only: - develop @@ -13,13 +12,6 @@ notifications: email: on_success: never on_failure: always - hipchat: - # hipchat_api@grav - rooms: - - secure: "bqO0wM1B7bJnQw2fuhquSXEqI9gw6WmFytIh9sEWXzbYTzTUP5t0PcKOd3FT2BNMRaDxPJLVl+vG/oqmqDUBkEmOGcG504IQjeNzZqnMz0tXQMIcCc22Las9tFfc4Jf6RVi/qGomFtHGE9Wgii+TAN4zqZaufbNjwd8SyjO0+W8=" - template: - - '%{repository}#%{build_number} (%{branch}): Travis Job Finished [%{duration}] (Details)' - format: html slack: secure: dowksPsxxCxGKT6nis5hUgkp6+ZDAhoqzQHF9rJnx4hx0iEygPhVBs7pKl9yL2jubYJoLs+EXwE7z1dYgDAEJh4BnfrCokCMLpFGcxVxQC/HeAUdSQ2/RtdBYR5PRT75ScaFpqM/SfXXZVtnwVXAw9Z+JC6BjQ9vmn23m51Jw4k= env: @@ -53,7 +45,7 @@ before_install: - if [ $TRAVIS_BRANCH == 'develop' ] || [ $TRAVIS_PULL_REQUEST != 'false' ]; then composer install --dev --prefer-dist; fi - - if [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "5.6" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then + - if [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "7.1" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then export TRAVIS_TAG=$(curl --fail --user "${GH_API_USER}" -s https://api.github.com/repos/getgrav/grav/releases/latest | grep tag_name | head -n 1 | cut -d '"' -f 4); eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=1.8 bash)"; go get github.com/aktau/github-release; @@ -70,7 +62,7 @@ script: vendor/bin/codecept run; fi - echo "Latest Release Tag - ${TRAVIS_TAG}" - - if [ ! -z "$TRAVIS_TAG" ] && [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "5.6" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then + - if [ ! -z "$TRAVIS_TAG" ] && [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "7.1" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then FILES="$RT_DEVTOOLS/grav-dist/*.zip"; for file in ${FILES[@]}; do NAME=${file##*/}; diff --git a/CHANGELOG.md b/CHANGELOG.md index 088dd41..ea3d604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,375 @@ +# v1.6.16 +## 09/19/2019 + +1. [](#bugfix) + * Fixed Flex user creation if file storage is being used [#2444](https://github.com/getgrav/grav/issues/2444) + * Fixed `Badly encoded JSON data` warning when uploading files [#2663](https://github.com/getgrav/grav/issues/2663) + +# v1.6.15 +## 08/20/2019 + +1. [](#improved) + * Improved robots.txt [#2632](https://github.com/getgrav/grav/issues/2632) +1. [](#bugfix) + * Fixed broken markdown Twig tag [#2635](https://github.com/getgrav/grav/issues/2635) + * Force Symfony 4.2 in Grav 1.6 to remove a bunch of deprecated messages + +# v1.6.14 +## 08/18/2019 + +1. [](#bugfix) + * Actually include fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627) + +# v1.6.13 +## 08/16/2019 + +1. [](#bugfix) + * Regression fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627) + +# v1.6.12 +## 08/14/2019 + +1. [](#new) + * Added support for custom `FormFlash` save locations + * Added a new `Utils::arrayLower()` method for lowercasing arrays + * Support new GRAV_BASEDIR environment variable [#2541](https://github.com/getgrav/grav/pull/2541) + * Allow users to override plugin handler priorities [#2165](https://github.com/getgrav/grav/pull/2165) +1. [](#improved) + * Use new `Utils::getSupportedPageTypes()` to enforce `html,htm` at the front of the list [#2531](https://github.com/getgrav/grav/issues/2531) + * Updated vendor libraries + * Markdown filter is now page-aware so that it works with modular references [admin#1731](https://github.com/getgrav/grav-plugin-admin/issues/1731) + * Check of `GRAV_USER_INSTANCE` constant is already defined [#2621](https://github.com/getgrav/grav/pull/2621) +1. [](#bugfix) + * Fixed some potential issues when `$grav['user']` is not set + * Fixed error when calling `Media::add($name, null)` + * Fixed `url()` returning wrong path if using stream with grav root path in it, eg: `user-data://shop` when Grav is in `/shop` + * Fixed `url()` not returning a path to non-existing file (`user-data://shop` => `/user/data/shop`) if it is set to fail gracefully + * Fixed `url()` returning false on unknown streams, such as `ftp://domain.com`, they should be treated as external URL + * Fixed Flex User to have permissions to save and delete his own user + * Fixed new Flex User creation not being possible because of username could not be given + * Fixed fatal error 'Expiration date must be an integer, a DateInterval or null, "double" given' [#2529](https://github.com/getgrav/grav/issues/2529) + * Fixed non-existing Flex object having a bad media folder + * Fixed collections using `page@.self:` should allow modular pages if requested + * Fixed an error when trying to delete a file from non-existing Flex Object + * Fixed `FlexObject::exists()` failing sometimes just after the object has been saved + * Fixed CSV formatter not encoding strings with `"` and `,` properly + * Fixed var order in `Validation.php` [#2610](https://github.com/getgrav/grav/issues/2610) + +# v1.6.11 +## 06/21/2019 + +1. [](#new) + * Added `FormTrait::getAllFlashes()` method to get all the available form flash objects for the form + * Added creation and update timestamps to `FormFlash` objects +1. [](#improved) + * Added `FormFlashInterface`, changed constructor to take `$config` array +1. [](#bugfix) + * Fixed error in `ImageMedium::url()` if the image cache folder does not exist + * Fixed empty form flash name after file upload or form state update + * Fixed a bug in `Route::withParam()` method + * Fixed issue with `FormFlash` objects when there is no session initialized + +# v1.6.10 +## 06/14/2019 + +1. [](#improved) + * Added **page blueprints** to `YamlLinter` CLI and Admin reports + * Removed `Gitter` and `Slack` [#2502](https://github.com/getgrav/grav/issues/2502) + * Optimizations for Plugin/Theme loading + * Generalized markdown classes so they can be used outside of `Page` scope with a custom `Excerpts` class instance + * Change minimal port number to 0 (unix socket) [#2452](https://github.com/getgrav/grav/issues/2452) +1. [](#bugfix) + * Force question to install demo content in theme update [#2493](https://github.com/getgrav/grav/issues/2493) + * Fixed GPM errors from blueprints not being logged [#2505](https://github.com/getgrav/grav/issues/2505) + * Don't error when IP is invalid [#2507](https://github.com/getgrav/grav/issues/2507) + * Fixed regression with `bin/plugin` not listing the plugins available (1c725c0) + * Fixed bitwise operator in `TwigExtension::exifFunc()` [#2518](https://github.com/getgrav/grav/issues/2518) + * Fixed issue with lang prefix incorrectly identifying as admin [#2511](https://github.com/getgrav/grav/issues/2511) + * Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511) + * Fixed regresssion issue of `Utils::Url()` not returning `false` on failure. Added new optional `fail_gracefully` 3rd attribute to return string that caused failure [#2524](https://github.com/getgrav/grav/issues/2524) + +# v1.6.9 +## 05/09/2019 + +1. [](#new) + * Added `Route::withoutParams()` methods + * Added `Pages::setCheckMethod()` method to override page configuration in Admin Plugin + * Added `Cache::clearCache('invalidate')` parameter for just invalidating the cache without deleting any cached files + * Made `UserCollectionInderface` to extend `Countable` to get the count of existing users +1. [](#improved) + * Flex admin: added default search options for flex objects + * Flex collection and object now fall back to the default template if template file doesn't exist + * Updated Vendor libraries including Twig 1.40.1 + * Updated language files from `https://crowdin.com/project/grav-core` +1. [](#bugfix) + * Fixed `$grav['route']` from being modified when the route instance gets modified + * Fixed Assets options array mixed with standalone priority [#2477](https://github.com/getgrav/grav/issues/2477) + * Fix for `avatar_url` provided by 3rd party providers + * Fixed non standard `lang` code lengths in `Utils` and `Session` detection + * Fixed saving a new object in Flex `SimpleStorage` + * Fixed exception in `Flex::getDirectories()` if the first parameter is set + * Output correct "Last Updated" in `bin/gpm info` command + * Checkbox getting interpreted as string, so created new `Validation::filterCheckbox()` + * Fixed backwards compatibility to `select` field with `selectize.create` set to true [git-sync#141](https://github.com/trilbymedia/grav-plugin-git-sync/issues/141) + * Fixed `YamlFormatter::decode()` to always return array [#2494](https://github.com/getgrav/grav/pull/2494) + * Fixed empty `$grav['request']->getAttribute('route')->getExtension()` + +# v1.6.8 +## 04/23/2019 + +1. [](#new) + * Added `FlexCollection::filterBy()` method +1. [](#bugfix) + * Revert `Use Null Coalesce Operator` [#2466](https://github.com/getgrav/grav/pull/2466) + * Fixed `FormTrait::render()` not providing config variable + * Updated `bin/grav clean` to clear `cache/compiled` and `user/config/security.yaml` + +# v1.6.7 +## 04/22/2019 + +1. [](#new) + * Added a new `bin/grav yamllinter` CLI command to find YAML Linting issues [#2468](https://github.com/getgrav/grav/issues/2468#issuecomment-485151681) +1. [](#improved) + * Improve `FormTrait` backwards compatibility with existing forms + * Added a new `Utils::getSubnet()` function for IPv4/IPv6 parsing [#2465](https://github.com/getgrav/grav/pull/2465) +1. [](#bugfix) + * Remove disabled fields from the form schema + * Fix issue when excluding `inlineJs` and `inlineCss` from Assets pipeline [#2468](https://github.com/getgrav/grav/issues/2468) + * Fix for manually set position on external URLs [#2470](https://github.com/getgrav/grav/issues/2470) + +# v1.6.6 +## 04/17/2019 + +1. [](#new) + * `FormInterface` now implements `RenderInterface` + * Added new `FormInterface::getTask()` method which reads the task from `form.task` in the blueprint +1. [](#improved) + * Updated vendor libraries to latest +1. [](#bugfix) + * Rollback `redirect_default_route` logic as it has issues with multi-lang [#2459](https://github.com/getgrav/grav/issues/2459) + * Fix potential issue with `|contains` Twig filter on PHP 7.3 + * Fixed bug in text field filtering: return empty string if value isn't a string or number [#2460](https://github.com/getgrav/grav/issues/2460) + * Force Asset `priority` to be an integer and not throw error if invalid string passed [#2461](https://github.com/getgrav/grav/issues/2461) + * Fixed bug in text field filtering: return empty string if value isn't a string or number + * Fixed `FlexForm` missing getter methods for defining form variables + +# v1.6.5 +## 04/15/2019 + +1. [](#bugfix) + * Backwards compatiblity with old `Uri::__toString()` output + +# v1.6.4 +## 04/15/2019 + +1. [](#bugfix) + * Improved `redirect_default_route` logic as well as `Uri::toArray()` to take into account `root_path` and `extension` + * Rework logic to pull out excluded files from pipeline more reliably [#2445](https://github.com/getgrav/grav/issues/2445) + * Better logic in `Utils::normalizePath` to handle externals properly [#2216](https://github.com/getgrav/grav/issues/2216) + * Fixed to force all `Page::taxonomy` to be treated as strings [#2446](https://github.com/getgrav/grav/issues/2446) + * Fixed issue with `Grav['user']` not being available [form#332](https://github.com/getgrav/grav-plugin-form/issues/332) + * Updated rounding logic for `Utils::parseSize()` [#2394](https://github.com/getgrav/grav/issues/2394) + * Fixed Flex simple storage not being properly initialized if used with caching + +# v1.6.3 +## 04/12/2019 + +1. [](#new) + * Added `Blueprint::addDynamicHandler()` method to allow custom dynamic handlers, for example `custom-options@: getCustomOptions` +1. [](#bugfix) + * Missed a `CacheCommand` reference in `bin/grav` [#2442](https://github.com/getgrav/grav/issues/2442) + * Fixed issue with `Utils::normalizePath` messing with external URLs [#2216](https://github.com/getgrav/grav/issues/2216) + * Fix for `vUndefined` versions when upgrading + +# v1.6.2 +## 04/11/2019 + +1. [](#bugfix) + * Revert renaming of `ClearCacheCommand` to ensure CLI GPM upgrades go smoothly + +# v1.6.1 +## 04/11/2019 + +1. [](#improved) + * Improved CSS for the bottom filter bar of DebugBar +1. [](#bugfix) + * Fixed issue with `@import` not being added to top of pipelined css [#2440](https://github.com/getgrav/grav/issues/2440) + +# v1.6.0 +## 04/11/2019 + +1. [](#new) + * Set minimum requirements to [PHP 7.1.3](https://getgrav.org/blog/raising-php-requirements-2018) + * New `Scheduler` functionality for periodic jobs + * New `Backup` functionality with multiple backup profiles and scheduler integration + * Refactored `Assets Manager` to be more powerful and flexible + * Updated Doctrine Collections to 1.6 + * Updated Doctrine Cache to 1.8 + * Updated Symfony Components to 4.2 + * Added new Cache purge functionality old cache manually via CLI/Admin as well as scheduler integration + * Added new `{% throw 404 'Not Found' %}` twig tag (with custom code/message) + * Added `Grav\Framework\File` classes for handling YAML, Markdown, JSON, INI and PHP serialized files + * Added `Grav\Framework\Collection\AbstractIndexCollection` class + * Added `Grav\Framework\Object\ObjectIndex` class + * Added `Grav\Framework\Flex` classes + * Added support for hiding form fields in blueprints by using dynamic property like `security@: admin.foobar`, `scope@: object` or `scope-ignore@: object` to any field + * New experimental **FlexObjects** powered `Users` for increased performance and capability (**disabled** by default) + * Added PSR-7 and PSR-15 classes + * Added `Grav\Framework\DI\Container` class + * Added `Grav\Framework\RequestHandler\RequestHandler` class + * Added `Page::httpResponseCode()` and `Page::httpHeaders()` methods + * Added `Grav\Framework\Form\Interfaces\FormInterface` + * Added `Grav\Framework\Form\Interfaces\FormFactoryInterface` + * Added `Grav\Framework\Form\FormTrait` + * Added `Page::forms()` method to get normalized list of all form headers defined in the page + * Added `onPageAction`, `onPageTask`, `onPageAction.{$action}` and `onPageTask.{$task}` events + * Added `Blueprint::processForm()` method to filter form inputs + * Move `processMarkdown()` method from `TwigExtension` to more general `Utils` class + * Added support to include extra files into `Media` (such as uploaded files) + * Added form preview support for `FlexObject`, including a way to render newly uploaded files before saving them + * Added `FlexObject::getChanges()` to determine what fields change during an update + * Added `arrayDiffMultidimensional`, `arrayIsAssociative`, `arrayCombine` Util functions + * New `$grav['users']` service to allow custom user classes implementing `UserInterface` + * Added `LogViewer` helper class and CLI command: `bin/grav logviewer` + * Added `select()` and `unselect()` methods to `CollectionInterface` and its base classes + * Added `orderBy()` and `limit()` methods to `ObjectCollectionInterface` and its base classes + * Added `user-data://` which is a writable stream (`user://data` is not and should be avoided) + * Added support for `/action:{$action}` (like task but used without nonce when only receiving data) + * Added `onAction.{$action}` event + * Added `Grav\Framework\Form\FormFlash` class to contain AJAX uploaded files in more reliable way + * Added `Grav\Framework\Form\FormFlashFile` class which implements `UploadedFileInterface` from PSR-7 + * Added `Grav\Framework\Filesystem\Filesystem` class with methods to manipulate stream URLs + * Added new `$grav['filesystem']` service using an instance of the new `Filesystem` object + * Added `{% render object layout: 'default' with { variable: true } %}` for Flex objects and collections + * Added `$grav->setup()` to simplify CLI and custom access points + * Added `CsvFormatter` and `CsvFile` classes + * Added new system config option to `pages.hide_empty_folders` if a folder has no valid `.md` file available. Default behavior is `false` for compatibility. + * Added new system config option for `languages.pages_fallback_only` forcing only 'fallback' to find page content through supported languages, default behavior is to display any language found if active language is missing + * Added `Utils::arrayFlattenDotNotation()` and `Utils::arrayUnflattenDotNotation()` helper methods +1. [](#improved) + * Add the page to onMarkdownInitialized event [#2412](https://github.com/getgrav/grav/issues/2412) + * Doctrine filecache is now namespaced with prefix to support purging + * Register all page types into `blueprint://pages` stream + * Removed `apc` and `xcache` support, made `apc` alias of `apcu` + * Support admin and regular translations via the `|t` twig filter and `t()` twig function + * Improved Grav Core installer/updater to run installer script + * Updated vendor libraries including Symfony `4.2.3` + * Renamed old `User` class to `Grav\Common\User\DataUser\User` with multiple improvements and small fixes + * `User` class now acts as a compatibility layer to older versions of Grav + * Deprecated `new User()`, `User::load()`, `User::find()` and `User::delete()` in favor of `$grav['users']` service + * `Media` constructor has now support to not to initialize the media objects + * Cleanly handle session corruption due to changing Flex object types + * Added `FlexObjectInterface::getDefaultValue()` and `FormInterface::getDefaultValue()` + * Added new `onPageContent()` event for every call to `Page::content()` + * Added phpstan: PHP Static Analysis Tool [#2393](https://github.com/getgrav/grav/pull/2393) + * Added `composer test-plugins` to test plugin issues with the current version of Grav + * Added `Flex::getObjects()` and `Flex::getMixedCollection()` methods for co-mingled collections + * Added support to use single Flex key parameter in `Flex::getObject()` method + * Added `FlexObjectInterface::search()` and `FlexCollectionInterface::search()` methods + * Override `system.media.upload_limit` with PHP's `post_max_size` or `upload_max_filesize` + * Class `Grav\Common\Page\Medium\AbstractMedia` now use array traits instead of extending `Grav\Common\Getters` + * Implemented `Grav\Framework\Psr7` classes as `Nyholm/psr7` decorators + * Added a new `cache-clear` scheduled job to go along with `cache-purge` + * Renamed `Grav\Framework\File\Formatter\FormatterInterface` to `Grav\Framework\File\Interfaces\FileFormatterInterface` + * Improved `File::save()` to use a temporary file if file isn't locked + * Improved `|t` filter to better support admin `|tu` style filter if in admin + * Update all classes to rely on `PageInterface` instead of `Page` class + * Better error checking in `bin/plugin` for existence and enabled + * Removed `media.upload_limit` references + * Twig `nicenumber`: do not use 0 + string casting hack + * Converted Twig tags to use namespaced Twig classes + * Site shows error on page rather than hard-crash when page has invalid frontmatter [#2343](https://github.com/getgrav/grav/issues/2343) + * Added `languages.default_lang` option to override the default lang (usually first supported language) + * Added `Content-Type: application/json` body support for PSR-7 `ServerRequest` + * Remove PHP time limit in `ZipArchive` + * DebugBar: Resolve twig templates in deprecated backtraces in order to help locating Twig issues + * Added `$grav['cache']->getSimpleCache()` method for getting PSR-16 compatible cache + * MediaTrait: Use PSR-16 cache + * Improved `Utils::normalizePath()` to support non-protocol URLs + * Added ability to reset `Page::metadata` to allow rebuilding from automatically generated values + * Added back missing `page.types` field in system content configuration [admin#1612](https://github.com/getgrav/grav-plugin-admin/issues/1612) + * Console commands: add method for invalidating cache + * Updated languages + * Improved `$page->forms()` call, added `$page->addForms()` + * Updated languages from crowdin + * Fixed `ImageMedium` constructor warning when file does not exist + * Improved `Grav\Common\User` class; added `$user->update()` method + * Added trim support for text input fields `validate: trim: true` + * Improved `Grav\Framework\File\Formatter` classes to have abstract parent class and some useful methods + * Support negotiated content types set via the Request `Accept:` header + * Support negotiated language types set via the Request `Accept-Language:` header + * Cleaned up and sorted the Service `idMap` + * Updated `Grav` container object to implement PSR-11 `ContainerInterface` + * Updated Grav `Processor` classes to implement PSR-15 `MiddlewareInterface` + * Make `Data` class to extend `JsonSerializable` + * Modified debugger icon to use retina space-dude version + * Added missing `Video::preload()` method + * Set session name based on `security.salt` rather than `GRAV_ROOT` [#2242](https://github.com/getgrav/grav/issues/2242) + * Added option to configure list of `xss_invalid_protocols` in `Security` config [#2250](https://github.com/getgrav/grav/issues/2250) + * Smarter `security.salt` checking now we use `security.yaml` for other options + * Added apcu autoloader optimization + * Additional helper methods in `Language`, `Languages`, and `LanguageCodes` classes + * Call `onFatalException` event also on internal PHP errors + * Built-in PHP Webserver: log requests before handling them + * Added support for syslog and syslog facility logging (default: 'file') + * Improved usability of `System` configuration blueprint with side-tabs + 1. [](#bugfix) + * Fixed issue with `Truncator::truncateWords` and `Truncator::truncateLetters` when string not wrapped in tags [#2432](https://github.com/getgrav/grav/issues/2432) + * Fixed `Undefined method closure::fields()` when getting avatar for user, thanks @Romarain [#2422](https://github.com/getgrav/grav/issues/2422) + * Fixed cached images not being updated when source image is modified + * Fixed deleting last list item in the form + * Fixed issue with `Utils::url()` method would append extra `base_url` if URL already included it + * Fixed `mkdir(...)` race condition + * Fixed `Obtaining write lock failed on file...` + * Fixed potential undefined property in `onPageNotFound` event handling + * Fixed some potential issues/bugs found by phpstan + * Fixed regression in GPM packages casted to Array (ref, getgrav/grav-plugin-admin@e3fc4ce) + * Fixed session_start(): Setting option 'session.name' failed [#2408](https://github.com/getgrav/grav/issues/2408) + * Fixed validation for select field type with selectize + * Fixed validation for boolean toggles + * Fixed non-namespaced exceptions in scheduler + * Fixed trailing slash redirect in multlang environment [#2350](https://github.com/getgrav/grav/issues/2350) + * Fixed some issues related to Medium objects losing query string attributes + * Broke out Medium timestamp so it's not cleared on `reset()`s + * Fixed issue with `redirect_trailing_slash` losing query string [#2269](https://github.com/getgrav/grav/issues/2269) + * Fixed failed login if user attempts to log in with upper case non-english letters + * Removed extra authenticated/authorized fields when saving existing user from a form + * Fixed `Grav\Framework\Route::__toString()` returning relative URL, not relative route + * Fixed handling of `append_url_extension` inside of `Page::templateFormat()` [#2264](https://github.com/getgrav/grav/issues/2264) + * Fixed a broken language string [#2261](https://github.com/getgrav/grav/issues/2261) + * Fixed clearing cache having no effect on Doctrine cache + * Fixed `Medium::relativePath()` for streams + * Fixed `Object` serialization breaking if overriding `jsonSerialize()` method + * Fixed `YamlFormatter::decode()` when calling `init_set()` with integer + * Fixed session throwing error in CLI if initialized + * Fixed `Uri::hasStandardPort()` to support reverse proxy configurations [#1786](https://github.com/getgrav/grav/issues/1786) + * Use `append_url_extension` from page header to set template format if set [#2604](https://github.com/getgrav/grav/pull/2064) + * Fixed some bugs in Grav environment selection logic + * Use login provider User avatar if set + * Fixed `Folder::doDelete($folder, false)` removing symlink when it should not + * Fixed asset manager to not add empty assets when they don't exist in the filesystem + * Update `script` and `style` Twig tags to use the new `Assets` classes + * Fixed asset pipeline to rewrite remote URLs as well as local [#2216](https://github.com/getgrav/grav/issues/2216) + +# v1.5.10 +## 03/21/2019 + +1. [](#new) + * Added new `deferred` Twig extension + +# v1.5.9 +## 03/20/2019 + +1. [](#new) + * Added new `onPageContent()` event for every call to `Page::content()` +1. [](#improved) + * Fixed phpdoc generation + * Updated vendor libraries + * Force Toolbox v1.4.2 +1. [](#bugfix) + * EXIF fix for streams + * Fix for User avatar not working due to uppercase or spaces in email [#2403](https://github.com/getgrav/grav/pull/2403) + # v1.5.8 ## 02/07/2019 @@ -16,7 +388,7 @@ * Updated vendor libraries 1. [](#bugfix) * Support spaces with filenames in responsive images [#2300](https://github.com/getgrav/grav/pull/2300) - + # v1.5.6 ## 12/14/2018 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da8896f..112fe4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ The issue tracker is the preferred channel for [bug reports](#bugs), requests](#pull-requests), but please respect the following restrictions: * Please **do not** use the issue tracker for support requests. Use - [the Forum](http://getgrav.org/forum) or [the Gitter chat](https://gitter.im/getgrav/grav). + [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/). @@ -110,7 +110,8 @@ Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. -**Please ask first** in [Slack](https://getgrav.org/slack) or in the Forum before embarking on any significant pull request (e.g. +**Please ask first** in [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/) +before embarking on any significant pull request (e.g. implementing features, refactoring code..), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. diff --git a/README.md b/README.md index 6ed3198..bf3a482 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,27 @@ # ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav -[![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) [![Build Status](https://travis-ci.org/getgrav/grav.svg?branch=develop)](https://travis-ci.org/getgrav/grav) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors) +[![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) + [![Build Status](https://travis-ci.org/getgrav/grav.svg?branch=develop)](https://travis-ci.org/getgrav/grav) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors) Grav is a **Fast**, **Simple**, and **Flexible**, file-based Web-platform. There is **Zero** installation required. Just extract the ZIP archive, and you are already up and running. It follows similar principles to other flat-file CMS platforms, but has a different design philosophy than most. Grav comes with a powerful **Package Management System** to allow for simple installation and upgrading of plugins and themes, as well as simple updating of Grav itself. The underlying architecture of Grav is designed to use well-established and _best-in-class_ technologies to ensure that Grav is simple to use and easy to extend. Some of these key technologies include: -* [Twig Templating](http://twig.sensiolabs.org/): for powerful control of the user interface -* [Markdown](http://en.wikipedia.org/wiki/Markdown): for easy content creation -* [YAML](http://yaml.org): for simple configuration -* [Parsedown](http://parsedown.org/): for fast Markdown and Markdown Extra support -* [Doctrine Cache](http://doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/reference/caching.html): layer for performance -* [Pimple Dependency Injection Container](http://pimple.sensiolabs.org/): for extensibility and maintainability -* [Symfony Event Dispatcher](http://symfony.com/doc/current/components/event_dispatcher/introduction.html): for plugin event handling -* [Symfony Console](http://symfony.com/doc/current/components/console/introduction.html): for CLI interface +* [Twig Templating](https://twig.sensiolabs.org/): for powerful control of the user interface +* [Markdown](https://en.wikipedia.org/wiki/Markdown): for easy content creation +* [YAML](https://yaml.org): for simple configuration +* [Parsedown](https://parsedown.org/): for fast Markdown and Markdown Extra support +* [Doctrine Cache](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/caching.html): layer for performance +* [Pimple Dependency Injection Container](https://pimple.sensiolabs.org/): for extensibility and maintainability +* [Symfony Event Dispatcher](https://symfony.com/doc/current/components/event_dispatcher/introduction.html): for plugin event handling +* [Symfony Console](https://symfony.com/doc/current/components/console/introduction.html): for CLI interface * [Gregwar Image Library](https://github.com/Gregwar/Image): for dynamic image manipulation # Requirements -- PHP 5.6.4 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements) +- PHP 7.1.3 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements) - Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements # QuickStart @@ -103,6 +106,7 @@ If you discover a possible security issue related to Grav or one of its plugins, * Dive into more [advanced](https://learn.getgrav.org/advanced) functions * Learn about the [Grav CLI](https://learn.getgrav.org/cli-console/grav-cli) * Review examples in the [Grav Cookbook](https://learn.getgrav.org/cookbook) +* More [Awesome Grav Stuff](https://github.com/getgrav/awesome-grav) # Backers Support Grav with a monthly donation to help us continue development. [[Become a backer](https://opencollective.com/grav#backer)] diff --git a/bin/composer.phar b/bin/composer.phar index 041775a..b8ce13f 100755 Binary files a/bin/composer.phar and b/bin/composer.phar differ diff --git a/bin/gpm b/bin/gpm index d4932a2..7ad1e4a 100755 --- a/bin/gpm +++ b/bin/gpm @@ -1,26 +1,24 @@ #!/usr/bin/env php arguments->add([ ] ]); $climate->arguments->parse(); -$environment = $climate->arguments->get('environment'); // Set up environment based on params. -Setup::$environment = $environment; +$environment = $climate->arguments->get('environment'); $grav = Grav::instance(array('loader' => $autoload)); -$grav['uri']->init(); +$grav->setup($environment); + $grav['config']->init(); -$grav['streams']; +$grav['uri']->init(); +$grav['users']; $app = new Application('Grav Package Manager', GRAV_VERSION); $app->addCommands(array( diff --git a/bin/grav b/bin/grav index e36f0cc..7120218 100755 --- a/bin/grav +++ b/bin/grav @@ -1,34 +1,52 @@ #!/usr/bin/env php arguments->add([ + 'environment' => [ + 'prefix' => 'e', + 'longPrefix' => 'env', + 'description' => 'Configuration Environment', + 'defaultValue' => 'localhost' + ] +]); +$climate->arguments->parse(); + +// Set up environment based on params. +$environment = $climate->arguments->get('environment'); + +$grav = Grav::instance(array('loader' => $autoload)); +$grav->setup($environment); + if (!ini_get('date.timezone')) { date_default_timezone_set('UTC'); } -if (!file_exists(ROOT_DIR . 'index.php')) { +if (!file_exists(GRAV_ROOT . '/index.php')) { exit('FATAL: Must be run from ROOT directory of Grav!'); } @@ -41,6 +59,9 @@ $app->addCommands(array( new \Grav\Console\Cli\ClearCacheCommand(), new \Grav\Console\Cli\BackupCommand(), new \Grav\Console\Cli\NewProjectCommand(), + new \Grav\Console\Cli\SchedulerCommand(), new \Grav\Console\Cli\SecurityCommand(), + new \Grav\Console\Cli\LogViewerCommand(), + new \Grav\Console\Cli\YamlLinterCommand(), )); $app->run(); diff --git a/bin/plugin b/bin/plugin index a8b089a..3c431e3 100755 --- a/bin/plugin +++ b/bin/plugin @@ -1,30 +1,28 @@ #!/usr/bin/env php arguments->add([ ] ]); $climate->arguments->parse(); + $environment = $climate->arguments->get('environment'); -// Set up environment based on params. -Setup::$environment = $environment; - $grav = Grav::instance(array('loader' => $autoload)); -$grav['uri']->init(); +$grav->setup($environment); + $grav['config']->init(); -$grav['streams']; +$grav['uri']->init(); +$grav['users']; $grav['plugins']->init(); $grav['themes']->init(); - $app = new Application('Grav Plugins Commands', GRAV_VERSION); $pattern = '([A-Z]\w+Command\.php)'; @@ -75,6 +72,7 @@ $argv = array_merge([$bin], $argv); $input = new ArgvInput($argv); +/** @var \Grav\Common\Data\Data $plugin */ $plugin = $grav['plugins']->get($name); $output = new ConsoleOutput(); @@ -83,13 +81,14 @@ $output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', nul if (!$name) { $output->writeln(''); - $output->writeln("Usage:"); + $output->writeln('Usage:'); $output->writeln(" {$bin} [slug] [command] [arguments]"); $output->writeln(''); - $output->writeln("Example:"); + $output->writeln('Example:'); $output->writeln(" {$bin} error log -l 1 --trace"); $list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 2]); + $total = 0; if (count($list)) { $available = []; $output->writeln(''); @@ -98,17 +97,31 @@ if (!$name) { $split = explode('/', $entry); $entry = array_shift($split); $index = str_pad($index++ + 1, 2, '0', STR_PAD_LEFT); - - if (in_array($entry, $available)) { + $total = str_pad($total++ + 1, 2, '0', STR_PAD_LEFT); + if (\in_array($entry, $available, true)) { + $total--; continue; } $available[] = $entry; - $output->writeln(' ' . $index . ". " . str_pad($entry, 15) . " ${bin} ${entry} list"); + $commands_count = $index - $total + 1; + $output->writeln(' ' . $total . '. ' . str_pad($entry, 15) . " {$bin} {$entry} list"); } } exit; +} else { + if (is_null($plugin)) { + $output->writeln(''); + $output->writeln("$name plugin not found"); + die; + } + + if (!$plugin->enabled) { + $output->writeln(''); + $output->writeln("$name not enabled"); + die; + } } if ($plugin === null) { diff --git a/composer.json b/composer.json index d56945e..60ca21a 100644 --- a/composer.json +++ b/composer.json @@ -6,47 +6,71 @@ "homepage": "https://getgrav.org", "license": "MIT", "require": { - "php": ">=5.6.4", - "twig/twig": "~1.24", - "erusev/parsedown": "1.6.4", - "erusev/parsedown-extra": "~0.7", - "symfony/yaml": "~3.4", - "symfony/console": "~3.4", - "symfony/event-dispatcher": "~3.4", - "symfony/var-dumper": "~3.4", - "symfony/polyfill-iconv": "~1.0", - "doctrine/cache": "^1.6", - "doctrine/collections": "^1.4", - "psr/simple-cache": "^1.0", - "psr/http-message": "^1.0", - "guzzlehttp/psr7": "^1.4", - "filp/whoops": "~2.0", - "matthiasmullie/minify": "^1.3", - "monolog/monolog": "~1.0", - "gregwar/image": "2.*", - "donatj/phpuseragentparser": "~0.3", - "pimple/pimple": "~3.2", - "rockettheme/toolbox": "~1.4", - "maximebf/debugbar": "~1.10", + "php": ">=7.1.3", + "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", "ext-curl": "*", "ext-zip": "*", - "ext-json": "*", - "league/climate": "^3.2", + "ext-dom": "*", + "symfony/polyfill-iconv": "^1.9", + "symfony/polyfill-php72": "^1.9", + "symfony/polyfill-php73": "^1.9", + + "psr/simple-cache": "^1.0", + "psr/http-message": "^1.0", + "psr/http-server-middleware": "^1.0", + + "kodus/psr7-server": "*", + "nyholm/psr7": "^1.0", + + "twig/twig": "~1.40", + "erusev/parsedown": "1.6.4", + "erusev/parsedown-extra": "~0.7", + "symfony/yaml": "~4.2.0", + "symfony/console": "~4.2.0", + "symfony/event-dispatcher": "~4.2.0", + "symfony/var-dumper": "~4.2.0", + "symfony/process": "~4.2.0", + "doctrine/cache": "^1.8", + "doctrine/collections": "^1.5", + "guzzlehttp/psr7": "^1.4", + "filp/whoops": "~2.2", + + "matthiasmullie/minify": "^1.3", + "monolog/monolog": "~1.0", + "gregwar/image": "2.*", + "donatj/phpuseragentparser": "~0.10", + "pimple/pimple": "~3.2", + "rockettheme/toolbox": "~1.4", + "maximebf/debugbar": "~1.15", + "league/climate": "^3.4", "antoligy/dom-string-iterators": "^1.0", - "miljar/php-exif": "^0.6.3", - "composer/ca-bundle": "^1.0" + "miljar/php-exif": "^0.6.4", + "composer/ca-bundle": "^1.0", + "dragonmantank/cron-expression": "^1.2", + "phive/twig-extensions-deferred": "^1.0", + "willdurand/negotiation": "^2.3" }, "require-dev": { - "codeception/codeception": "^2.1", - "phpunit/php-code-coverage": "~2.0", - "fzaninotto/faker": "^1.5", + "codeception/codeception": "^2.4", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-deprecation-rules": "^0.11.0", + "phpunit/php-code-coverage": "~6.0", + "fzaninotto/faker": "^1.8", "victorjonsson/markdowndocs": "dev-master" }, + "suggest": { + "ext-zend-opcache": "Recommended for better performance", + "ext-intl": "Recommended for multi-language sites", + "ext-memcache": "Needed to support Memcache servers", + "ext-memcached": "Needed to support Memcached servers", + "ext-redis": "Needed to support Redis servers" + }, "config": { + "apcu-autoloader": true, "platform": { - "php": "5.6.4" + "php": "7.1.3" } }, "repositories": [ @@ -65,7 +89,12 @@ "exclude": ["VERSION"] }, "scripts": { + "api-16": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.16.md", + "api-15": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.md", "post-create-project-cmd": "bin/grav install", + "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M", + "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M", + "test-plugins": "vendor/bin/phpstan analyse -l 0 -c ./tests/phpstan/plugins.neon user/plugins --memory-limit=256M", "test": "vendor/bin/codecept run unit", "test-windows": "vendor\\bin\\codecept run unit" }, diff --git a/composer.lock b/composer.lock index 7427b03..647b081 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": "544658e69ae737e742e014c6c674cc70", + "content-hash": "171fc8a6af028b50024e0f5f8c3eca82", "packages": [ { "name": "antoligy/dom-string-iterators", @@ -52,16 +52,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.4", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527" + "reference": "f26a67e397be0e5c00d7c52ec7b5010098e15ce5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527", - "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/f26a67e397be0e5c00d7c52ec7b5010098e15ce5", + "reference": "f26a67e397be0e5c00d7c52ec7b5010098e15ce5", "shasum": "" }, "require": { @@ -104,37 +104,42 @@ "ssl", "tls" ], - "time": "2019-08-30T08:44:50+00:00" + "time": "2019-08-02T09:05:43+00:00" }, { "name": "doctrine/cache", - "version": "v1.6.2", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b" + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b", - "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b", + "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57", + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57", "shasum": "" }, "require": { - "php": "~5.5|~7.0" + "php": "~7.1" }, "conflict": { "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0", - "predis/predis": "~1.0", - "satooshi/php-coveralls": "~0.6" + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^4.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -169,43 +174,45 @@ } ], "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org", "keywords": [ "cache", "caching" ], - "time": "2017-07-22T12:49:21+00:00" + "time": "2018-08-21T18:01:43+00:00" }, { "name": "doctrine/collections", - "version": "v1.4.0", + "version": "v1.6.2", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" + "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", + "url": "https://api.github.com/repos/doctrine/collections/zipball/c5e0bc17b1620e97c968ac409acbff28b8b850be", + "reference": "c5e0bc17b1620e97c968ac409acbff28b8b850be", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1.3" }, "require-dev": { - "doctrine/coding-standard": "~0.1@dev", - "phpunit/phpunit": "^5.7" + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan-shim": "^0.9.2", + "phpunit/phpunit": "^7.0", + "vimeo/psalm": "^3.2.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Collections\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Collections\\": "lib/Doctrine/Common/Collections" } }, "notification-url": "https://packagist.org/downloads/", @@ -234,14 +241,15 @@ "email": "schmittjoh@gmail.com" } ], - "description": "Collections Abstraction library", - "homepage": "http://www.doctrine-project.org", + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", "keywords": [ "array", "collections", - "iterator" + "iterators", + "php" ], - "time": "2017-01-03T10:49:41+00:00" + "time": "2019-06-09T13:48:14+00:00" }, { "name": "donatj/phpuseragentparser", @@ -294,6 +302,50 @@ ], "time": "2019-03-08T20:52:23+00:00" }, + { + "name": "dragonmantank/cron-expression", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "time": "2017-01-23T04:29:33+00:00" + }, { "name": "erusev/parsedown", "version": "1.6.4", @@ -428,8 +480,8 @@ "authors": [ { "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" + "role": "Developer", + "homepage": "https://github.com/filp" } ], "description": "php error handling for cool kids", @@ -611,21 +663,76 @@ "time": "2019-07-01T23:21:34+00:00" }, { - "name": "league/climate", - "version": "3.4.1", + "name": "kodus/psr7-server", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/climate.git", - "reference": "d657a19837c1f79a891381fb128b755aa3386381" + "url": "https://github.com/kodus/psr7-server.git", + "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/climate/zipball/d657a19837c1f79a891381fb128b755aa3386381", - "reference": "d657a19837c1f79a891381fb128b755aa3386381", + "url": "https://api.github.com/repos/kodus/psr7-server/zipball/dcfd0116451b0f0e7c6b23b831757ed288347278", + "reference": "dcfd0116451b0f0e7c6b23b831757ed288347278", "shasum": "" }, "require": { - "php": "^5.6|^7.0", + "php": "^7.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "replace": { + "nyholm/psr7-server": "^0.3" + }, + "require-dev": { + "nyholm/nsa": "^1.1", + "nyholm/psr7": "^1.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nyholm\\Psr7Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "Helper classes to handle PSR-7 server requests", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "time": "2019-06-17T10:48:13+00:00" + }, + { + "name": "league/climate", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/climate.git", + "reference": "0d2fdbf8829f60f6ba6433df68d6f3fe1271b8e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/climate/zipball/0d2fdbf8829f60f6ba6433df68d6f3fe1271b8e6", + "reference": "0d2fdbf8829f60f6ba6433df68d6f3fe1271b8e6", + "shasum": "" + }, + "require": { + "php": "^7.1", "psr/log": "^1.0", "seld/cli-prompt": "^1.0" }, @@ -650,15 +757,15 @@ "authors": [ { "name": "Craig Duncan", + "role": "Developer", "email": "git@duncanc.co.uk", - "homepage": "https://github.com/duncan3dc", - "role": "Developer" + "homepage": "https://github.com/duncan3dc" }, { "name": "Joe Tannenbaum", + "role": "Developer", "email": "hey@joe.codes", - "homepage": "http://joe.codes/", - "role": "Developer" + "homepage": "http://joe.codes/" } ], "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.", @@ -669,7 +776,7 @@ "php", "terminal" ], - "time": "2018-04-29T16:43:54+00:00" + "time": "2019-02-10T18:25:19+00:00" }, { "name": "matthiasmullie/minify", @@ -715,9 +822,9 @@ "authors": [ { "name": "Matthias Mullie", + "role": "Developer", "email": "minify@mullie.eu", - "homepage": "http://www.mullie.eu", - "role": "Developer" + "homepage": "http://www.mullie.eu" } ], "description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.", @@ -765,9 +872,9 @@ "authors": [ { "name": "Matthias Mullie", + "role": "Developer", "email": "pathconverter@mullie.eu", - "homepage": "http://www.mullie.eu", - "role": "Developer" + "homepage": "http://www.mullie.eu" } ], "description": "Relative path converter", @@ -883,8 +990,8 @@ "authors": [ { "name": "Tom Van Herreweghe", - "homepage": "http://theanalogguy.be", - "role": "Developer" + "role": "Developer", + "homepage": "http://theanalogguy.be" } ], "description": "Object-Oriented EXIF parsing", @@ -899,16 +1006,16 @@ }, { "name": "monolog/monolog", - "version": "1.25.1", + "version": "1.24.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf" + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf", - "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", "shasum": "" }, "require": { @@ -973,7 +1080,162 @@ "logging", "psr-3" ], - "time": "2019-09-06T13:49:17+00:00" + "time": "2018-11-05T09:00:11+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "701fe7ea8c12c07b985b156d589134d328160cf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/701fe7ea8c12c07b985b156d589134d328160cf7", + "reference": "701fe7ea8c12c07b985b156d589134d328160cf7", + "shasum": "" + }, + "require": { + "php": "^7.1", + "php-http/message-factory": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "dev-master", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^7.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "http://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "time": "2019-02-16T17:20:43+00:00" + }, + { + "name": "phive/twig-extensions-deferred", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/rybakit/twig-deferred-extension.git", + "reference": "5a2426d622afa74034e754ca5ea1d1ff7887627f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rybakit/twig-deferred-extension/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" + ], + "time": "2017-03-17T21:39:21+00:00" + }, + { + "name": "php-http/message-factory", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "reference": "a478cb11f66a6ac48d8954216cfed9aa06a501a1", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "time": "2015-12-19T14:08:53+00:00" }, { "name": "pimple/pimple", @@ -1025,6 +1287,52 @@ ], "time": "2018-01-21T07:42:36+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.0.0", @@ -1074,6 +1382,58 @@ ], "time": "2017-02-14T16:28:37+00:00" }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2019-04-30T12:38:16+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -1124,6 +1484,112 @@ ], "time": "2016-08-06T14:39:51+00:00" }, + { + "name": "psr/http-server-handler", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "time": "2018-10-30T16:46:14+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "time": "2018-10-30T17:12:04+00:00" + }, { "name": "psr/log", "version": "1.1.0", @@ -1359,21 +1825,21 @@ }, { "name": "symfony/console", - "version": "v3.4.31", + "version": "v4.2.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "4510f04e70344d70952566e4262a0b11df39cb10" + "reference": "fc2e274aade6567a750551942094b2145ade9b6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/4510f04e70344d70952566e4262a0b11df39cb10", - "reference": "4510f04e70344d70952566e4262a0b11df39cb10", + "url": "https://api.github.com/repos/symfony/console/zipball/fc2e274aade6567a750551942094b2145ade9b6c", + "reference": "fc2e274aade6567a750551942094b2145ade9b6c", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/debug": "~2.8|~3.0|~4.0", + "php": "^7.1.3", + "symfony/contracts": "^1.0", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -1385,11 +1851,11 @@ }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~3.3|~4.0", + "symfony/config": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", - "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/event-dispatcher": "~3.4|~4.0", "symfony/lock": "~3.4|~4.0", - "symfony/process": "~3.3|~4.0" + "symfony/process": "~3.4|~4.0" }, "suggest": { "psr/log": "For using the console logger", @@ -1400,7 +1866,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1427,44 +1893,57 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-07-24T17:13:20+00:00" }, { - "name": "symfony/debug", - "version": "v3.4.31", + "name": "symfony/contracts", + "version": "v1.1.5", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "0b600300918780001e2821db77bc28b677794486" + "url": "https://github.com/symfony/contracts.git", + "reference": "3f3f796d5f24a098a9da62828b8daa1b11494c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/0b600300918780001e2821db77bc28b677794486", - "reference": "0b600300918780001e2821db77bc28b677794486", + "url": "https://api.github.com/repos/symfony/contracts/zipball/3f3f796d5f24a098a9da62828b8daa1b11494c1b", + "reference": "3f3f796d5f24a098a9da62828b8daa1b11494c1b", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "psr/log": "~1.0" + "php": "^7.1.3", + "psr/cache": "^1.0", + "psr/container": "^1.0" }, - "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + "replace": { + "symfony/cache-contracts": "self.version", + "symfony/event-dispatcher-contracts": "self.version", + "symfony/http-client-contracts": "self.version", + "symfony/service-contracts": "self.version", + "symfony/translation-contracts": "self.version" }, "require-dev": { - "symfony/http-kernel": "~2.8|~3.0|~4.0" + "symfony/polyfill-intl-idn": "^1.10" + }, + "suggest": { + "psr/event-dispatcher": "When using the EventDispatcher contracts", + "symfony/cache-implementation": "", + "symfony/event-dispatcher-implementation": "", + "symfony/http-client-implementation": "", + "symfony/service-implementation": "", + "symfony/translation-implementation": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "1.1-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Debug\\": "" + "Symfony\\Contracts\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "**/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1473,44 +1952,53 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Debug Component", + "description": "A set of abstractions extracted out of the Symfony components", "homepage": "https://symfony.com", - "time": "2019-08-20T13:31:17+00:00" + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "time": "2019-06-20T06:46:26+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.4.31", + "version": "v4.2.11", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "3e922c4c3430b9de624e8a285dada5e61e230959" + "reference": "852548c7c704f14d2f6700c8d872a05bd2028732" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3e922c4c3430b9de624e8a285dada5e61e230959", - "reference": "3e922c4c3430b9de624e8a285dada5e61e230959", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/852548c7c704f14d2f6700c8d872a05bd2028732", + "reference": "852548c7c704f14d2f6700c8d872a05bd2028732", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3", + "symfony/contracts": "^1.0" }, "conflict": { - "symfony/dependency-injection": "<3.3" + "symfony/dependency-injection": "<3.4" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0|~4.0", - "symfony/dependency-injection": "~3.3|~4.0", - "symfony/expression-language": "~2.8|~3.0|~4.0", - "symfony/stopwatch": "~2.8|~3.0|~4.0" + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/stopwatch": "~3.4|~4.0" }, "suggest": { "symfony/dependency-injection": "", @@ -1519,7 +2007,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1546,7 +2034,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2019-08-23T08:05:57+00:00" + "time": "2019-06-26T06:46:55+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1725,39 +2213,208 @@ "time": "2019-08-06T08:03:45+00:00" }, { - "name": "symfony/var-dumper", - "version": "v3.4.31", + "name": "symfony/polyfill-php72", + "version": "v1.12.0", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "5408ad7194737ee1bc5ab7a9683fb6925f92c3e4" + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "04ce3335667451138df4307d6a9b61565560199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5408ad7194737ee1bc5ab7a9683fb6925f92c3e4", - "reference": "5408ad7194737ee1bc5ab7a9683fb6925f92c3e4", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", + "reference": "04ce3335667451138df4307d6a9b61565560199e", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.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": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188", + "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "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": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-08-06T08:03:45+00:00" + }, + { + "name": "symfony/process", + "version": "v4.2.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2", + "reference": "808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2019-05-30T16:06:08+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v4.2.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "4e18e041a477edbb8c54e053f179672f9413816c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4e18e041a477edbb8c54e053f179672f9413816c", + "reference": "4e18e041a477edbb8c54e053f179672f9413816c", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php72": "~1.5" }, "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", + "symfony/console": "<3.4" }, "require-dev": { "ext-iconv": "*", + "symfony/console": "~3.4|~4.0", + "symfony/process": "~3.4|~4.0", "twig/twig": "~1.34|~2.4" }, "suggest": { "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", "ext-intl": "To show region name in time zone dump", - "ext-symfony_debug": "" + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" }, + "bin": [ + "Resources/bin/var-dump-server" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1791,24 +2448,24 @@ "debug", "dump" ], - "time": "2019-08-26T07:50:50+00:00" + "time": "2019-07-27T06:42:33+00:00" }, { "name": "symfony/yaml", - "version": "v3.4.31", + "version": "v4.2.11", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "3dc414b7db30695bae671a1d86013d03f4ae9834" + "reference": "9468fef6f1c740b96935e9578560a9e9189ca154" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/3dc414b7db30695bae671a1d86013d03f4ae9834", - "reference": "3dc414b7db30695bae671a1d86013d03f4ae9834", + "url": "https://api.github.com/repos/symfony/yaml/zipball/9468fef6f1c740b96935e9578560a9e9189ca154", + "reference": "9468fef6f1c740b96935e9578560a9e9189ca154", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", + "php": "^7.1.3", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -1823,7 +2480,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1850,30 +2507,30 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2019-08-20T13:31:17+00:00" + "time": "2019-07-24T14:47:26+00:00" }, { "name": "twig/twig", - "version": "v1.42.3", + "version": "v1.42.2", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "201baee843e0ffe8b0b956f336dd42b2a92fae4e" + "reference": "21707d6ebd05476854805e4f91b836531941bcd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/201baee843e0ffe8b0b956f336dd42b2a92fae4e", - "reference": "201baee843e0ffe8b0b956f336dd42b2a92fae4e", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/21707d6ebd05476854805e4f91b836531941bcd4", + "reference": "21707d6ebd05476854805e4f91b836531941bcd4", "shasum": "" }, "require": { - "php": ">=5.5.0", + "php": ">=5.4.0", "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "psr/container": "^1.0", - "symfony/debug": "^3.4|^4.2", - "symfony/phpunit-bridge": "^4.4@dev|^5.0" + "symfony/debug": "^2.7", + "symfony/phpunit-bridge": "^3.4.19|^4.1.8|^5.0" }, "type": "library", "extra": { @@ -1896,19 +2553,19 @@ "authors": [ { "name": "Fabien Potencier", + "role": "Lead Developer", "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Twig Team", - "homepage": "https://twig.symfony.com/contributors", - "role": "Contributors" + "homepage": "http://fabien.potencier.org" }, { "name": "Armin Ronacher", - "email": "armin.ronacher@active-4.com", - "role": "Project Founder" + "role": "Project Founder", + "email": "armin.ronacher@active-4.com" + }, + { + "name": "Twig Team", + "role": "Contributors", + "homepage": "https://twig.symfony.com/contributors" } ], "description": "Twig, the flexible, fast, and secure template language for PHP", @@ -1916,7 +2573,59 @@ "keywords": [ "templating" ], - "time": "2019-08-24T12:51:03+00:00" + "time": "2019-06-18T15:35:16+00:00" + }, + { + "name": "willdurand/negotiation", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Negotiation.git", + "reference": "03436ededa67c6e83b9b12defac15384cb399dc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/03436ededa67c6e83b9b12defac15384cb399dc9", + "reference": "03436ededa67c6e83b9b12defac15384cb399dc9", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Negotiation\\": "src/Negotiation" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + } + ], + "description": "Content Negotiation tools for PHP provided as a standalone library.", + "homepage": "http://williamdurand.fr/Negotiation/", + "keywords": [ + "accept", + "content", + "format", + "header", + "negotiation" + ], + "time": "2017-05-14T17:21:12+00:00" } ], "packages-dev": [ @@ -2073,30 +2782,27 @@ }, { "name": "codeception/phpunit-wrapper", - "version": "6.0.10", + "version": "7.7.1", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "7057e599d97b02b4efb009681a43b327dbce138a" + "reference": "ab04a956264291505ea84998f43cf91639b4575d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/7057e599d97b02b4efb009681a43b327dbce138a", - "reference": "7057e599d97b02b4efb009681a43b327dbce138a", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/ab04a956264291505ea84998f43cf91639b4575d", + "reference": "ab04a956264291505ea84998f43cf91639b4575d", "shasum": "" }, "require": { - "phpunit/php-code-coverage": ">=2.2.4 <6.0", - "phpunit/phpunit": ">=4.8.28 <5.0.0 || >=5.6.3 <7.0", - "sebastian/comparator": ">1.1 <3.0", - "sebastian/diff": ">=1.4 <4.0" - }, - "replace": { - "codeception/phpunit-wrapper": "*" + "phpunit/php-code-coverage": "^6.0", + "phpunit/phpunit": "7.5.*", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0" }, "require-dev": { "codeception/specify": "*", - "vlucas/phpdotenv": "^2.4" + "vlucas/phpdotenv": "^3.0" }, "type": "library", "autoload": { @@ -2115,24 +2821,24 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2018-06-20T20:08:14+00:00" + "time": "2019-02-26T20:35:32+00:00" }, { "name": "codeception/stub", - "version": "2.0.4", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e" + "reference": "853657f988942f7afb69becf3fd0059f192c705a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/f50bc271f392a2836ff80690ce0c058efe1ae03e", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a", + "reference": "853657f988942f7afb69becf3fd0059f192c705a", "shasum": "" }, "require": { - "phpunit/phpunit": ">=4.8 <8.0" + "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3" }, "type": "library", "autoload": { @@ -2145,36 +2851,82 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-07-26T11:55:37+00:00" + "time": "2019-03-02T15:35:10+00:00" }, { - "name": "doctrine/instantiator", - "version": "1.0.5", + "name": "composer/xdebug-handler", + "version": "1.3.3", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f", + "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "time": "2019-05-27T17:52:04+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "a2c590166b2133a4633738648b6b064edae0814a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", + "reference": "a2c590166b2133a4633738648b6b064edae0814a", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -2194,12 +2946,12 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2019-03-17T17:37:11+00:00" }, { "name": "facebook/webdriver", @@ -2427,6 +3179,831 @@ ], "time": "2016-12-20T10:07:11+00:00" }, + { + "name": "jean85/pretty-package-versions", + "version": "1.2", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/75c7effcf3f77501d0e0caa75111aff4daa0dd48", + "reference": "75c7effcf3f77501d0e0caa75111aff4daa0dd48", + "shasum": "" + }, + "require": { + "ocramius/package-versions": "^1.2.0", + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A wrapper for ocramius/package-versions to get pretty versions strings", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "time": "2018-06-13T13:22:40+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", + "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2019-08-09T12:45:53+00:00" + }, + { + "name": "nette/bootstrap", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/nette/bootstrap.git", + "reference": "e1075af05c211915e03e0c86542f3ba5433df4a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/e1075af05c211915e03e0c86542f3ba5433df4a3", + "reference": "e1075af05c211915e03e0c86542f3ba5433df4a3", + "shasum": "" + }, + "require": { + "nette/di": "^3.0", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "require-dev": { + "latte/latte": "^2.2", + "nette/application": "^3.0", + "nette/caching": "^3.0", + "nette/database": "^3.0", + "nette/forms": "^3.0", + "nette/http": "^3.0", + "nette/mail": "^3.0", + "nette/robot-loader": "^3.0", + "nette/safe-stream": "^2.2", + "nette/security": "^3.0", + "nette/tester": "^2.0", + "tracy/tracy": "^2.6" + }, + "suggest": { + "nette/robot-loader": "to use Configurator::createRobotLoader()", + "tracy/tracy": "to use Configurator::enableTracy()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", + "homepage": "https://nette.org", + "keywords": [ + "bootstrapping", + "configurator", + "nette" + ], + "time": "2019-03-26T12:59:07+00:00" + }, + { + "name": "nette/di", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/nette/di.git", + "reference": "4aff517a1c6bb5c36fa09733d4cea089f529de6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/di/zipball/4aff517a1c6bb5c36fa09733d4cea089f529de6d", + "reference": "4aff517a1c6bb5c36fa09733d4cea089f529de6d", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/neon": "^3.0", + "nette/php-generator": "^3.2.2", + "nette/robot-loader": "^3.2", + "nette/schema": "^1.0", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "conflict": { + "nette/bootstrap": "<3.0" + }, + "require-dev": { + "nette/tester": "^2.2", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/compatibility.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.", + "homepage": "https://nette.org", + "keywords": [ + "compiled", + "di", + "dic", + "factory", + "ioc", + "nette", + "static" + ], + "time": "2019-08-07T12:11:33+00:00" + }, + { + "name": "nette/finder", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/nette/finder.git", + "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/finder/zipball/6be1b83ea68ac558aff189d640abe242e0306fe2", + "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4 || ~3.0.0", + "php": ">=7.1" + }, + "conflict": { + "nette/nette": "<2.2" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette Finder: find files and directories with an intuitive API.", + "homepage": "https://nette.org", + "keywords": [ + "filesystem", + "glob", + "iterator", + "nette" + ], + "time": "2019-02-28T18:13:25+00:00" + }, + { + "name": "nette/neon", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/nette/neon.git", + "reference": "cbff32059cbdd8720deccf9e9eace6ee516f02eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/neon/zipball/cbff32059cbdd8720deccf9e9eace6ee516f02eb", + "reference": "cbff32059cbdd8720deccf9e9eace6ee516f02eb", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": ">=7.0" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette NEON: encodes and decodes NEON file format.", + "homepage": "http://ne-on.org", + "keywords": [ + "export", + "import", + "neon", + "nette", + "yaml" + ], + "time": "2019-02-05T21:30:40+00:00" + }, + { + "name": "nette/php-generator", + "version": "v3.2.3", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/aea6e81437bb238e5f0e5b5ce06337433908e63b", + "reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b", + "shasum": "" + }, + "require": { + "nette/utils": "^2.4.2 || ~3.0.0", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.3 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "time": "2019-07-05T13:01:56+00:00" + }, + { + "name": "nette/robot-loader", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/nette/robot-loader.git", + "reference": "0712a0e39ae7956d6a94c0ab6ad41aa842544b5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/0712a0e39ae7956d6a94c0ab6ad41aa842544b5c", + "reference": "0712a0e39ae7956d6a94c0ab6ad41aa842544b5c", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "nette/finder": "^2.5", + "nette/utils": "^3.0", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.0", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "? Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.", + "homepage": "https://nette.org", + "keywords": [ + "autoload", + "class", + "interface", + "nette", + "trait" + ], + "time": "2019-03-08T21:57:24+00:00" + }, + { + "name": "nette/schema", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "6241d8d4da39e825dd6cb5bfbe4242912f4d7e4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/6241d8d4da39e825dd6cb5bfbe4242912f4d7e4d", + "reference": "6241d8d4da39e825dd6cb5bfbe4242912f4d7e4d", + "shasum": "" + }, + "require": { + "nette/utils": "^3.0.1", + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.2", + "tracy/tracy": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "time": "2019-04-03T15:53:25+00:00" + }, + { + "name": "nette/utils", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bd961f49b211997202bda1d0fbc410905be370d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bd961f49b211997202bda1d0fbc410905be370d4", + "reference": "bd961f49b211997202bda1d0fbc410905be370d4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "~2.0", + "tracy/tracy": "^2.3" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize() and toAscii()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-xml": "to use Strings::length() etc. when mbstring is not available" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "time": "2019-03-22T01:00:30+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.2.3", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "e612609022e935f3d0337c1295176505b41188c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/e612609022e935f3d0337c1295176505b41188c8", + "reference": "e612609022e935f3d0337c1295176505b41188c8", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2019-08-12T20:17:41+00:00" + }, + { + "name": "ocramius/package-versions", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/Ocramius/PackageVersions.git", + "reference": "a4d4b60d0e60da2487bd21a2c6ac089f85570dbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/a4d4b60d0e60da2487bd21a2c6ac089f85570dbb", + "reference": "a4d4b60d0e60da2487bd21a2c6ac089f85570dbb", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0.0", + "php": "^7.1.0" + }, + "require-dev": { + "composer/composer": "^1.6.3", + "doctrine/coding-standard": "^5.0.1", + "ext-zip": "*", + "infection/infection": "^0.7.1", + "phpunit/phpunit": "^7.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "time": "2019-02-21T12:16:21+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "role": "Developer", + "email": "arne@blankerts.de" + }, + { + "name": "Sebastian Heuer", + "role": "Developer", + "email": "sebastian@phpeople.de" + }, + { + "name": "Sebastian Bergmann", + "role": "Developer", + "email": "sebastian@phpunit.de" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "role": "Developer", + "email": "arne@blankerts.de" + }, + { + "name": "Sebastian Heuer", + "role": "Developer", + "email": "sebastian@phpeople.de" + }, + { + "name": "Sebastian Bergmann", + "role": "Developer", + "email": "sebastian@phpunit.de" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "1.0.1", @@ -2483,29 +4060,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.3.2", + "version": "4.3.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" + "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", - "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", + "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "php": "^7.0", "phpdocumentor/reflection-common": "^1.0.0", "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, "autoload": { "psr-4": { "phpDocumentor\\Reflection\\": [ @@ -2524,7 +4107,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-10T14:09:06+00:00" + "time": "2019-04-30T17:48:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2637,40 +4220,214 @@ "time": "2019-06-13T12:50:23+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "2.2.4", + "name": "phpstan/phpdoc-parser", + "version": "0.3.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/8c4ef2aefd9788238897b678a985e1d5c8df6db4", + "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4", "shasum": "" }, "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" + "php": "~7.1" }, "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.2.1", - "ext-xmlwriter": "*" + "consistence/coding-standard": "^3.5", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan": "^0.10", + "phpunit/phpunit": "^6.3", + "slevomat/coding-standard": "^4.7.2", + "squizlabs/php_codesniffer": "^3.3.2", + "symfony/process": "^3.4 || ^4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "time": "2019-06-07T19:13:52+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "0.11.15", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "1be5b3a706db16ac472a4c40ec03cf4c810b118d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1be5b3a706db16ac472a4c40ec03cf4c810b118d", + "reference": "1be5b3a706db16ac472a4c40ec03cf4c810b118d", + "shasum": "" + }, + "require": { + "composer/xdebug-handler": "^1.3.0", + "jean85/pretty-package-versions": "^1.0.3", + "nette/bootstrap": "^2.4 || ^3.0", + "nette/di": "^2.4.7 || ^3.0", + "nette/robot-loader": "^3.0.1", + "nette/schema": "^1.0", + "nette/utils": "^2.4.5 || ^3.0", + "nikic/php-parser": "^4.2.3", + "php": "~7.1", + "phpstan/phpdoc-parser": "^0.3.5", + "symfony/console": "~3.2 || ~4.0", + "symfony/finder": "~3.2 || ~4.0" + }, + "conflict": { + "symfony/console": "3.4.16 || 4.1.5" + }, + "require-dev": { + "brianium/paratest": "^2.0 || ^3.0", + "consistence/coding-standard": "^3.5", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "ext-intl": "*", + "ext-mysqli": "*", + "ext-simplexml": "*", + "ext-soap": "*", + "ext-zip": "*", + "jakub-onderka/php-parallel-lint": "^1.0", + "localheinz/composer-normalize": "^1.1.0", + "phing/phing": "^2.16.0", + "phpstan/phpstan-deprecation-rules": "^0.11", + "phpstan/phpstan-php-parser": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.5.14 || ^8.0", + "slevomat/coding-standard": "^4.7.2", + "squizlabs/php_codesniffer": "^3.3.2" + }, + "bin": [ + "bin/phpstan" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.11-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": [ + "src/", + "build/PHPStan" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "time": "2019-08-18T20:51:53+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "0.11.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "5685fe48873efc5af1f2cc95d9c1b8ae82c728fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/5685fe48873efc5af1f2cc95d9c1b8ae82c728fe", + "reference": "5685fe48873efc5af1f2cc95d9c1b8ae82c728fe", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.0", + "php": "~7.1", + "phpstan/phpstan": "^0.11.8" + }, + "require-dev": { + "consistence/coding-standard": "^3.0.1", + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "jakub-onderka/php-parallel-lint": "^1.0", + "phing/phing": "^2.16.0", + "phpstan/phpstan-phpunit": "^0.11", + "phpunit/phpunit": "^7.0", + "slevomat/coding-standard": "^4.5.2" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "0.11-dev" + }, + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "time": "2019-05-28T19:54:04+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.1-dev" } }, "autoload": { @@ -2685,8 +4442,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", @@ -2696,29 +4453,32 @@ "testing", "xunit" ], - "time": "2015-10-06T15:47:00+00:00" + "time": "2018-10-31T16:06:48+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.5", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "reference": "050bedf145a257b1ff02746c31894800e5122946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -2733,8 +4493,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "FilterIterator implementation that filters files based on a list of suffixes.", @@ -2743,7 +4503,7 @@ "filesystem", "iterator" ], - "time": "2017-11-27T13:52:08+00:00" + "time": "2018-09-13T20:33:42+00:00" }, { "name": "phpunit/php-text-template", @@ -2775,8 +4535,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "Simple template engine.", @@ -2788,28 +4548,28 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.9", + "version": "2.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", - "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -2824,8 +4584,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "Utility class for timing", @@ -2833,33 +4593,33 @@ "keywords": [ "timer" ], - "time": "2017-02-26T11:10:40+00:00" + "time": "2019-06-07T04:22:29+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.12", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" + "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", - "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a", + "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.3.3" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2882,45 +4642,57 @@ "keywords": [ "tokenizer" ], - "time": "2017-12-04T08:55:13+00:00" + "time": "2019-07-25T05:29:42+00:00" }, { "name": "phpunit/phpunit", - "version": "4.8.36", + "version": "7.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" + "reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", - "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2834789aeb9ac182ad69bfdf9ae91856a59945ff", + "reference": "2834789aeb9ac182ad69bfdf9ae91856a59945ff", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.1", "ext-dom": "*", "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.2.2", - "sebastian/diff": "~1.2", - "sebastian/environment": "~1.3", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", - "symfony/yaml": "~2.1|~3.0" + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" + }, + "require-dev": { + "ext-pdo": "*" }, "suggest": { - "phpunit/php-invoker": "~1.1" + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" }, "bin": [ "phpunit" @@ -2928,7 +4700,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.8.x-dev" + "dev-master": "7.5-dev" } }, "autoload": { @@ -2943,8 +4715,8 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "The PHP Unit Testing framework.", @@ -2954,38 +4726,32 @@ "testing", "xunit" ], - "time": "2017-06-21T08:07:12+00:00" + "time": "2019-07-15T06:24:08+00:00" }, { - "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", - "phpunit/php-text-template": "~1.2", - "sebastian/exporter": "~1.2" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "suggest": { - "ext-soap": "*" + "phpunit/phpunit": "^5.7 || ^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -3000,45 +4766,39 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "abandoned": true, - "time": "2015-10-02T06:51:40+00:00" + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" }, { "name": "sebastian/comparator", - "version": "1.2.4", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" + "php": "^7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3069,38 +4829,39 @@ } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", "equality" ], - "time": "2017-01-29T09:50:25+00:00" + "time": "2018-07-12T15:12:46+00:00" }, { "name": "sebastian/diff", - "version": "1.4.3", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3125,34 +4886,40 @@ "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ - "diff" + "diff", + "udiff", + "unidiff", + "unified diff" ], - "time": "2017-05-22T07:24:03+00:00" + "time": "2019-02-04T06:01:07+00:00" }, { "name": "sebastian/environment", - "version": "1.3.8", + "version": "4.2.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", + "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3177,34 +4944,34 @@ "environment", "hhvm" ], - "time": "2016-08-18T05:49:44+00:00" + "time": "2019-05-05T09:05:15+00:00" }, { "name": "sebastian/exporter", - "version": "1.2.2", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + "reference": "06a9a5947f47b3029d76118eb5c22802e5869687" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687", + "reference": "06a9a5947f47b3029d76118eb5c22802e5869687", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" + "php": "^7.0", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -3217,6 +4984,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -3225,17 +4996,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -3244,27 +5011,27 @@ "export", "exporter" ], - "time": "2016-06-17T09:04:28+00:00" + "time": "2019-08-11T12:43:14+00:00" }, { "name": "sebastian/global-state", - "version": "1.1.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-uopz": "*" @@ -3272,7 +5039,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -3295,32 +5062,124 @@ "keywords": [ "global state" ], - "time": "2015-10-12T03:26:01+00:00" + "time": "2017-04-27T15:39:26+00:00" }, { - "name": "sebastian/recursion-context", - "version": "1.0.5", + "name": "sebastian/object-enumerator", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", - "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -3348,23 +5207,31 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-10-03T07:41:43+00:00" + "time": "2017-03-03T06:23:57+00:00" }, { - "name": "sebastian/version", - "version": "1.0.6", + "name": "sebastian/resource-operations", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", "shasum": "" }, + "require": { + "php": "^7.1" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -3377,35 +5244,79 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2018-10-04T04:07:39+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "role": "lead", + "email": "sebastian@phpunit.de" } ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21T13:59:46+00:00" + "time": "2016-10-03T07:35:21+00:00" }, { "name": "symfony/browser-kit", - "version": "v3.4.31", + "version": "v4.3.3", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "1a3406a6b9b61b492592a816ca056afcc9e976c0" + "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/1a3406a6b9b61b492592a816ca056afcc9e976c0", - "reference": "1a3406a6b9b61b492592a816ca056afcc9e976c0", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/a29dd02a1f3f81b9a15c7730cc3226718ddb55ca", + "reference": "a29dd02a1f3f81b9a15c7730cc3226718ddb55ca", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", - "symfony/dom-crawler": "~2.8|~3.0|~4.0" + "php": "^7.1.3", + "symfony/dom-crawler": "~3.4|~4.0" }, "require-dev": { - "symfony/css-selector": "~2.8|~3.0|~4.0", - "symfony/process": "~2.8|~3.0|~4.0" + "symfony/css-selector": "~3.4|~4.0", + "symfony/http-client": "^4.3", + "symfony/mime": "^4.3", + "symfony/process": "~3.4|~4.0" }, "suggest": { "symfony/process": "" @@ -3413,7 +5324,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -3440,29 +5351,29 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-06-11T15:41:59+00:00" }, { "name": "symfony/css-selector", - "version": "v3.4.31", + "version": "v4.3.3", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41" + "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/e18c5c4b35e7f17513448a25d02f7af34a4bdb41", - "reference": "e18c5c4b35e7f17513448a25d02f7af34a4bdb41", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/105c98bb0c5d8635bea056135304bd8edcc42b4d", + "reference": "105c98bb0c5d8635bea056135304bd8edcc42b4d", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -3493,29 +5404,33 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2019-08-20T13:31:17+00:00" + "time": "2019-01-16T21:53:39+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.4.31", + "version": "v4.3.3", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "8558d1bc4554f5cb0b66e50377457967a8969263" + "reference": "291397232a2eefb3347eaab9170409981eaad0e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/8558d1bc4554f5cb0b66e50377457967a8969263", - "reference": "8558d1bc4554f5cb0b66e50377457967a8969263", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/291397232a2eefb3347eaab9170409981eaad0e2", + "reference": "291397232a2eefb3347eaab9170409981eaad0e2", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8", + "php": "^7.1.3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "masterminds/html5": "<2.6" + }, "require-dev": { - "symfony/css-selector": "~2.8|~3.0|~4.0" + "masterminds/html5": "^2.6", + "symfony/css-selector": "~3.4|~4.0" }, "suggest": { "symfony/css-selector": "" @@ -3523,7 +5438,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -3550,29 +5465,29 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "time": "2019-06-13T11:03:18+00:00" }, { "name": "symfony/finder", - "version": "v3.4.31", + "version": "v4.3.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1fcad80b440abcd1451767349906b6f9d3961d37" + "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1fcad80b440abcd1451767349906b6f9d3961d37", - "reference": "1fcad80b440abcd1451767349906b6f9d3961d37", + "url": "https://api.github.com/repos/symfony/finder/zipball/9638d41e3729459860bb96f6247ccb61faaa45f2", + "reference": "9638d41e3729459860bb96f6247ccb61faaa45f2", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -3599,56 +5514,47 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-08-14T09:39:58+00:00" + "time": "2019-06-28T13:16:30+00:00" }, { - "name": "symfony/process", - "version": "v3.4.31", + "name": "theseer/tokenizer", + "version": "1.1.3", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d822cb654000a95b7855362c0d5b127f6a6d8baa", - "reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Arne Blankerts", + "role": "Developer", + "email": "arne@blankerts.de" } ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2019-08-26T07:52:58+00:00" + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2019-06-13T22:48:21+00:00" }, { "name": "victorjonsson/markdowndocs", @@ -3698,16 +5604,16 @@ }, { "name": "webmozart/assert", - "version": "1.5.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", + "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", "shasum": "" }, "require": { @@ -3715,7 +5621,8 @@ "symfony/polyfill-ctype": "^1.8" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" }, "type": "library", "extra": { @@ -3744,7 +5651,7 @@ "check", "validate" ], - "time": "2019-08-24T08:43:50+00:00" + "time": "2018-12-25T11:19:39+00:00" } ], "aliases": [], @@ -3755,15 +5662,16 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.6.4", + "php": ">=7.1.3", + "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", "ext-curl": "*", "ext-zip": "*", - "ext-json": "*" + "ext-dom": "*" }, "platform-dev": [], "platform-overrides": { - "php": "5.6.4" + "php": "7.1.3" } } diff --git a/fixperms.sh b/fixperms.sh deleted file mode 100644 index 38890c7..0000000 --- a/fixperms.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -chgrp www-data . -chgrp -R www-data * -sh -c "find . -type f | xargs chmod 664" -sh -c "find ./bin -type f | xargs chmod 775" -sh -c "find . -type d | xargs chmod 775" -sh -c "find . -type d | xargs chmod +s" -sh -c "umask 0002" diff --git a/index.php b/index.php index 75d0cfe..64154df 100644 --- a/index.php +++ b/index.php @@ -9,35 +9,34 @@ namespace Grav; -define('GRAV_PHP_MIN', '5.6.4'); - -// Ensure vendor libraries exist -$autoload = __DIR__ . '/vendor/autoload.php'; -if (!is_file($autoload)) { - die("Please run: bin/grav install"); -} - -if (PHP_SAPI === 'cli-server') { - if (!isset($_SERVER['PHP_CLI_ROUTER'])) { - die("PHP webserver requires a router to run Grav, please use:
php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php
"); - } -} - -use Grav\Common\Grav; -use RocketTheme\Toolbox\Event\Event; +\define('GRAV_REQUEST_TIME', microtime(true)); +\define('GRAV_PHP_MIN', '7.1.3'); if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) { die(sprintf('You are running PHP %s, but Grav needs at least PHP %s to run.', $ver, $req)); } +if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) { + die("PHP webserver requires a router to run Grav, please use:
php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php
"); +} + +// Ensure vendor libraries exist +$autoload = __DIR__ . '/vendor/autoload.php'; +if (!is_file($autoload)) { + die('Please run: bin/grav install'); +} + // Register the auto-loader. $loader = require $autoload; +use Grav\Common\Grav; +use RocketTheme\Toolbox\Event\Event; + // Set timezone to default, falls back to system if php.ini not set date_default_timezone_set(@date_default_timezone_get()); // Set internal encoding if mbstring loaded -if (!extension_loaded('mbstring')) { +if (!\extension_loaded('mbstring')) { die("'mbstring' extension is not loaded. This is required for Grav to run correctly"); } mb_internal_encoding('UTF-8'); @@ -52,6 +51,9 @@ $grav = Grav::instance( // Process the page try { $grav->process(); +} catch (\Error $e) { + $grav->fireEvent('onFatalException', new Event(array('exception' => $e))); + throw $e; } catch (\Exception $e) { $grav->fireEvent('onFatalException', new Event(array('exception' => $e))); throw $e; diff --git a/now.json b/now.json new file mode 100644 index 0000000..fe7b94b --- /dev/null +++ b/now.json @@ -0,0 +1,4 @@ + { + "version": 2, + "builds": [{ "src": "*.php", "use": "@now/php" }] +} diff --git a/robots.txt b/robots.txt index 41ea9da..96406e4 100644 --- a/robots.txt +++ b/robots.txt @@ -10,3 +10,4 @@ Disallow: /user/ Allow: /user/pages/ Allow: /user/themes/ Allow: /user/images/ +Allow: / diff --git a/system/assets/debugger.css b/system/assets/debugger.css index 556da6a..475dca9 100644 --- a/system/assets/debugger.css +++ b/system/assets/debugger.css @@ -30,9 +30,25 @@ div.phpdebugbar { } .phpdebugbar .phpdebugbar-widgets-toolbar { + border-top: 1px solid #ddd; padding-left: 5px; + padding-right: 2px; + padding-top: 2px; + background-color: #fafafa !important; + width: auto !important; + left: 0; + right: 0; } +.phpdebugbar .phpdebugbar-widgets-toolbar input { + background: transparent !important; +} + +.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter { + +} + + .phpdebugbar input[type=text] { padding: 0; display: inline; diff --git a/system/assets/grav.png b/system/assets/grav.png index 3379934..9ae6d97 100644 Binary files a/system/assets/grav.png and b/system/assets/grav.png differ diff --git a/system/blueprints/config/backups.yaml b/system/blueprints/config/backups.yaml new file mode 100644 index 0000000..03c7806 --- /dev/null +++ b/system/blueprints/config/backups.yaml @@ -0,0 +1,125 @@ +title: PLUGIN_ADMIN.BACKUPS + +form: + validation: loose + + fields: + history_title: + type: section + title: PLUGIN_ADMIN.BACKUPS_HISTORY + underline: true + + history: + type: backupshistory + + config_title: + type: section + title: PLUGIN_ADMIN.BACKUPS_PURGE_CONFIG + underline: true + + purge.trigger: + type: select + label: PLUGIN_ADMIN.BACKUPS_STORAGE_PURGE_TRIGGER + size: medium + default: space + options: + space: Maximum Backup Space + number: Maximum Number of Backups + time: maximum Retention Time + validate: + required: true + + purge.max_backups_count: + type: number + label: PLUGIN_ADMIN.BACKUPS_MAX_COUNT + default: 25 + size: x-small + help: PLUGIN_ADMIN.BACKUPS_MAX_COUNT + validate: + min: 0 + type: number + required: true + message: Must be a number 0 or greater + + purge.max_backups_space: + type: number + label: PLUGIN_ADMIN.BACKUPS_MAX_SPACE + append: in GB + size: x-small + default: 5 + validate: + min: 1 + type: number + required: true + message: Space must be 1GB or greater + + purge.max_backups_time: + type: number + label: PLUGIN_ADMIN.BACKUPS_MAX_RETENTION_TIME + append: PLUGIN_ADMIN.BACKUPS_MAX_RETENTION_TIME_APPEND + size: x-small + default: 365 + validate: + min: 7 + type: number + required: true + message: Rentenion days must be 7 or greater + + profiles_title: + type: section + title: PLUGIN_ADMIN.BACKUPS_PROFILES + underline: true + + profiles: + type: list + style: vertical + label: + classes: backups-list compact + sort: false + + fields: + .name: + type: text + label: PLUGIN_ADMIN.NAME + placeholder: PLUGIN_ADMIN.BACKUPS_PROFILE_NAME + validate: + max: 20 + message: 'Name must be less than 20 characters' + required: true + .root: + type: text + label: PLUGIN_ADMIN.BACKUPS_PROFILE_ROOT_FOLDER + help: PLUGIN_ADMIN.BACKUPS_PROFILE_ROOT_FOLDER_HELP + placeholder: '/' + default: '/' + validate: + required: true + .exclude_paths: + type: textarea + label: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_PATHS + rows: 5 + placeholder: "/backup\r/cache\r/images\r/logs\r/tmp" + help: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_PATHS_HELP + .exclude_files: + type: textarea + label: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_FILES + rows: 5 + placeholder: ".DS_Store\r.git\r.svn\r.hg\r.idea\r.vscode\rnode_modules" + help: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_FILES_HELP + .schedule: + type: toggle + label: PLUGIN_ADMIN.BACKUPS_PROFILE_SCHEDULE + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + .schedule_at: + type: cron + label: PLUGIN_ADMIN.BACKUPS_PROFILE_SCHEDULE_AT + default: '* 3 * * *' + validate: + required: true + diff --git a/system/blueprints/config/scheduler.yaml b/system/blueprints/config/scheduler.yaml new file mode 100644 index 0000000..c145045 --- /dev/null +++ b/system/blueprints/config/scheduler.yaml @@ -0,0 +1,76 @@ +title: PLUGIN_ADMIN.SCHEDULER + +form: + validation: loose + + fields: + + status_title: + type: section + title: PLUGIN_ADMIN.SCHEDULER_STATUS + underline: true + + status: + type: cronstatus + validate: + type: commalist + + jobs_title: + type: section + title: PLUGIN_ADMIN.SCHEDULER_JOBS + underline: true + + custom_jobs: + type: list + style: vertical + label: + classes: cron-job-list compact + key: id + fields: + .id: + type: key + label: ID + placeholder: 'process-name' + validate: + required: true + pattern: '[a-zа-я0-9_\-]+' + max: 20 + message: 'ID must be lowercase with dashes/underscores only and less than 20 characters' + .command: + type: text + label: PLUGIN_ADMIN.COMMAND + placeholder: 'cd ~;ls -lah;' + validate: + required: true + .args: + type: text + label: PLUGIN_ADMIN.EXTRA_ARGUMENTS + .at: + type: cron + label: PLUGIN_ADMIN.SCHEDULER_RUNAT + help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP + placeholder: '* * * * *' + validate: + required: true + .output: + type: text + label: PLUGIN_ADMIN.SCHEDULER_OUTPUT + help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_HELP + placeholder: 'logs/ls-cron.out' + .output_mode: + type: select + label: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE + help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE_HELP + default: append + options: + append: Append + overwrite: Overwrite + .email: + type: text + label: PLUGIN_ADMIN.SCHEDULER_EMAIL + help: PLUGIN_ADMIN.SCHEDULER_EMAIL_HELP + placeholder: 'notifications@yoursite.com' + + + + diff --git a/system/blueprints/config/security.yaml b/system/blueprints/config/security.yaml index 9f72416..e0296be 100644 --- a/system/blueprints/config/security.yaml +++ b/system/blueprints/config/security.yaml @@ -41,6 +41,14 @@ form: validate: type: bool + xss_invalid_protocols: + type: selectize + size: large + label: PLUGIN_ADMIN.XSS_INVALID_PROTOCOLS_LIST + classes: fancy + validate: + type: commalist + xss_enabled.moz_binding: type: toggle label: PLUGIN_ADMIN.XSS_MOZ_BINDINGS diff --git a/system/blueprints/config/site.yaml b/system/blueprints/config/site.yaml index 57fd3c5..6603823 100644 --- a/system/blueprints/config/site.yaml +++ b/system/blueprints/config/site.yaml @@ -65,7 +65,7 @@ form: summary.size: type: text - size: x-small + size: small append: PLUGIN_ADMIN.CHARACTERS label: PLUGIN_ADMIN.SUMMARY_SIZE help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index 5bc2e49..f67b92c 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -4,1241 +4,1407 @@ form: validation: loose fields: - content: - type: section - title: PLUGIN_ADMIN.CONTENT - underline: true + system_tabs: + type: tabs + classes: side-tabs fields: - home.alias: - type: pages - size: large - classes: fancy - label: PLUGIN_ADMIN.HOME_PAGE - show_all: false - show_modular: false - show_root: false - show_slug: true - help: PLUGIN_ADMIN.HOME_PAGE_HELP - - home.hide_in_urls: - type: toggle - label: PLUGIN_ADMIN.HIDE_HOME_IN_URLS - help: PLUGIN_ADMIN.HIDE_HOME_IN_URLS_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - - pages.theme: - type: themeselect - classes: fancy - selectize: true - size: medium - label: PLUGIN_ADMIN.DEFAULT_THEME - help: PLUGIN_ADMIN.DEFAULT_THEME_HELP - - pages.process: - type: checkboxes - label: PLUGIN_ADMIN.PROCESS - help: PLUGIN_ADMIN.PROCESS_HELP - default: [markdown: true, twig: true] - options: - markdown: Markdown - twig: Twig - use: keys - - timezone: - type: select - label: PLUGIN_ADMIN.TIMEZONE - size: medium - classes: fancy - help: PLUGIN_ADMIN.TIMEZONE_HELP - data-options@: '\Grav\Common\Utils::timezones' - default: '' - options: - '': 'Default (Server Timezone)' - - pages.dateformat.default: - type: select - size: medium - selectize: - create: true - label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT - help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP - placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER - data-options@: '\Grav\Common\Utils::dateFormats' - validate: - type: string - - pages.dateformat.short: - type: dateformat - size: medium - classes: fancy - label: PLUGIN_ADMIN.SHORT_DATE_FORMAT - help: PLUGIN_ADMIN.SHORT_DATE_FORMAT_HELP - default: "jS M Y" - options: - "F jS \\a\\t g:ia": Date1 - "l jS \\of F g:i A": Date2 - "D, d M Y G:i:s": Date3 - "d-m-y G:i": Date4 - "jS M Y": Date5 - - pages.dateformat.long: - type: dateformat - size: medium - classes: fancy - label: PLUGIN_ADMIN.LONG_DATE_FORMAT - help: PLUGIN_ADMIN.LONG_DATE_FORMAT_HELP - options: - "F jS \\a\\t g:ia": Date1 - "l jS \\of F g:i A": Date2 - "D, d M Y G:i:s": Date3 - "d-m-y G:i": Date4 - "jS M Y": Date5 - - pages.order.by: - type: select - size: large - classes: fancy - label: PLUGIN_ADMIN.DEFAULT_ORDERING - help: PLUGIN_ADMIN.DEFAULT_ORDERING_HELP - options: - default: PLUGIN_ADMIN.DEFAULT_ORDERING_DEFAULT - folder: PLUGIN_ADMIN.DEFAULT_ORDERING_FOLDER - title: PLUGIN_ADMIN.DEFAULT_ORDERING_TITLE - date: PLUGIN_ADMIN.DEFAULT_ORDERING_DATE - - pages.order.dir: - type: toggle - label: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION - highlight: asc - default: desc - help: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION_HELP - options: - asc: PLUGIN_ADMIN.ASCENDING - desc: PLUGIN_ADMIN.DESCENDING - - pages.list.count: - type: text - size: x-small - append: PLUGIN_ADMIN.PAGES - label: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT - help: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT_HELP - validate: - type: number - min: 1 - - pages.publish_dates: - type: toggle - label: PLUGIN_ADMIN.DATE_BASED_PUBLISHING - help: PLUGIN_ADMIN.DATE_BASED_PUBLISHING_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.events: - type: checkboxes - label: PLUGIN_ADMIN.EVENTS - help: PLUGIN_ADMIN.EVENTS_HELP - default: [page: true, twig: true] - options: - page: Page Events - twig: Twig Events - use: keys - - pages.append_url_extension: - type: text - size: x-small - placeholder: "e.g. .html" - label: PLUGIN_ADMIN.APPEND_URL_EXT - help: PLUGIN_ADMIN.APPEND_URL_EXT_HELP - - pages.redirect_default_route: - type: toggle - label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE - help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.redirect_default_code: - type: select - size: medium - classes: fancy - label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE - help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP - options: - 301: 301 - Permanent - 302: 302 - Found - 303: 303 - Other - 304: 304 - Not Modified - - pages.redirect_trailing_slash: - type: toggle - label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH - help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.ignore_hidden: - type: toggle - label: PLUGIN_ADMIN.IGNORE_HIDDEN - help: PLUGIN_ADMIN.IGNORE_HIDDEN_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.ignore_files: - type: selectize - size: large - label: PLUGIN_ADMIN.IGNORE_FILES - help: PLUGIN_ADMIN.IGNORE_FILES_HELP - classes: fancy - validate: - type: commalist - - pages.ignore_folders: - type: selectize - size: large - label: PLUGIN_ADMIN.IGNORE_FOLDERS - help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP - classes: fancy - validate: - type: commalist - - pages.url_taxonomy_filters: - type: toggle - label: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS - help: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.twig_first: - type: toggle - label: PLUGIN_ADMIN.TWIG_FIRST - help: PLUGIN_ADMIN.TWIG_FIRST_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.never_cache_twig: - type: toggle - label: PLUGIN_ADMIN.NEVER_CACHE_TWIG - help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.frontmatter.process_twig: - type: toggle - label: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG - help: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - pages.frontmatter.ignore_fields: - type: selectize - size: large - placeholder: "e.g. forms" - label: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS - help: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS_HELP - classes: fancy - validate: - type: commalist - - languages: - type: section - title: PLUGIN_ADMIN.LANGUAGES - underline: true - - fields: - - languages.supported: - type: selectize - size: large - placeholder: "e.g. en, fr" - label: PLUGIN_ADMIN.SUPPORTED - help: PLUGIN_ADMIN.SUPPORTED_HELP - classes: fancy - validate: - type: commalist - - languages.include_default_lang: - type: toggle - label: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG - help: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - - languages.translations: - type: toggle - label: PLUGIN_ADMIN.TRANSLATIONS_ENABLED - help: PLUGIN_ADMIN.TRANSLATIONS_ENABLED_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - languages.translations_fallback: - type: toggle - label: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK - help: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - languages.session_store_active: - type: toggle - label: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION - help: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - languages.http_accept_language: - type: toggle - label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE - help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - languages.override_locale: - type: toggle - label: PLUGIN_ADMIN.OVERRIDE_LOCALE - help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - http_headers: - type: section - title: PLUGIN_ADMIN.HTTP_HEADERS - underline: true - - fields: - pages.expires: - type: text - size: small - append: NICETIME.SECOND_PLURAL - label: PLUGIN_ADMIN.EXPIRES - help: PLUGIN_ADMIN.EXPIRES_HELP - validate: - type: number - min: 1 - pages.cache_control: - type: text - size: medium - label: PLUGIN_ADMIN.CACHE_CONTROL - help: PLUGIN_ADMIN.CACHE_CONTROL_HELP - placeholder: 'e.g. public, max-age=31536000' - pages.last_modified: - type: toggle - label: PLUGIN_ADMIN.LAST_MODIFIED - help: PLUGIN_ADMIN.LAST_MODIFIED_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - pages.etag: - type: toggle - label: PLUGIN_ADMIN.ETAG - help: PLUGIN_ADMIN.ETAG_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - pages.vary_accept_encoding: - type: toggle - label: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING - help: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - markdown: - type: section - title: Markdown - underline: true - - fields: - pages.markdown.extra: - type: toggle - label: Markdown extra - help: PLUGIN_ADMIN.MARKDOWN_EXTRA_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - pages.markdown.auto_line_breaks: - type: toggle - label: PLUGIN_ADMIN.AUTO_LINE_BREAKS - help: PLUGIN_ADMIN.AUTO_LINE_BREAKS_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - pages.markdown.auto_url_links: - type: toggle - label: PLUGIN_ADMIN.AUTO_URL_LINKS - help: PLUGIN_ADMIN.AUTO_URL_LINKS_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - pages.markdown.escape_markup: - type: toggle - label: PLUGIN_ADMIN.ESCAPE_MARKUP - help: PLUGIN_ADMIN.ESCAPE_MARKUP_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - caching: - type: section - title: PLUGIN_ADMIN.CACHING - underline: true - - fields: - cache.enabled: - type: toggle - label: PLUGIN_ADMIN.CACHING - help: PLUGIN_ADMIN.CACHING_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - cache.check.method: - type: select - size: medium - classes: fancy - label: PLUGIN_ADMIN.CACHE_CHECK_METHOD - help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP - options: - file: Markdown + Yaml file timestamps - folder: Folder timestamps - hash: All files timestamps - none: No timestamp checking - - cache.driver: - type: select - size: small - classes: fancy - label: PLUGIN_ADMIN.CACHE_DRIVER - help: PLUGIN_ADMIN.CACHE_DRIVER_HELP - options: - auto: Auto detect - file: File - apc: APC - apcu: APCu - xcache: Xcache - memcache: Memcache - memcached: Memcached - wincache: WinCache - redis: Redis - - cache.prefix: - type: text - size: x-small - label: PLUGIN_ADMIN.CACHE_PREFIX - help: PLUGIN_ADMIN.CACHE_PREFIX_HELP - placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER - - cache.clear_images_by_default: - type: toggle - label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT - help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - cache.cli_compatibility: - type: toggle - label: PLUGIN_ADMIN.CLI_COMPATIBILITY - help: PLUGIN_ADMIN.CLI_COMPATIBILITY_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - cache.lifetime: - type: text - size: small - append: NICETIME.SECOND_PLURAL - label: PLUGIN_ADMIN.LIFETIME - help: PLUGIN_ADMIN.LIFETIME_HELP - validate: - type: number - - cache.gzip: - type: toggle - label: PLUGIN_ADMIN.GZIP_COMPRESSION - help: PLUGIN_ADMIN.GZIP_COMPRESSION_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - cache.allow_webserver_gzip: - type: toggle - label: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP - help: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - cache.memcache.server: - type: text - size: medium - label: PLUGIN_ADMIN.MEMCACHE_SERVER - help: PLUGIN_ADMIN.MEMCACHE_SERVER_HELP - placeholder: "localhost" - - cache.memcache.port: - type: text - size: small - label: PLUGIN_ADMIN.MEMCACHE_PORT - help: PLUGIN_ADMIN.MEMCACHE_PORT_HELP - placeholder: "11211" - - cache.memcached.server: - type: text - size: medium - label: PLUGIN_ADMIN.MEMCACHED_SERVER - help: PLUGIN_ADMIN.MEMCACHED_SERVER_HELP - placeholder: "localhost" - - cache.memcached.port: - type: text - size: small - label: PLUGIN_ADMIN.MEMCACHED_PORT - help: PLUGIN_ADMIN.MEMCACHED_PORT_HELP - placeholder: "11211" - - cache.redis.socket: - type: text - size: medium - label: PLUGIN_ADMIN.REDIS_SOCKET - help: PLUGIN_ADMIN.REDIS_SOCKET_HELP - placeholder: "/var/run/redis/redis.sock" - - cache.redis.server: - type: text - size: medium - label: PLUGIN_ADMIN.REDIS_SERVER - help: PLUGIN_ADMIN.REDIS_SERVER_HELP - placeholder: "localhost" - - cache.redis.port: - type: text - size: small - label: PLUGIN_ADMIN.REDIS_PORT - help: PLUGIN_ADMIN.REDIS_PORT_HELP - placeholder: "6379" - - cache.redis.password: - type: text - size: small - label: PLUGIN_ADMIN.REDIS_PASSWORD - - - - twig: - type: section - title: PLUGIN_ADMIN.TWIG_TEMPLATING - underline: true - - fields: - twig.cache: - type: toggle - label: PLUGIN_ADMIN.TWIG_CACHING - help: PLUGIN_ADMIN.TWIG_CACHING_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - twig.debug: - type: toggle - label: PLUGIN_ADMIN.TWIG_DEBUG - help: PLUGIN_ADMIN.TWIG_DEBUG_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - twig.auto_reload: - type: toggle - label: PLUGIN_ADMIN.DETECT_CHANGES - help: PLUGIN_ADMIN.DETECT_CHANGES_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - twig.autoescape: - type: toggle - label: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES - help: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - twig.umask_fix: - type: toggle - label: PLUGIN_ADMIN.TWIG_UMASK_FIX - help: PLUGIN_ADMIN.TWIG_UMASK_FIX_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets: - type: section - title: PLUGIN_ADMIN.ASSETS - underline: true - - fields: - assets.css_pipeline: - type: toggle - label: PLUGIN_ADMIN.CSS_PIPELINE - help: PLUGIN_ADMIN.CSS_PIPELINE_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.css_pipeline_include_externals: - type: toggle - label: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS - help: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.css_pipeline_before_excludes: - type: toggle - label: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES - help: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.css_minify: - type: toggle - label: PLUGIN_ADMIN.CSS_MINIFY - help: PLUGIN_ADMIN.CSS_MINIFY_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.css_minify_windows: - type: toggle - label: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE - help: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.css_rewrite: - type: toggle - label: PLUGIN_ADMIN.CSS_REWRITE - help: PLUGIN_ADMIN.CSS_REWRITE_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.js_pipeline: - type: toggle - label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE - help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.js_pipeline_include_externals: - type: toggle - label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS - help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.js_pipeline_before_excludes: - type: toggle - label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES - help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.js_minify: - type: toggle - label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY - help: PLUGIN_ADMIN.JAVASCRIPT_MINIFY_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.enable_asset_timestamp: - type: toggle - label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS - help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - assets.collections: - type: multilevel - label: PLUGIN_ADMIN.COLLECTIONS - placeholder_key: collection_name - placeholder_value: collection_path - validate: - type: array - - errors: - type: section - title: PLUGIN_ADMIN.ERROR_HANDLER - underline: true - - fields: - errors.display: - type: select - label: PLUGIN_ADMIN.DISPLAY_ERRORS - help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP - size: medium - highlight: 1 - options: - -1: PLUGIN_ADMIN.ERROR_SYSTEM - 0: PLUGIN_ADMIN.ERROR_SIMPLE - 1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE - validate: - type: int - - - errors.log: - type: toggle - label: PLUGIN_ADMIN.LOG_ERRORS - help: PLUGIN_ADMIN.LOG_ERRORS_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - debugger: - type: section - title: PLUGIN_ADMIN.DEBUGGER - underline: true - - fields: - debugger.enabled: - type: toggle - label: PLUGIN_ADMIN.DEBUGGER - help: PLUGIN_ADMIN.DEBUGGER_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - debugger.shutdown.close_connection: - type: toggle - label: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION - help: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - media: - type: section - title: PLUGIN_ADMIN.MEDIA - underline: true - - fields: - images.default_image_quality: - type: range - append: '%' - label: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY - help: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY_HELP - validate: - min: 1 - max: 100 - - images.cache_all: - type: toggle - label: PLUGIN_ADMIN.CACHE_ALL - help: PLUGIN_ADMIN.CACHE_ALL_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - images.cache_perms: - type: select - size: small - label: PLUGIN_ADMIN.CACHE_PERMS - help: PLUGIN_ADMIN.CACHE_PERMS_HELP - highlight: '0755' - options: - '0755': '0755' - '0775': '0775' - - images.debug: - type: toggle - label: PLUGIN_ADMIN.IMAGES_DEBUG - help: PLUGIN_ADMIN.IMAGES_DEBUG_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - images.auto_fix_orientation: - type: toggle - label: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION - help: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - media.enable_media_timestamp: - type: toggle - label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP - help: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - media.auto_metadata_exif: - type: toggle - label: PLUGIN_ADMIN.ENABLE_AUTO_METADATA - help: PLUGIN_ADMIN.ENABLE_AUTO_METADATA_HELP - highlight: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - - - media.allowed_fallback_types: - type: selectize - size: large - label: PLUGIN_ADMIN.FALLBACK_TYPES - help: PLUGIN_ADMIN.FALLBACK_TYPES_HELP - classes: fancy - validate: - type: commalist - - media.unsupported_inline_types: - type: selectize - size: large - label: PLUGIN_ADMIN.INLINE_TYPES - help: PLUGIN_ADMIN.INLINE_TYPES_HELP - classes: fancy - validate: - type: commalist - - session: - type: section - title: PLUGIN_ADMIN.SESSION - underline: true - - fields: - session.enabled: - type: hidden - label: PLUGIN_ADMIN.ENABLED - help: PLUGIN_ADMIN.SESSION_ENABLED_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - default: true - validate: - type: bool - - session.initialize: - type: toggle - label: PLUGIN_ADMIN.SESSION_INITIALIZE - help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - default: true - validate: - type: bool - - session.timeout: - type: text - size: small - append: NICETIME.SECOND_PLURAL - label: PLUGIN_ADMIN.TIMEOUT - help: PLUGIN_ADMIN.TIMEOUT_HELP - validate: - type: number - min: 0 - - session.name: - type: text - size: small - label: PLUGIN_ADMIN.NAME - help: PLUGIN_ADMIN.SESSION_NAME_HELP - - session.secure: - type: toggle - label: PLUGIN_ADMIN.SESSION_SECURE - help: PLUGIN_ADMIN.SESSION_SECURE_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - default: false - validate: - type: bool - - session.httponly: - type: toggle - label: PLUGIN_ADMIN.SESSION_HTTPONLY - help: PLUGIN_ADMIN.SESSION_HTTPONLY_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - default: true - validate: - type: bool - - session.path: - type: text - size: small - label: PLUGIN_ADMIN.SESSION_PATH - help: PLUGIN_ADMIN.SESSION_PATH_HELP - - session.split: - type: toggle - label: PLUGIN_ADMIN.SESSION_SPLIT - help: PLUGIN_ADMIN.SESSION_SPLIT_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - default: true - validate: - type: bool - - advanced: - type: section - title: PLUGIN_ADMIN.ADVANCED - underline: true - - fields: - gpm.releases: - type: toggle - label: PLUGIN_ADMIN.GPM_RELEASES - highlight: stable - help: PLUGIN_ADMIN.GPM_RELEASES_HELP - options: - 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 - highlight: auto - help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP - highlight: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - default: true - validate: - type: bool - - gpm.verify_peer: - type: toggle - label: PLUGIN_ADMIN.GPM_VERIFY_PEER - highlight: 1 - help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - reverse_proxy_setup: - type: toggle - label: PLUGIN_ADMIN.REVERSE_PROXY - highlight: 0 - help: PLUGIN_ADMIN.REVERSE_PROXY_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - username_regex: - type: text - size: large - label: PLUGIN_ADMIN.USERNAME_REGEX - help: PLUGIN_ADMIN.USERNAME_REGEX_HELP - - pwd_regex: - type: text - size: large - label: PLUGIN_ADMIN.PWD_REGEX - help: PLUGIN_ADMIN.PWD_REGEX_HELP - - intl_enabled: - type: toggle - label: PLUGIN_ADMIN.INTL_ENABLED - highlight: 1 - help: PLUGIN_ADMIN.INTL_ENABLED_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - wrapped_site: - type: toggle - label: PLUGIN_ADMIN.WRAPPED_SITE - highlight: 0 - help: PLUGIN_ADMIN.WRAPPED_SITE_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - absolute_urls: - type: toggle - label: PLUGIN_ADMIN.ABSOLUTE_URLS - highlight: 0 - help: PLUGIN_ADMIN.ABSOLUTE_URLS_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - param_sep: - type: select - size: medium - label: PLUGIN_ADMIN.PARAMETER_SEPARATOR - classes: fancy - help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP - default: '' - options: - ':': ': (default)' - ';': '; (for Apache running on Windows)' - - force_ssl: - type: toggle - label: PLUGIN_ADMIN.FORCE_SSL - highlight: 0 - help: PLUGIN_ADMIN.FORCE_SSL_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - force_lowercase_urls: - type: toggle - label: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS - highlight: 1 - default: 1 - help: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - custom_base_url: - type: text - size: medium - placeholder: "e.g. http://yoursite.com/yourpath" - label: PLUGIN_ADMIN.CUSTOM_BASE_URL - help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP - - strict_mode.yaml_compat: - type: toggle - label: PLUGIN_ADMIN.STRICT_YAML_COMPAT - highlight: 1 - default: 1 - help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - - strict_mode.twig_compat: - type: toggle - label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT - highlight: 1 - default: 1 - help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool + content: + type: tab + title: PLUGIN_ADMIN.CONTENT + + fields: + content_section: + type: section + title: PLUGIN_ADMIN.CONTENT + underline: true + + home.alias: + type: pages + size: large + classes: fancy + label: PLUGIN_ADMIN.HOME_PAGE + show_all: false + show_modular: false + show_root: false + show_slug: true + help: PLUGIN_ADMIN.HOME_PAGE_HELP + + home.hide_in_urls: + type: toggle + label: PLUGIN_ADMIN.HIDE_HOME_IN_URLS + help: PLUGIN_ADMIN.HIDE_HOME_IN_URLS_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.theme: + type: themeselect + classes: fancy + selectize: true + size: medium + label: PLUGIN_ADMIN.DEFAULT_THEME + help: PLUGIN_ADMIN.DEFAULT_THEME_HELP + + pages.process: + type: checkboxes + label: PLUGIN_ADMIN.PROCESS + help: PLUGIN_ADMIN.PROCESS_HELP + default: [markdown: true, twig: true] + options: + markdown: Markdown + twig: Twig + use: keys + + pages.types: + type: array + label: PLUGIN_ADMIN.PAGE_TYPES + help: PLUGIN_ADMIN.PAGE_TYPES_HELP + size: small + default: ['html','htm','json','xml','txt','rss','atom'] + value_only: true + + timezone: + type: select + label: PLUGIN_ADMIN.TIMEZONE + size: medium + classes: fancy + help: PLUGIN_ADMIN.TIMEZONE_HELP + data-options@: '\Grav\Common\Utils::timezones' + default: '' + options: + '': 'Default (Server Timezone)' + + pages.dateformat.default: + type: select + size: medium + selectize: + create: true + label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT + help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP + placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER + data-options@: '\Grav\Common\Utils::dateFormats' + validate: + type: string + + pages.dateformat.short: + type: dateformat + size: medium + classes: fancy + label: PLUGIN_ADMIN.SHORT_DATE_FORMAT + help: PLUGIN_ADMIN.SHORT_DATE_FORMAT_HELP + default: "jS M Y" + options: + "F jS \\a\\t g:ia": Date1 + "l jS \\of F g:i A": Date2 + "D, d M Y G:i:s": Date3 + "d-m-y G:i": Date4 + "jS M Y": Date5 + + pages.dateformat.long: + type: dateformat + size: medium + classes: fancy + label: PLUGIN_ADMIN.LONG_DATE_FORMAT + help: PLUGIN_ADMIN.LONG_DATE_FORMAT_HELP + options: + "F jS \\a\\t g:ia": Date1 + "l jS \\of F g:i A": Date2 + "D, d M Y G:i:s": Date3 + "d-m-y G:i": Date4 + "jS M Y": Date5 + + pages.order.by: + type: select + size: large + classes: fancy + label: PLUGIN_ADMIN.DEFAULT_ORDERING + help: PLUGIN_ADMIN.DEFAULT_ORDERING_HELP + options: + default: PLUGIN_ADMIN.DEFAULT_ORDERING_DEFAULT + folder: PLUGIN_ADMIN.DEFAULT_ORDERING_FOLDER + title: PLUGIN_ADMIN.DEFAULT_ORDERING_TITLE + date: PLUGIN_ADMIN.DEFAULT_ORDERING_DATE + + pages.order.dir: + type: toggle + label: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION + highlight: asc + default: desc + help: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION_HELP + options: + asc: PLUGIN_ADMIN.ASCENDING + desc: PLUGIN_ADMIN.DESCENDING + + pages.list.count: + type: text + size: x-small + append: PLUGIN_ADMIN.PAGES + label: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT + help: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT_HELP + validate: + type: number + min: 1 + + pages.publish_dates: + type: toggle + label: PLUGIN_ADMIN.DATE_BASED_PUBLISHING + help: PLUGIN_ADMIN.DATE_BASED_PUBLISHING_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.events: + type: checkboxes + label: PLUGIN_ADMIN.EVENTS + help: PLUGIN_ADMIN.EVENTS_HELP + default: [page: true, twig: true] + options: + page: Page Events + twig: Twig Events + use: keys + + pages.append_url_extension: + type: text + size: x-small + placeholder: "e.g. .html" + label: PLUGIN_ADMIN.APPEND_URL_EXT + help: PLUGIN_ADMIN.APPEND_URL_EXT_HELP + + pages.redirect_default_route: + type: toggle + label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE + help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.redirect_default_code: + type: select + size: medium + classes: fancy + label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE + help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP + options: + 301: 301 - Permanent + 302: 302 - Found + 303: 303 - Other + 304: 304 - Not Modified + + pages.redirect_trailing_slash: + type: toggle + label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH + help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.ignore_hidden: + type: toggle + label: PLUGIN_ADMIN.IGNORE_HIDDEN + help: PLUGIN_ADMIN.IGNORE_HIDDEN_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.ignore_files: + type: selectize + size: large + label: PLUGIN_ADMIN.IGNORE_FILES + help: PLUGIN_ADMIN.IGNORE_FILES_HELP + classes: fancy + validate: + type: commalist + + pages.ignore_folders: + type: selectize + size: large + label: PLUGIN_ADMIN.IGNORE_FOLDERS + help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP + classes: fancy + validate: + type: commalist + + pages.hide_empty_folders: + type: selectize + size: large + label: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS + help: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS_HELP + classes: fancy + validate: + type: commalist + + pages.url_taxonomy_filters: + type: toggle + label: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS + help: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.twig_first: + type: toggle + label: PLUGIN_ADMIN.TWIG_FIRST + help: PLUGIN_ADMIN.TWIG_FIRST_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.never_cache_twig: + type: toggle + label: PLUGIN_ADMIN.NEVER_CACHE_TWIG + help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.frontmatter.process_twig: + type: toggle + label: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG + help: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + pages.frontmatter.ignore_fields: + type: selectize + size: large + placeholder: "e.g. forms" + label: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS + help: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS_HELP + classes: fancy + validate: + type: commalist + + languages: + type: tab + title: PLUGIN_ADMIN.LANGUAGES + + fields: + languages-section: + type: section + title: PLUGIN_ADMIN.LANGUAGES + underline: true + + languages.supported: + type: selectize + size: large + placeholder: "e.g. en, fr" + label: PLUGIN_ADMIN.SUPPORTED + help: PLUGIN_ADMIN.SUPPORTED_HELP + classes: fancy + validate: + type: commalist + + languages.default_lang: + type: text + size: x-small + label: PLUGIN_ADMIN.DEFAULT_LANG + help: PLUGIN_ADMIN.DEFAULT_LANG_HELP + + languages.include_default_lang: + type: toggle + label: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG + help: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + languages.pages_fallback_only: + type: toggle + label: PLUGIN_ADMIN.PAGES_FALLBACK_ONLY + help: PLUGIN_ADMIN.PAGES_FALLBACK_ONLY_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + languages.translations: + type: toggle + label: PLUGIN_ADMIN.TRANSLATIONS_ENABLED + help: PLUGIN_ADMIN.TRANSLATIONS_ENABLED_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + languages.translations_fallback: + type: toggle + label: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK + help: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + languages.session_store_active: + type: toggle + label: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION + help: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + languages.http_accept_language: + type: toggle + label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE + help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + languages.override_locale: + type: toggle + label: PLUGIN_ADMIN.OVERRIDE_LOCALE + help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + http_headers: + type: tab + title: PLUGIN_ADMIN.HTTP_HEADERS + + fields: + http_headers_section: + type: section + title: PLUGIN_ADMIN.HTTP_HEADERS + underline: true + + pages.expires: + type: text + size: small + append: GRAV.NICETIME.SECOND_PLURAL + label: PLUGIN_ADMIN.EXPIRES + help: PLUGIN_ADMIN.EXPIRES_HELP + validate: + type: number + min: 1 + pages.cache_control: + type: text + size: medium + label: PLUGIN_ADMIN.CACHE_CONTROL + help: PLUGIN_ADMIN.CACHE_CONTROL_HELP + placeholder: 'e.g. public, max-age=31536000' + pages.last_modified: + type: toggle + label: PLUGIN_ADMIN.LAST_MODIFIED + help: PLUGIN_ADMIN.LAST_MODIFIED_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + pages.etag: + type: toggle + label: PLUGIN_ADMIN.ETAG + help: PLUGIN_ADMIN.ETAG_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + pages.vary_accept_encoding: + type: toggle + label: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING + help: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + markdown: + type: tab + title: PLUGIN_ADMIN.MARKDOWN + + fields: + markdow_section: + type: section + title: PLUGIN_ADMIN.MARKDOWN + underline: true + + pages.markdown.extra: + type: toggle + label: Markdown extra + help: PLUGIN_ADMIN.MARKDOWN_EXTRA_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + pages.markdown.auto_line_breaks: + type: toggle + label: PLUGIN_ADMIN.AUTO_LINE_BREAKS + help: PLUGIN_ADMIN.AUTO_LINE_BREAKS_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + pages.markdown.auto_url_links: + type: toggle + label: PLUGIN_ADMIN.AUTO_URL_LINKS + help: PLUGIN_ADMIN.AUTO_URL_LINKS_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + pages.markdown.escape_markup: + type: toggle + label: PLUGIN_ADMIN.ESCAPE_MARKUP + help: PLUGIN_ADMIN.ESCAPE_MARKUP_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + caching: + type: tab + title: PLUGIN_ADMIN.CACHING + + fields: + caching_section: + type: section + title: PLUGIN_ADMIN.CACHING + underline: true + + cache.enabled: + type: toggle + label: PLUGIN_ADMIN.CACHING + help: PLUGIN_ADMIN.CACHING_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + cache.check.method: + type: select + size: medium + classes: fancy + label: PLUGIN_ADMIN.CACHE_CHECK_METHOD + help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP + options: + file: Markdown + Yaml file timestamps + folder: Folder timestamps + hash: All files timestamps + none: No timestamp checking + + cache.driver: + type: select + size: small + classes: fancy + label: PLUGIN_ADMIN.CACHE_DRIVER + help: PLUGIN_ADMIN.CACHE_DRIVER_HELP + options: + auto: Auto detect + file: File + apc: APC + apcu: APCu + xcache: Xcache + memcache: Memcache + memcached: Memcached + wincache: WinCache + redis: Redis + + cache.prefix: + type: text + size: x-small + label: PLUGIN_ADMIN.CACHE_PREFIX + help: PLUGIN_ADMIN.CACHE_PREFIX_HELP + placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER + + cache.purge_at: + type: cron + label: PLUGIN_ADMIN.CACHE_PURGE_JOB + help: PLUGIN_ADMIN.CACHE_PURGE_JOB_HELP + default: '* 4 * * *' + + cache.clear_at: + type: cron + label: PLUGIN_ADMIN.CACHE_CLEAR_JOB + help: PLUGIN_ADMIN.CACHE_CLEAR_JOB_HELP + default: '* 3 * * *' + + cache.clear_job_type: + type: select + size: medium + label: PLUGIN_ADMIN.CACHE_JOB_TYPE + help: PLUGIN_ADMIN.CACHE_JOB_TYPE_HELP + options: + standard: Standard Cache Folders + all: All Cache Folders + + cache.clear_images_by_default: + type: toggle + label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT + help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + cache.cli_compatibility: + type: toggle + label: PLUGIN_ADMIN.CLI_COMPATIBILITY + help: PLUGIN_ADMIN.CLI_COMPATIBILITY_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + cache.lifetime: + type: text + size: small + append: GRAV.NICETIME.SECOND_PLURAL + label: PLUGIN_ADMIN.LIFETIME + help: PLUGIN_ADMIN.LIFETIME_HELP + validate: + type: number + + cache.gzip: + type: toggle + label: PLUGIN_ADMIN.GZIP_COMPRESSION + help: PLUGIN_ADMIN.GZIP_COMPRESSION_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + cache.allow_webserver_gzip: + type: toggle + label: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP + help: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + cache.memcache.server: + type: text + size: medium + label: PLUGIN_ADMIN.MEMCACHE_SERVER + help: PLUGIN_ADMIN.MEMCACHE_SERVER_HELP + placeholder: "localhost" + + cache.memcache.port: + type: text + size: small + label: PLUGIN_ADMIN.MEMCACHE_PORT + help: PLUGIN_ADMIN.MEMCACHE_PORT_HELP + placeholder: "11211" + + cache.memcached.server: + type: text + size: medium + label: PLUGIN_ADMIN.MEMCACHED_SERVER + help: PLUGIN_ADMIN.MEMCACHED_SERVER_HELP + placeholder: "localhost" + + cache.memcached.port: + type: text + size: small + label: PLUGIN_ADMIN.MEMCACHED_PORT + help: PLUGIN_ADMIN.MEMCACHED_PORT_HELP + placeholder: "11211" + + cache.redis.socket: + type: text + size: medium + label: PLUGIN_ADMIN.REDIS_SOCKET + help: PLUGIN_ADMIN.REDIS_SOCKET_HELP + placeholder: "/var/run/redis/redis.sock" + + cache.redis.server: + type: text + size: medium + label: PLUGIN_ADMIN.REDIS_SERVER + help: PLUGIN_ADMIN.REDIS_SERVER_HELP + placeholder: "localhost" + + cache.redis.port: + type: text + size: small + label: PLUGIN_ADMIN.REDIS_PORT + help: PLUGIN_ADMIN.REDIS_PORT_HELP + placeholder: "6379" + + cache.redis.password: + type: text + size: small + label: PLUGIN_ADMIN.REDIS_PASSWORD + + twig: + type: tab + title: PLUGIN_ADMIN.TWIG_TEMPLATING + + fields: + twig_section: + type: section + title: PLUGIN_ADMIN.TWIG_TEMPLATING + underline: true + + twig.cache: + type: toggle + label: PLUGIN_ADMIN.TWIG_CACHING + help: PLUGIN_ADMIN.TWIG_CACHING_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + twig.debug: + type: toggle + label: PLUGIN_ADMIN.TWIG_DEBUG + help: PLUGIN_ADMIN.TWIG_DEBUG_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + twig.auto_reload: + type: toggle + label: PLUGIN_ADMIN.DETECT_CHANGES + help: PLUGIN_ADMIN.DETECT_CHANGES_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + twig.autoescape: + type: toggle + label: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES + help: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + twig.umask_fix: + type: toggle + label: PLUGIN_ADMIN.TWIG_UMASK_FIX + help: PLUGIN_ADMIN.TWIG_UMASK_FIX_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets: + type: tab + title: PLUGIN_ADMIN.ASSETS + + fields: + assets_section: + type: section + title: PLUGIN_ADMIN.ASSETS + underline: true + + assets.css_pipeline: + type: toggle + label: PLUGIN_ADMIN.CSS_PIPELINE + help: PLUGIN_ADMIN.CSS_PIPELINE_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.css_pipeline_include_externals: + type: toggle + label: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS + help: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.css_pipeline_before_excludes: + type: toggle + label: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES + help: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.css_minify: + type: toggle + label: PLUGIN_ADMIN.CSS_MINIFY + help: PLUGIN_ADMIN.CSS_MINIFY_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.css_minify_windows: + type: toggle + label: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE + help: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.css_rewrite: + type: toggle + label: PLUGIN_ADMIN.CSS_REWRITE + help: PLUGIN_ADMIN.CSS_REWRITE_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.js_pipeline: + type: toggle + label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE + help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.js_pipeline_include_externals: + type: toggle + label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS + help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.js_pipeline_before_excludes: + type: toggle + label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES + help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.js_minify: + type: toggle + label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY + help: PLUGIN_ADMIN.JAVASCRIPT_MINIFY_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.enable_asset_timestamp: + type: toggle + label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS + help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + assets.collections: + type: multilevel + label: PLUGIN_ADMIN.COLLECTIONS + placeholder_key: collection_name + placeholder_value: collection_path + validate: + type: array + + errors: + type: tab + title: PLUGIN_ADMIN.ERROR_HANDLER + + fields: + errors_section: + type: section + title: PLUGIN_ADMIN.ERROR_HANDLER + underline: true + + errors.display: + type: select + label: PLUGIN_ADMIN.DISPLAY_ERRORS + help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP + size: medium + highlight: 1 + options: + -1: PLUGIN_ADMIN.ERROR_SYSTEM + 0: PLUGIN_ADMIN.ERROR_SIMPLE + 1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE + validate: + type: int + + + errors.log: + type: toggle + label: PLUGIN_ADMIN.LOG_ERRORS + help: PLUGIN_ADMIN.LOG_ERRORS_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + log.handler: + type: select + size: small + label: PLUGIN_ADMIN.LOG_HANDLER + help: PLUGIN_ADMIN.LOG_HANDLER_HELP + default: 'file' + options: + 'file': 'File' + 'syslog': 'Syslog' + + log.syslog.facility: + type: select + size: small + label: PLUGIN_ADMIN.SYSLOG_FACILITY + help: PLUGIN_ADMIN.SYSLOG_FACILITY_HELP + default: local6 + options: + auth: auth + authpriv: authpriv + cron: cron + daemon: daemon + kern: kern + lpr: lpr + mail: mail + news: news + syslog: syslog + user: user + uucp: uucp + local0: local0 + local1: local1 + local2: local2 + local3: local3 + local4: local4 + local5: local5 + local6: local6 + local7: local7 + + debugger: + type: tab + title: PLUGIN_ADMIN.DEBUGGER + + fields: + debugger_section: + type: section + title: PLUGIN_ADMIN.DEBUGGER + underline: true + + debugger.enabled: + type: toggle + label: PLUGIN_ADMIN.DEBUGGER + help: PLUGIN_ADMIN.DEBUGGER_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + debugger.shutdown.close_connection: + type: toggle + label: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION + help: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + media: + type: tab + title: PLUGIN_ADMIN.MEDIA + + fields: + media_section: + type: section + title: PLUGIN_ADMIN.MEDIA + underline: true + + images.default_image_quality: + type: range + append: '%' + label: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY + help: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY_HELP + validate: + min: 1 + max: 100 + + images.cache_all: + type: toggle + label: PLUGIN_ADMIN.CACHE_ALL + help: PLUGIN_ADMIN.CACHE_ALL_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + images.cache_perms: + type: select + size: small + label: PLUGIN_ADMIN.CACHE_PERMS + help: PLUGIN_ADMIN.CACHE_PERMS_HELP + highlight: '0755' + options: + '0755': '0755' + '0775': '0775' + + images.debug: + type: toggle + label: PLUGIN_ADMIN.IMAGES_DEBUG + help: PLUGIN_ADMIN.IMAGES_DEBUG_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + images.auto_fix_orientation: + type: toggle + label: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION + help: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + images.seofriendly: + type: toggle + label: PLUGIN_ADMIN.IMAGES_SEOFRIENDLY + help: PLUGIN_ADMIN.IMAGES_SEOFRIENDLY_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + media.enable_media_timestamp: + type: toggle + label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP + help: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + media.auto_metadata_exif: + type: toggle + label: PLUGIN_ADMIN.ENABLE_AUTO_METADATA + help: PLUGIN_ADMIN.ENABLE_AUTO_METADATA_HELP + highlight: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + + media.allowed_fallback_types: + type: selectize + size: large + label: PLUGIN_ADMIN.FALLBACK_TYPES + help: PLUGIN_ADMIN.FALLBACK_TYPES_HELP + classes: fancy + validate: + type: commalist + + media.unsupported_inline_types: + type: selectize + size: large + label: PLUGIN_ADMIN.INLINE_TYPES + help: PLUGIN_ADMIN.INLINE_TYPES_HELP + classes: fancy + validate: + type: commalist + + session: + type: tab + title: PLUGIN_ADMIN.SESSION + + fields: + session_section: + type: section + title: PLUGIN_ADMIN.SESSION + underline: true + + session.enabled: + type: hidden + label: PLUGIN_ADMIN.ENABLED + help: PLUGIN_ADMIN.SESSION_ENABLED_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: true + validate: + type: bool + + session.initialize: + type: toggle + label: PLUGIN_ADMIN.SESSION_INITIALIZE + help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: true + validate: + type: bool + + session.timeout: + type: text + size: small + append: GRAV.NICETIME.SECOND_PLURAL + label: PLUGIN_ADMIN.TIMEOUT + help: PLUGIN_ADMIN.TIMEOUT_HELP + validate: + type: number + min: 0 + + session.name: + type: text + size: small + label: PLUGIN_ADMIN.NAME + help: PLUGIN_ADMIN.SESSION_NAME_HELP + + session.uniqueness: + type: select + size: medium + label: PLUGIN_ADMIN.SESSION_UNIQUENESS + help: PLUGIN_ADMIN.SESSION_UNIQUENESS_HELP + highlight: path + default: path + options: + path: Grav's root file path + salt: Grav's random security salt + + session.secure: + type: toggle + label: PLUGIN_ADMIN.SESSION_SECURE + help: PLUGIN_ADMIN.SESSION_SECURE_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: false + validate: + type: bool + + session.httponly: + type: toggle + label: PLUGIN_ADMIN.SESSION_HTTPONLY + help: PLUGIN_ADMIN.SESSION_HTTPONLY_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: true + validate: + type: bool + + session.path: + type: text + size: small + label: PLUGIN_ADMIN.SESSION_PATH + help: PLUGIN_ADMIN.SESSION_PATH_HELP + + session.split: + type: toggle + label: PLUGIN_ADMIN.SESSION_SPLIT + help: PLUGIN_ADMIN.SESSION_SPLIT_HELP + highlight: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: true + validate: + type: bool + + advanced: + type: tab + title: PLUGIN_ADMIN.ADVANCED + + fields: + advanced_section: + type: section + title: PLUGIN_ADMIN.ADVANCED + underline: true + + gpm.releases: + type: toggle + label: PLUGIN_ADMIN.GPM_RELEASES + highlight: stable + help: PLUGIN_ADMIN.GPM_RELEASES_HELP + options: + 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 + highlight: 1 + help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + default: true + validate: + type: bool + + gpm.verify_peer: + type: toggle + label: PLUGIN_ADMIN.GPM_VERIFY_PEER + highlight: 1 + help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + reverse_proxy_setup: + type: toggle + label: PLUGIN_ADMIN.REVERSE_PROXY + highlight: 0 + help: PLUGIN_ADMIN.REVERSE_PROXY_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + username_regex: + type: text + size: large + label: PLUGIN_ADMIN.USERNAME_REGEX + help: PLUGIN_ADMIN.USERNAME_REGEX_HELP + + pwd_regex: + type: text + size: large + label: PLUGIN_ADMIN.PWD_REGEX + help: PLUGIN_ADMIN.PWD_REGEX_HELP + + intl_enabled: + type: toggle + label: PLUGIN_ADMIN.INTL_ENABLED + highlight: 1 + help: PLUGIN_ADMIN.INTL_ENABLED_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + wrapped_site: + type: toggle + label: PLUGIN_ADMIN.WRAPPED_SITE + highlight: 0 + help: PLUGIN_ADMIN.WRAPPED_SITE_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + absolute_urls: + type: toggle + label: PLUGIN_ADMIN.ABSOLUTE_URLS + highlight: 0 + help: PLUGIN_ADMIN.ABSOLUTE_URLS_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + param_sep: + type: select + size: medium + label: PLUGIN_ADMIN.PARAMETER_SEPARATOR + classes: fancy + help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP + default: '' + options: + ':': ': (default)' + ';': '; (for Apache running on Windows)' + + force_ssl: + type: toggle + label: PLUGIN_ADMIN.FORCE_SSL + highlight: 0 + help: PLUGIN_ADMIN.FORCE_SSL_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + force_lowercase_urls: + type: toggle + label: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS + highlight: 1 + default: 1 + help: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + custom_base_url: + type: text + size: medium + placeholder: "e.g. http://yoursite.com/yourpath" + label: PLUGIN_ADMIN.CUSTOM_BASE_URL + help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP + + accounts.type: + type: hidden + + accounts.storage: + type: hidden + + strict_mode.yaml_compat: + type: toggle + label: PLUGIN_ADMIN.STRICT_YAML_COMPAT + highlight: 1 + default: 1 + help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + + strict_mode.twig_compat: + type: toggle + label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT + highlight: 1 + default: 1 + help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool diff --git a/system/blueprints/user/account.yaml b/system/blueprints/user/account.yaml index ac7717b..897cc98 100644 --- a/system/blueprints/user/account.yaml +++ b/system/blueprints/user/account.yaml @@ -4,122 +4,124 @@ form: fields: - info: - type: userinfo - size: large + info: + type: userinfo + size: large - avatar: - type: file - size: large - destination: 'user://accounts/avatars' - multiple: false - random_name: true + avatar: + type: file + size: large + destination: 'user://accounts/avatars' + multiple: false + random_name: true - content: - type: section - title: PLUGIN_ADMIN.ACCOUNT - underline: true + content: + type: section + title: PLUGIN_ADMIN.ACCOUNT + underline: true - username: - type: text - size: large - label: PLUGIN_ADMIN.USERNAME - disabled: true - readonly: true + username: + type: text + size: large + label: PLUGIN_ADMIN.USERNAME + disabled: true + readonly: true - email: - type: email - size: large - label: PLUGIN_ADMIN.EMAIL - validate: - type: email - message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE - required: true + email: + type: email + size: large + label: PLUGIN_ADMIN.EMAIL + validate: + type: email + message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE + required: true - password: - type: password - size: large - label: PLUGIN_ADMIN.PASSWORD - autocomplete: new-password - validate: - required: false - message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE - config-pattern@: system.pwd_regex + password: + type: password + size: large + label: PLUGIN_ADMIN.PASSWORD + autocomplete: new-password + validate: + required: false + message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE + config-pattern@: system.pwd_regex - fullname: - type: text - size: large - label: PLUGIN_ADMIN.FULL_NAME - validate: - required: true + fullname: + type: text + size: large + label: PLUGIN_ADMIN.FULL_NAME + validate: + required: true - title: - type: text - size: large - label: PLUGIN_ADMIN.TITLE + title: + type: text + size: large + label: PLUGIN_ADMIN.TITLE - language: - type: select - label: PLUGIN_ADMIN.LANGUAGE - size: medium - classes: fancy - data-options@: '\Grav\Plugin\Admin\Admin::adminLanguages' - default: 'en' - help: PLUGIN_ADMIN.LANGUAGE_HELP + language: + type: select + label: PLUGIN_ADMIN.LANGUAGE + size: medium + classes: fancy + data-options@: '\Grav\Plugin\Admin\Admin::adminLanguages' + default: 'en' + help: PLUGIN_ADMIN.LANGUAGE_HELP - twofa_check: - type: conditional - condition: config.plugins.admin.twofa_enabled + twofa_check: + type: conditional + condition: config.plugins.admin.twofa_enabled - fields: + fields: - twofa: - title: PLUGIN_ADMIN.2FA_TITLE - type: section - underline: true + twofa: + title: PLUGIN_ADMIN.2FA_TITLE + type: section + underline: true - twofa_enabled: - type: toggle - label: PLUGIN_ADMIN.2FA_ENABLED - classes: twofa-toggle - highlight: 1 - default: 0 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool + twofa_enabled: + type: toggle + label: PLUGIN_ADMIN.2FA_ENABLED + classes: twofa-toggle + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool - twofa_secret: - type: 2fa_secret - outerclasses: 'twofa-secret' - markdown: true - label: PLUGIN_ADMIN.2FA_SECRET - sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP + twofa_secret: + type: 2fa_secret + outerclasses: 'twofa-secret' + markdown: true + label: PLUGIN_ADMIN.2FA_SECRET + sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP - security: - title: PLUGIN_ADMIN.ACCESS_LEVELS - type: section - security: admin.super - underline: true + security: + security@: admin.super + title: PLUGIN_ADMIN.ACCESS_LEVELS + type: section + underline: true - fields: - groups: - type: select - multiple: true - size: large - label: PLUGIN_ADMIN.GROUPS - data-options@: '\Grav\Common\User\Group::groupNames' - classes: fancy - help: PLUGIN_ADMIN.GROUPS_HELP - validate: - type: commalist + fields: + groups: + security@: admin.super + type: select + multiple: true + size: large + label: PLUGIN_ADMIN.GROUPS + data-options@: '\Grav\Common\User\Group::groupNames' + classes: fancy + help: PLUGIN_ADMIN.GROUPS_HELP + validate: + type: commalist - access: - type: permissions - label: PLUGIN_ADMIN.PERMISSIONS - ignore_empty: true - validate: - type: array + access: + security@: admin.super + type: permissions + label: PLUGIN_ADMIN.PERMISSIONS + ignore_empty: true + validate: + type: array diff --git a/system/blueprints/user/account_new.yaml b/system/blueprints/user/account_new.yaml index 6c6505b..7a92518 100644 --- a/system/blueprints/user/account_new.yaml +++ b/system/blueprints/user/account_new.yaml @@ -12,5 +12,7 @@ form: type: text label: PLUGIN_ADMIN.USERNAME help: PLUGIN_ADMIN.USERNAME_HELP + unset-disabled@: true + unset-readonly@: true validate: required: true diff --git a/system/blueprints/user/accounts.yaml b/system/blueprints/user/accounts.yaml new file mode 100644 index 0000000..49b0f71 --- /dev/null +++ b/system/blueprints/user/accounts.yaml @@ -0,0 +1,39 @@ +title: User Accounts +description: User Accounts +type: flex-objects + +extends@: 'user/account' + +config: + admin: + list: + fields: + username: + link: edit + search: true + email: + search: true + fullname: + search: true + options: + per_page: 20 + order: + by: username + dir: asc + + menu: + list: + route: '/accounts' + title: Accounts + icon: fa-users + authorize: ['admin.users', 'admin.accounts', 'admin.super'] + +form: + fields: + username: + flex-disabled@: exists + disabled: false + flex-readonly@: exists + readonly: false + validate: + required: true \ No newline at end of file diff --git a/system/config/backups.yaml b/system/config/backups.yaml new file mode 100644 index 0000000..0d3c41e --- /dev/null +++ b/system/config/backups.yaml @@ -0,0 +1,15 @@ +purge: + trigger: space + max_backups_count: 25 + max_backups_space: 5 + max_backups_time: 365 + +profiles: + - + name: 'Default Site Backup' + root: '/' + schedule: false + schedule_at: '0 3 * * *' + exclude_paths: "/backup\r\n/cache\r\n/images\r\n/logs\r\n/tmp" + exclude_files: ".DS_Store\r\n.git\r\n.svn\r\n.hg\r\n.idea\r\n.vscode\r\nnode_modules" + diff --git a/system/config/security.yaml b/system/config/security.yaml index 77e5de0..964e682 100644 --- a/system/config/security.yaml +++ b/system/config/security.yaml @@ -5,6 +5,13 @@ xss_enabled: moz_binding: true html_inline_styles: true dangerous_tags: true +xss_invalid_protocols: + - javascript + - livescript + - vbscript + - mocha + - feed + - data xss_dangerous_tags: - applet - meta diff --git a/system/config/site.yaml b/system/config/site.yaml index bb2ee6e..292e49d 100644 --- a/system/config/site.yaml +++ b/system/config/site.yaml @@ -17,17 +17,17 @@ summary: delimiter: === # The summary delimiter redirects: -# '/redirect-test': '/' # Redirect test goes to home page -# '/old/(.*)': '/new/$1' # Would redirect /old/my-page to /new/my-page +# '/redirect-test': '/' # Redirect test goes to home page +# '/old/(.*)': '/new/$1' # Would redirect /old/my-page to /new/my-page routes: -# '/something/else': '/blog/sample-3' # Alias for /blog/sample-3 -# '/new/(.*)': '/blog/$1' # Regex any /new/my-page URL to /blog/my-page Route +# '/something/else': '/blog/sample-3' # Alias for /blog/sample-3 +# '/new/(.*)': '/blog/$1' # Regex any /new/my-page URL to /blog/my-page Route blog: route: '/blog' # Custom value added (accessible via system.blog.route) -#menu: # Sample Menu Example +#menu: # Menu Example # - text: Source # icon: github # url: https://github.com/getgrav/grav diff --git a/system/config/streams.yaml b/system/config/streams.yaml index 9053b90..1af0024 100644 --- a/system/config/streams.yaml +++ b/system/config/streams.yaml @@ -1,6 +1,6 @@ schemes: image: - type: ReadOnlyStream + type: Stream paths: - user://images - system://images diff --git a/system/config/system.yaml b/system/config/system.yaml index 065e059..405a961 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -13,7 +13,9 @@ intl_enabled: true # Special logic for PHP Interna languages: supported: [] # List of languages supported. eg: [en, fr, de] + default_lang: # Default is the first supported language. Must be one of the supported languages include_default_lang: true # Include the default lang prefix in all URLs + pages_fallback_only: false # Only fallback to find page content through supported languages translations: true # Enable translations by default translations_fallback: true # Fallback through supported translations if active lang doesn't exist session_store_active: false # Store active language in session @@ -52,7 +54,7 @@ pages: special_chars: # List of special characters to automatically convert to entities '>': 'gt' '<': 'lt' - types: [txt,xml,html,htm,json,rss,atom] # list of valid page types + types: [html,htm,xml,txt,json,rss,atom] # list of valid page types append_url_extension: '' # Append page's extension in Page urls (e.g. '.html' results in /path/page.html) expires: 604800 # Page expires time in seconds (604800 seconds = 7 days) cache_control: # Can be blank for no setting, or a valid `cache-control` text value @@ -65,6 +67,7 @@ pages: ignore_files: [.DS_Store] # Files to ignore in Pages ignore_folders: [.git, .idea] # Folders to ignore in Pages ignore_hidden: true # Ignore all Hidden files and folders + hide_empty_folders: false # If folder has no .md file, should it be hidden url_taxonomy_filters: true # Enable auto-magic URL-based taxonomy filters for page collections frontmatter: process_twig: false # Should the frontmatter be processed to replace Twig variables? @@ -74,8 +77,11 @@ cache: enabled: true # Set to true to enable caching check: method: file # Method to check for updates in pages: file|folder|hash|none - driver: auto # One of: auto|file|apc|xcache|memcache|wincache + driver: auto # One of: auto|file|apcu|memcache|wincache prefix: 'g' # Cache prefix string (prevents cache conflicts) + 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 cli_compatibility: false # Ensures only non-volatile drivers are used (file, redis, memcache, etc.) lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite) @@ -112,6 +118,11 @@ errors: display: 0 # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error log: true # Log errors to /logs folder +log: + handler: file # Log handler. Currently supported: file | syslog + syslog: + facility: local6 # Syslog facilities output + debugger: enabled: false # Enable Grav debugger and following settings shutdown: @@ -123,6 +134,7 @@ images: cache_perms: '0755' # MUST BE IN QUOTES!! Default cache folder perms. Usually '0755' or '0775' debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example auto_fix_orientation: false # Automatically fix the image orientation based on the Exif data + seofriendly: false # SEO-friendly processed image names media: enable_media_timestamp: false # Enable media timestamps @@ -135,6 +147,7 @@ session: initialize: true # Initialize session from Grav (if false, plugin needs to start the session) timeout: 1800 # Timeout in seconds name: grav-site # Name prefix of the session cookie. Use alphanumeric, dashes or underscores only. Do not use dots in the session name + uniqueness: path # Should sessions be `path` based or `security.salt` based secure: false # Set session secure. If true, indicates that communication for this cookie must be over an encrypted transmission. Enable this only on sites that run exclusively on HTTPS httponly: true # Set session HTTP only. If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed. split: true # Sessions should be independent between site and plugins (such as admin) @@ -147,6 +160,10 @@ gpm: 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 +accounts: + type: data # Account type: data or flex + storage: file # Flex storage type: file or folder + strict_mode: yaml_compat: true # Grav 1.5+: Enables YAML backwards compatibility twig_compat: true # Grav 1.5+: Enables deprecated Twig autoescape setting (autoescape: false) diff --git a/system/defines.php b/system/defines.php index 0c21d8d..85d88d8 100644 --- a/system/defines.php +++ b/system/defines.php @@ -1,19 +1,19 @@ فشل التحقق من صحة:' - INVALID_INPUT: إدخال غير صحيح في - MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:' -MONTHS_OF_THE_YEAR: - - كانون الثاني - - شباط - - آذار/ مارس - - نيسان - - أيار - - حزيران - - تموز - - آب - - أيلول - - تشرين الأول - - تشرين الثاني - - كانون الأول -DAYS_OF_THE_WEEK: - - الاثنين - - الثلاثاء - - الأربعاء - - الخميس - - الجمعة - - السبت - - الأحد +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nالعنوان: %1$s\n---\n# خطأ: مادة أمامية غير صحيحة\n\nمسار: '%2$s'\n\n**%3$s**\n\n, , ,\n\n%4$s\n, , ," + NICETIME: + NO_DATE_PROVIDED: لم يتم تقديم التاريخ + BAD_DATE: تاريخ خاطئ + AGO: من قبل + FROM_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: 'حقل مطلوب مفقود:' + MONTHS_OF_THE_YEAR: + - 'كانون الثاني' + - 'شباط' + - 'آذار/ مارس' + - 'نيسان' + - 'أيار' + - 'حزيران' + - 'تموز' + - 'آب' + - 'أيلول' + - 'تشرين الأول' + - 'تشرين الثاني' + - 'كانون الأول' + DAYS_OF_THE_WEEK: + - 'الاثنين' + - 'الثلاثاء' + - 'الأربعاء' + - 'الخميس' + - 'الجمعة' + - 'السبت' + - 'الأحد' diff --git a/system/languages/bg.yaml b/system/languages/bg.yaml new file mode 100644 index 0000000..48daceb --- /dev/null +++ b/system/languages/bg.yaml @@ -0,0 +1,62 @@ +--- +GRAV: + NICETIME: + NO_DATE_PROVIDED: Не е въведена дата + BAD_DATE: Невалидна дата + AGO: преди + FROM_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: 'Липсва задължително поле:' + MONTHS_OF_THE_YEAR: + - 'януари' + - 'февруари' + - 'март' + - 'април' + - 'май' + - 'юни' + - 'юли' + - 'август' + - 'септември' + - 'октомври' + - 'ноември' + - 'декември' + DAYS_OF_THE_WEEK: + - 'понеделник' + - 'вторник' + - 'сряда' + - 'четвъртък' + - 'петък' + - 'събота' + - 'неделя' diff --git a/system/languages/ca.yaml b/system/languages/ca.yaml index b9906e6..bd04474 100644 --- a/system/languages/ca.yaml +++ b/system/languages/ca.yaml @@ -1,75 +1,62 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - - # S'ha produït un error: Frontmatter invàlid - - Ruta: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -NICETIME: - NO_DATE_PROVIDED: "No s'ha proporcionat data" - BAD_DATE: Data invàlida - AGO: abans - FROM_NOW: "des d'ara" - SECOND: segon - MINUTE: minut - HOUR: hora - DAY: dia - WEEK: setmana - MONTH: mes - YEAR: any - DECADE: dècada - SEC: s - MIN: min - HR: h - WK: setm. - MO: m. - YR: a. - DEC: dèc. - SECOND_PLURAL: segons - MINUTE_PLURAL: minuts - HOUR_PLURAL: hores - DAY_PLURAL: dies - WEEK_PLURAL: setmanes - MONTH_PLURAL: mesos - YEAR_PLURAL: anys - DECADE_PLURAL: dècades - SEC_PLURAL: s - MIN_PLURAL: min - HR_PLURAL: h - WK_PLURAL: setm. - MO_PLURAL: mesos - YR_PLURAL: anys - DEC_PLURAL: dèc. -FORM: - VALIDATION_FAIL: 'Ha fallat la validació:' - INVALID_INPUT: Entrada no vàlida a - MISSING_REQUIRED_FIELD: 'Falta camp obligatori:' -MONTHS_OF_THE_YEAR: - - Gener - - Febrer - - Març - - Abril - - Maig - - Juny - - Juliol - - Agost - - Setembre - - Octubre - - Novembre - - Desembre -DAYS_OF_THE_WEEK: - - Dilluns - - Dimarts - - Dimecres - - Dijous - - Divendres - - Dissabte - - Diumenge +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# S'ha produït un error: Frontmatter invàlid\n\nRuta: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: No s'ha proporcionat data + BAD_DATE: Data invàlida + AGO: abans + FROM_NOW: des d'ara + SECOND: segon + MINUTE: minut + HOUR: hora + DAY: dia + WEEK: setmana + MONTH: mes + YEAR: any + DECADE: dècada + SEC: s + HR: h + WK: setm. + MO: m. + YR: a. + DEC: dèc. + SECOND_PLURAL: segons + MINUTE_PLURAL: minuts + HOUR_PLURAL: hores + DAY_PLURAL: dies + WEEK_PLURAL: setmanes + MONTH_PLURAL: mesos + YEAR_PLURAL: anys + DECADE_PLURAL: dècades + SEC_PLURAL: s + MIN_PLURAL: min + HR_PLURAL: h + WK_PLURAL: setm. + MO_PLURAL: mesos + YR_PLURAL: anys + DEC_PLURAL: dèc. + FORM: + VALIDATION_FAIL: Ha fallat la validació: + INVALID_INPUT: Entrada no vàlida a + MISSING_REQUIRED_FIELD: 'Falta camp obligatori:' + MONTHS_OF_THE_YEAR: + - 'Gener' + - 'Febrer' + - 'Març' + - 'Abril' + - 'Maig' + - 'Juny' + - 'Juliol' + - 'Agost' + - 'Setembre' + - 'Octubre' + - 'Novembre' + - 'Desembre' + DAYS_OF_THE_WEEK: + - 'Dilluns' + - 'Dimarts' + - 'Dimecres' + - 'Dijous' + - 'Divendres' + - 'Dissabte' + - 'Diumenge' diff --git a/system/languages/cs.yaml b/system/languages/cs.yaml index 68a49eb..80844c9 100644 --- a/system/languages/cs.yaml +++ b/system/languages/cs.yaml @@ -1,81 +1,144 @@ --- -INFLECTOR_UNCOUNTABLE: - - vybavení - - informace - - rýže - - peníze - - druhy - - série - - ryba - - ovce -INFLECTOR_IRREGULAR: - person: lidé - man: muži - child: děti - sex: pohlaví - move: pohyby -INFLECTOR_ORDINALS: - default: '.' - first: '.' - second: '.' - third: '.' -NICETIME: - NO_DATE_PROVIDED: Datum nebylo vloženo - BAD_DATE: Chybné datum - AGO: zpět - FROM_NOW: od teď - SECOND: sekunda - MINUTE: minuta - HOUR: hodina - DAY: den - WEEK: týden - MONTH: měsíc - YEAR: rok - DECADE: dekáda - SEC: sek - MIN: min - HR: hod - WK: t - MO: m - YR: r - DEC: dek - SECOND_PLURAL: sekundy - MINUTE_PLURAL: minuty - HOUR_PLURAL: hodiny - DAY_PLURAL: dny - WEEK_PLURAL: týdny - MONTH_PLURAL: měsíce - YEAR_PLURAL: roky - DECADE_PLURAL: dekády - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: hod - WK_PLURAL: t - MO_PLURAL: m - YR_PLURAL: r - DEC_PLURAL: dek -FORM: - VALIDATION_FAIL: 'Ověření se nezdařilo:' - INVALID_INPUT: Neplatný vstup v - MISSING_REQUIRED_FIELD: 'Chybí požadované pole:' -MONTHS_OF_THE_YEAR: - - ledna - - února - - března - - dubna - - května - - června - - července - - srpna - - září - - října - - listopadu - - prosince -DAYS_OF_THE_WEEK: - - Pondělí - - Úterý - - Středa - - Čtvrtek - - Pátek - - Sobota - - Neděle +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Chyba: Chybný frontmatter\n\nPath: `%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: + - 'vybavení' + - 'informace' + - 'rýže' + - 'peníze' + - 'druhy' + - 'série' + - 'ryba' + - 'ovce' + INFLECTOR_IRREGULAR: + 'person': 'lidé' + 'man': 'muži' + 'child': 'děti' + 'sex': 'pohlaví' + 'move': 'pohyby' + INFLECTOR_ORDINALS: + 'default': '.' + 'first': '.' + 'second': '.' + 'third': '.' + NICETIME: + NO_DATE_PROVIDED: Datum nebylo vloženo + BAD_DATE: Chybné datum + AGO: zpět + FROM_NOW: od teď + JUST_NOW: právě teď + SECOND: sekunda + MINUTE: minuta + HOUR: hodina + DAY: den + WEEK: týden + MONTH: měsíc + YEAR: rok + DECADE: dekáda + SEC: sek + MIN: min + HR: hod + WK: t + MO: m + YR: r + DEC: dek + SECOND_PLURAL: sekundy + MINUTE_PLURAL: minuty + HOUR_PLURAL: hodiny + DAY_PLURAL: dny + WEEK_PLURAL: týdny + MONTH_PLURAL: měsíce + YEAR_PLURAL: roky + DECADE_PLURAL: dekády + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: hod + WK_PLURAL: t + MO_PLURAL: m + YR_PLURAL: r + DEC_PLURAL: dek + FORM: + VALIDATION_FAIL: Ověření se nezdařilo: + INVALID_INPUT: Neplatný vstup v + MISSING_REQUIRED_FIELD: 'Chybí požadované pole:' + MONTHS_OF_THE_YEAR: + - 'leden' + - 'únor' + - 'březen' + - 'duben' + - 'květen' + - 'červen' + - 'červenec' + - 'srpen' + - 'září' + - 'říjen' + - 'listopad' + - 'prosinec' + DAYS_OF_THE_WEEK: + - 'pondělí' + - 'úterý' + - 'středa' + - 'čtvrtek' + - 'pátek' + - 'sobota' + - 'neděle' + CRON: + EVERY: každý + EVERY_HOUR: každou hodinu + EVERY_MINUTE: každou minutu + EVERY_DAY_OF_WEEK: každý den v týdnu + EVERY_DAY_OF_MONTH: každý den v měsíci + EVERY_MONTH: každý měsíc + TEXT_PERIOD: Every + TEXT_MINS: ' at minute(s) past the hour' + TEXT_TIME: ' at :' + TEXT_DOW: ' on ' + TEXT_MONTH: ' of ' + TEXT_DOM: ' on ' + ERROR1: Tag %s není podporován! + ERROR2: Chybný počet prvků + ERROR3: jquery_element musí být nastaven v nastaveních pro jqCron + ERROR4: Nerozpoznaný výraz diff --git a/system/languages/da.yaml b/system/languages/da.yaml index 47f5a45..17ce683 100644 --- a/system/languages/da.yaml +++ b/system/languages/da.yaml @@ -1,75 +1,62 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - Titel: %1$s - --- - - # Fejl: Ugyldigt frontmatter - - Sti: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -NICETIME: - NO_DATE_PROVIDED: Ingen dato angivet - BAD_DATE: Ugyldig dato - AGO: siden - FROM_NOW: fra nu - SECOND: sekund - MINUTE: minut - HOUR: time - DAY: dag - WEEK: uge - MONTH: måned - YEAR: år - DECADE: årti - SEC: sek - MIN: min - HR: t - WK: u - MO: md - YR: år - DEC: årti - SECOND_PLURAL: sekunder - MINUTE_PLURAL: minutter - HOUR_PLURAL: timer - DAY_PLURAL: dage - WEEK_PLURAL: uger - MONTH_PLURAL: måneder - YEAR_PLURAL: år - DECADE_PLURAL: årtier - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: timer - WK_PLURAL: uger - MO_PLURAL: mdr - YR_PLURAL: år - DEC_PLURAL: årtier -FORM: - VALIDATION_FAIL: 'Validering mislykkedes:' - INVALID_INPUT: Ugyldigt input i - MISSING_REQUIRED_FIELD: 'Mangler obligatorisk felt:' -MONTHS_OF_THE_YEAR: - - Januar - - Februar - - Marts - - April - - Maj - - Juni - - Juli - - August - - September - - Oktober - - November - - December -DAYS_OF_THE_WEEK: - - Mandag - - Tirsdag - - Onsdag - - Torsdag - - Fredag - - Lørdag - - Søndag +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nTitel: %1$s\n---\n\n# Fejl: Ugyldigt frontmatter\n\nSti: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: Ingen dato angivet + BAD_DATE: Ugyldig dato + AGO: siden + FROM_NOW: fra nu + SECOND: sekund + MINUTE: minut + HOUR: time + DAY: dag + WEEK: uge + MONTH: måned + YEAR: år + DECADE: årti + SEC: sek + HR: t + WK: u + MO: md + YR: år + DEC: årti + SECOND_PLURAL: sekunder + MINUTE_PLURAL: minutter + HOUR_PLURAL: timer + DAY_PLURAL: dage + WEEK_PLURAL: uger + MONTH_PLURAL: måneder + YEAR_PLURAL: år + DECADE_PLURAL: årtier + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: timer + WK_PLURAL: uger + MO_PLURAL: mdr + YR_PLURAL: år + DEC_PLURAL: årtier + FORM: + VALIDATION_FAIL: Validering mislykkedes: + INVALID_INPUT: Ugyldigt input i + MISSING_REQUIRED_FIELD: 'Mangler obligatorisk felt:' + MONTHS_OF_THE_YEAR: + - 'januar' + - 'februar' + - 'mars' + - 'april' + - 'mai' + - 'juni' + - 'juli' + - 'august' + - 'september' + - 'oktober' + - 'november' + - 'desember' + DAYS_OF_THE_WEEK: + - 'mandag' + - 'tirsdag' + - 'onsdag' + - 'torsdag' + - 'fredag' + - 'lørdag' + - 'søndag' diff --git a/system/languages/de.yaml b/system/languages/de.yaml index 9dff4cb..e9a2d4b 100644 --- a/system/languages/de.yaml +++ b/system/languages/de.yaml @@ -1,89 +1,144 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - # Fehler: Frontmatter enthält Fehler - - Pfad: `%2$s` - - **%3$s ** - - ``` - %4$s - ``` -INFLECTOR_UNCOUNTABLE: - 1: Informationen - 2: Reis - 3: Geld -INFLECTOR_IRREGULAR: - person: Personen - man: Menschen - child: Kinder - sex: Geschlecht - move: Züge -INFLECTOR_ORDINALS: - default: '.' - first: '.' - second: '.' - third: '.' -NICETIME: - NO_DATE_PROVIDED: Kein Datum angegeben - BAD_DATE: Falsches Datum - AGO: her - FROM_NOW: ab jetzt - SECOND: Sekunde - MINUTE: Minute - HOUR: Stunde - DAY: Tag - WEEK: Woche - MONTH: Monat - YEAR: Jahr - DECADE: Jahrzehnt - SEC: Sek. - MIN: Min. - HR: Std. - WK: Wo. - MO: Mo. - YR: J. - DEC: Dek. - SECOND_PLURAL: Sekunden - MINUTE_PLURAL: Minuten - HOUR_PLURAL: Stunden - DAY_PLURAL: Tage - WEEK_PLURAL: Wochen - MONTH_PLURAL: Monate - YEAR_PLURAL: Jahre - DECADE_PLURAL: Jahrzehnte - SEC_PLURAL: Sekunden - MIN_PLURAL: Minuten - HR_PLURAL: Stunden - WK_PLURAL: Wochen - MO_PLURAL: Monate - YR_PLURAL: Jahre - DEC_PLURAL: Jahrzehnten -FORM: - VALIDATION_FAIL: 'Überprüfung fehlgeschlagen:' - INVALID_INPUT: Ungültige Eingabe in - MISSING_REQUIRED_FIELD: 'Erforderliches Feld fehlt:' -MONTHS_OF_THE_YEAR: - - Januar - - Februar - - März - - April - - Mai - - Juni - - Juli - - August - - September - - Oktober - - November - - Dezember -DAYS_OF_THE_WEEK: - - Montag - - Dienstag - - Mittwoch - - Donnerstag - - Freitag - - Samstag - - Sonntag +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n# Fehler: Frontmatter enthält Fehler\n\nPfad: `%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': '\1ice' + '/(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': '\1ies' + '/(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\2ves' + '/([ti])a$/i': '\1um' + '/(n)ews$/i': '\1ews' + INFLECTOR_UNCOUNTABLE: + - 'Ausstattung' + - 'Informationen' + - 'Reis' + - 'Geld' + - 'Arten' + - 'Serie' + - 'Fisch' + - 'Schaf' + INFLECTOR_IRREGULAR: + 'person': 'Personen' + 'man': 'Menschen' + 'child': 'Kinder' + 'sex': 'Geschlecht' + 'move': 'Züge' + INFLECTOR_ORDINALS: + 'default': '.' + 'first': '.' + 'second': '.' + 'third': '.' + NICETIME: + NO_DATE_PROVIDED: Kein Datum angegeben + BAD_DATE: Falsches Datum + AGO: her + FROM_NOW: ab jetzt + JUST_NOW: jetzt gerade + SECOND: Sekunde + MINUTE: Minute + HOUR: Stunde + DAY: Tag + WEEK: Woche + MONTH: Monat + YEAR: Jahr + DECADE: Jahrzehnt + SEC: Sek. + MIN: Min. + HR: Std. + WK: Wo. + MO: Mo. + YR: J. + DEC: Dez + SECOND_PLURAL: Sekunden + MINUTE_PLURAL: Minuten + HOUR_PLURAL: Stunden + DAY_PLURAL: Tage + WEEK_PLURAL: Wochen + MONTH_PLURAL: Monate + YEAR_PLURAL: Jahre + DECADE_PLURAL: Jahrzehnte + SEC_PLURAL: Sekunden + MIN_PLURAL: Minuten + HR_PLURAL: Stunden + WK_PLURAL: Wochen + MO_PLURAL: Monate + YR_PLURAL: Jahre + DEC_PLURAL: Jahrzehnten + FORM: + VALIDATION_FAIL: Überprüfung fehlgeschlagen: + INVALID_INPUT: Ungültige Eingabe in + MISSING_REQUIRED_FIELD: 'Erforderliches Feld fehlt:' + MONTHS_OF_THE_YEAR: + - 'Januar' + - 'Februar' + - 'März' + - 'April' + - 'Mai' + - 'Juni' + - 'Juli' + - 'August' + - 'September' + - 'Oktober' + - 'November' + - 'Dezember' + DAYS_OF_THE_WEEK: + - 'Montag' + - 'Dienstag' + - 'Mittwoch' + - 'Donnerstag' + - 'Freitag' + - 'Samstag' + - 'Sonntag' + CRON: + EVERY: jede + EVERY_HOUR: jede Stunde + EVERY_MINUTE: Jede Minute + EVERY_DAY_OF_WEEK: jeden Tag der Woche + EVERY_DAY_OF_MONTH: jeden Tag des Monats + EVERY_MONTH: jeden Monat + TEXT_PERIOD: Alle + TEXT_MINS: ' bei Minuten nach der vollen Stunde (n)' + TEXT_TIME: ' bei :' + TEXT_DOW: ' auf ' + TEXT_MONTH: ' von ' + TEXT_DOM: ' auf ' + ERROR1: Der Tag %s wird nicht unterstützt! + ERROR2: Ungültige Anzahl von Elementen + ERROR3: jquery_element sollte in den jqCron Einstellungen gesetzt werden + ERROR4: Unbekannter Ausdruck diff --git a/system/languages/el.yaml b/system/languages/el.yaml index 16d24cb..1faa8dd 100644 --- a/system/languages/el.yaml +++ b/system/languages/el.yaml @@ -1,22 +1,63 @@ --- -MONTHS_OF_THE_YEAR: - - Ιανουάριος - - Φεβρουάριος - - Μάρτιος - - Απρίλιος - - Μάιος - - Ιούνιος - - Ιούλιος - - Αύγουστος - - Σεπτέμβριος - - Οκτώβριος - - Νοέμβριος - - Δεκέμβριος -DAYS_OF_THE_WEEK: - - Δευτέρα - - Τρίτη - - Τετάρτη - - Πέμπτη - - Παρασκευή - - Σάββατο - - Κυριακή +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nΤίτλος: %1$s\n---\n\n# Σφάλμα: Μη έγκυρη διαδρομή Frontmatter\n\nΔιαδρομή: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: Δεν δόθηκε καμία ημερομηνία + BAD_DATE: Εσφαλμένη ημερομηνία + AGO: πρίν + FROM_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: 'Λείπει το απαιτούμενο πεδίο:' + MONTHS_OF_THE_YEAR: + - 'Ιανουάριος' + - 'Φεβρουάριος' + - 'Μάρτιος' + - 'Απρίλιος' + - 'Μάιος' + - 'Ιούνιος' + - 'Ιούλιος' + - 'Αύγουστος' + - 'Σεπτέμβριος' + - 'Οκτώβριος' + - 'Νοέμβριος' + - 'Δεκέμβριος' + DAYS_OF_THE_WEEK: + - 'Δευτέρα' + - 'Τρίτη' + - 'Τετάρτη' + - 'Πέμπτη' + - 'Παρασκευή' + - 'Σάββατο' + - 'Κυριακή' diff --git a/system/languages/en.yaml b/system/languages/en.yaml index 7bda791..7c351e6 100644 --- a/system/languages/en.yaml +++ b/system/languages/en.yaml @@ -1,99 +1,120 @@ -FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%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' - '/s$/i': '' -INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'] -INFLECTOR_IRREGULAR: - 'person': 'people' - 'man': 'men' - 'child': 'children' - 'sex': 'sexes' - 'move': 'moves' -INFLECTOR_ORDINALS: - 'default': 'th' - 'first': 'st' - 'second': 'nd' - 'third': 'rd' -NICETIME: - NO_DATE_PROVIDED: No date provided - BAD_DATE: Bad date - AGO: ago - FROM_NOW: from now - JUST_NOW: just now - SECOND: second - MINUTE: minute - HOUR: hour - DAY: day - WEEK: week - MONTH: month - YEAR: year - DECADE: decade - SEC: sec - MIN: min - HR: hr - WK: wk - MO: mo - YR: yr - DEC: dec - SECOND_PLURAL: seconds - MINUTE_PLURAL: minutes - HOUR_PLURAL: hours - DAY_PLURAL: days - WEEK_PLURAL: weeks - MONTH_PLURAL: months - YEAR_PLURAL: years - DECADE_PLURAL: decades - SEC_PLURAL: secs - MIN_PLURAL: mins - HR_PLURAL: hrs - WK_PLURAL: wks - MO_PLURAL: mos - YR_PLURAL: yrs - DEC_PLURAL: decs -FORM: - VALIDATION_FAIL: Validation failed: - INVALID_INPUT: Invalid input in - MISSING_REQUIRED_FIELD: Missing required field: -MONTHS_OF_THE_YEAR: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] -DAYS_OF_THE_WEEK: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%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' + '/s$/i': '' + INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep'] + INFLECTOR_IRREGULAR: + 'person': 'people' + 'man': 'men' + 'child': 'children' + 'sex': 'sexes' + 'move': 'moves' + INFLECTOR_ORDINALS: + 'default': 'th' + 'first': 'st' + 'second': 'nd' + 'third': 'rd' + NICETIME: + NO_DATE_PROVIDED: No date provided + BAD_DATE: Bad date + AGO: ago + FROM_NOW: from now + JUST_NOW: just now + SECOND: second + MINUTE: minute + HOUR: hour + DAY: day + WEEK: week + MONTH: month + YEAR: year + DECADE: decade + SEC: sec + MIN: min + HR: hr + WK: wk + MO: mo + YR: yr + DEC: dec + SECOND_PLURAL: seconds + MINUTE_PLURAL: minutes + HOUR_PLURAL: hours + DAY_PLURAL: days + WEEK_PLURAL: weeks + MONTH_PLURAL: months + YEAR_PLURAL: years + DECADE_PLURAL: decades + SEC_PLURAL: secs + MIN_PLURAL: mins + HR_PLURAL: hrs + WK_PLURAL: wks + MO_PLURAL: mos + YR_PLURAL: yrs + DEC_PLURAL: decs + FORM: + VALIDATION_FAIL: Validation failed: + INVALID_INPUT: Invalid input in + MISSING_REQUIRED_FIELD: Missing required field: + MONTHS_OF_THE_YEAR: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] + DAYS_OF_THE_WEEK: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + YES: "Yes" + NO: "No" + CRON: + EVERY: every + EVERY_HOUR: every hour + EVERY_MINUTE: every minute + EVERY_DAY_OF_WEEK: every day of the week + EVERY_DAY_OF_MONTH: every day of the month + EVERY_MONTH: every month + TEXT_PERIOD: Every + TEXT_MINS: ' at minute(s) past the hour' + TEXT_TIME: ' at :' + TEXT_DOW: ' on ' + TEXT_MONTH: ' of ' + TEXT_DOM: ' on ' + ERROR1: The tag %s is not supported! + ERROR2: Bad number of elements + ERROR3: The jquery_element should be set into jqCron settings + ERROR4: Unrecognized expression diff --git a/system/languages/es.yaml b/system/languages/es.yaml index b19b620..7bacfeb 100644 --- a/system/languages/es.yaml +++ b/system/languages/es.yaml @@ -1,90 +1,95 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - - # Error: Frontmatter Inválido - - Ruta: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -INFLECTOR_UNCOUNTABLE: - 0: equipo - 1: información - 3: dinero - 5: series - 6: pescado - 7: oveja -INFLECTOR_IRREGULAR: - man: hombres - child: niños - sex: sexos -INFLECTOR_ORDINALS: - first: ro - second: do - third: ro -NICETIME: - NO_DATE_PROVIDED: No se proporcionó fecha - BAD_DATE: Fecha erronea - AGO: antes - FROM_NOW: desde ahora - SECOND: segundo - MINUTE: minuto - HOUR: hora - DAY: día - WEEK: semana - MONTH: mes - YEAR: año - DECADE: década - SEC: seg - MIN: min - HR: h - WK: sem - MO: mes - YR: año - DEC: dec - SECOND_PLURAL: segundos - MINUTE_PLURAL: minutos - HOUR_PLURAL: horas - DAY_PLURAL: días - WEEK_PLURAL: semanas - MONTH_PLURAL: meses - YEAR_PLURAL: años - DECADE_PLURAL: décadas - SEC_PLURAL: segs - MIN_PLURAL: mins - HR_PLURAL: hs - WK_PLURAL: sem - MO_PLURAL: mes - YR_PLURAL: años - DEC_PLURAL: décadas -FORM: - VALIDATION_FAIL: 'Falló la validación. ' - INVALID_INPUT: 'Dato inválido en: ' - MISSING_REQUIRED_FIELD: 'Falta el campo requerido: ' -MONTHS_OF_THE_YEAR: - - Enero - - Febrero - - Marzo - - Abril - - Mayo - - Junio - - Julio - - Agosto - - Septiembre - - Octubre - - Noviembre - - Diciembre -DAYS_OF_THE_WEEK: - - Lunes - - Martes - - Miércoles - - Jueves - - Viernes - - Sábado - - Domingo +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntítulo: %1$s\n---\n\n# Error: Frontmatter no válido\n\nRuta: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + INFLECTOR_UNCOUNTABLE: + - 'equipo' + - 'información' + - 'rice' + - 'dinero' + - 'species' + - 'series' + - 'pescado' + - 'oveja' + INFLECTOR_IRREGULAR: + 'man': 'hombres' + 'child': 'niños' + 'sex': 'sexos' + INFLECTOR_ORDINALS: + 'first': 'ro' + 'second': 'do' + 'third': 'ro' + NICETIME: + NO_DATE_PROVIDED: No se proporcionó fecha + BAD_DATE: Fecha errónea + AGO: antes + FROM_NOW: desde ahora + JUST_NOW: hace un momento + SECOND: segundo + MINUTE: minuto + HOUR: hora + DAY: día + WEEK: semana + MONTH: mes + YEAR: año + DECADE: década + SEC: seg + HR: h + WK: sem + MO: mes + YR: año + SECOND_PLURAL: segundos + MINUTE_PLURAL: minutos + HOUR_PLURAL: horas + DAY_PLURAL: días + WEEK_PLURAL: semanas + MONTH_PLURAL: meses + YEAR_PLURAL: años + DECADE_PLURAL: décadas + SEC_PLURAL: segs + HR_PLURAL: hs + WK_PLURAL: sem + MO_PLURAL: mes + YR_PLURAL: años + DEC_PLURAL: décadas + FORM: + VALIDATION_FAIL: 'Falló la validación: ' + INVALID_INPUT: 'Dato inválido en: ' + MISSING_REQUIRED_FIELD: 'Falta el campo requerido: ' + MONTHS_OF_THE_YEAR: + - 'Enero' + - 'Febrero' + - 'Marzo' + - 'Abril' + - 'Mayo' + - 'Junio' + - 'Julio' + - 'Agosto' + - 'Septiembre' + - 'Octubre' + - 'Noviembre' + - 'Diciembre' + DAYS_OF_THE_WEEK: + - 'Lunes' + - 'Martes' + - 'Miércoles' + - 'Jueves' + - 'Viernes' + - 'Sábado' + - 'Domingo' + CRON: + EVERY: cada + EVERY_HOUR: cada hora + EVERY_MINUTE: cada minuto + EVERY_DAY_OF_WEEK: cada día de la semana + EVERY_DAY_OF_MONTH: cada día del mes + EVERY_MONTH: cada mes + TEXT_PERIOD: Cada + TEXT_MINS: ' a minuto(s) despues de la hora' + TEXT_TIME: ' a :' + TEXT_DOW: ' en ' + TEXT_MONTH: ' de' + TEXT_DOM: ' en' + ERROR1: La etiqueta %s no está soportada! + ERROR2: El número de elementos es erroneo + ERROR3: El jquery_element debería establecerse en la configuración del jqCron + ERROR4: Expresión no reconocida diff --git a/system/languages/et.yaml b/system/languages/et.yaml new file mode 100644 index 0000000..619aa1a --- /dev/null +++ b/system/languages/et.yaml @@ -0,0 +1,85 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\npealkiri: %1$s\n---\n\n# Viga: vigane Frontmatter'i\n\nasukoht: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + INFLECTOR_UNCOUNTABLE: + - 'equipment' + - 'informatsioon' + - 'rice' + - 'money' + - 'species' + - 'series' + - 'kala' + - 'lammas' + INFLECTOR_IRREGULAR: + 'person': 'inimesed' + 'man': 'mees' + 'child': 'lapsed' + INFLECTOR_ORDINALS: + 'default': '.' + 'first': '.' + 'second': '.' + 'third': '.' + NICETIME: + NO_DATE_PROVIDED: Kuupäev määramata + BAD_DATE: Vigane kuupäev + AGO: tagasi + FROM_NOW: praegusest + JUST_NOW: just nüüd + SECOND: sekund + MINUTE: minut + HOUR: tundi + DAY: päev + WEEK: nädal + MONTH: kuu + YEAR: aasta + DECADE: 10 aastat + SEC: sek + HR: t + WK: näd + MO: k. + YR: a. + DEC: dekaad + SECOND_PLURAL: sekundit + MINUTE_PLURAL: minutit + HOUR_PLURAL: tundi + DAY_PLURAL: päeva + WEEK_PLURAL: nädalat + MONTH_PLURAL: kuud + YEAR_PLURAL: aastat + DECADE_PLURAL: dekaadi + SEC_PLURAL: sekundit + MIN_PLURAL: min + HR_PLURAL: t + WK_PLURAL: näd + MO_PLURAL: kuud + YR_PLURAL: aastat + DEC_PLURAL: dek. + FORM: + VALIDATION_FAIL: Kinnitamine nurjus: + INVALID_INPUT: 'Vigane sisend:' + MISSING_REQUIRED_FIELD: 'Nõutud väli puudub:' + MONTHS_OF_THE_YEAR: + - 'jaanuar' + - 'veebruar' + - 'märts' + - 'aprill' + - 'mai' + - 'juuni' + - 'juuli' + - 'august' + - 'september' + - 'oktoober' + - 'november' + - 'detsember' + DAYS_OF_THE_WEEK: + - 'esmaspäev' + - 'teisipäev' + - 'kolmapäev' + - 'neljapäev' + - 'reede' + - 'laupäev' + - 'pühapäev' + CRON: + EVERY: iga + EVERY_MONTH: iga kuu + TEXT_PERIOD: Iga diff --git a/system/languages/eu.yaml b/system/languages/eu.yaml new file mode 100644 index 0000000..4a9a58e --- /dev/null +++ b/system/languages/eu.yaml @@ -0,0 +1,62 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "--- title: %1$s --- # Errorea: Baliogabeko Frontmatter Bidea: `%2$s` **%3$s** ``` %4$s ```" + NICETIME: + NO_DATE_PROVIDED: Ez da datarik ezarri + BAD_DATE: Okerreko data + AGO: ' duela' + FROM_NOW: oraindik aurrera + SECOND: segundo + MINUTE: minutu + HOUR: ordua + DAY: egun + WEEK: astea + MONTH: hilabetea + YEAR: urtea + DECADE: hamarkada + SEC: seg + HR: h + WK: ast + MO: hil + YR: urt + DEC: ham + SECOND_PLURAL: segundo + MINUTE_PLURAL: minutu + HOUR_PLURAL: ordu + DAY_PLURAL: egun + WEEK_PLURAL: aste + MONTH_PLURAL: hilabete + YEAR_PLURAL: urte + DECADE_PLURAL: hamarkada + SEC_PLURAL: segundo + MIN_PLURAL: minutu + HR_PLURAL: h + WK_PLURAL: ast + MO_PLURAL: hil + YR_PLURAL: urt + DEC_PLURAL: ham + FORM: + VALIDATION_FAIL: Balidazioak huts egin du + INVALID_INPUT: Baliogabeko sarrera + MISSING_REQUIRED_FIELD: 'Derrigorrezko eremua bete gabe:' + MONTHS_OF_THE_YEAR: + - 'Urtarrila' + - 'Otsaila' + - 'Martxoa' + - 'Apirila' + - 'Maiatza' + - 'Ekaina' + - 'Uztaila' + - 'Abuztua' + - 'Iraila' + - 'Urria' + - 'Azaroa' + - 'Abendua' + DAYS_OF_THE_WEEK: + - 'Astelehena' + - 'Asteartea' + - 'Azteazkena' + - 'Osteguna' + - 'Ostirala' + - 'Larunbata' + - 'Igandea' diff --git a/system/languages/fa.yaml b/system/languages/fa.yaml new file mode 100644 index 0000000..186f79d --- /dev/null +++ b/system/languages/fa.yaml @@ -0,0 +1,62 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nعنوان: %1$s\n---\n\n# خطا: Frontmatter غلط\n\nمسیر: %2$s\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: تاریخی ارائه نشده + BAD_DATE: تاریخ اشتباه + AGO: قبل + FROM_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: هفته + YR_PLURAL: سال + DEC_PLURAL: دهه + FORM: + VALIDATION_FAIL: سنجش اعتبار ناموفق بود + INVALID_INPUT: ورودی نامعتبر در + MISSING_REQUIRED_FIELD: 'قسمت ضروری جا افتاده:' + MONTHS_OF_THE_YEAR: + - 'ژانویه' + - 'فوریه' + - 'مارس' + - 'آوریل' + - 'می' + - 'ژوئن' + - 'ژوئیه' + - 'اوت' + - 'سپتامبر' + - 'اکتبر' + - 'نوامبر' + - 'دسامبر' + DAYS_OF_THE_WEEK: + - 'دوشنبه' + - 'سه‌ شنبه' + - 'چهارشنبه' + - 'پنج شنبه' + - 'جمعه' + - 'شنبه' + - 'یک‌شنبه' diff --git a/system/languages/fi.yaml b/system/languages/fi.yaml index dd5a793..e84c19f 100644 --- a/system/languages/fi.yaml +++ b/system/languages/fi.yaml @@ -1,60 +1,134 @@ --- -NICETIME: - NO_DATE_PROVIDED: Päivämäärää ei annettu - BAD_DATE: Virheellinen päivämäärä - AGO: sitten - FROM_NOW: tästä lähtien - SECOND: sekunti - MINUTE: minuutti - HOUR: tunti - DAY: päivä - WEEK: viikko - MONTH: kuukausi - YEAR: vuosi - DECADE: vuosikymmen - SEC: sek - MIN: min - HR: h - WK: vk - MO: kk - YR: v - DEC: vuosikymmen - SECOND_PLURAL: sekuntia - MINUTE_PLURAL: minuuttia - HOUR_PLURAL: tuntia - DAY_PLURAL: päivää - WEEK_PLURAL: viikkoa - MONTH_PLURAL: kuukautta - YEAR_PLURAL: vuotta - DECADE_PLURAL: vuosikymmentä - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: h - WK_PLURAL: v - MO_PLURAL: kk - YR_PLURAL: v - DEC_PLURAL: vuosikymmentä -FORM: - VALIDATION_FAIL: 'Vahvistus epäonnistui:' - MISSING_REQUIRED_FIELD: 'Puuttuva pakollinen kenttä:' -MONTHS_OF_THE_YEAR: - - Tammikuu - - Helmikuu - - Maaliskuu - - Huhtikuu - - Toukokuu - - Kesäkuuta - - Heinäkuu - - Elokuu - - Syyskuu - - Lokakuu - - Marraskuu - - Joulukuu -DAYS_OF_THE_WEEK: - - Maanantai - - Tiistai - - Keskiviikko - - Torstai - - Perjantai - - Lauantai - - Sunnuntai +GRAV: + FRONTMATTER_ERROR_PAGE: "---\notsikko: %1$s\n---\n\n# Virhe: Virheellinen Frontmatter\n\nPolku: `%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: + - 'equipment' + - 'information' + - 'riisi' + - 'raha' + - 'lajit' + - 'series' + - 'kala' + - 'lammas' + INFLECTOR_IRREGULAR: + 'person': 'ihmiset' + 'man': 'miehet' + 'child': 'lapset' + 'sex': 'sukupuoli' + INFLECTOR_ORDINALS: + 'default': '.' + 'first': '.' + 'second': '.' + 'third': '.' + NICETIME: + NO_DATE_PROVIDED: Päivämäärää ei annettu + BAD_DATE: Virheellinen päivämäärä + AGO: sitten + FROM_NOW: tästä lähtien + JUST_NOW: juuri nyt + SECOND: sekuntti + MINUTE: minuutti + HOUR: tunti + DAY: päivä + WEEK: viikko + MONTH: kuukausi + YEAR: vuosi + DECADE: vuosikymmen + SEC: sek + MIN: min + HR: h + WK: vk + MO: kk + YR: v + DEC: vuosikymmen + SECOND_PLURAL: sekuntia + MINUTE_PLURAL: minuuttia + HOUR_PLURAL: tuntia + DAY_PLURAL: päivää + WEEK_PLURAL: viikkoa + MONTH_PLURAL: kuukautta + YEAR_PLURAL: vuotta + DECADE_PLURAL: vuosikymmentä + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: h + WK_PLURAL: v + MO_PLURAL: kk + YR_PLURAL: v + DEC_PLURAL: vuosikymmentä + FORM: + VALIDATION_FAIL: Vahvistus epäonnistui: + INVALID_INPUT: Syöte ei kelpaa + MISSING_REQUIRED_FIELD: 'Puuttuva pakollinen kenttä:' + MONTHS_OF_THE_YEAR: + - 'Tammikuu' + - 'Helmikuu' + - 'Maaliskuu' + - 'Huhtikuu' + - 'Toukokuu' + - 'Kesäkuuta' + - 'Heinäkuu' + - 'Elokuu' + - 'Syyskuu' + - 'Lokakuu' + - 'Marraskuu' + - 'Joulukuu' + DAYS_OF_THE_WEEK: + - 'Maanantai' + - 'Tiistai' + - 'Keskiviikko' + - 'Torstai' + - 'Perjantai' + - 'Lauantai' + - 'Sunnuntai' + CRON: + EVERY: joka + EVERY_HOUR: joka tunti + EVERY_MINUTE: joka minuutti + EVERY_DAY_OF_WEEK: viikon jokaisena päivänä + EVERY_DAY_OF_MONTH: kuukauden jokaisena päivänä + EVERY_MONTH: joka kuukausi + TEXT_PERIOD: Joka diff --git a/system/languages/fr.yaml b/system/languages/fr.yaml index d6dd912..9284f7e 100644 --- a/system/languages/fr.yaml +++ b/system/languages/fr.yaml @@ -1,138 +1,123 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - - # Erreur : Frontmatter invalide - - Path: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -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: - - équipement - - informations - - riz - - argent - - espèces - - séries - - poisson - - mouton -INFLECTOR_IRREGULAR: - person: personnes - man: hommes - child: enfants - sex: sexes - move: déplacements -INFLECTOR_ORDINALS: - default: ème - first: er - second: ème - third: ème -NICETIME: - NO_DATE_PROVIDED: Aucune date fournie - BAD_DATE: Date erronée - AGO: plus tôt - FROM_NOW: à partir de maintenant - SECOND: seconde - MINUTE: minute - HOUR: heure - DAY: jour - WEEK: semaine - MONTH: mois - YEAR: année - DECADE: décennie - SEC: s - MIN: m - HR: h - WK: sem - MO: m - YR: an - DEC: déc - SECOND_PLURAL: secondes - MINUTE_PLURAL: minutes - HOUR_PLURAL: heures - DAY_PLURAL: jours - WEEK_PLURAL: semaines - MONTH_PLURAL: mois - YEAR_PLURAL: années - DECADE_PLURAL: décennies - SEC_PLURAL: s - MIN_PLURAL: m - HR_PLURAL: h - WK_PLURAL: sem - MO_PLURAL: mois - YR_PLURAL: a - DEC_PLURAL: décs -FORM: - VALIDATION_FAIL: 'La validation a échoué :' - INVALID_INPUT: Saisie non valide - MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :' -MONTHS_OF_THE_YEAR: - - Janvier - - Février - - Mars - - Avril - - Mai - - Juin - - Juillet - - Août - - Septembre - - Octobre - - Novembre - - Décembre -DAYS_OF_THE_WEEK: - - Lundi - - Mardi - - Mercredi - - Jeudi - - Vendredi - - Samedi - - Dimanche +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitre: %1$s\n---\n\n# Erreur : Frontmatter invalide\n\nChemin: `%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': '\1es' + '/(bu)s$/i': 'Bus' + '/(alias|status)/i': 'alias|status' + '/(ax|test)is$/i': '\1s' + '/s$/i': 's' + '/$/': 's' + INFLECTOR_SINGULAR: + '/(quiz)zes$/i': '\1' + '/(alias|status)es$/i': '\1' + '/([octop|vir])i$/i': '\1us' + INFLECTOR_UNCOUNTABLE: + - 'équipement' + - 'information' + - 'riz' + - 'argent' + - 'espèces' + - 'séries' + - 'poisson' + - 'mouton' + INFLECTOR_IRREGULAR: + 'person': 'personnes' + 'man': 'hommes' + 'child': 'enfants' + 'sex': 'sexes' + 'move': 'déplacements' + INFLECTOR_ORDINALS: + 'default': 'ème' + 'first': 'er' + 'second': 'ème' + 'third': 'ème' + NICETIME: + NO_DATE_PROVIDED: Aucune date fournie + BAD_DATE: Date erronée + AGO: plus tôt + FROM_NOW: à partir de maintenant + JUST_NOW: à l'instant + SECOND: seconde + MINUTE: minute + HOUR: heure + DAY: jour + WEEK: semaine + MONTH: mois + YEAR: année + DECADE: décennie + SEC: s + MIN: m + HR: h + WK: sem + MO: m + YR: an + DEC: déc + SECOND_PLURAL: secondes + MINUTE_PLURAL: minutes + HOUR_PLURAL: heures + DAY_PLURAL: jours + WEEK_PLURAL: semaines + MONTH_PLURAL: mois + YEAR_PLURAL: années + DECADE_PLURAL: décennies + SEC_PLURAL: s + MIN_PLURAL: m + HR_PLURAL: h + WK_PLURAL: sem + MO_PLURAL: mois + YR_PLURAL: a + DEC_PLURAL: décs + FORM: + VALIDATION_FAIL: La validation a échoué : + INVALID_INPUT: Saisie non valide + MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :' + MONTHS_OF_THE_YEAR: + - 'Janvier' + - 'Février' + - 'Mars' + - 'Avril' + - 'Mai' + - 'Juin' + - 'Juillet' + - 'Août' + - 'Septembre' + - 'Octobre' + - 'Novembre' + - 'Décembre' + DAYS_OF_THE_WEEK: + - 'Lundi' + - 'Mardi' + - 'Mercredi' + - 'Jeudi' + - 'Vendredi' + - 'Samedi' + - 'Dimanche' + CRON: + EVERY: chaque + EVERY_HOUR: toutes les heures + EVERY_MINUTE: chaque minute + EVERY_DAY_OF_WEEK: tous les jours de la semaine + EVERY_DAY_OF_MONTH: tous les jours du mois + EVERY_MONTH: chaque mois + TEXT_PERIOD: Chaque + TEXT_MINS: ' à minute(s) après l''heure' + TEXT_TIME: ' à:' + TEXT_DOW: ' sur ' + TEXT_MONTH: ' de ' + TEXT_DOM: ' sur ' + ERROR1: La balise %s n'est pas supportée! + 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/he.yaml b/system/languages/he.yaml new file mode 100644 index 0000000..afb63d7 --- /dev/null +++ b/system/languages/he.yaml @@ -0,0 +1,63 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nכותרת: %1$s\n---\n# שגיאה: Fronmatter לא חוקי\nנתיב: `%2$s`\n**%3$s**\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: לא סופק תאריך + BAD_DATE: תאריך פגום + AGO: לפני + FROM_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: 'שדות חובה חסרים:' + MONTHS_OF_THE_YEAR: + - 'ינואר' + - 'פברואר' + - 'מרץ' + - 'אפריל' + - 'מאי' + - 'יוני' + - 'יולי' + - 'אוגוסט' + - 'ספטמבר' + - 'אוקטובר' + - 'נובמבר' + - 'דצמבר' + DAYS_OF_THE_WEEK: + - 'שני' + - 'שלישי' + - 'רביעי' + - 'חמישי' + - 'שישי' + - 'שבת' + - 'ראשון' diff --git a/system/languages/hr.yaml b/system/languages/hr.yaml index adcdde5..1b530b5 100644 --- a/system/languages/hr.yaml +++ b/system/languages/hr.yaml @@ -1,75 +1,76 @@ --- -INFLECTOR_UNCOUNTABLE: - - oprema - - informacije - - riža - - novac - - vrsta - - serija - - riba - - ovca -INFLECTOR_IRREGULAR: - person: osobe - man: ljudi - child: djeca - sex: spolovi - move: Pomakni -NICETIME: - NO_DATE_PROVIDED: Datum nije upisan - BAD_DATE: Pogrešan datum - AGO: prije - FROM_NOW: od sada - SECOND: sekunda - MINUTE: minuta - HOUR: sat - DAY: dan - WEEK: tjedan - MONTH: mjesec - YEAR: godina - DECADE: desetljeće - SEC: sek - HR: sat - WK: t - MO: m - YR: g - DEC: des - SECOND_PLURAL: sekundi - MINUTE_PLURAL: minuta - HOUR_PLURAL: sati - DAY_PLURAL: dan - WEEK_PLURAL: tjedana - MONTH_PLURAL: mjeseci - YEAR_PLURAL: godina - DECADE_PLURAL: desetljeća - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: sat - WK_PLURAL: t - MO_PLURAL: m - YR_PLURAL: g - DEC_PLURAL: des -FORM: - VALIDATION_FAIL: 'Validacija nije uspjela:' - INVALID_INPUT: Pogrešan unos u - MISSING_REQUIRED_FIELD: 'Nedostaje obavezno polje:' -MONTHS_OF_THE_YEAR: - - Siječanj - - Veljača - - Ožujak - - Travanj - - Svibanj - - Lipanj - - Srpanj - - Kolovoz - - Rujan - - Listopad - - Studeni - - Prosinac -DAYS_OF_THE_WEEK: - - Ponedjeljak - - Utorak - - Srijeda - - Četvrtak - - Petak - - Subota - - Nedjelja +GRAV: + INFLECTOR_UNCOUNTABLE: + - 'oprema' + - 'informacije' + - 'riža' + - 'novac' + - 'vrsta' + - 'serija' + - 'riba' + - 'ovca' + INFLECTOR_IRREGULAR: + 'person': 'osobe' + 'man': 'ljudi' + 'child': 'djeca' + 'sex': 'spolovi' + 'move': 'Pomakni' + NICETIME: + NO_DATE_PROVIDED: Datum nije upisan + BAD_DATE: Pogrešan datum + AGO: prije + FROM_NOW: od sada + SECOND: sekunda + MINUTE: minuta + HOUR: sat + DAY: dan + WEEK: tjedan + MONTH: mjesec + YEAR: godina + DECADE: desetljeće + SEC: sek + HR: sat + WK: t + MO: m + YR: g + DEC: des + SECOND_PLURAL: sekundi + MINUTE_PLURAL: minuta + HOUR_PLURAL: sati + DAY_PLURAL: dan + WEEK_PLURAL: tjedana + MONTH_PLURAL: mjeseci + YEAR_PLURAL: godina + DECADE_PLURAL: desetljeća + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: sat + WK_PLURAL: t + MO_PLURAL: m + YR_PLURAL: g + DEC_PLURAL: des + FORM: + VALIDATION_FAIL: Validacija nije uspjela: + INVALID_INPUT: Pogrešan unos u + MISSING_REQUIRED_FIELD: 'Nedostaje obavezno polje:' + MONTHS_OF_THE_YEAR: + - 'Siječanj' + - 'Veljača' + - 'Ožujak' + - 'Travanj' + - 'Svibanj' + - 'Lipanj' + - 'Srpanj' + - 'Kolovoz' + - 'Rujan' + - 'Listopad' + - 'Studeni' + - 'Prosinac' + DAYS_OF_THE_WEEK: + - 'Ponedjeljak' + - 'Utorak' + - 'Srijeda' + - 'Četvrtak' + - 'Petak' + - 'Subota' + - 'Nedjelja' diff --git a/system/languages/hu.yaml b/system/languages/hu.yaml index ae7662e..a834c45 100644 --- a/system/languages/hu.yaml +++ b/system/languages/hu.yaml @@ -1,138 +1,97 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - cím: %1$s - --- - - # Hiba: Érvénytelen Frontmatter - - Elérési út: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -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: - - felszerelés - - információ - - rizs - - pénz - - fajok - - sorozat - - hal - - juh -INFLECTOR_IRREGULAR: - person: személyek - man: férfiak - child: gyerekek - sex: nemek - move: lépések -INFLECTOR_ORDINALS: - default: '.' - first: '.' - second: '.' - third: '.' -NICETIME: - NO_DATE_PROVIDED: Nincs dátum megadva - BAD_DATE: Hibás dátum - AGO: elteltével - FROM_NOW: mostantól - SECOND: másodperc - MINUTE: perc - HOUR: óra - DAY: nap - WEEK: hét - MONTH: hónap - YEAR: év - DECADE: évtized - SEC: mp - MIN: p - HR: ó - WK: hét - MO: hó - YR: év - DEC: évt - SECOND_PLURAL: másodperc - MINUTE_PLURAL: perc - HOUR_PLURAL: óra - DAY_PLURAL: nap - WEEK_PLURAL: hét - MONTH_PLURAL: hónap - YEAR_PLURAL: év - DECADE_PLURAL: évtized - SEC_PLURAL: mp - MIN_PLURAL: perc - HR_PLURAL: ó - WK_PLURAL: hét - MO_PLURAL: hó - YR_PLURAL: év - DEC_PLURAL: évt -FORM: - VALIDATION_FAIL: 'A validáció hibát talált:' - INVALID_INPUT: 'Az itt megadott érték érvénytelen:' - MISSING_REQUIRED_FIELD: 'Ez a kötelező mező nincs kitöltve:' -MONTHS_OF_THE_YEAR: - - január - - február - - március - - április - - május - - június - - július - - augusztus - - szeptember - - október - - november - - december -DAYS_OF_THE_WEEK: - - hétfő - - kedd - - szerda - - csütörtök - - péntek - - szombat - - vasárnap +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ncím: %1$s\n---\n\n# Hiba: Érvénytelen Frontmatter\n\nElérési út: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + INFLECTOR_UNCOUNTABLE: + - 'felszerelés' + - 'információ' + - 'rizs' + - 'pénz' + - 'fajok' + - 'sorozat' + - 'hal' + - 'juh' + INFLECTOR_IRREGULAR: + 'person': 'személyek' + 'man': 'férfiak' + 'child': 'gyerekek' + 'sex': 'nemek' + 'move': 'lépések' + INFLECTOR_ORDINALS: + 'default': '.' + 'first': '.' + 'second': '.' + 'third': '.' + NICETIME: + NO_DATE_PROVIDED: Nincs dátum megadva + BAD_DATE: Hibás dátum + AGO: elteltével + FROM_NOW: mostantól + JUST_NOW: épp most + SECOND: másodperc + MINUTE: perc + HOUR: óra + DAY: nap + WEEK: hét + MONTH: hónap + YEAR: év + DECADE: évtized + SEC: mp + MIN: p + HR: ó + WK: hét + MO: hó + YR: év + DEC: évt + SECOND_PLURAL: másodperc + MINUTE_PLURAL: perc + HOUR_PLURAL: óra + DAY_PLURAL: nap + WEEK_PLURAL: hét + MONTH_PLURAL: hónap + YEAR_PLURAL: év + DECADE_PLURAL: évtized + SEC_PLURAL: mp + MIN_PLURAL: perc + HR_PLURAL: ó + WK_PLURAL: hét + MO_PLURAL: hó + YR_PLURAL: év + DEC_PLURAL: évt + FORM: + VALIDATION_FAIL: Érvényesítés nem sikerült: + INVALID_INPUT: 'A megadott érték érvénytelen:' + MISSING_REQUIRED_FIELD: 'Ez a kötelező mező nincs kitöltve:' + MONTHS_OF_THE_YEAR: + - 'január' + - 'február' + - 'március' + - 'április' + - 'május' + - 'június' + - 'július' + - 'augusztus' + - 'szeptember' + - 'október' + - 'november' + - 'december' + DAYS_OF_THE_WEEK: + - 'hétfő' + - 'kedd' + - 'szerda' + - 'csütörtök' + - 'péntek' + - 'szombat' + - 'vasárnap' + CRON: + EVERY: minden + EVERY_HOUR: óránként + EVERY_MINUTE: percenként + EVERY_DAY_OF_WEEK: a hét minden napján + EVERY_DAY_OF_MONTH: a hónap minden napján + EVERY_MONTH: minden hónapban + TEXT_PERIOD: Minden + TEXT_MINS: ' perccel az óra elteltével' + ERROR1: A %s címke nem engedélyezett! + ERROR2: Hibás elemszám + ERROR3: A jquery_element-et a jqCron beállítsokban kell meghatározni + ERROR4: Ismeretlen kifejezés diff --git a/system/languages/id.yaml b/system/languages/id.yaml new file mode 100644 index 0000000..41431f1 --- /dev/null +++ b/system/languages/id.yaml @@ -0,0 +1,95 @@ +--- +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_UNCOUNTABLE: + - 'peralatan' + - 'informasi' + - 'nasi' + - 'uang' + - 'spesies' + - 'rangkaian' + - 'ikan' + - 'domba' + INFLECTOR_IRREGULAR: + 'person': 'orang-orang' + 'man': 'laki-laki' + 'child': 'anak-anak' + 'sex': 'jenis kelamin' + 'move': 'pindahkan' + NICETIME: + NO_DATE_PROVIDED: Tanggal tidak tersedia + BAD_DATE: Format tanggal salah + AGO: yang lalu + FROM_NOW: dari saat ini + JUST_NOW: baru saja + SECOND: detik + MINUTE: menit + HOUR: jam + DAY: hari + WEEK: pekan + MONTH: bulan + YEAR: tahun + DECADE: dekade + SEC: dtk + MIN: mnt + HR: j + WK: mng + MO: bln + YR: thn + DEC: desimal + SECOND_PLURAL: detik + MINUTE_PLURAL: menit + HOUR_PLURAL: jam + DAY_PLURAL: hari + WEEK_PLURAL: pekan + 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 + DEC_PLURAL: dekade + FORM: + VALIDATION_FAIL: Validasi gagal: + INVALID_INPUT: Input tidak valid di + MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:' + MONTHS_OF_THE_YEAR: + - 'Januari' + - 'Februari' + - 'Maret' + - 'April' + - 'Mei' + - 'Juni' + - 'Juli' + - 'Agustus' + - 'September' + - 'Oktober' + - 'November' + - 'Desember' + DAYS_OF_THE_WEEK: + - 'Senin' + - 'Selasa' + - 'Rabu' + - 'Kamis' + - 'Jumat' + - 'Sabtu' + - 'Minggu' + 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_MONTH: setiap bulan + TEXT_PERIOD: Setiap + 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 diff --git a/system/languages/is.yaml b/system/languages/is.yaml new file mode 100644 index 0000000..6f9761c --- /dev/null +++ b/system/languages/is.yaml @@ -0,0 +1,80 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitill: %1$s\n---\n\n# Villa: Ógilt efni á forsíðu\n\nSlóð: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + INFLECTOR_UNCOUNTABLE: + - 'equipment' + - 'upplýsingar' + - 'rice' + - 'money' + - 'species' + - 'series' + - 'fish' + - 'sheep' + NICETIME: + NO_DATE_PROVIDED: Engin dagsetning gefin + BAD_DATE: Röng dagsetning + AGO: síðan + JUST_NOW: í þessu + SECOND: sekúndu + MINUTE: mínútu + HOUR: klukkustund + DAY: degi + WEEK: viku + MONTH: mánuði + YEAR: ári + DECADE: áratug + SEC: sek + MIN: mín + HR: klst + WK: vk + MO: mán + YR: ár + DEC: árat + SECOND_PLURAL: sekúndum + MINUTE_PLURAL: mínútum + HOUR_PLURAL: klukkustundum + DAY_PLURAL: dögum + WEEK_PLURAL: vikum + MONTH_PLURAL: mánuðum + YEAR_PLURAL: árum + DECADE_PLURAL: áratugum + SEC_PLURAL: sek + MIN_PLURAL: mín + HR_PLURAL: klst + WK_PLURAL: vik + MO_PLURAL: mán + YR_PLURAL: árum + DEC_PLURAL: árat + FORM: + VALIDATION_FAIL: Sannvottun mistókst: + INVALID_INPUT: Ógilt inntak í + MISSING_REQUIRED_FIELD: 'Vantar nauðsynlegan reit:' + MONTHS_OF_THE_YEAR: + - 'janúar' + - 'Febrúar' + - 'Mars' + - 'Apríl' + - 'Maí' + - 'Júní' + - 'Júlí' + - 'Ágúst' + - 'September' + - 'Október' + - 'Nóvember' + - 'Desember' + DAYS_OF_THE_WEEK: + - 'Mánudagur' + - 'Þriðjudagur' + - 'Miðvikudagur' + - 'Fimmtudagur' + - 'Föstudagur' + - 'Laugardagur' + - 'Sunnudagur' + CRON: + TEXT_TIME: ' á :' + TEXT_DOW: ' á ' + TEXT_MONTH: ' af ' + TEXT_DOM: ' á ' + ERROR1: Merkið %s er ekki stutt! + ERROR3: Það ætti að setja jquery_element inn í stillingar jqCron + ERROR4: Óþekkt segð diff --git a/system/languages/it.yaml b/system/languages/it.yaml index 58a8998..741f721 100644 --- a/system/languages/it.yaml +++ b/system/languages/it.yaml @@ -1,62 +1,101 @@ --- -FRONTMATTER_ERROR_PAGE: "---Titolo: %1$s---# Errore: Frontmatter non valido: '%2$s' * *%3$s * * ' '%4$s ' '" -NICETIME: - NO_DATE_PROVIDED: Nessuna data fornita - BAD_DATE: Data non valida - AGO: fa - FROM_NOW: da adesso - SECOND: secondo - MINUTE: minuto - HOUR: ora - DAY: giorno - WEEK: settimana - MONTH: mese - YEAR: anno - DECADE: decennio - SEC: sec - MIN: min - HR: ora - WK: settimana - MO: mese - YR: anno - DEC: decennio - SECOND_PLURAL: secondi - MINUTE_PLURAL: minuti - HOUR_PLURAL: ore - DAY_PLURAL: giorni - WEEK_PLURAL: settimane - MONTH_PLURAL: mesi - YEAR_PLURAL: anni - DECADE_PLURAL: decadi - SEC_PLURAL: secondi - MIN_PLURAL: minuti - HR_PLURAL: ore - WK_PLURAL: settimane - MO_PLURAL: mesi - YR_PLURAL: anni - DEC_PLURAL: decenni -FORM: - VALIDATION_FAIL: 'Validazione fallita:' - INVALID_INPUT: Input non valido in - MISSING_REQUIRED_FIELD: 'Campo richiesto mancante:' -MONTHS_OF_THE_YEAR: - - Gennaio - - Febbraio - - Marzo - - Aprile - - Maggio - - Giugno - - Luglio - - Agosto - - Settembre - - Ottobre - - Novembre - - Dicembre -DAYS_OF_THE_WEEK: - - Lunedì - - Martedì - - Mercoledì - - Giovedì - - Venerdì - - Sabato - - Domenica +GRAV: + FRONTMATTER_ERROR_PAGE: "---Titolo: %1$s---# Errore: Frontmatter non valido: '%2$s' * *%3$s * * ' '%4$s ' '" + INFLECTOR_UNCOUNTABLE: + - 'dotazione' + - 'informazione' + - 'riso' + - 'denaro' + - 'specie' + - 'serie' + - 'pesce' + - 'pecora' + INFLECTOR_IRREGULAR: + 'person': 'persone' + 'man': 'uomini' + 'child': 'bambino' + 'sex': 'sessi' + 'move': 'sposta' + INFLECTOR_ORDINALS: + 'default': '°' + 'first': '°' + 'second': 'o' + 'third': 'o' + NICETIME: + NO_DATE_PROVIDED: Nessuna data fornita + BAD_DATE: Data non valida + AGO: fa + FROM_NOW: da adesso + JUST_NOW: ora + SECOND: secondo + MINUTE: minuto + HOUR: ora + DAY: giorno + WEEK: settimana + MONTH: mese + YEAR: anno + DECADE: decennio + SEC: sec + MIN: min + HR: ora + WK: settimana + MO: mese + YR: anno + DEC: decennio + SECOND_PLURAL: secondi + MINUTE_PLURAL: minuti + HOUR_PLURAL: ore + DAY_PLURAL: giorni + WEEK_PLURAL: settimane + MONTH_PLURAL: mesi + YEAR_PLURAL: anni + DECADE_PLURAL: decadi + SEC_PLURAL: secondi + MIN_PLURAL: minuti + HR_PLURAL: ore + WK_PLURAL: settimane + MO_PLURAL: mesi + YR_PLURAL: anni + DEC_PLURAL: decenni + FORM: + VALIDATION_FAIL: Validazione fallita: + INVALID_INPUT: Input non valido in + MISSING_REQUIRED_FIELD: 'Campo richiesto mancante:' + MONTHS_OF_THE_YEAR: + - 'Gennaio' + - 'Febbraio' + - 'Marzo' + - 'Aprile' + - 'Maggio' + - 'Giugno' + - 'Luglio' + - 'Agosto' + - 'Settembre' + - 'Ottobre' + - 'Novembre' + - 'Dicembre' + DAYS_OF_THE_WEEK: + - 'Lunedì' + - 'Martedì' + - 'Mercoledì' + - 'Giovedì' + - 'Venerdì' + - 'Sabato' + - 'Domenica' + CRON: + EVERY: ogni + EVERY_HOUR: ogni ora + EVERY_MINUTE: ogni minuto + EVERY_DAY_OF_WEEK: ogni giorno della settimana + EVERY_DAY_OF_MONTH: ogni giorno del mese + EVERY_MONTH: ogni mese + TEXT_PERIOD: Ogni + TEXT_MINS: ' a minuto(i) dall''inizio dell''ora' + TEXT_TIME: ' alle :' + TEXT_DOW: ' su ' + TEXT_MONTH: ' di ' + TEXT_DOM: ' di ' + ERROR1: Il tag %s non è supportato! + ERROR2: Numero di elementi non valido + ERROR3: Il jquery_element deve essere impostato nelle impostazioni di jqCron + ERROR4: Espressione non riconosciuta diff --git a/system/languages/ja.yaml b/system/languages/ja.yaml index c2d8ac9..0e3fa16 100644 --- a/system/languages/ja.yaml +++ b/system/languages/ja.yaml @@ -1,24 +1,15 @@ -FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" -INFLECTOR_PLURALS: [] - -INFLECTOR_SINGULAR: [] - -INFLECTOR_UNCOUNTABLE: [] - -INFLECTOR_IRREGULAR: +--- +GRAV: + INFLECTOR_IRREGULAR: 'person': 'みんな' 'man': '人' 'child': '子供' 'sex': '性別' 'move': '移動' - -INFLECTOR_ORDINALS: [] - -NICETIME: + NICETIME: NO_DATE_PROVIDED: 日付が設定されていません BAD_DATE: 不正な日付 AGO: 前 - FROM_NOW: from now SECOND: 秒 MINUTE: 分 HOUR: 時 @@ -33,7 +24,6 @@ NICETIME: WK: 週 MO: 月 YR: 年 - DEC: dec SECOND_PLURAL: 秒 MINUTE_PLURAL: 分 HOUR_PLURAL: 時 @@ -49,9 +39,28 @@ NICETIME: MO_PLURAL: 月 YR_PLURAL: 年 DEC_PLURAL: 10年 -FORM: + FORM: VALIDATION_FAIL: バリデーション失敗 : INVALID_INPUT: 不正な入力: - MISSING_REQUIRED_FIELD: 必須項目が入力されていません: -MONTHS_OF_THE_YEAR: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'] -DAYS_OF_THE_WEEK: ['月', '火', '水', '木', '金', '土', '日'] + MISSING_REQUIRED_FIELD: '必須項目が入力されていません:' + MONTHS_OF_THE_YEAR: + - '1月' + - '2月' + - '3月' + - '4月' + - '5月' + - '6月' + - '7月' + - '8月' + - '9月' + - '10月' + - '11月' + - '12月' + DAYS_OF_THE_WEEK: + - '月' + - '火' + - '水' + - '木' + - '金' + - '土' + - '日' diff --git a/system/languages/ko.yaml b/system/languages/ko.yaml new file mode 100644 index 0000000..6676a6d --- /dev/null +++ b/system/languages/ko.yaml @@ -0,0 +1,63 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 오류: 무효의 Frontmatter\n\n경로: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: 제공된 날짜가 없습니다 + BAD_DATE: 잘못된 날짜 + AGO: 전 + FROM_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: '누락 된 필수 필드:' + MONTHS_OF_THE_YEAR: + - '일월' + - '이월' + - '삼월' + - '사월' + - '오월' + - '유월' + - '칠월' + - '팔월' + - '구월' + - '시월' + - '십일월' + - '십이월' + DAYS_OF_THE_WEEK: + - '월요일' + - '화요일' + - '수요일' + - '목요일' + - '금요일' + - '토요일' + - '일요일' diff --git a/system/languages/lt.yaml b/system/languages/lt.yaml index 6b77aa1..aca0fc3 100644 --- a/system/languages/lt.yaml +++ b/system/languages/lt.yaml @@ -1,69 +1,78 @@ --- -INFLECTOR_UNCOUNTABLE: - 2: ryžiai - 3: pinigai - 4: prieskoniai - 5: serijos - 6: žuvis - 7: avis -INFLECTOR_IRREGULAR: - person: žmonės - man: žmogus - child: vaikai - sex: lytys - move: juda -NICETIME: - NO_DATE_PROVIDED: Nenurodyta data - BAD_DATE: Neteisinga data - AGO: prieš - FROM_NOW: nuo dabar - SECOND: sekundė - MINUTE: minutė - HOUR: valanda - DAY: diena - WEEK: savaitė - MONTH: mėnuo - YEAR: metai - DECADE: dešimtmetis - SEC: sek - MIN: min - HR: val - WK: sav - MO: mėn - YR: m - MINUTE_PLURAL: minutės - HOUR_PLURAL: valandos - DAY_PLURAL: dienos - WEEK_PLURAL: savaitės - MONTH_PLURAL: mėnesiai - YEAR_PLURAL: metai - DECADE_PLURAL: dešimtmečiai - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: val - WK_PLURAL: sav - MO_PLURAL: mėn - YR_PLURAL: m -FORM: - MISSING_REQUIRED_FIELD: 'Būtina užpildyti laukelį:' -MONTHS_OF_THE_YEAR: - - Sausis - - Vasaris - - Kovas - - Balandis - - Gegužė - - Birželis - - Liepa - - Rugpjūtis - - Rugsėjis - - Spalis - - Lakpritis - - Gruodis -DAYS_OF_THE_WEEK: - - Pirmadienis - - Antradienis - - Trečiadienis - - Ketvirtadienis - - Penktadienis - - Šeštadienis - - Sekmadienis +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Klaida: klaidinga įžanginė konfigūracija\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n %4$s\n```" + INFLECTOR_UNCOUNTABLE: + - 'equipment' + - 'information' + - 'ryžiai' + - 'pinigai' + - 'prieskoniai' + - 'serijos' + - 'žuvis' + - 'avis' + INFLECTOR_IRREGULAR: + 'person': 'žmonės' + 'man': 'žmogus' + 'child': 'vaikai' + 'sex': 'lytys' + 'move': 'juda' + NICETIME: + NO_DATE_PROVIDED: Nenurodyta data + BAD_DATE: Neteisinga data + AGO: prieš + FROM_NOW: nuo dabar + SECOND: sekundė + MINUTE: minutė + HOUR: valanda + DAY: diena + WEEK: savaitė + MONTH: mėnuo + YEAR: metai + DECADE: dešimtmetis + SEC: sek. + MIN: min. + HR: val. + WK: sav. + MO: mėn. + YR: m. + DEC: dešimtmetis + SECOND_PLURAL: sekundės + MINUTE_PLURAL: minutės + HOUR_PLURAL: valandos + DAY_PLURAL: dienos + WEEK_PLURAL: savaitės + MONTH_PLURAL: mėnesiai + YEAR_PLURAL: metai + DECADE_PLURAL: dešimtmečiai + SEC_PLURAL: sek. + MIN_PLURAL: min. + HR_PLURAL: val. + WK_PLURAL: sav. + MO_PLURAL: mėn. + YR_PLURAL: m. + DEC_PLURAL: dešimtmečiai + FORM: + VALIDATION_FAIL: Patvirtinimas nepavyko: + INVALID_INPUT: Neteisingai įvesta į + MISSING_REQUIRED_FIELD: 'Būtina užpildyti laukelį:' + MONTHS_OF_THE_YEAR: + - 'Sausis' + - 'Vasaris' + - 'Kovas' + - 'Balandis' + - 'Gegužė' + - 'Birželis' + - 'Liepa' + - 'Rugpjūtis' + - 'Rugsėjis' + - 'Spalis' + - 'Lakpritis' + - 'Gruodis' + DAYS_OF_THE_WEEK: + - 'Pirmadienis' + - 'Antradienis' + - 'Trečiadienis' + - 'Ketvirtadienis' + - 'Penktadienis' + - 'Šeštadienis' + - 'Sekmadienis' diff --git a/system/languages/nb.yaml b/system/languages/nb.yaml index 629cd39..8033e79 100644 --- a/system/languages/nb.yaml +++ b/system/languages/nb.yaml @@ -1,2 +1,4 @@ -MONTHS_OF_THE_YEAR: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'] -DAYS_OF_THE_WEEK: ['mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag', 'søndag'] +--- +GRAV: + MONTHS_OF_THE_YEAR: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'] + DAYS_OF_THE_WEEK: ['mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag', 'søndag'] diff --git a/system/languages/nl.yaml b/system/languages/nl.yaml index 757fb72..9706d03 100644 --- a/system/languages/nl.yaml +++ b/system/languages/nl.yaml @@ -1,64 +1,144 @@ --- -INFLECTOR_IRREGULAR: - person: personen - man: mensen - child: kinderen - sex: geslacht - move: verplaatsen -NICETIME: - NO_DATE_PROVIDED: geen datum opgegeven - BAD_DATE: Datumformaat onjuist - AGO: geleden - FROM_NOW: vanaf nu - SECOND: seconde - MINUTE: minuut - HOUR: uur - DAY: dag - WEEK: week - MONTH: maand - YEAR: jaar - DECADE: decenium - SEC: s - MIN: min - HR: u - MO: ma - YR: j - SECOND_PLURAL: seconden - MINUTE_PLURAL: minuten - HOUR_PLURAL: uren - DAY_PLURAL: dagen - WEEK_PLURAL: weken - MONTH_PLURAL: maanden - YEAR_PLURAL: jaren - DECADE_PLURAL: decennia - SEC_PLURAL: seconden - MIN_PLURAL: minuten - HR_PLURAL: uren - WK_PLURAL: weken - MO_PLURAL: maanden - YR_PLURAL: jaren -FORM: - VALIDATION_FAIL: 'Validatie mislukt:' - INVALID_INPUT: Ongeldige invoer in - MISSING_REQUIRED_FIELD: 'Verplicht veld ontbreekt:' -MONTHS_OF_THE_YEAR: - - Januari - - Februari - - Maart - - april - - Mei - - Juni - - Juli - - Augustus - - september - - Oktober - - november - - december -DAYS_OF_THE_WEEK: - - Maandag - - Dinsdag - - Woensdag - - Donderdag - - Vrijdag - - Zaterdag - - Zondag +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitel: %1$s\n---\n\n# Fout: ongeldige frontmatter\n\nPad: `%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: + - 'uitrusting' + - 'informatie' + - 'rijst' + - 'geld' + - 'soorten' + - 'reeks' + - 'vis' + - 'schaap' + INFLECTOR_IRREGULAR: + 'person': 'personen' + 'man': 'mensen' + 'child': 'kinderen' + 'sex': 'geslacht' + 'move': 'verplaatsen' + INFLECTOR_ORDINALS: + 'default': 'th' + 'first': 'st' + 'second': 'nd' + 'third': 'rd' + NICETIME: + NO_DATE_PROVIDED: geen datum opgegeven + BAD_DATE: Datumformaat onjuist + AGO: geleden + FROM_NOW: vanaf nu + JUST_NOW: zojuist + SECOND: seconde + MINUTE: minuut + HOUR: uur + DAY: dag + WEEK: week + MONTH: maand + YEAR: jaar + DECADE: decennium + SEC: s + MIN: min + HR: u + WK: week + MO: ma + YR: j + DEC: decennia + SECOND_PLURAL: seconden + MINUTE_PLURAL: minuten + HOUR_PLURAL: uren + DAY_PLURAL: dagen + WEEK_PLURAL: weken + MONTH_PLURAL: maanden + YEAR_PLURAL: jaren + DECADE_PLURAL: decennia + SEC_PLURAL: seconden + MIN_PLURAL: minuten + HR_PLURAL: uren + WK_PLURAL: weken + MO_PLURAL: maanden + YR_PLURAL: jaren + DEC_PLURAL: decennia + FORM: + VALIDATION_FAIL: Validatie mislukt: + INVALID_INPUT: Ongeldige invoer in + MISSING_REQUIRED_FIELD: 'Ontbrekend verplicht veld:' + MONTHS_OF_THE_YEAR: + - 'Januari' + - 'Februari' + - 'Maart' + - 'April' + - 'Mei' + - 'Juni' + - 'Juli' + - 'Augustus' + - 'September' + - 'Oktober' + - 'November' + - 'December' + DAYS_OF_THE_WEEK: + - 'Maandag' + - 'Dinsdag' + - 'Woensdag' + - 'Donderdag' + - 'Vrijdag' + - 'Zaterdag' + - 'Zondag' + CRON: + EVERY: elke + EVERY_HOUR: elk uur + EVERY_MINUTE: elke minuut + EVERY_DAY_OF_WEEK: elke dag van de week + EVERY_DAY_OF_MONTH: elke dag van de maand + EVERY_MONTH: elke maand + TEXT_PERIOD: Elke + TEXT_MINS: ' minuten te laat' + TEXT_TIME: ' op :' + TEXT_DOW: ' op ' + TEXT_MONTH: ' van ' + TEXT_DOM: ' op ' + ERROR1: De tag %s wordt niet ondersteund! + ERROR2: Slecht aantal elementen + ERROR3: Het jquery_element moet ingesteld worden in de jqCron instellingen + ERROR4: Onbekende expressie diff --git a/system/languages/no.yaml b/system/languages/no.yaml index 4a74da2..5b57938 100644 --- a/system/languages/no.yaml +++ b/system/languages/no.yaml @@ -1,93 +1,77 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - Tittel: %1$s - --- - - # Feilmelding: Ugyldig Frontmatter - - Pane: '%2$s' - - **%3$s ** - - ``` - %4$s - ``` -INFLECTOR_PLURALS: - /(quiz)$/i: '\1zes' - /^(ox)$/i: '\1en' -INFLECTOR_UNCOUNTABLE: - - utstyr - - informasjon - - ris - - penger - - arter - - serier - - fisk - - sau -INFLECTOR_IRREGULAR: - person: folk - man: menn - child: barn - sex: kjønn - move: trekk -NICETIME: - NO_DATE_PROVIDED: Ingen dato gitt - BAD_DATE: Dårlig dato - AGO: siden - FROM_NOW: fra nå - SECOND: sekund - MINUTE: minutt - HOUR: time - DAY: dag - WEEK: uke - MONTH: måned - YEAR: år - DECADE: tiår - SEC: sek - MIN: min - HR: t - WK: uke - MO: må - YR: år - DEC: des - SECOND_PLURAL: sekunder - MINUTE_PLURAL: minutter - HOUR_PLURAL: timer - DAY_PLURAL: dager - WEEK_PLURAL: uker - MONTH_PLURAL: måneder - YEAR_PLURAL: år - DECADE_PLURAL: tiår - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: timer - WK_PLURAL: uker - MO_PLURAL: mdr - YR_PLURAL: år - DEC_PLURAL: årtier -FORM: - VALIDATION_FAIL: 'Validering mislyktes:' - INVALID_INPUT: Ugyldig innhold i - MISSING_REQUIRED_FIELD: 'Mangler påkrevd felt:' -MONTHS_OF_THE_YEAR: - - januar - - februar - - mars - - april - - mai - - juni - - juli - - august - - september - - oktober - - november - - desember -DAYS_OF_THE_WEEK: - - mandag - - tirsdag - - onsdag - - torsdag - - fredag - - lørdag - - søndag +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nTittel: %1$s\n---\n\n# Feilmelding: Ugyldig Frontmatter\n\nSti: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + INFLECTOR_UNCOUNTABLE: + - 'utstyr' + - 'informasjon' + - 'ris' + - 'penger' + - 'arter' + - 'serier' + - 'fisk' + - 'sau' + INFLECTOR_IRREGULAR: + 'person': 'folk' + 'man': 'menn' + 'child': 'barn' + 'sex': 'kjønn' + 'move': 'trekk' + NICETIME: + NO_DATE_PROVIDED: Ingen dato gitt + BAD_DATE: Ugyldig dato + AGO: siden + FROM_NOW: fra nå + SECOND: sekund + MINUTE: minutt + HOUR: time + DAY: dag + WEEK: uke + MONTH: måned + YEAR: år + DECADE: tiår + SEC: sek + HR: t + WK: uke + MO: må + YR: år + DEC: tiår + SECOND_PLURAL: sekunder + MINUTE_PLURAL: minutter + HOUR_PLURAL: timer + DAY_PLURAL: dager + WEEK_PLURAL: uker + MONTH_PLURAL: måneder + YEAR_PLURAL: år + DECADE_PLURAL: tiår + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: timer + WK_PLURAL: uker + MO_PLURAL: md + YR_PLURAL: år + DEC_PLURAL: årtier + FORM: + VALIDATION_FAIL: Godkjenning mislyktes: + INVALID_INPUT: Ugyldig innhold i + MISSING_REQUIRED_FIELD: 'Mangler påkrevd felt:' + MONTHS_OF_THE_YEAR: + - 'januar' + - 'februar' + - 'mars' + - 'april' + - 'mai' + - 'juni' + - 'juli' + - 'august' + - 'september' + - 'oktober' + - 'november' + - 'desember' + DAYS_OF_THE_WEEK: + - 'mandag' + - 'tirsdag' + - 'onsdag' + - 'torsdag' + - 'fredag' + - 'lørdag' + - 'søndag' diff --git a/system/languages/pl.yaml b/system/languages/pl.yaml index 6a479bf..bf6fe37 100644 --- a/system/languages/pl.yaml +++ b/system/languages/pl.yaml @@ -1,75 +1,62 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - - # Error: Nieprawidłowy Frontmatter - - Path: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -NICETIME: - NO_DATE_PROVIDED: Nie podano daty - BAD_DATE: Zła data - AGO: temu - FROM_NOW: od teraz - SECOND: sekunda - MINUTE: minuta - HOUR: godzina - DAY: dzień - WEEK: tydzień - MONTH: miesiąc - YEAR: rok - DECADE: dekada - SEC: sek - MIN: min - HR: godz - WK: tydz - MO: m-c - YR: rok - DEC: dekada - SECOND_PLURAL: sekund - MINUTE_PLURAL: minut - HOUR_PLURAL: godzin - DAY_PLURAL: dni - WEEK_PLURAL: tygodnie - MONTH_PLURAL: miesięcy - YEAR_PLURAL: lat - DECADE_PLURAL: dekad - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: godz - WK_PLURAL: tyg - MO_PLURAL: m-ce - YR_PLURAL: lat - DEC_PLURAL: dekad -FORM: - VALIDATION_FAIL: 'Weryfikacja nie powiodła się:' - INVALID_INPUT: Nieprawidłowe dane wejściowe - MISSING_REQUIRED_FIELD: 'Opuszczono wymagane pole:' -MONTHS_OF_THE_YEAR: - - Styczeń - - Luty - - Marzec - - Kwiecień - - Maj - - Czerwiec - - Lipiec - - Sierpień - - Wrzesień - - Październik - - Listopad - - Grudzień -DAYS_OF_THE_WEEK: - - Poniedziałek - - Wtorek - - Środa - - Czwartek - - Piątek - - Sobota - - Niedziela +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Nieprawidłowy Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: Nie podano daty + BAD_DATE: Zła data + AGO: temu + FROM_NOW: od teraz + SECOND: sekunda + MINUTE: minuta + HOUR: godzina + DAY: dzień + WEEK: tydzień + MONTH: miesiąc + YEAR: rok + DECADE: dekada + SEC: sek + HR: godz + WK: tydz + MO: m-c + YR: rok + DEC: dekada + SECOND_PLURAL: sekund + MINUTE_PLURAL: minut + HOUR_PLURAL: godzin + DAY_PLURAL: dni + WEEK_PLURAL: tygodnie + MONTH_PLURAL: miesięcy + YEAR_PLURAL: lat + DECADE_PLURAL: dekad + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: godz + WK_PLURAL: tyg + MO_PLURAL: m-ce + YR_PLURAL: lat + DEC_PLURAL: dekad + FORM: + VALIDATION_FAIL: Weryfikacja nie powiodła się: + INVALID_INPUT: Nieprawidłowe dane wejściowe + MISSING_REQUIRED_FIELD: 'Opuszczono wymagane pole:' + MONTHS_OF_THE_YEAR: + - 'Styczeń' + - 'Luty' + - 'Marzec' + - 'Kwiecień' + - 'Maj' + - 'Czerwiec' + - 'Lipiec' + - 'Sierpień' + - 'Wrzesień' + - 'Październik' + - 'Listopad' + - 'Grudzień' + DAYS_OF_THE_WEEK: + - 'Poniedziałek' + - 'Wtorek' + - 'Środa' + - 'Czwartek' + - 'Piątek' + - 'Sobota' + - 'Niedziela' diff --git a/system/languages/pt.yaml b/system/languages/pt.yaml index a9da66a..590b5e5 100644 --- a/system/languages/pt.yaml +++ b/system/languages/pt.yaml @@ -1,79 +1,46 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - título: %1$s - --- - - # Erro: Frontmatter inválida - - Caminho: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -INFLECTOR_UNCOUNTABLE: - 1: informação - 2: arroz - 3: dinheiro -INFLECTOR_IRREGULAR: - man: homens - sex: sexos -NICETIME: - NO_DATE_PROVIDED: Não foi fornecida data - BAD_DATE: Data inválida - AGO: atrás - FROM_NOW: a partir de agora - SECOND: segundo - MINUTE: minuto - HOUR: hora - DAY: dia - WEEK: semana - MONTH: mês - YEAR: ano - DECADE: década - SEC: seg - MIN: mín - HR: h - WK: sem - MO: m - YR: a - DEC: dec - SECOND_PLURAL: segundos - MINUTE_PLURAL: minutos - HOUR_PLURAL: horas - DAY_PLURAL: dias - WEEK_PLURAL: semanas - MONTH_PLURAL: meses - YEAR_PLURAL: anos - DECADE_PLURAL: décadas - SEC_PLURAL: seg - MIN_PLURAL: mins - HR_PLURAL: hrs - WK_PLURAL: sems - YR_PLURAL: anos -FORM: - VALIDATION_FAIL: 'Validação falhada: ' - MISSING_REQUIRED_FIELD: 'Campo obrigatório ausente:' -MONTHS_OF_THE_YEAR: - - Janeiro - - Fevereiro - - Março - - Abril - - Maio - - Junho - - Julho - - Agosto - - Setembro - - Outubro - - Novembro - - Dezembro -DAYS_OF_THE_WEEK: - - Segunda - - Terça - - Quarta - - Quinta - - Sexta - - Sábado - - Domingo +GRAV: + NICETIME: + NO_DATE_PROVIDED: Nenhuma data fornecida + AGO: atrás + SECOND: segundo + MINUTE: minuto + HOUR: hora + DAY: dia + WEEK: semana + MONTH: mês + YEAR: ano + DECADE: década + SEC: segundos + MIN: minutos + MINUTE_PLURAL: minutos + DAY_PLURAL: dias + WEEK_PLURAL: semanas + MONTH_PLURAL: meses + YEAR_PLURAL: anos + DECADE_PLURAL: decadas + FORM: + VALIDATION_FAIL: Falha na validação! + MISSING_REQUIRED_FIELD: 'Campo obrigatório requerido:' + MONTHS_OF_THE_YEAR: + - 'Janeiro' + - 'Fevereiro' + - 'Março' + - 'Abril' + - 'Maio' + - 'Junho' + - 'Julho' + - 'Agosto' + - 'Setembro' + - 'Outubro' + - 'Novembro' + - 'Dezembro' + INFLECTOR_UNCOUNTABLE: + - 'equipment' + - 'information' + - 'arroz' + - 'money' + - 'species' + - 'series' + - 'fish' + - 'sheep' diff --git a/system/languages/ro.yaml b/system/languages/ro.yaml index a84a3c2..5bee578 100644 --- a/system/languages/ro.yaml +++ b/system/languages/ro.yaml @@ -1,101 +1,96 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - Titlu: %1$s - --- - # Eroare: Frontmatter este invalid - - Calea: `%2$s` - - **%3$s** - - ``` - %4$s -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' -INFLECTOR_UNCOUNTABLE: - - echipament - - informaţie - - orez - - bani - - specii - - serii - - peşte - - oaie -INFLECTOR_IRREGULAR: - person: persoane - man: bărbați - child: copii - sex: sexe - move: mutări -NICETIME: - NO_DATE_PROVIDED: Nu există o dată prevăzută - BAD_DATE: Dată incorectă - AGO: în urmă - FROM_NOW: de acum - SECOND: secundă - MINUTE: minut - HOUR: oră - DAY: zi - WEEK: săptămână - MONTH: lună - YEAR: an - DECADE: decadă - SEC: sec - MIN: min - HR: oră - WK: săpt - MO: lună - YR: an - DEC: decadă - SECOND_PLURAL: secunde - MINUTE_PLURAL: minute - HOUR_PLURAL: ore - DAY_PLURAL: zile - WEEK_PLURAL: săptămâni - MONTH_PLURAL: luni - YEAR_PLURAL: ani - DECADE_PLURAL: decade - SEC_PLURAL: sec - MIN_PLURAL: min - HR_PLURAL: ore - WK_PLURAL: săpt - MO_PLURAL: luni - YR_PLURAL: ani - DEC_PLURAL: decenii -FORM: - VALIDATION_FAIL: 'Validare nereușită' - INVALID_INPUT: Date incorecte în - MISSING_REQUIRED_FIELD: 'Câmp obligatoriu lipsă:' -MONTHS_OF_THE_YEAR: - - Ianuarie - - Februarie - - Martie - - Aprilie - - Mai - - Iunie - - Iulie - - August - - Septembrie - - Octombrie - - Noiembrie - - Decembrie -DAYS_OF_THE_WEEK: - - Luni - - Marți - - Miercuri - - Joi - - Vineri - - Sâmbătă - - Duminică +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nTitlu: %1$s\n---\n# Eroare: Frontmatter este invalid\n\nCalea: `%2$s`\n\n**%3$s**\n\n```\n%4$s" + INFLECTOR_UNCOUNTABLE: + - 'echipament' + - 'informaţie' + - 'orez' + - 'bani' + - 'specii' + - 'serii' + - 'peşte' + - 'oaie' + INFLECTOR_IRREGULAR: + 'person': 'persoane' + 'man': 'bărbați' + 'child': 'copii' + 'sex': 'sexe' + 'move': 'mutări' + NICETIME: + NO_DATE_PROVIDED: Nu există o dată prevăzută + BAD_DATE: Dată incorectă + AGO: în urmă + FROM_NOW: de acum + JUST_NOW: chiar acum + SECOND: secundă + MINUTE: minut + HOUR: oră + DAY: zi + WEEK: săptămână + MONTH: lună + YEAR: an + DECADE: decadă + SEC: secunde + MIN: minute + HR: oră + WK: săpt + MO: lună + YR: an + DEC: decadă + SECOND_PLURAL: secunde + MINUTE_PLURAL: minute + HOUR_PLURAL: ore + DAY_PLURAL: zile + WEEK_PLURAL: săptămâni + MONTH_PLURAL: luni + YEAR_PLURAL: ani + DECADE_PLURAL: decade + SEC_PLURAL: sec + MIN_PLURAL: min + HR_PLURAL: ore + WK_PLURAL: săpt + MO_PLURAL: luni + YR_PLURAL: ani + DEC_PLURAL: decenii + FORM: + VALIDATION_FAIL: Validare nereușită + INVALID_INPUT: Date incorecte în + MISSING_REQUIRED_FIELD: 'Câmp obligatoriu lipsă:' + MONTHS_OF_THE_YEAR: + - 'Ianuarie' + - 'Februarie' + - 'Martie' + - 'Aprilie' + - 'Mai' + - 'Iunie' + - 'Iulie' + - 'August' + - 'Septembrie' + - 'Octombrie' + - 'Noiembrie' + - 'Decembrie' + DAYS_OF_THE_WEEK: + - 'Luni' + - 'Marți' + - 'Miercuri' + - 'Joi' + - 'Vineri' + - 'Sâmbătă' + - 'Duminică' + CRON: + EVERY: la fiecare + EVERY_HOUR: la fiecare oră + EVERY_MINUTE: la fiecare minut + EVERY_DAY_OF_WEEK: fiecare zi a săptămânii + EVERY_DAY_OF_MONTH: fiecare zi a lunii + EVERY_MONTH: fiecare lună + TEXT_PERIOD: Fiecare + TEXT_MINS: ' la minut(e) ale fiecărei ore' + TEXT_TIME: ' la :' + TEXT_DOW: ' pe ' + TEXT_MONTH: 'al(e) ' + TEXT_DOM: ' pe ' + ERROR1: Eticheta %s nu este acceptată! + ERROR2: Număr nevalid de elemente + ERROR3: jquery_element ar trebui setat în opțiunile jqCron + ERROR4: Expresie necunoscută diff --git a/system/languages/ru.yaml b/system/languages/ru.yaml index c8ba63e..78c5963 100644 --- a/system/languages/ru.yaml +++ b/system/languages/ru.yaml @@ -1,81 +1,101 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - - # Ошибка: Недопустимое содержимое - - Path: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -INFLECTOR_IRREGULAR: - person: люди - man: человек - child: ребенок - sex: пол - move: движется -NICETIME: - NO_DATE_PROVIDED: Дата не указана - BAD_DATE: Неверная дата - AGO: назад - FROM_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: 'Отсутствует необходимое поле:' -MONTHS_OF_THE_YEAR: - - Январь - - Февраль - - Март - - Апрель - - Май - - Июнь - - Июль - - Август - - Сентябрь - - Октябрь - - Ноябрь - - Декабрь -DAYS_OF_THE_WEEK: - - Понедельник - - Вторник - - Среда - - Четверг - - Пятница - - Суббота - - Воскресенье +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Ошибка: недопустимое содержимое Frontmatter\n\nПуть: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + INFLECTOR_UNCOUNTABLE: + - 'экипировка' + - 'информация' + - 'рис' + - 'деньги' + - 'виды' + - 'серии' + - 'рыба' + - 'овца' + INFLECTOR_IRREGULAR: + 'person': 'люди' + 'man': 'человек' + 'child': 'ребенок' + 'sex': 'пол' + 'move': 'движется' + INFLECTOR_ORDINALS: + 'default': 'й' + 'first': 'й' + 'second': 'й' + 'third': 'й' + 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: 'Отсутствует необходимое поле:' + MONTHS_OF_THE_YEAR: + - 'январь' + - 'февраль' + - 'март' + - 'апрель' + - 'май' + - 'июнь' + - 'Июль' + - 'Август' + - 'Сентябрь' + - 'Октябрь' + - 'Ноябрь' + - 'Декабрь' + DAYS_OF_THE_WEEK: + - 'понедельник' + - 'вторник' + - 'среда' + - 'четверг' + - 'пятница' + - 'суббота' + - 'воскресенье' + 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/sk.yaml b/system/languages/sk.yaml index 8651e7f..89a7f56 100644 --- a/system/languages/sk.yaml +++ b/system/languages/sk.yaml @@ -1,42 +1,144 @@ --- -NICETIME: - NO_DATE_PROVIDED: Neposkytnutý žiaden dátum - BAD_DATE: Nesprávny dátum - AGO: pred - FROM_NOW: odteraz - SECOND: sekunda - MINUTE: minúta - HOUR: hodina - DAY: deň - WEEK: týždeň - MONTH: mesiac - YEAR: rok - DECADE: desaťročie - SEC: sek - MIN: min - HR: hod -FORM: - VALIDATION_FAIL: 'Overenie zlyhalo:' - INVALID_INPUT: Neplatný vstup v - MISSING_REQUIRED_FIELD: 'Chýba vyžadované pole:' -MONTHS_OF_THE_YEAR: - - Január - - Február - - Marec - - Apríl - - Máj - - Jún - - Júl - - August - - September - - Október - - November - - December -DAYS_OF_THE_WEEK: - - Pondelok - - Utorok - - Streda - - Štvrtok - - Piatok - - Sobota - - Nedeľa +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Chyba: Chybný frontmatter\n\nPath: `%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: + - 'vybavenie' + - 'informácie' + - 'ryža' + - 'peniaze' + - 'druhy' + - 'séria' + - 'ryba' + - 'ovce' + INFLECTOR_IRREGULAR: + 'person': 'ľudia' + 'man': 'muži' + 'child': 'deti' + 'sex': 'pohlavia' + 'move': 'pohyby' + INFLECTOR_ORDINALS: + 'default': '.' + 'first': '.' + 'second': '.' + 'third': '.' + NICETIME: + NO_DATE_PROVIDED: Neposkytnutý žiaden dátum + BAD_DATE: Nesprávny dátum + AGO: pred + FROM_NOW: odteraz + JUST_NOW: práve teraz + SECOND: sekunda + MINUTE: minúta + HOUR: hodina + DAY: deň + WEEK: týždeň + MONTH: mesiac + YEAR: rok + DECADE: desaťročie + SEC: sek + MIN: min + HR: hod + WK: t + MO: m + YR: r + DEC: dec + SECOND_PLURAL: sekúnd + MINUTE_PLURAL: minút + HOUR_PLURAL: hodín + DAY_PLURAL: dní + WEEK_PLURAL: týždňov + MONTH_PLURAL: mesiacov + YEAR_PLURAL: rokov + DECADE_PLURAL: dekád + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: hod + WK_PLURAL: t + MO_PLURAL: mes. + YR_PLURAL: rokov + DEC_PLURAL: dekád + FORM: + VALIDATION_FAIL: Overenie zlyhalo: + INVALID_INPUT: Neplatný vstup v + MISSING_REQUIRED_FIELD: 'Chýba vyžadované pole:' + MONTHS_OF_THE_YEAR: + - 'Január' + - 'Február' + - 'Marec' + - 'Apríl' + - 'Máj' + - 'Jún' + - 'Júl' + - 'August' + - 'September' + - 'Október' + - 'November' + - 'December' + DAYS_OF_THE_WEEK: + - 'Pondelok' + - 'Utorok' + - 'Streda' + - 'Štvrtok' + - 'Piatok' + - 'Sobota' + - 'Nedeľa' + CRON: + EVERY: každý + EVERY_HOUR: každú hodinu + EVERY_MINUTE: každú minútu + EVERY_DAY_OF_WEEK: každý deň v týždni + EVERY_DAY_OF_MONTH: každý deň v mesiaci + EVERY_MONTH: každý mesiac + TEXT_PERIOD: Každý + TEXT_MINS: ' at minute(s) past the hour' + TEXT_TIME: ' at :' + TEXT_DOW: ' on ' + TEXT_MONTH: ' of ' + TEXT_DOM: ' on ' + ERROR1: Tag %s nieje podporovaný! + ERROR2: Chybný počet položiek + ERROR3: jquery_element musí byť nastavený v nastaveniach pre jqCron + ERROR4: Neznámy výraz diff --git a/system/languages/sl.yaml b/system/languages/sl.yaml new file mode 100644 index 0000000..ae6032c --- /dev/null +++ b/system/languages/sl.yaml @@ -0,0 +1,62 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Napaka: Neveljavna Frontmatter\n\nPath: `%2$s`\n\n**%3$s ** \n\n```\n%4$s \n```" + NICETIME: + NO_DATE_PROVIDED: Datum ni na voljo + BAD_DATE: Neveljaven datum + AGO: pred + FROM_NOW: od zdaj + SECOND: sekunda + MINUTE: minuta + HOUR: ura + DAY: dan + WEEK: teden + MONTH: mesec + YEAR: leto + DECADE: desetletje + SEC: sek + HR: ur + WK: T. + MO: m + YR: l + DEC: des + SECOND_PLURAL: sekund + MINUTE_PLURAL: minut + HOUR_PLURAL: ure + DAY_PLURAL: dnevi + WEEK_PLURAL: tednov + MONTH_PLURAL: mesecev + YEAR_PLURAL: leta + DECADE_PLURAL: desetletja + SEC_PLURAL: s + MIN_PLURAL: min + HR_PLURAL: ur + WK_PLURAL: t + MO_PLURAL: m + YR_PLURAL: l + DEC_PLURAL: des + FORM: + VALIDATION_FAIL: Preverjanje veljavnosti ni uspelo: + INVALID_INPUT: Neveljaven vnos v + MISSING_REQUIRED_FIELD: 'Manjka obvezno polje:' + MONTHS_OF_THE_YEAR: + - 'Januar' + - 'Februar' + - 'Marec' + - 'April' + - 'Maj' + - 'Junij' + - 'Julij' + - 'Avgust' + - 'September' + - 'Oktober' + - 'November' + - 'December' + DAYS_OF_THE_WEEK: + - 'Ponedeljek' + - 'Torek' + - 'Sreda' + - 'Četrtek' + - 'Petek' + - 'Sobota' + - 'Nedelja' diff --git a/system/languages/sv.yaml b/system/languages/sv.yaml index 8212eb3..16afa16 100644 --- a/system/languages/sv.yaml +++ b/system/languages/sv.yaml @@ -1,62 +1,72 @@ --- -FRONTMATTER_ERROR_PAGE: '--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```' -NICETIME: - NO_DATE_PROVIDED: Inget datum har angivits - BAD_DATE: Ogiltigt datum - AGO: sedan - FROM_NOW: från nu - SECOND: sekund - MINUTE: minut - HOUR: timme - DAY: dag - WEEK: vecka - MONTH: månad - YEAR: år - DECADE: årtionde - SEC: sek - MIN: min - HR: t - WK: v - MO: m - YR: år - DEC: dec - SECOND_PLURAL: sekunder - MINUTE_PLURAL: minuter - HOUR_PLURAL: timmar - DAY_PLURAL: dagar - WEEK_PLURAL: veckor - MONTH_PLURAL: månader - YEAR_PLURAL: år - DECADE_PLURAL: årtionden - SEC_PLURAL: sek - MIN_PLURAL: min - HR_PLURAL: t - WK_PLURAL: v - MO_PLURAL: må - YR_PLURAL: år - DEC_PLURAL: dec -FORM: - VALIDATION_FAIL: 'Kontrollen misslyckades:' - INVALID_INPUT: Ogiltig indata i - MISSING_REQUIRED_FIELD: 'Obligatoriskt fält måste fyllas i:' -MONTHS_OF_THE_YEAR: - - Januari - - Februrari - - Mars - - April - - Maj - - Juni - - Juli - - Augusti - - September - - Oktober - - November - - December -DAYS_OF_THE_WEEK: - - Måndag - - Tisdag - - Onsdag - - Torsdag - - Fredag - - Lördag - - Söndag +GRAV: + FRONTMATTER_ERROR_PAGE: "--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```" + INFLECTOR_UNCOUNTABLE: + - 'utrustning' + - 'information' + - 'ris' + - 'pengar' + - 'arter' + - 'serier' + - 'fisk' + - 'får' + INFLECTOR_IRREGULAR: + 'person': 'personer' + NICETIME: + NO_DATE_PROVIDED: Inget datum har angivits + BAD_DATE: Ogiltigt datum + AGO: sedan + FROM_NOW: fr.o.m nu + SECOND: sekund + MINUTE: minut + HOUR: timme + DAY: dag + WEEK: vecka + MONTH: månad + YEAR: år + DECADE: årtionde + SEC: sek + HR: t + WK: v + MO: m + YR: år + SECOND_PLURAL: sekunder + MINUTE_PLURAL: minuter + HOUR_PLURAL: timmar + DAY_PLURAL: dagar + WEEK_PLURAL: veckor + MONTH_PLURAL: månader + YEAR_PLURAL: år + DECADE_PLURAL: årtionden + SEC_PLURAL: sek + MIN_PLURAL: min + HR_PLURAL: t + WK_PLURAL: v + MO_PLURAL: må + YR_PLURAL: år + DEC_PLURAL: dec + FORM: + VALIDATION_FAIL: Kontrollen misslyckades: + INVALID_INPUT: Ogiltig indata i + MISSING_REQUIRED_FIELD: 'Obligatoriskt fält måste fyllas i:' + MONTHS_OF_THE_YEAR: + - 'Januari' + - 'Februari' + - 'Mars' + - 'April' + - 'Maj' + - 'Juni' + - 'Juli' + - 'Augusti' + - 'September' + - 'Oktober' + - 'November' + - 'December' + DAYS_OF_THE_WEEK: + - 'Måndag' + - 'Tisdag' + - 'Onsdag' + - 'Torsdag' + - 'Fredag' + - 'Lördag' + - 'Söndag' diff --git a/system/languages/th.yaml b/system/languages/th.yaml index d0023c2..e6ccfab 100644 --- a/system/languages/th.yaml +++ b/system/languages/th.yaml @@ -1,75 +1,56 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - ชื่อเรื่อง: %1$s - --- - - # ข้อผิดพลาด: Invalid Frontmatter - - Path: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -NICETIME: - NO_DATE_PROVIDED: ไม่มีวันที่ให้ - BAD_DATE: รูปแบบวันที่ผิด - AGO: ที่ผ่านมา - FROM_NOW: จากตอนนี้ - SECOND: วินาที - MINUTE: นาที - HOUR: ชั่วโมง - DAY: วัน - WEEK: สัปดาห์ - MONTH: เดือน - YEAR: ปี - DECADE: ทศวรรษที่ผ่านมา - SEC: วิ - MIN: นาที - HR: ชม. - WK: wk - MO: mo - YR: yr - DEC: 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: wks - MO_PLURAL: mos - YR_PLURAL: ปี - DEC_PLURAL: decs -FORM: - VALIDATION_FAIL: 'ตรวจสอบล้มเหลว: ' - INVALID_INPUT: ป้อนข้อมูลไม่ถูกต้องใน - MISSING_REQUIRED_FIELD: 'ขาดข้อมูลที่จำเป็น:' -MONTHS_OF_THE_YEAR: - - มกราคม - - กุมภาพันธ์ - - มีนาคม - - เมษายน - - พฤษภาคม - - มิถุนายน - - กรกฏาคม - - สิงหาคม - - กันยายน - - ตุลาคม - - พฤศจิกายน - - ธันวาคม -DAYS_OF_THE_WEEK: - - จันทร์ - - อังคาร - - พุธ - - พฤหัสบดี - - ศุกร์ - - เสาร์ - - อาทิตย์ +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nชื่อเรื่อง: %1$s\n---\n\n# ข้อผิดพลาด: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: ไม่มีวันที่ให้ + BAD_DATE: รูปแบบวันที่ผิด + AGO: ที่ผ่านมา + FROM_NOW: จากตอนนี้ + SECOND: วินาที + MINUTE: นาที + HOUR: ชั่วโมง + DAY: วัน + WEEK: สัปดาห์ + MONTH: เดือน + YEAR: ปี + DECADE: ทศวรรษที่ผ่านมา + SEC: วิ + MIN: นาที + HR: ชม. + SECOND_PLURAL: วินาที + MINUTE_PLURAL: นาที + HOUR_PLURAL: ชั่วโมง + DAY_PLURAL: วัน + WEEK_PLURAL: สัปดาห์ + MONTH_PLURAL: เดือน + YEAR_PLURAL: ปี + DECADE_PLURAL: ทศวรรษที่ผ่านมา + SEC_PLURAL: วินาที + MIN_PLURAL: นาที + HR_PLURAL: ชั่วโมง + YR_PLURAL: ปี + FORM: + VALIDATION_FAIL: 'ตรวจสอบล้มเหลว: ' + INVALID_INPUT: ป้อนข้อมูลไม่ถูกต้องใน + MISSING_REQUIRED_FIELD: 'ขาดข้อมูลที่จำเป็น:' + MONTHS_OF_THE_YEAR: + - 'มกราคม' + - 'กุมภาพันธ์' + - 'มีนาคม' + - 'เมษายน' + - 'พฤษภาคม' + - 'มิถุนายน' + - 'กรกฏาคม' + - 'สิงหาคม' + - 'กันยายน' + - 'ตุลาคม' + - 'พฤศจิกายน' + - 'ธันวาคม' + DAYS_OF_THE_WEEK: + - 'จันทร์' + - 'อังคาร' + - 'พุธ' + - 'พฤหัสบดี' + - 'ศุกร์' + - 'เสาร์' + - 'อาทิตย์' diff --git a/system/languages/tr.yaml b/system/languages/tr.yaml index 68e3a85..2143f31 100644 --- a/system/languages/tr.yaml +++ b/system/languages/tr.yaml @@ -1,59 +1,63 @@ --- -NICETIME: - NO_DATE_PROVIDED: Tarih yok - BAD_DATE: Yanlış tarih - AGO: önce - FROM_NOW: (şimdiden) - SECOND: saniye - MINUTE: dakika - HOUR: saat - DAY: gün - WEEK: hafta - MONTH: ay - YEAR: yıl - DECADE: onyıl - SEC: sn - MIN: dk - HR: sa - WK: hft - MO: ay - YR: yl - DEC: onyl - SECOND_PLURAL: saniye - MINUTE_PLURAL: dakika - HOUR_PLURAL: saat - DAY_PLURAL: gün - WEEK_PLURAL: hafta - MONTH_PLURAL: ay - YEAR_PLURAL: yıl - DECADE_PLURAL: onyıl - SEC_PLURAL: sn - MIN_PLURAL: dk - HR_PLURAL: sa - WK_PLURAL: hft - MO_PLURAL: ay - YR_PLURAL: yl - DEC_PLURAL: onyl -FORM: - VALIDATION_FAIL: 'Doğrulama başarısız:' -MONTHS_OF_THE_YEAR: - - Ocak - - Şubat - - Mart - - Nisan - - Mayıs - - Haziran - - Temmuz - - Ağustos - - Eylül - - Ekim - - Kasım - - Aralık -DAYS_OF_THE_WEEK: - - Pazartesi - - Salı - - Çarşamba - - Perşembe - - Cuma - - Cumartesi - - Pazar +GRAV: + FRONTMATTER_ERROR_PAGE: "---\nBaşlık: %1$s\n---\n\n# Hata: Geçersiz Önbölüm\n\nYol: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: Sağlanan tarih yok + BAD_DATE: Yanlış tarih + AGO: önce + FROM_NOW: şu andan itibaren + SECOND: saniye + MINUTE: dakika + HOUR: saat + DAY: gün + WEEK: hafta + MONTH: ay + YEAR: yıl + DECADE: onyıl + SEC: sn + MIN: dk + HR: sa + WK: hft + MO: ay + YR: yl + DEC: onyl + SECOND_PLURAL: saniye + MINUTE_PLURAL: dakika + HOUR_PLURAL: saat + DAY_PLURAL: gün + WEEK_PLURAL: hafta + MONTH_PLURAL: ay + YEAR_PLURAL: yıl + DECADE_PLURAL: onyıl + SEC_PLURAL: sn + MIN_PLURAL: dk + HR_PLURAL: sa + WK_PLURAL: hft + MO_PLURAL: ay + YR_PLURAL: yıl + DEC_PLURAL: onyl + FORM: + VALIDATION_FAIL: Doğrulama başarısız: + INVALID_INPUT: Geçersiz bilgi girişi + MISSING_REQUIRED_FIELD: 'Gerekli alan eksik:' + MONTHS_OF_THE_YEAR: + - 'Ocak' + - 'Şubat' + - 'Mart' + - 'Nisan' + - 'Mayıs' + - 'Haziran' + - 'Temmuz' + - 'Ağustos' + - 'Eylül' + - 'Ekim' + - 'Kasım' + - 'Aralık' + DAYS_OF_THE_WEEK: + - 'Pazartesi' + - 'Salı' + - 'Çarşamba' + - 'Perşembe' + - 'Cuma' + - 'Cumartesi' + - 'Pazar' diff --git a/system/languages/uk.yaml b/system/languages/uk.yaml index 8ab7991..d264fba 100644 --- a/system/languages/uk.yaml +++ b/system/languages/uk.yaml @@ -1,75 +1,63 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - - # Помилка: Недопустимий вміст - - Path: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -NICETIME: - NO_DATE_PROVIDED: Не вказана дата - BAD_DATE: Невірна дата - AGO: назад - FROM_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: 'Відсутнє необхідне поле:' -MONTHS_OF_THE_YEAR: - - Січень - - Лютий - - Березень - - Квітень - - Травень - - Червень - - Липень - - Серпень - - Вересень - - Жовтень - - Листопад - - Грудень -DAYS_OF_THE_WEEK: - - Понеділок - - Вівторок - - Середа - - Четвер - - "П'ятниця" - - Субота - - Неділя +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Помилка: Недопустимий вміст\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: Не вказана дата + BAD_DATE: Невірна дата + AGO: назад + FROM_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: 'Відсутнє обов''язкове поле:' + MONTHS_OF_THE_YEAR: + - 'Січень' + - 'Лютий' + - 'Березень' + - 'Квітень' + - 'Травень' + - 'Червень' + - 'Липень' + - 'Серпень' + - 'Вересень' + - 'Жовтень' + - 'Листопад' + - 'Грудень' + DAYS_OF_THE_WEEK: + - 'Понеділок' + - 'Вівторок' + - 'Середа' + - 'Четвер' + - 'П''ятниця' + - 'Субота' + - 'Неділя' diff --git a/system/languages/vi.yaml b/system/languages/vi.yaml index e426993..0dc7e92 100644 --- a/system/languages/vi.yaml +++ b/system/languages/vi.yaml @@ -1,75 +1,63 @@ --- -FRONTMATTER_ERROR_PAGE: | - --- - title: %1$s - --- - - # Error: Invalid Frontmatter - - Path: `%2$s` - - **%3$s** - - ``` - %4$s - ``` -NICETIME: - NO_DATE_PROVIDED: Không có ngày được cung cấp - BAD_DATE: Ngày không hợp lệ - AGO: cách đây - FROM_NOW: từ bây giờ - SECOND: giây - MINUTE: phút - HOUR: giờ - DAY: ngày - WEEK: tuần - MONTH: tháng - YEAR: năm - DECADE: thập kỷ - SEC: giây - MIN: phút - HR: giờ - WK: tuần - MO: tháng - YR: năm - DEC: thập kỷ - SECOND_PLURAL: giây - MINUTE_PLURAL: phút - HOUR_PLURAL: giờ - DAY_PLURAL: ngày - WEEK_PLURAL: tuần - MONTH_PLURAL: tháng - YEAR_PLURAL: năm - DECADE_PLURAL: thập kỷ - SEC_PLURAL: giây - MIN_PLURAL: phút - HR_PLURAL: giờ - WK_PLURAL: tuần - MO_PLURAL: tháng - YR_PLURAL: năm - DEC_PLURAL: thập kỷ -FORM: - VALIDATION_FAIL: 'Xác nhận thất bại:' - INVALID_INPUT: Dữ liệu nhập không hợp lệ cho - MISSING_REQUIRED_FIELD: 'Thiếu trường bắt buộc:' -MONTHS_OF_THE_YEAR: - - Tháng 1 - - Tháng 2 - - Tháng 3 - - Tháng 4 - - Tháng 5 - - Tháng 6 - - Tháng 7 - - Tháng 8 - - Tháng 9 - - Tháng 10 - - Tháng Mười 11 - - Tháng 12 -DAYS_OF_THE_WEEK: - - Thứ 2 - - Thứ 3 - - Thứ 4 - - Thứ 5 - - Thứ 6 - - Thứ 7 - - Chủ Nhật +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntiêu đề: %1$s\n---\n\n# Error: Trang không hợp lệ\n\nĐường dẫn: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + NICETIME: + NO_DATE_PROVIDED: Không có ngày được cung cấp + BAD_DATE: Ngày không hợp lệ + AGO: cách đây + FROM_NOW: từ bây giờ + SECOND: giây + MINUTE: phút + HOUR: giờ + DAY: ngày + WEEK: tuần + MONTH: tháng + YEAR: năm + DECADE: thập kỷ + SEC: giây + MIN: phút + HR: giờ + WK: tuần + MO: tháng + YR: năm + DEC: thập kỷ + SECOND_PLURAL: giây + MINUTE_PLURAL: phút + HOUR_PLURAL: giờ + DAY_PLURAL: ngày + WEEK_PLURAL: tuần + MONTH_PLURAL: tháng + YEAR_PLURAL: năm + DECADE_PLURAL: thập kỷ + SEC_PLURAL: giây + MIN_PLURAL: phút + HR_PLURAL: giờ + WK_PLURAL: tuần + MO_PLURAL: tháng + YR_PLURAL: năm + DEC_PLURAL: thập kỷ + FORM: + VALIDATION_FAIL: Xác nhận thất bại: + INVALID_INPUT: Dữ liệu nhập không hợp lệ cho + MISSING_REQUIRED_FIELD: 'Thiếu trường bắt buộc:' + MONTHS_OF_THE_YEAR: + - 'Tháng 1' + - 'Tháng 2' + - 'Tháng 3' + - 'Tháng 4' + - 'Tháng 5' + - 'Tháng 6' + - 'Tháng 7' + - 'Tháng 8' + - 'Tháng 9' + - 'Tháng 10' + - 'Tháng 11' + - 'Tháng 12' + DAYS_OF_THE_WEEK: + - 'Thứ 2' + - 'Thứ 3' + - 'Thứ 4' + - 'Thứ 5' + - 'Thứ 6' + - 'Thứ 7' + - 'Chủ Nhật' diff --git a/system/languages/zh-tw.yaml b/system/languages/zh-tw.yaml new file mode 100644 index 0000000..6cb39c9 --- /dev/null +++ b/system/languages/zh-tw.yaml @@ -0,0 +1,62 @@ +--- +GRAV: + FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 錯誤: 不正確的 Frontmatter\n\n路徑: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" + 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: + MISSING_REQUIRED_FIELD: 遺漏必填欄位: + MONTHS_OF_THE_YEAR: + - '一月' + - '二月' + - '三月' + - '四月' + - '五月' + - '六月' + - '七月' + - '八月' + - '九月' + - '十月' + - '十一月' + - '十二月' + DAYS_OF_THE_WEEK: + - '星期一' + - '星期二' + - '星期三' + - '星期四' + - '星期五' + - '星期六' + - '星期日' diff --git a/system/languages/zh.yaml b/system/languages/zh.yaml new file mode 100644 index 0000000..4912aa9 --- /dev/null +++ b/system/languages/zh.yaml @@ -0,0 +1,144 @@ +--- +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': '\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: + - '装备' + - '信息' + - '大米' + - '钱' + - '物种' + - '系列' + - '鱼' + - '羊' + INFLECTOR_IRREGULAR: + 'person': '人员' + 'man': '男人' + 'child': '儿童' + 'sex': '性别' + 'move': '移动' + INFLECTOR_ORDINALS: + 'default': 'th' + 'first': 'st' + 'second': 'md' + '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: 必填字段缺失: + MONTHS_OF_THE_YEAR: + - '1月' + - '2月' + - '3月' + - '4月' + - '5月' + - '6月' + - '7月' + - '8月' + - '9月' + - '10月' + - '11月' + - '12月' + DAYS_OF_THE_WEEK: + - '星期一' + - '星期二' + - '星期三' + - '星期四' + - '星期五' + - '星期六' + - '星期日' + CRON: + EVERY: 每隔 + EVERY_HOUR: 每小时 + EVERY_MINUTE: 每分钟 + EVERY_DAY_OF_WEEK: 一周中的每一天 + EVERY_DAY_OF_MONTH: 月份中的每一天 + EVERY_MONTH: 每月 + TEXT_PERIOD: 所有 + TEXT_MINS: ' 在 小时过后的分钟' + TEXT_TIME: ' 在 :' + TEXT_DOW: ' on ' + TEXT_MONTH: ' of ' + TEXT_DOM: ' on ' + ERROR1: 不支持分享类型 %s + ERROR2: 无效数字 + ERROR3: 请在 jqCron 设置中设定 jquery_element + ERROR4: 无法识别表达式 diff --git a/system/router.php b/system/router.php index 2e5af82..5e3c477 100644 --- a/system/router.php +++ b/system/router.php @@ -1,13 +1,14 @@ or \n"; - } - - - return $output; - } - - /** - * Build the JavaScript script tags. - * - * @param string $group name of the group - * @param array $attributes - * - * @return string - */ - public function js($group = 'head', $attributes = []) - { - if (!$this->js && !$this->inline_js) { - return null; - } - - // Sort array by priorities (larger priority first) - uasort($this->js, array($this, 'sortAssetsByPriorityThenOrder')); - uasort($this->inline_js, array($this, 'sortAssetsByPriorityThenOrder')); - - $inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline'; - - $attributes = $this->attributes($attributes); - - $output = ''; - $inline_js = ''; - - if ($this->js_pipeline) { - $pipeline_result = $this->pipelineJs($group, !$inlineGroup); - $pipeline_html = ($inlineGroup ? '' : '' . "\n"); - - if ($this->js_pipeline_before_excludes && $pipeline_result) { - if ($inlineGroup) { - $inline_js .= $pipeline_result; - } - else { - $output .= $pipeline_html; - } - } - foreach ($this->js_no_pipeline as $file) { - if ($group && $file['group'] == $group) { - if ($file['loading'] === 'inline') { - $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n"; - } - else { - $output .= '' . "\n"; - } - } - } - if (!$this->js_pipeline_before_excludes && $pipeline_result) { - if ($inlineGroup) { - $inline_js .= $pipeline_result; - } - else { - $output .= $pipeline_html; - } - } - } else { - foreach ($this->js as $file) { - if ($group && $file['group'] == $group) { - if ($inlineGroup || $file['loading'] === 'inline') { - $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n"; - } - else { - $output .= '' . "\n"; - } - } - } - } - - // Render Inline JS - foreach ($this->inline_js as $inline) { - if ($group && $inline['group'] == $group) { - $inline_js .= $inline['asset'] . "\n"; - } - } - - if ($inline_js) { - $attribute_string = isset($inline) && $inline['type'] ? " type=\"" . $inline['type'] . "\"" : ''; - $output .= "\n\n" . $inline_js . "\n\n"; - } - - return $output; - } - - /** - * Minify and concatenate CSS - * - * @param string $group - * @param bool $returnURL true if pipeline should return the URL, otherwise the content - * - * @return bool|string URL or generated content if available, else false - */ - protected function pipelineCss($group = 'head', $returnURL = true) - { - // temporary list of assets to pipeline - $temp_css = []; - - // clear no-pipeline assets lists - $this->css_no_pipeline = []; - - // Compute uid based on assets and timestamp - $uid = md5(json_encode($this->css) . $this->css_minify . $this->css_rewrite . $group); - $file = $uid . '.css'; - $inline_file = $uid . '-inline.css'; - - $relative_path = "{$this->base_url}{$this->assets_url}/{$file}"; - - // If inline files exist set them on object - if (file_exists($this->assets_dir . $inline_file)) { - $this->css_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true); - } - - // If pipeline exist return its URL or content - if (file_exists($this->assets_dir . $file)) { - if ($returnURL) { - return $relative_path . $this->getTimestamp(); - } - else { - return file_get_contents($this->assets_dir . $file) . "\n"; - } - } - - // Remove any non-pipeline files - foreach ($this->css as $id => $asset) { - if ($asset['group'] == $group) { - if (!$asset['pipeline'] || - ($asset['remote'] && $this->css_pipeline_include_externals === false)) { - $this->css_no_pipeline[$id] = $asset; + // If pipeline disabled, set to position if provided, else after + if (isset($options['pipeline'])) { + if ($options['pipeline'] === false) { + $exclude_type = ($type === $this::JS_TYPE || $type === $this::INLINE_JS_TYPE) ? $this::JS_TYPE : $this::CSS_TYPE; + $excludes = strtolower($exclude_type . '_pipeline_before_excludes'); + if ($this->{$excludes}) { + $default = 'after'; } else { - $temp_css[$id] = $asset; + $default = 'before'; } + + $options['position'] = $options['position'] ?? $default; } + + unset($options['pipeline']); } - //if nothing found get out of here! - if (count($temp_css) == 0) { - return false; + // Add timestamp + $options['timestamp'] = $this->timestamp; + + // Set order + $options['order'] = \count($this->$collection); + + // Create asset of correct type + $asset_class = "\\Grav\\Common\\Assets\\{$type}"; + $asset_object = new $asset_class(); + + // If exists + if ($asset_object->init($asset, $options)) { + $this->$collection[md5($asset)] = $asset_object; } - // Write non-pipeline files out - if (!empty($this->css_no_pipeline)) { - file_put_contents($this->assets_dir . $inline_file, json_encode($this->css_no_pipeline)); - } + return $this; - - $css_minify = $this->css_minify; - - // If this is a Windows server, and minify_windows is false (default value) skip the - // minification process because it will cause Apache to die/crash due to insufficient - // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689 - if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN' && !$this->css_minify_windows) { - $css_minify = false; - } - - // Concatenate files - $buffer = $this->gatherLinks($temp_css, CSS_ASSET); - if ($css_minify) { - $minifier = new \MatthiasMullie\Minify\CSS(); - $minifier->add($buffer); - $buffer = $minifier->minify(); - } - - // Write file - if (strlen(trim($buffer)) > 0) { - file_put_contents($this->assets_dir . $file, $buffer); - - if ($returnURL) { - return $relative_path . $this->getTimestamp(); - } - else { - return $buffer . "\n"; - } - } else { - return false; - } } /** - * Minify and concatenate JS files. + * Add a CSS asset or a collection of assets. * - * @param string $group - * @param bool $returnURL true if pipeline should return the URL, otherwise the content - * - * @return bool|string URL or generated content if available, else false + * @return $this */ - protected function pipelineJs($group = 'head', $returnURL = true) + public function addCss($asset) { - // temporary list of assets to pipeline - $temp_js = []; - - // clear no-pipeline assets lists - $this->js_no_pipeline = []; - - // Compute uid based on assets and timestamp - $uid = md5(json_encode($this->js) . $this->js_minify . $group); - $file = $uid . '.js'; - $inline_file = $uid . '-inline.js'; - - $relative_path = "{$this->base_url}{$this->assets_url}/{$file}"; - - // If inline files exist set them on object - if (file_exists($this->assets_dir . $inline_file)) { - $this->js_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true); - } - - // If pipeline exist return its URL or content - if (file_exists($this->assets_dir . $file)) { - if ($returnURL) { - return $relative_path . $this->getTimestamp(); - } - else { - return file_get_contents($this->assets_dir . $file) . "\n"; - } - } - - // Remove any non-pipeline files - foreach ($this->js as $id => $asset) { - if ($asset['group'] == $group) { - if (!$asset['pipeline'] || - ($asset['remote'] && $this->js_pipeline_include_externals === false)) { - $this->js_no_pipeline[] = $asset; - } else { - $temp_js[$id] = $asset; - } - } - } - - //if nothing found get out of here! - if (count($temp_js) == 0) { - return false; - } - - // Write non-pipeline files out - if (!empty($this->js_no_pipeline)) { - file_put_contents($this->assets_dir . $inline_file, json_encode($this->js_no_pipeline)); - } - - // Concatenate files - $buffer = $this->gatherLinks($temp_js, JS_ASSET); - if ($this->js_minify) { - $minifier = new \MatthiasMullie\Minify\JS(); - $minifier->add($buffer); - $buffer = $minifier->minify(); - } - - // Write file - if (strlen(trim($buffer)) > 0) { - file_put_contents($this->assets_dir . $file, $buffer); - - if ($returnURL) { - return $relative_path . $this->getTimestamp(); - } - else { - return $buffer . "\n"; - } - } else { - return false; - } + return $this->addType(Assets::CSS_COLLECTION,Assets::CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::CSS_TYPE)); } /** - * Return the array of all the registered CSS assets - * If a $key is provided, it will try to return only that asset - * else it will return null + * Add an Inline CSS asset or a collection of assets. * - * @param null|string $key the asset key - * @return array + * @return $this */ - public function getCss($key = null) + public function addInlineCss($asset) { - if (!empty($key)) { - $asset_key = md5($key); - if (isset($this->css[$asset_key])) { - return $this->css[$asset_key]; - } else { - return null; - } - } - - return $this->css; + return $this->addType(Assets::CSS_COLLECTION, Assets::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_CSS_TYPE)); } /** - * Return the array of all the registered JS assets - * If a $key is provided, it will try to return only that asset - * else it will return null + * Add a JS asset or a collection of assets. * - * @param null|string $key the asset key - * @return array + * @return $this */ - public function getJs($key = null) + public function addJs($asset) { - if (!empty($key)) { - $asset_key = md5($key); - if (isset($this->js[$asset_key])) { - return $this->js[$asset_key]; - } else { - return null; - } - } - - return $this->js; + return $this->addType(Assets::JS_COLLECTION, Assets::JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::JS_TYPE)); } /** - * Set the whole array of CSS assets + * Add an Inline JS asset or a collection of assets. * - * @param $css + * @return $this */ - public function setCss($css) + public function addInlineJs($asset) { - $this->css = $css; + return $this->addType(Assets::JS_COLLECTION, Assets::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_JS_TYPE)); } - /** - * Set the whole array of JS assets - * - * @param $js - */ - public function setJs($js) - { - $this->js = $js; - } - - /** - * Removes an item from the CSS array if set - * - * @param string $key The asset key - */ - public function removeCss($key) - { - $asset_key = md5($key); - if (isset($this->css[$asset_key])) { - unset($this->css[$asset_key]); - } - } - - /** - * Removes an item from the JS array if set - * - * @param string $key The asset key - */ - public function removeJs($key) - { - $asset_key = md5($key); - if (isset($this->js[$asset_key])) { - unset($this->js[$asset_key]); - } - } - - /** - * Return the array of all the registered collections - * - * @return array - */ - public function getCollections() - { - return $this->collections; - } - - /** - * Set the array of collections explicitly - * - * @param $collections - */ - public function setCollection($collections) - { - $this->collections = $collections; - } - - /** - * Determines if an asset exists as a collection, CSS or JS reference - * - * @param $asset - * - * @return bool - */ - public function exists($asset) - { - if (isset($this->collections[$asset]) || isset($this->css[$asset]) || isset($this->js[$asset])) { - return true; - } else { - return false; - } - } /** * Add/replace collection. @@ -1000,445 +264,112 @@ class Assets return $this; } - /** - * Reset all assets. - * - * @return $this - */ - public function reset() + protected function filterAssets($assets, $key, $value, $sort = false) { - return $this->resetCss()->resetJs(); - } + $results = array_filter($assets, function($asset) use ($key, $value) { - /** - * Reset JavaScript assets. - * - * @return $this - */ - public function resetJs() - { - $this->js = []; - $this->inline_js = []; + if ($key === 'position' && $value === 'pipeline') { - return $this; - } + $type = $asset->getType(); - /** - * Reset CSS assets. - * - * @return $this - */ - public function resetCss() - { - $this->css = []; - $this->inline_css = []; + if ($asset->getRemote() && $this->{$type . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline' ) { + if ($this->{$type . '_pipeline_before_excludes'}) { + $asset->setPosition('after'); + } else { + $asset->setPosition('before'); + } + return false; + } - return $this; - } - - /** - * Add all JavaScript assets within $directory - * - * @param string $directory Relative to the Grav root path, or a stream identifier - * - * @return $this - */ - public function addDirJs($directory) - { - return $this->addDir($directory, self::JS_REGEX); - } - - /** - * Add all CSS assets within $directory - * - * @param string $directory Relative to the Grav root path, or a stream identifier - * - * @return $this - */ - public function addDirCss($directory) - { - return $this->addDir($directory, self::CSS_REGEX); - } - - /** - * Add all assets matching $pattern within $directory. - * - * @param string $directory Relative to the Grav root path, or a stream identifier - * @param string $pattern (regex) - * - * @return $this - * @throws Exception - */ - public function addDir($directory, $pattern = self::DEFAULT_REGEX) - { - $root_dir = rtrim(ROOT_DIR, '/'); - - // Check if $directory is a stream. - if (strpos($directory, '://')) { - $directory = Grav::instance()['locator']->findResource($directory, null); - } - - // Get files - $files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/'); - - // No luck? Nothing to do - if (!$files) { - return $this; - } - - // Add CSS files - if ($pattern === self::CSS_REGEX) { - foreach ($files as $file) { - $this->addCss($file); } - return $this; - } - - // Add JavaScript files - if ($pattern === self::JS_REGEX) { - foreach ($files as $file) { - $this->addJs($file); - } - - return $this; - } - - // Unknown pattern. - foreach ($files as $asset) { - $this->add($asset); - } - - return $this; - } - - /** - * Determine whether a link is local or remote. - * - * Understands both "http://" and "https://" as well as protocol agnostic links "//" - * - * @param string $link - * - * @return bool - */ - protected function isRemoteLink($link) - { - $base = Grav::instance()['uri']->rootUrl(true); - - // sanity check for local URLs with absolute URL's enabled - if (Utils::startsWith($link, $base)) { + if ($asset[$key] === $value) return true; return false; + }); + + if ($sort && !empty($results)) { + $results = $this->sortAssets($results); } - return ('http://' === substr($link, 0, 7) || 'https://' === substr($link, 0, 8) || '//' === substr($link, 0, - 2)); + + return $results; } - /** - * Build local links including grav asset shortcodes - * - * @param string $asset the asset string reference - * @param bool $absolute build absolute asset link - * - * @return string the final link url to the asset - */ - protected function buildLocalLink($asset, $absolute = false) + protected function sortAssets($assets) { - try { - $asset = Grav::instance()['locator']->findResource($asset, $absolute); - } catch (\Exception $e) { - } - - $uri = $absolute ? $asset : $this->base_url . ltrim($asset, '/'); - return $asset ? $uri : false; + uasort ($assets, function($a, $b) { + if ($a['priority'] == $b['priority']) { + return $a['order'] - $b['order']; + } + return $b['priority'] - $a['priority']; + }); + return $assets; } - /** - * Get the last modification time of asset - * - * @param string $asset the asset string reference - * - * @return string the last modifcation time or false on error - */ - protected function getLastModificationTime($asset) + public function render($type, $group = 'head', $attributes = []) { - $file = GRAV_ROOT . $asset; - if (Grav::instance()['locator']->isStream($asset)) { - $file = $this->buildLocalLink($asset, true); + $before_output = ''; + $pipeline_output = ''; + $after_output = ''; + + $assets = 'assets_' . $type; + $pipeline_enabled = $type . '_pipeline'; + $render_pipeline = 'render' . ucfirst($type); + + $group_assets = $this->filterAssets($this->$assets, 'group', $group); + $pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true); + $before_assets = $this->filterAssets($group_assets, 'position', 'before', true); + $after_assets = $this->filterAssets($group_assets, 'position', 'after', true); + + // Pipeline + if ($this->{$pipeline_enabled}) { + $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]); + + $pipeline = new Pipeline($options); + $pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes); + } else { + foreach ($pipeline_assets as $asset) { + $pipeline_output .= $asset->render(); + } } - return file_exists($file) ? filemtime($file) : false; + // Before Pipeline + foreach ($before_assets as $asset) { + $before_output .= $asset->render(); + } + + // After Pipeline + foreach ($after_assets as $asset) { + $after_output .= $asset->render(); + } + + return $before_output . $pipeline_output . $after_output; } + /** - * Build an HTML attribute string from an array. + * Build the CSS link tags. * - * @param array $attributes + * @param string $group name of the group + * @param array $attributes * * @return string */ - protected function attributes(array $attributes) + public function css($group = 'head', $attributes = []) { - $html = ''; - $no_key = ['loading']; - - foreach ($attributes as $key => $value) { - // For numeric keys we will assume that the key and the value are the same - // as this will convert HTML attributes such as "required" to a correct - // form like required="required" instead of using incorrect numerics. - if (is_numeric($key)) { - $key = $value; - } - if (is_array($value)) { - $value = implode(' ', $value); - } - - if (in_array($key, $no_key)) { - $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false); - } else { - $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"'; - } - - $html .= ' ' . $element; - } - - return $html; + return $this->render('css', $group, $attributes); } /** - * Download and concatenate the content of several links. + * Build the JavaScript script tags. * - * @param array $links - * @param bool $css + * @param string $group name of the group + * @param array $attributes * * @return string */ - protected function gatherLinks(array $links, $css = true) + public function js($group = 'head', $attributes = []) { - $buffer = ''; - - - foreach ($links as $asset) { - $relative_dir = ''; - $local = true; - - $link = $asset['asset']; - $relative_path = $link; - - if ($this->isRemoteLink($link)) { - $local = false; - if ('//' === substr($link, 0, 2)) { - $link = 'http:' . $link; - } - } else { - // Fix to remove relative dir if grav is in one - if (($this->base_url != '/') && (strpos($this->base_url, $link) == 0)) { - $base_url = '#' . preg_quote($this->base_url, '#') . '#'; - $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/'); - } - - $relative_dir = dirname($relative_path); - $link = ROOT_DIR . $relative_path; - } - - $file = ($this->fetch_command instanceof Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link); - - // No file found, skip it... - if ($file === false) { - continue; - } - - // Double check last character being - if (!$css) { - $file = rtrim($file, ' ;') . ';'; - } - - // If this is CSS + the file is local + rewrite enabled - if ($css && $local && $this->css_rewrite) { - $file = $this->cssRewrite($file, $relative_dir); - } - - $file = rtrim($file) . PHP_EOL; - $buffer .= $file; - } - - // Pull out @imports and move to top - if ($css) { - $buffer = $this->moveImports($buffer); - } - - return $buffer; + return $this->render('js', $group, $attributes); } - - /** - * Finds relative CSS urls() and rewrites the URL with an absolute one - * - * @param string $file the css source file - * @param string $relative_path relative path to the css file - * - * @return mixed - */ - protected function cssRewrite($file, $relative_path) - { - // Strip any sourcemap comments - $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file); - - // Find any css url() elements, grab the URLs and calculate an absolute path - // Then replace the old url with the new one - $file = preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($relative_path) { - - $old_url = $matches[2]; - - // Ensure link is not rooted to webserver, a data URL, or to a remote host - if (Utils::startsWith($old_url, '/') || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) { - return $matches[0]; - } - - $new_url = $this->base_url . ltrim(Utils::normalizePath($relative_path . '/' . $old_url), '/'); - - return str_replace($old_url, $new_url, $matches[0]); - }, $file); - - return $file; - } - - /** - * Moves @import statements to the top of the file per the CSS specification - * - * @param string $file the file containing the combined CSS files - * - * @return string the modified file with any @imports at the top of the file - */ - protected function moveImports($file) - { - $this->imports = []; - - $file = preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) { - $this->imports[] = $matches[0]; - - return ''; - }, $file); - - return implode("\n", $this->imports) . "\n\n" . $file; - } - - /** - * Recursively get files matching $pattern within $directory. - * - * @param string $directory - * @param string $pattern (regex) - * @param string $ltrim Will be trimmed from the left of the file path - * - * @return array - */ - protected function rglob($directory, $pattern, $ltrim = null) - { - $iterator = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory, - FilesystemIterator::SKIP_DOTS)), $pattern); - $offset = strlen($ltrim); - $files = []; - - foreach ($iterator as $file) { - $files[] = substr($file->getPathname(), $offset); - } - - return $files; - } - - /** - * Sets the state of CSS Pipeline - * - * @param boolean $value - */ - public function setCssPipeline($value) - { - $this->css_pipeline = (bool)$value; - } - - /** - * Sets the state of JS Pipeline - * - * @param boolean $value - */ - public function setJsPipeline($value) - { - $this->js_pipeline = (bool)$value; - } - - /** - * Explicitly set's a timestamp for assets - * - * @param $value - */ - public function setTimestamp($value) - { - $this->timestamp = $value; - } - - /** - * Get the timestamp for assets - * - * @return string - */ - public function getTimestamp($include_join = true) - { - if ($this->timestamp) { - $timestamp = $include_join ? '?' . $this->timestamp : $this->timestamp; - return $timestamp; - } - return; - } - - /** - * - * - * @param $asset - * @return string - */ - public function getQuerystring($asset) - { - $querystring = ''; - - if (!empty($asset['query'])) { - if (Utils::contains($asset['asset'], '?')) { - $querystring .= '&' . $asset['query']; - } else { - $querystring .= '?' . $asset['query']; - } - } - - if ($this->timestamp) { - if (Utils::contains($asset['asset'], '?') || $querystring) { - $querystring .= '&' . $this->timestamp; - } else { - $querystring .= '?' . $this->timestamp; - } - } - - return $querystring; - } - - /** - * @return string - */ - public function __toString() - { - return ''; - } - - /** - * @param $a - * @param $b - * - * @return mixed - */ - protected function sortAssetsByPriorityThenOrder($a, $b) - { - if ($a['priority'] == $b['priority']) { - return $a['order'] - $b['order']; - } - - return $b['priority'] - $a['priority']; - } - } diff --git a/system/src/Grav/Common/Assets/BaseAsset.php b/system/src/Grav/Common/Assets/BaseAsset.php new file mode 100644 index 0000000..49e3864 --- /dev/null +++ b/system/src/Grav/Common/Assets/BaseAsset.php @@ -0,0 +1,198 @@ + 'head', + 'position' => 'pipeline', + 'priority' => 10, + 'modified' => null, + 'asset' => null + ]; + + // Merge base defaults + $elements = array_merge($base_config, $elements); + + parent::__construct($elements, $key); + } + + public function init($asset, $options) + { + $config = Grav::instance()['config']; + $uri = Grav::instance()['uri']; + + // set attributes + foreach ($options as $key => $value) { + if ($this->hasProperty($key)) { + $this->setProperty($key, $value); + } else { + $this->attributes[$key] = $value; + } + } + + // Force priority to be an int + $this->priority = (int) $this->priority; + + // Do some special stuff for CSS/JS (not inline) + if (!Utils::startsWith($this->getType(), 'inline')) { + $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/'; + $this->remote = static::isRemoteLink($asset); + + // Move this to render? + if (!$this->remote) { + + $asset_parts = parse_url($asset); + if (isset($asset_parts['query'])) { + $this->query = $asset_parts['query']; + unset($asset_parts['query']); + $asset = Uri::buildUrl($asset_parts); + } + + $locator = Grav::instance()['locator']; + + if ($locator->isStream($asset)) { + $path = $locator->findResource($asset, true); + } else { + $path = GRAV_ROOT . $asset; + } + + // If local file is missing return + if ($path === false) { + return false; + } + + $file = new \SplFileInfo($path); + + $asset = $this->buildLocalLink($file->getPathname()); + + $this->modified = $file->isFile() ? $file->getMTime() : false; + } + } + + $this->asset = $asset; + + return $this; + } + + public function getAsset() + { + return $this->asset; + } + + public function getRemote() + { + return $this->remote; + } + + public function setPosition($position) + { + $this->position = $position; + return $this; + } + + + /** + * + * Get the last modification time of asset + * + * @param string $asset the asset string reference + * + * @return string the last modifcation time or false on error + */ +// protected function getLastModificationTime($asset) +// { +// $file = GRAV_ROOT . $asset; +// if (Grav::instance()['locator']->isStream($asset)) { +// $file = $this->buildLocalLink($asset, true); +// } +// +// return file_exists($file) ? filemtime($file) : false; +// } + + /** + * + * Build local links including grav asset shortcodes + * + * @param string $asset the asset string reference + * + * @return string the final link url to the asset + */ + protected function buildLocalLink($asset) + { + if ($asset) { + return $this->base_url . ltrim(Utils::replaceFirstOccurrence(GRAV_ROOT, '', $asset), '/'); + } + return false; + } + + + /** + * Implements JsonSerializable interface. + * + * @return array + */ + public function jsonSerialize() + { + return ['type' => $this->getType(), 'elements' => $this->getElements()]; + } + + /** + * Placeholder for AssetUtilsTrait method + * + * @param string $file + * @param string $dir + * @param bool $local + */ + protected function cssRewrite($file, $dir, $local) + { + return; + } +} diff --git a/system/src/Grav/Common/Assets/Css.php b/system/src/Grav/Common/Assets/Css.php new file mode 100644 index 0000000..405d8a3 --- /dev/null +++ b/system/src/Grav/Common/Assets/Css.php @@ -0,0 +1,40 @@ + 'css', + 'attributes' => [ + 'type' => 'text/css', + 'rel' => 'stylesheet' + ] + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + public function render() + { + if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') { + $buffer = $this->gatherLinks( [$this], self::CSS_ASSET); + return "\n"; + } + + return 'renderAttributes() . ">\n"; + } +} diff --git a/system/src/Grav/Common/Assets/InlineCss.php b/system/src/Grav/Common/Assets/InlineCss.php new file mode 100644 index 0000000..c48dc48 --- /dev/null +++ b/system/src/Grav/Common/Assets/InlineCss.php @@ -0,0 +1,32 @@ + 'css', + 'position' => 'after' + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + public function render() + { + return 'renderAttributes(). ">\n" . trim($this->asset) . "\n\n"; + } +} diff --git a/system/src/Grav/Common/Assets/InlineJs.php b/system/src/Grav/Common/Assets/InlineJs.php new file mode 100644 index 0000000..11e4442 --- /dev/null +++ b/system/src/Grav/Common/Assets/InlineJs.php @@ -0,0 +1,32 @@ + 'js', + 'position' => 'after' + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + public function render() + { + return 'renderAttributes(). ">\n" . trim($this->asset) . "\n\n"; + } +} diff --git a/system/src/Grav/Common/Assets/Js.php b/system/src/Grav/Common/Assets/Js.php new file mode 100644 index 0000000..cce86d9 --- /dev/null +++ b/system/src/Grav/Common/Assets/Js.php @@ -0,0 +1,36 @@ + 'js', + ]; + + $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements); + + parent::__construct($merged_attributes, $key); + } + + public function render() + { + if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') { + $buffer = $this->gatherLinks( [$this], self::JS_ASSET); + return 'renderAttributes() . ">\n" . trim($buffer) . "\n\n"; + } + + return '\n"; + } +} diff --git a/system/src/Grav/Common/Assets/Pipeline.php b/system/src/Grav/Common/Assets/Pipeline.php new file mode 100644 index 0000000..b009585 --- /dev/null +++ b/system/src/Grav/Common/Assets/Pipeline.php @@ -0,0 +1,273 @@ +base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/'; + $this->assets_dir = $locator->findResource('asset://') . DS; + $this->assets_url = $locator->findResource('asset://', false); + } + + /** + * Minify and concatenate CSS + * + * @param array $assets + * @param string $group + * @param array $attributes + * + * @return bool|string URL or generated content if available, else false + */ + public function renderCss($assets, $group, $attributes = []) + { + // temporary list of assets to pipeline + $inline_group = false; + + if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') { + $inline_group = true; + unset($attributes['loading']); + } + + // Store Attributes + $this->attributes = array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes); + + // Compute uid based on assets and timestamp + $json_assets = json_encode($assets); + $uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group); + $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"; + } else { + //if nothing found get out of here! + if (empty($assets)) { + return false; + } + + // Concatenate files + $buffer = $this->gatherLinks($assets, self::CSS_ASSET); + + // Minify if required + if ($this->shouldMinify('css')) { + $minifier = new \MatthiasMullie\Minify\CSS(); + $minifier->add($buffer); + $buffer = $minifier->minify(); + } + + // Write file + if (trim($buffer) !== '') { + file_put_contents($this->assets_dir . $file, $buffer); + } + } + + if ($inline_group) { + $output = "\n"; + } else { + $this->asset = $relative_path; + $output = 'renderAttributes() . ">\n"; + } + + return $output; + } + + /** + * Minify and concatenate JS files. + * + * @param array $assets + * @param string $group + * @param array $attributes + * + * @return bool|string URL or generated content if available, else false + */ + public function renderJs($assets, $group, $attributes = []) + { + // temporary list of assets to pipeline + $inline_group = false; + + if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') { + $inline_group = true; + unset($attributes['loading']); + } + + // Store Attributes + $this->attributes = $attributes; + + // Compute uid based on assets and timestamp + $json_assets = json_encode($assets); + $uid = md5($json_assets . $this->js_minify . $group); + $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"; + } else { + //if nothing found get out of here! + if (empty($assets)) { + return false; + } + + // Concatenate files + $buffer = $this->gatherLinks($assets, self::JS_ASSET); + + // Minify if required + if ($this->shouldMinify('js')) { + $minifier = new \MatthiasMullie\Minify\JS(); + $minifier->add($buffer); + $buffer = $minifier->minify(); + } + + // Write file + if (trim($buffer) !== '') { + file_put_contents($this->assets_dir . $file, $buffer); + } + } + + if ($inline_group) { + $output = 'renderAttributes(). ">\n" . $buffer . "\n\n"; + } else { + $this->asset = $relative_path; + $output = '\n"; + } + + return $output; + } + + + /** + * Finds relative CSS urls() and rewrites the URL with an absolute one + * + * @param string $file the css source file + * @param string $dir , $local relative path to the css file + * @param bool $local is this a local or remote asset + * + * @return mixed + */ + protected function cssRewrite($file, $dir, $local) + { + // Strip any sourcemap comments + $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file); + + // Find any css url() elements, grab the URLs and calculate an absolute path + // Then replace the old url with the new one + $file = (string)preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($dir, $local) { + + $old_url = $matches[2]; + + // Ensure link is not rooted to web server, a data URL, or to a remote host + if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) { + return $matches[0]; + } + + // clean leading / + $old_url = Utils::normalizePath($dir . '/' . $old_url); + if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) { + $old_url = ltrim($old_url, '/'); + } + + $new_url = ($local ? $this->base_url: '') . $old_url; + + $fixed = str_replace($matches[2], $new_url, $matches[0]); + + return $fixed; + }, $file); + + return $file; + } + + + + private function shouldMinify($type = 'css') + { + $check = $type . '_minify'; + $win_check = $type . '_minify_windows'; + + $minify = (bool) $this->$check; + + // If this is a Windows server, and minify_windows is false (default value) skip the + // minification process because it will cause Apache to die/crash due to insufficient + // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689 + if (stripos(php_uname('s'), 'WIN') === 0 && !$this->{$win_check}) { + $minify = false; + } + + return $minify; + } +} diff --git a/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php new file mode 100644 index 0000000..0a1d149 --- /dev/null +++ b/system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php @@ -0,0 +1,182 @@ +rootUrl(true); + + // Sanity check for local URLs with absolute URL's enabled + if (Utils::startsWith($link, $base)) { + return false; + } + + return (0 === strpos($link, 'http://') || 0 === strpos($link, 'https://') || 0 === strpos($link, '//')); + } + + /** + * Download and concatenate the content of several links. + * + * @param array $assets + * @param bool $css + * + * @return string + */ + protected function gatherLinks(array $assets, $css = true) + { + $buffer = ''; + + + foreach ($assets as $id => $asset) { + $local = true; + + $link = $asset->getAsset(); + $relative_path = $link; + + if (static::isRemoteLink($link)) { + $local = false; + if (0 === strpos($link, '//')) { + $link = 'http:' . $link; + } + $relative_dir = \dirname($relative_path); + } else { + // Fix to remove relative dir if grav is in one + if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) { + $base_url = '#' . preg_quote($this->base_url, '#') . '#'; + $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/'); + } + + $relative_dir = \dirname($relative_path); + $link = ROOT_DIR . $relative_path; + } + + $file = ($this->fetch_command instanceof \Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link); + + // No file found, skip it... + if ($file === false) { + continue; + } + + // Double check last character being + if (!$css) { + $file = rtrim($file, ' ;') . ';'; + } + + // If this is CSS + the file is local + rewrite enabled + if ($css && $this->css_rewrite) { + $file = $this->cssRewrite($file, $relative_dir, $local); + } + + $file = rtrim($file) . PHP_EOL; + $buffer .= $file; + } + + // Pull out @imports and move to top + if ($css) { + $buffer = $this->moveImports($buffer); + } + + return $buffer; + } + + /** + * Moves @import statements to the top of the file per the CSS specification + * + * @param string $file the file containing the combined CSS files + * + * @return string the modified file with any @imports at the top of the file + */ + protected function moveImports($file) + { + $imports = []; + + $file = (string)preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) use (&$imports) { + $imports[] = $matches[0]; + + return ''; + }, $file); + + return implode("\n", $imports) . "\n\n" . $file; + } + + /** + * + * Build an HTML attribute string from an array. + * + * @return string + */ + protected function renderAttributes() + { + $html = ''; + $no_key = ['loading']; + + foreach ($this->attributes as $key => $value) { + if (is_numeric($key)) { + $key = $value; + } + if (\is_array($value)) { + $value = implode(' ', $value); + } + + if (\in_array($key, $no_key, true)) { + $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false); + } else { + $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"'; + } + + $html .= ' ' . $element; + } + + return $html; + } + + /** + * Render Querystring + * + * @param string $asset + * @return string + */ + protected function renderQueryString($asset = null) + { + $querystring = ''; + + $asset = $asset ?? $this->asset; + + if (!empty($this->query)) { + if (Utils::contains($asset, '?')) { + $querystring .= '&' . $this->query; + } else { + $querystring .= '?' . $this->query; + } + } + + if ($this->timestamp) { + if (Utils::contains($asset, '?') || $querystring) { + $querystring .= '&' . $this->timestamp; + } else { + $querystring .= '?' . $this->timestamp; + } + } + + return $querystring; + } +} diff --git a/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php new file mode 100644 index 0000000..229c2ab --- /dev/null +++ b/system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php @@ -0,0 +1,125 @@ + null, 'pipeline' => true, 'loading' => null, 'group' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + break; + + case(Assets::INLINE_JS_TYPE): + $defaults = ['priority' => null, 'group' => null, 'attributes' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + + // special case to handle old attributes being passed in + if (isset($arguments['attributes'])) { + $old_attributes = $arguments['attributes']; + $arguments = array_merge($arguments, $old_attributes); + } + unset($arguments['attributes']); + + break; + + case(Assets::INLINE_CSS_TYPE): + $defaults = ['priority' => null, 'group' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + break; + + default: + case(Assets::CSS_TYPE): + $defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null]; + $arguments = $this->createArgumentsFromLegacy($args, $defaults); + } + + return $arguments; + } + + protected function createArgumentsFromLegacy(array $args, array $defaults) + { + // Remove arguments with old default values. + $arguments = []; + foreach ($args as $arg) { + $default = current($defaults); + if ($arg !== $default) { + $arguments[key($defaults)] = $arg; + } + next($defaults); + } + + return $arguments; + } + + /** + * Convenience wrapper for async loading of JavaScript + * + * @param string|array $asset + * @param int $priority + * @param bool $pipeline + * @param string $group name of the group + * + * @return \Grav\Common\Assets + * @deprecated Please use dynamic method with ['loading' => 'async']. + */ + public function addAsyncJs($asset, $priority = 10, $pipeline = true, $group = 'head') + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'async\']', E_USER_DEPRECATED); + + return $this->addJs($asset, $priority, $pipeline, 'async', $group); + } + + /** + * Convenience wrapper for deferred loading of JavaScript + * + * @param string|array $asset + * @param int $priority + * @param bool $pipeline + * @param string $group name of the group + * + * @return \Grav\Common\Assets + * @deprecated Please use dynamic method with ['loading' => 'defer']. + */ + public function addDeferJs($asset, $priority = 10, $pipeline = true, $group = 'head') + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'defer\']', E_USER_DEPRECATED); + + return $this->addJs($asset, $priority, $pipeline, 'defer', $group); + } + +} diff --git a/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php new file mode 100644 index 0000000..84b83e3 --- /dev/null +++ b/system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php @@ -0,0 +1,343 @@ +collections[$asset]) || isset($this->assets_css[$asset]) || isset($this->assets_js[$asset]); + } + + /** + * Return the array of all the registered collections + * + * @return array + */ + public function getCollections() + { + return $this->collections; + } + + /** + * Set the array of collections explicitly + * + * @param array $collections + * + * @return $this + */ + public function setCollection($collections) + { + $this->collections = $collections; + + return $this; + } + + /** + * Return the array of all the registered CSS assets + * If a $key is provided, it will try to return only that asset + * else it will return null + * + * @param null|string $key the asset key + * @return array + */ + public function getCss($key = null) + { + if (null !== $key) { + $asset_key = md5($key); + + return $this->assets_css[$asset_key] ?? null; + } + + return $this->assets_css; + } + + /** + * Return the array of all the registered JS assets + * If a $key is provided, it will try to return only that asset + * else it will return null + * + * @param null|string $key the asset key + * @return array + */ + public function getJs($key = null) + { + if (null !== $key) { + $asset_key = md5($key); + + return $this->assets_js[$asset_key] ?? null; + } + + return $this->assets_js; + } + + /** + * Set the whole array of CSS assets + * + * @param array $css + * + * @return $this + */ + public function setCss($css) + { + $this->assets_css = $css; + + return $this; + } + + /** + * Set the whole array of JS assets + * + * @param array $js + * + * @return $this + */ + public function setJs($js) + { + $this->assets_js = $js; + + return $this; + } + + /** + * Removes an item from the CSS array if set + * + * @param string $key The asset key + * + * @return $this + */ + public function removeCss($key) + { + $asset_key = md5($key); + if (isset($this->assets_css[$asset_key])) { + unset($this->assets_css[$asset_key]); + } + + return $this; + } + + /** + * Removes an item from the JS array if set + * + * @param string $key The asset key + * + * @return $this + */ + public function removeJs($key) + { + $asset_key = md5($key); + if (isset($this->assets_js[$asset_key])) { + unset($this->assets_js[$asset_key]); + } + + return $this; + } + + /** + * Sets the state of CSS Pipeline + * + * @param bool $value + * + * @return $this + */ + public function setCssPipeline($value) + { + $this->css_pipeline = (bool)$value; + + return $this; + } + + /** + * Sets the state of JS Pipeline + * + * @param bool $value + * + * @return $this + */ + public function setJsPipeline($value) + { + $this->js_pipeline = (bool)$value; + + return $this; + } + + /** + * Reset all assets. + * + * @return $this + */ + public function reset() + { + $this->resetCss(); + $this->resetJs(); + $this->setCssPipeline(false); + $this->setJsPipeline(false); + + return $this; + } + + /** + * Reset JavaScript assets. + * + * @return $this + */ + public function resetJs() + { + $this->assets_js = []; + + return $this; + } + + /** + * Reset CSS assets. + * + * @return $this + */ + public function resetCss() + { + $this->assets_css = []; + + return $this; + } + + /** + * Explicitly set's a timestamp for assets + * + * @param string|int $value + */ + public function setTimestamp($value) + { + $this->timestamp = $value; + } + + /** + * Get the timestamp for assets + * + * @param bool $include_join + * @return string + */ + public function getTimestamp($include_join = true) + { + if ($this->timestamp) { + return $include_join ? '?' . $this->timestamp : $this->timestamp; + } + + return null; + } + + /** + * Add all assets matching $pattern within $directory. + * + * @param string $directory Relative to the Grav root path, or a stream identifier + * @param string $pattern (regex) + * + * @return $this + */ + public function addDir($directory, $pattern = self::DEFAULT_REGEX) + { + $root_dir = rtrim(ROOT_DIR, '/'); + + // Check if $directory is a stream. + if (strpos($directory, '://')) { + $directory = Grav::instance()['locator']->findResource($directory, null); + } + + // Get files + $files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/'); + + // No luck? Nothing to do + if (!$files) { + return $this; + } + + // Add CSS files + if ($pattern === self::CSS_REGEX) { + foreach ($files as $file) { + $this->addCss($file); + } + + return $this; + } + + // Add JavaScript files + if ($pattern === self::JS_REGEX) { + foreach ($files as $file) { + $this->addJs($file); + } + + return $this; + } + + // Unknown pattern. + foreach ($files as $asset) { + $this->add($asset); + } + + return $this; + } + + /** + * Add all JavaScript assets within $directory + * + * @param string $directory Relative to the Grav root path, or a stream identifier + * + * @return $this + */ + public function addDirJs($directory) + { + return $this->addDir($directory, self::JS_REGEX); + } + + /** + * Add all CSS assets within $directory + * + * @param string $directory Relative to the Grav root path, or a stream identifier + * + * @return $this + */ + public function addDirCss($directory) + { + return $this->addDir($directory, self::CSS_REGEX); + } + + /** + * Recursively get files matching $pattern within $directory. + * + * @param string $directory + * @param string $pattern (regex) + * @param string $ltrim Will be trimmed from the left of the file path + * + * @return array + */ + protected function rglob($directory, $pattern, $ltrim = null) + { + $iterator = new \RegexIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory, + \FilesystemIterator::SKIP_DOTS)), $pattern); + $offset = \strlen($ltrim); + $files = []; + + foreach ($iterator as $file) { + $files[] = substr($file->getPathname(), $offset); + } + + return $files; + } + + +} diff --git a/system/src/Grav/Common/Backup/Backups.php b/system/src/Grav/Common/Backup/Backups.php new file mode 100644 index 0000000..9582b9e --- /dev/null +++ b/system/src/Grav/Common/Backup/Backups.php @@ -0,0 +1,261 @@ +addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']); + Grav::instance()->fireEvent('onBackupsInitialized', new Event(['backups' => $this])); + } + + public function setup() + { + if (null === static::$backup_dir) { + static::$backup_dir = Grav::instance()['locator']->findResource('backup://', true, true); + Folder::create(static::$backup_dir); + } + } + + public function onSchedulerInitialized(Event $event) + { + /** @var Scheduler $scheduler */ + $scheduler = $event['scheduler']; + + /** @var Inflector $inflector */ + $inflector = Grav::instance()['inflector']; + + foreach (static::getBackupProfiles() as $id => $profile) { + $at = $profile['schedule_at']; + $name = $inflector::hyphenize($profile['name']); + $logs = 'logs/backup-' . $name . '.out'; + /** @var Job $job */ + $job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name ); + $job->at($at); + $job->output($logs); + $job->backlink('/tools/backups'); + } + } + + public function getBackupDownloadUrl($backup, $base_url) + { + $param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':'); + $download = urlencode(base64_encode($backup)); + $url = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim($base_url, + '/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form'); + + return $url; + } + + public static function getBackupProfiles() + { + return Grav::instance()['config']->get('backups.profiles'); + } + + public static function getPurgeConfig() + { + return Grav::instance()['config']->get('backups.purge'); + } + + public function getBackupNames() + { + return array_column(static::getBackupProfiles(), 'name'); + } + + public static function getTotalBackupsSize() + { + $backups = static::getAvailableBackups(); + $size = array_sum(array_column($backups, 'size')); + + return $size ?? 0; + } + + public static function getAvailableBackups($force = false) + { + if ($force || null === static::$backups) { + static::$backups = []; + $backups_itr = new \GlobIterator(static::$backup_dir . '/*.zip', \FilesystemIterator::KEY_AS_FILENAME); + $inflector = Grav::instance()['inflector']; + $long_date_format = DATE_RFC2822; + + /** + * @var string $name + * @var \SplFileInfo $file + */ + foreach ($backups_itr as $name => $file) { + + if (preg_match(static::BACKUP_FILENAME_REGEXZ, $name, $matches)) { + $date = \DateTime::createFromFormat(static::BACKUP_DATE_FORMAT, $matches[2]); + $timestamp = $date->getTimestamp(); + $backup = new \stdClass(); + $backup->title = $inflector->titleize($matches[1]); + $backup->time = $date; + $backup->date = $date->format($long_date_format); + $backup->filename = $name; + $backup->path = $file->getPathname(); + $backup->size = $file->getSize(); + static::$backups[$timestamp] = $backup; + } + } + // Reverse Key Sort to get in reverse date order + krsort(static::$backups); + } + + return static::$backups; + } + + /** + * Backup + * + * @param int $id + * @param callable|null $status + * + * @return null|string + */ + public static function backup($id = 0, callable $status = null) + { + $profiles = static::getBackupProfiles(); + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + if (isset($profiles[$id])) { + $backup = (object) $profiles[$id]; + } else { + throw new \RuntimeException('No backups defined...'); + } + + $name = Grav::instance()['inflector']->underscorize($backup->name); + $date = date(static::BACKUP_DATE_FORMAT, time()); + $filename = trim($name, '_') . '--' . $date . '.zip'; + $destination = static::$backup_dir . DS . $filename; + $max_execution_time = ini_set('max_execution_time', 600); + $backup_root = $backup->root; + + if ($locator->isStream($backup_root)) { + $backup_root = $locator->findResource($backup_root); + } else { + $backup_root = rtrim(GRAV_ROOT . $backup_root, '/'); + } + + if (!file_exists($backup_root)) { + throw new \RuntimeException("Backup location: {$backup_root} does not exist..."); + } + + $options = [ + 'exclude_files' => static::convertExclude($backup->exclude_files ?? ''), + 'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''), + ]; + + /** @var Archiver $archiver */ + $archiver = Archiver::create('zip'); + $archiver->setArchive($destination)->setOptions($options)->compress($backup_root, $status)->addEmptyFolders($options['exclude_paths'], $status); + + $status && $status([ + 'type' => 'message', + 'message' => 'Done...', + ]); + + $status && $status([ + 'type' => 'progress', + 'complete' => true + ]); + + if ($max_execution_time !== false) { + ini_set('max_execution_time', $max_execution_time); + } + + // Log the backup + Grav::instance()['log']->notice('Backup Created: ' . $destination); + + // Fire Finished event + Grav::instance()->fireEvent('onBackupFinished', new Event(['backup' => $destination])); + + // Purge anything required + static::purge(); + + // Log + $log = JsonFile::instance(Grav::instance()['locator']->findResource("log://backup.log", true, true)); + $log->content([ + 'time' => time(), + 'location' => $destination + ]); + $log->save(); + + return $destination; + } + + public static function purge() + { + $purge_config = static::getPurgeConfig(); + $trigger = $purge_config['trigger']; + $backups = static::getAvailableBackups(true); + + switch ($trigger) + { + case 'number': + $backups_count = count($backups); + if ($backups_count > $purge_config['max_backups_count']) { + $last = end($backups); + unlink ($last->path); + static::purge(); + } + break; + + case 'time': + $last = end($backups); + $now = new \DateTime(); + $interval = $now->diff($last->time); + if ($interval->days > $purge_config['max_backups_time']) { + unlink($last->path); + static::purge(); + } + break; + + default: + $used_space = static::getTotalBackupsSize(); + $max_space = $purge_config['max_backups_space'] * 1024 * 1024 * 1024; + if ($used_space > $max_space) { + $last = end($backups); + unlink($last->path); + static::purge(); + } + break; + } + } + + protected static function convertExclude($exclude) + { + $lines = preg_split("/[\s,]+/", $exclude); + return array_map('trim', $lines, array_fill(0, \count($lines), '/')); + } +} diff --git a/system/src/Grav/Common/Backup/ZipBackup.php b/system/src/Grav/Common/Backup/ZipBackup.php deleted file mode 100644 index 517d7da..0000000 --- a/system/src/Grav/Common/Backup/ZipBackup.php +++ /dev/null @@ -1,144 +0,0 @@ -findResource('backup://', true); - - if (!$destination) { - throw new \RuntimeException('The backup folder is missing.'); - } - } - - $name = substr(strip_tags(Grav::instance()['config']->get('site.title', basename(GRAV_ROOT))), 0, 20); - - $inflector = new Inflector(); - - if (is_dir($destination)) { - $date = date('YmdHis', time()); - $filename = trim($inflector->hyphenize($name), '-') . '-' . $date . '.zip'; - $destination = rtrim($destination, DS) . DS . $filename; - } - - $messager && $messager([ - 'type' => 'message', - 'level' => 'info', - 'message' => 'Creating new Backup "' . $destination . '"' - ]); - $messager && $messager([ - 'type' => 'message', - 'level' => 'info', - 'message' => '' - ]); - - $zip = new \ZipArchive(); - $zip->open($destination, \ZipArchive::CREATE); - - $max_execution_time = ini_set('max_execution_time', 600); - - static::folderToZip(GRAV_ROOT, $zip, strlen(rtrim(GRAV_ROOT, DS) . DS), $messager); - - $messager && $messager([ - 'type' => 'progress', - 'percentage' => false, - 'complete' => true - ]); - - $messager && $messager([ - 'type' => 'message', - 'level' => 'info', - 'message' => '' - ]); - $messager && $messager([ - 'type' => 'message', - 'level' => 'info', - 'message' => 'Saving and compressing archive...' - ]); - - $zip->close(); - - if ($max_execution_time !== false) { - ini_set('max_execution_time', $max_execution_time); - } - - return $destination; - } - - /** - * @param $folder - * @param $zipFile - * @param $exclusiveLength - * @param $messager - */ - private static function folderToZip($folder, \ZipArchive $zipFile, $exclusiveLength, callable $messager = null) - { - $handle = opendir($folder); - while (false !== $f = readdir($handle)) { - if ($f !== '.' && $f !== '..') { - $filePath = "$folder/$f"; - // Remove prefix from file path before add to zip. - $localPath = substr($filePath, $exclusiveLength); - - if (in_array($f, static::$ignoreFolders)) { - continue; - } - if (in_array($localPath, static::$ignorePaths)) { - $zipFile->addEmptyDir($f); - continue; - } - - if (is_file($filePath)) { - $zipFile->addFile($filePath, $localPath); - - $messager && $messager([ - 'type' => 'progress', - 'percentage' => false, - 'complete' => false - ]); - } elseif (is_dir($filePath)) { - // Add sub-directory. - $zipFile->addEmptyDir($localPath); - static::folderToZip($filePath, $zipFile, $exclusiveLength, $messager); - } - } - } - closedir($handle); - } -} diff --git a/system/src/Grav/Common/Browser.php b/system/src/Grav/Common/Browser.php index bb0a3fb..037d454 100644 --- a/system/src/Grav/Common/Browser.php +++ b/system/src/Grav/Common/Browser.php @@ -1,8 +1,9 @@ getLongVersion()); - return intval($version[0]); + return (int)$version[0]; } /** @@ -134,4 +135,15 @@ class Browser return true; } + + /** + * Determine if “Do Not Track” is set by browser + * @see https://www.w3.org/TR/tracking-dnt/ + * + * @return bool + */ + public function isTrackable(): bool + { + return !(isset($_SERVER['HTTP_DNT']) && $_SERVER['HTTP_DNT'] === '1'); + } } diff --git a/system/src/Grav/Common/Cache.php b/system/src/Grav/Common/Cache.php index 71eb29f..94eae59 100644 --- a/system/src/Grav/Common/Cache.php +++ b/system/src/Grav/Common/Cache.php @@ -1,8 +1,9 @@ config = $grav['config']; $this->now = time(); - $this->cache_dir = $grav['locator']->findResource('cache://doctrine', true, true); + if (null === $this->enabled) { + $this->enabled = (bool)$this->config->get('system.cache.enabled'); + } /** @var Uri $uri */ $uri = $grav['uri']; $prefix = $this->config->get('system.cache.prefix'); - - if (is_null($this->enabled)) { - $this->enabled = (bool)$this->config->get('system.cache.enabled'); - } + $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') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), - 2, 8); - + $this->key = ($prefix ? $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(); - - // Set the cache namespace to our unique key $this->driver->setNamespace($this->key); + + /** @var EventDispatcher $dispatcher */ + $dispatcher = Grav::instance()['events']; + $dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']); + } + + /** + * @return CacheInterface + */ + public function getSimpleCache() + { + if (null === $this->simpleCache) { + $cache = new \Grav\Framework\Cache\Adapter\DoctrineCache($this->driver, '', $this->getLifetime()); + + // Disable cache key validation. + $cache->setValidation(false); + + $this->simpleCache = $cache; + } + + return $this->simpleCache; + } + + /** + * Deletes the old out of date file-based caches + * + * @return int + */ + public function purgeOldCache() + { + $cache_dir = dirname($this->cache_dir); + $current = basename($this->cache_dir); + $count = 0; + + foreach (new \DirectoryIterator($cache_dir) as $file) { + $dir = $file->getBasename(); + if ($dir === $current || $file->isDot() || $file->isFile()) { + continue; + } + + Folder::delete($file->getPathname()); + $count++; + } + + return $count; } /** * Public accessor to set the enabled state of the cache * - * @param $enabled + * @param bool|int $enabled */ public function setEnabled($enabled) { - $this->enabled = (bool) $enabled; + $this->enabled = (bool)$enabled; } /** @@ -184,19 +231,15 @@ class Cache extends Getters // CLI compatibility requires a non-volatile cache driver if ($this->config->get('system.cache.cli_compatibility') && ( - $setting == 'auto' || $this->isVolatileDriver($setting))) { + $setting === 'auto' || $this->isVolatileDriver($setting))) { $setting = $driver_name; } - if (!$setting || $setting == 'auto') { + if (!$setting || $setting === 'auto') { if (extension_loaded('apcu')) { $driver_name = 'apcu'; - } elseif (extension_loaded('apc')) { - $driver_name = 'apc'; } elseif (extension_loaded('wincache')) { $driver_name = 'wincache'; - } elseif (extension_loaded('xcache')) { - $driver_name = 'xcache'; } } else { $driver_name = $setting; @@ -206,9 +249,6 @@ class Cache extends Getters switch ($driver_name) { case 'apc': - $driver = new DoctrineCache\ApcCache(); - break; - case 'apcu': $driver = new DoctrineCache\ApcuCache(); break; @@ -217,45 +257,53 @@ class Cache extends Getters $driver = new DoctrineCache\WinCacheCache(); break; - case 'xcache': - $driver = new DoctrineCache\XcacheCache(); - break; - case 'memcache': - $memcache = new \Memcache(); - $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'), - $this->config->get('system.cache.memcache.port', 11211)); - $driver = new DoctrineCache\MemcacheCache(); - $driver->setMemcache($memcache); + if (extension_loaded('memcache')) { + $memcache = new \Memcache(); + $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'), + $this->config->get('system.cache.memcache.port', 11211)); + $driver = new DoctrineCache\MemcacheCache(); + $driver->setMemcache($memcache); + } else { + throw new \LogicException('Memcache PHP extension has not been installed'); + } break; case 'memcached': - $memcached = new \Memcached(); - $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'), - $this->config->get('system.cache.memcached.port', 11211)); - $driver = new DoctrineCache\MemcachedCache(); - $driver->setMemcached($memcached); + if (extension_loaded('memcached')) { + $memcached = new \Memcached(); + $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'), + $this->config->get('system.cache.memcached.port', 11211)); + $driver = new DoctrineCache\MemcachedCache(); + $driver->setMemcached($memcached); + } else { + throw new \LogicException('Memcached PHP extension has not been installed'); + } break; case 'redis': - $redis = new \Redis(); - $socket = $this->config->get('system.cache.redis.socket', false); - $password = $this->config->get('system.cache.redis.password', false); + if (extension_loaded('redis')) { + $redis = new \Redis(); + $socket = $this->config->get('system.cache.redis.socket', false); + $password = $this->config->get('system.cache.redis.password', false); - if ($socket) { - $redis->connect($socket); + if ($socket) { + $redis->connect($socket); + } else { + $redis->connect($this->config->get('system.cache.redis.server', 'localhost'), + $this->config->get('system.cache.redis.port', 6379)); + } + + // Authenticate with password if set + if ($password && !$redis->auth($password)) { + throw new \RedisException('Redis authentication failed'); + } + + $driver = new DoctrineCache\RedisCache(); + $driver->setRedis($redis); } else { - $redis->connect($this->config->get('system.cache.redis.server', 'localhost'), - $this->config->get('system.cache.redis.port', 6379)); + throw new \LogicException('Redis PHP extension has not been installed'); } - - // Authenticate with password if set - if ($password && !$redis->auth($password)) { - throw new \RedisException('Redis authentication failed'); - } - - $driver = new DoctrineCache\RedisCache(); - $driver->setRedis($redis); break; default: @@ -277,9 +325,9 @@ class Cache extends Getters { if ($this->enabled) { return $this->driver->fetch($id); - } else { - return false; } + + return false; } /** @@ -310,6 +358,21 @@ class Cache extends Getters if ($this->enabled) { return $this->driver->delete($id); } + + return false; + } + + /** + * Deletes all cache + * + * @return bool + */ + public function deleteAll() + { + if ($this->enabled) { + return $this->driver->deleteAll(); + } + return false; } @@ -324,6 +387,7 @@ class Cache extends Getters if ($this->enabled) { return $this->driver->contains(($id)); } + return false; } @@ -373,6 +437,9 @@ class Cache extends Getters case 'tmp-only': $remove_paths = self::$tmp_remove; break; + case 'invalidate': + $remove_paths = []; + break; default: if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) { $remove_paths = self::$standard_remove; @@ -382,6 +449,12 @@ class Cache extends Getters } + // Delete entries in the doctrine cache if required + if (in_array($remove, ['all', 'standard'])) { + $cache = Grav::instance()['cache']; + $cache->driver->deleteAll(); + } + // Clearing cache event to add paths to clear Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths])); @@ -422,7 +495,7 @@ class Cache extends Getters $output[] = ''; - if (($remove == 'all' || $remove == 'standard') && file_exists($user_config)) { + if (($remove === 'all' || $remove === 'standard') && file_exists($user_config)) { touch($user_config); $output[] = 'Touched: ' . $user_config; @@ -440,6 +513,23 @@ class Cache extends Getters return $output; } + public static function invalidateCache() + { + $user_config = USER_DIR . 'config/system.yaml'; + + if (file_exists($user_config)) { + touch($user_config); + } + + // Clear stat cache + @clearstatcache(); + + // Clear opcache + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + + } /** * Set the cache lifetime programmatically @@ -452,7 +542,7 @@ class Cache extends Getters return; } - $interval = $future - $this->now; + $interval = (int)($future - $this->now); if ($interval > 0 && $interval < $this->getLifetime()) { $this->lifetime = $interval; } @@ -467,7 +557,7 @@ class Cache extends Getters public function getLifetime() { if ($this->lifetime === null) { - $this->lifetime = $this->config->get('system.cache.lifetime') ?: 604800; // 1 week default + $this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default } return $this->lifetime; @@ -496,15 +586,71 @@ class Cache extends Getters /** * is this driver a volatile driver in that it resides in PHP process memory * - * @param $setting + * @param string $setting * @return bool */ public function isVolatileDriver($setting) { if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) { return true; - } else { - return false; } + + return false; } + + /** + * Static function to call as a scheduled Job to purge old Doctrine files + */ + public static function purgeJob() + { + /** @var Cache $cache */ + $cache = Grav::instance()['cache']; + $deleted_folders = $cache->purgeOldCache(); + + echo 'Purged ' . $deleted_folders . ' old cache folders...'; + } + + /** + * Static function to call as a scheduled Job to clear Grav cache + * + * @param string $type + */ + public static function clearJob($type) + { + $result = static::clearCache($type); + static::invalidateCache(); + + echo strip_tags(implode("\n", $result)); + } + + public function onSchedulerInitialized(Event $event) + { + /** @var Scheduler $scheduler */ + $scheduler = $event['scheduler']; + $config = Grav::instance()['config']; + + // File Cache Purge + $at = $config->get('system.cache.purge_at'); + $name = 'cache-purge'; + $logs = 'logs/' . $name . '.out'; + + $job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [], $name ); + $job->at($at); + $job->output($logs); + $job->backlink('/config/system#caching'); + + // Cache Clear + $at = $config->get('system.cache.clear_at'); + $clear_type = $config->get('system.cache.clear_job_type'); + $name = 'cache-clear'; + $logs = 'logs/' . $name . '.out'; + + $job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name ); + $job->at($at); + $job->output($logs); + $job->backlink('/config/system#caching'); + + } + + } diff --git a/system/src/Grav/Common/Composer.php b/system/src/Grav/Common/Composer.php index 2671925..c6abb2c 100644 --- a/system/src/Grav/Common/Composer.php +++ b/system/src/Grav/Common/Composer.php @@ -1,8 +1,9 @@ checksum)) { + if (null === $this->checksum) { $this->checksum = md5(json_encode($this->files) . $this->version); } @@ -197,11 +198,9 @@ abstract class CompiledBase $cache = include $filename; if ( - !is_array($cache) - || !isset($cache['checksum']) - || !isset($cache['data']) - || !isset($cache['@class']) - || $cache['@class'] != get_class($this) + !\is_array($cache) + || !isset($cache['checksum'], $cache['data'], $cache['@class']) + || $cache['@class'] !== \get_class($this) ) { return false; } @@ -212,7 +211,7 @@ abstract class CompiledBase } $this->createObject($cache['data']); - $this->timestamp = isset($cache['timestamp']) ? $cache['timestamp'] : 0; + $this->timestamp = $cache['timestamp'] ?? 0; $this->finalizeObject(); @@ -243,7 +242,7 @@ abstract class CompiledBase } $cache = [ - '@class' => get_class($this), + '@class' => \get_class($this), 'timestamp' => time(), 'checksum' => $this->checksum(), 'files' => $this->files, diff --git a/system/src/Grav/Common/Config/CompiledBlueprints.php b/system/src/Grav/Common/Config/CompiledBlueprints.php index a29ecde..9ad7be6 100644 --- a/system/src/Grav/Common/Config/CompiledBlueprints.php +++ b/system/src/Grav/Common/Config/CompiledBlueprints.php @@ -1,27 +1,30 @@ version = 2; + } /** * Returns checksum from the configuration files. diff --git a/system/src/Grav/Common/Config/CompiledConfig.php b/system/src/Grav/Common/Config/CompiledConfig.php index 6f21123..1c92edd 100644 --- a/system/src/Grav/Common/Config/CompiledConfig.php +++ b/system/src/Grav/Common/Config/CompiledConfig.php @@ -1,8 +1,9 @@ version = 1; + } + /** * Set blueprints for the configuration. * @@ -63,7 +61,7 @@ class CompiledConfig extends CompiledBase */ protected function createObject(array $data = []) { - if ($this->withDefaults && empty($data) && is_callable($this->callable)) { + if ($this->withDefaults && empty($data) && \is_callable($this->callable)) { $blueprints = $this->callable; $data = $blueprints()->getDefaults(); } diff --git a/system/src/Grav/Common/Config/CompiledLanguages.php b/system/src/Grav/Common/Config/CompiledLanguages.php index 610e347..ef18145 100644 --- a/system/src/Grav/Common/Config/CompiledLanguages.php +++ b/system/src/Grav/Common/Config/CompiledLanguages.php @@ -1,8 +1,9 @@ version = 1; + } /** * Create configuration object. diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index a8e2bdf..7b80846 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -1,8 +1,9 @@ checksum(); + if (null === $this->key) { + $this->key = md5($this->checksum . $this->timestamp); + } + + return $this->key; } public function checksum($checksum = null) @@ -90,7 +101,7 @@ class Config extends Data { $setup = Grav::instance()['setup']->toArray(); foreach ($setup as $key => $value) { - if ($key === 'streams' || !is_array($value)) { + if ($key === 'streams' || !\is_array($value)) { // Optimized as streams and simple values are fully defined in setup. $this->items[$key] = $value; } else { @@ -98,14 +109,13 @@ class Config extends Data } } - // Override the media.upload_limit based on PHP values - $upload_limit = Utils::getUploadLimit(); - $this->items['system']['media']['upload_limit'] = $upload_limit > 0 ? $upload_limit : 1024*1024*1024; + // Legacy value - Override the media.upload_limit based on PHP values + $this->items['system']['media']['upload_limit'] = Utils::getUploadLimit(); } /** * @return mixed - * @deprecated + * @deprecated 1.5 Use Grav::instance()['languages'] instead. */ public function getLanguages() { diff --git a/system/src/Grav/Common/Config/ConfigFileFinder.php b/system/src/Grav/Common/Config/ConfigFileFinder.php index 83b13ec..1e7adca 100644 --- a/system/src/Grav/Common/Config/ConfigFileFinder.php +++ b/system/src/Grav/Common/Config/ConfigFileFinder.php @@ -1,8 +1,9 @@ items = Utils::arrayMergeRecursiveUnique($this->items, $data); } + + public function flattenByLang($lang) + { + $language = $this->items[$lang]; + return Utils::arrayFlattenDotNotation($language); + } + + public function unflatten($array) + { + return Utils::arrayUnflattenDotNotation($array); + } } diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php index fba7fd1..6a4d336 100644 --- a/system/src/Grav/Common/Config/Setup.php +++ b/system/src/Grav/Common/Config/Setup.php @@ -1,8 +1,9 @@ 'unknown', + '127.0.0.1' => 'localhost', + '::1' => 'localhost' + ]; + + /** + * @var string Current environment normalized to lower case. + */ public static $environment; protected $streams = [ @@ -38,7 +51,7 @@ class Setup extends Data // If not defined, environment will be set up in the constructor. ], 'asset' => [ - 'type' => 'ReadOnlyStream', + 'type' => 'Stream', 'prefixes' => [ '' => ['assets'], ] @@ -109,7 +122,7 @@ class Setup extends Data ] ], 'image' => [ - 'type' => 'ReadOnlyStream', + 'type' => 'Stream', 'prefixes' => [ '' => ['user://images', 'system://images'] ] @@ -120,6 +133,13 @@ class Setup extends Data '' => ['user://pages'] ] ], + 'user-data' => [ + 'type' => 'Stream', + 'force' => true, + 'prefixes' => [ + '' => ['user://data'] + ] + ], 'account' => [ 'type' => 'ReadOnlyStream', 'prefixes' => [ @@ -133,12 +153,26 @@ class Setup extends Data */ public function __construct($container) { - $environment = null !== static::$environment ? static::$environment : ($container['uri']->environment() ?: 'localhost'); + // If no environment is set, make sure we get one (CLI or hostname). + if (!static::$environment) { + if (\defined('GRAV_CLI')) { + static::$environment = 'cli'; + } else { + /** @var ServerRequestInterface $request */ + $request = $container['request']; + $host = $request->getUri()->getHost(); + + static::$environment = Utils::substrToString($host, ':'); + } + } + + // Resolve server aliases to the proper environment. + $environment = $this->environments[static::$environment] ?? static::$environment; // Pre-load setup.php which contains our initial configuration. // Configuration may contain dynamic parts, which is why we need to always load it. - // If "GRAVE_SETUP_PATH" has been defined, use it, otherwise use defaults. - $file = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : GRAV_ROOT . '/setup.php'; + // If "GRAV_SETUP_PATH" has been defined, use it, otherwise use defaults. + $file = \defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : GRAV_ROOT . '/setup.php'; $setup = is_file($file) ? (array) include $file : []; // Add default streams defined in beginning of the class. @@ -151,8 +185,8 @@ class Setup extends Data parent::__construct($setup); // Set up environment. - $this->def('environment', $environment ?: 'cli'); - $this->def('streams.schemes.environment.prefixes', ['' => $environment ? ["user://{$this->environment}"] : []]); + $this->def('environment', $environment); + $this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->get('environment')}"]]); } /** @@ -212,8 +246,8 @@ class Setup extends Data $locator->addPath($scheme, '', $config['paths']); } - $override = isset($config['override']) ? $config['override'] : false; - $force = isset($config['force']) ? $config['force'] : false; + $override = $config['override'] ?? false; + $force = $config['force'] ?? false; if (isset($config['prefixes'])) { foreach ((array)$config['prefixes'] as $prefix => $paths) { @@ -232,7 +266,7 @@ class Setup extends Data { $schemes = []; foreach ((array) $this->get('streams.schemes') as $scheme => $config) { - $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream'; + $type = $config['type'] ?? 'ReadOnlyStream'; if ($type[0] !== '\\') { $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type; } @@ -251,8 +285,8 @@ class Setup extends Data */ protected function check(UniformResourceLocator $locator) { - $streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null; - if (!is_array($streams)) { + $streams = $this->items['streams']['schemes'] ?? null; + if (!\is_array($streams)) { throw new \InvalidArgumentException('Configuration is missing streams.schemes!'); } $diff = array_keys(array_diff_key($this->streams, $streams)); @@ -271,10 +305,14 @@ class Setup extends Data // Create security.yaml if it doesn't exist. $filename = $locator->findResource('config://security.yaml', true, true); - $file = YamlFile::instance($filename); - if (!$file->exists()) { - $file->save(['salt' => Utils::generateRandomString(14)]); - $file->free(); + $security_file = CompiledYamlFile::instance($filename); + $security_content = (array)$security_file->content(); + + if (!isset($security_content['salt'])) { + $security_content = array_merge($security_content, ['salt' => Utils::generateRandomString(14)]); + $security_file->content($security_content); + $security_file->save(); + $security_file->free(); } } catch (\RuntimeException $e) { throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e); diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index bf33d41..e703777 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -1,8 +1,9 @@ blueprintSchema) { + $this->blueprintSchema = clone $this->blueprintSchema; + } + } + + public function setScope($scope) + { + $this->scope = $scope; + } + /** * Set default values for field types. * @@ -51,7 +68,60 @@ class Blueprint extends BlueprintForm { $this->initInternals(); - return $this->blueprintSchema->getDefaults(); + if (null === $this->defaults) { + $this->defaults = $this->blueprintSchema->getDefaults(); + } + + return $this->defaults; + } + + /** + * Initialize blueprints with its dynamic fields. + * + * @return $this + */ + public function init() + { + foreach ($this->dynamic as $key => $data) { + // Locate field. + $path = explode('/', $key); + $current = &$this->items; + + foreach ($path as $field) { + if (\is_object($current)) { + // Handle objects. + if (!isset($current->{$field})) { + $current->{$field} = []; + } + + $current = &$current->{$field}; + } else { + // Handle arrays and scalars. + if (!\is_array($current)) { + $current = [$field => []]; + } elseif (!isset($current[$field])) { + $current[$field] = []; + } + + $current = &$current[$field]; + } + } + + // Set dynamic property. + foreach ($data as $property => $call) { + $action = $call['action']; + $method = 'dynamic' . ucfirst($action); + + if (isset($this->handlers[$action])) { + $callable = $this->handlers[$action]; + $callable($current, $property, $call); + } elseif (method_exists($this, $method)) { + $this->{$method}($current, $property, $call); + } + } + } + + return $this; } /** @@ -70,6 +140,20 @@ class Blueprint extends BlueprintForm return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator); } + /** + * Process data coming from a form. + * + * @param array $data + * @param array $toggles + * @return array + */ + public function processForm(array $data, array $toggles = []) + { + $this->initInternals(); + + return $this->blueprintSchema->processForm($data, $toggles); + } + /** * Return data fields that do not exist in blueprints. * @@ -101,15 +185,32 @@ class Blueprint extends BlueprintForm * Filter data by using blueprints. * * @param array $data + * @param bool $missingValuesAsNull + * @param bool $keepEmptyValues * @return array */ - public function filter(array $data) + public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false) { $this->initInternals(); - return $this->blueprintSchema->filter($data); + return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues); } + + /** + * Flatten data by using blueprints. + * + * @param array $data + * @return array + */ + public function flattenData(array $data) + { + $this->initInternals(); + + return $this->blueprintSchema->flattenData($data); + } + + /** * Return blueprint data schema. * @@ -122,20 +223,28 @@ class Blueprint extends BlueprintForm return $this->blueprintSchema; } + public function addDynamicHandler(string $name, callable $callable): void + { + $this->handlers[$name] = $callable; + } + /** * Initialize validator. */ protected function initInternals() { - if (!isset($this->blueprintSchema)) { + if (null === $this->blueprintSchema) { $types = Grav::instance()['plugins']->formFieldTypes; $this->blueprintSchema = new BlueprintSchema; + if ($types) { $this->blueprintSchema->setTypes($types); } + $this->blueprintSchema->embed('', $this->items); $this->blueprintSchema->init(); + $this->defaults = null; } } @@ -162,17 +271,19 @@ class Blueprint extends BlueprintForm /** @var UniformResourceLocator $locator */ $locator = Grav::instance()['locator']; - if (is_string($path) && !$locator->isStream($path)) { + if (\is_string($path) && !$locator->isStream($path)) { // Find path overrides. - $paths = isset($this->overrides[$path]) ? (array) $this->overrides[$path] : []; + $paths = (array) ($this->overrides[$path] ?? null); // Add path pointing to default context. if ($context === null) { $context = $this->context; } - if ($context && $context[strlen($context)-1] !== '/') { + + if ($context && $context[\strlen($context)-1] !== '/') { $context .= '/'; } + $path = $context . $path; if (!preg_match('/\.yaml$/', $path)) { @@ -186,7 +297,7 @@ class Blueprint extends BlueprintForm $files = []; foreach ($paths as $lookup) { - if (is_string($lookup) && strpos($lookup, '://')) { + if (\is_string($lookup) && strpos($lookup, '://')) { $files = array_merge($files, $locator->findResources($lookup)); } else { $files[] = $lookup; @@ -205,27 +316,29 @@ class Blueprint extends BlueprintForm { $params = $call['params']; - if (is_array($params)) { + if (\is_array($params)) { $function = array_shift($params); } else { $function = $params; $params = []; } - list($o, $f) = preg_split('/::/', $function, 2); + [$o, $f] = explode('::', $function, 2); + + $data = null; if (!$f) { - if (function_exists($o)) { - $data = call_user_func_array($o, $params); + if (\function_exists($o)) { + $data = \call_user_func_array($o, $params); } } else { if (method_exists($o, $f)) { - $data = call_user_func_array(array($o, $f), $params); + $data = \call_user_func_array([$o, $f], $params); } } // If function returns a value, - if (isset($data)) { - if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) { + if (null !== $data) { + if (\is_array($data) && isset($field[$property]) && \is_array($field[$property])) { // Combine field and @data-field together. $field[$property] += $data; } else { @@ -243,12 +356,73 @@ class Blueprint extends BlueprintForm protected function dynamicConfig(array &$field, $property, array &$call) { $value = $call['params']; - - $default = isset($field[$property]) ? $field[$property] : null; + $default = $field[$property] ?? null; $config = Grav::instance()['config']->get($value, $default); - if (!is_null($config)) { + if (null !== $config) { $field[$property] = $config; } } + + /** + * @param array $field + * @param string $property + * @param array $call + */ + protected function dynamicSecurity(array &$field, $property, array &$call) + { + if ($property || !empty($field['validate']['ignore'])) { + return; + } + + $grav = Grav::instance(); + $actions = (array)$call['params']; + + /** @var UserInterface|null $user */ + $user = $grav['user'] ?? null; + foreach ($actions as $action) { + if (!$user || !$user->authorize($action)) { + $this->addPropertyRecursive($field, 'validate', ['ignore' => true]); + return; + } + } + } + + /** + * @param array $field + * @param string $property + * @param array $call + */ + protected function dynamicScope(array &$field, $property, array &$call) + { + if ($property && $property !== 'ignore') { + return; + } + + $scopes = (array)$call['params']; + $matches = \in_array($this->scope, $scopes, true); + if ($this->scope && $property !== 'ignore') { + $matches = !$matches; + } + + if ($matches) { + $this->addPropertyRecursive($field, 'validate', ['ignore' => true]); + return; + } + } + + protected function addPropertyRecursive(array &$field, $property, $value) + { + if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) { + $field[$property] = array_merge_recursive($field[$property], $value); + } else { + $field[$property] = $value; + } + + if (!empty($field['fields'])) { + foreach ($field['fields'] as $key => &$child) { + $this->addPropertyRecursive($child, $property, $value); + } + } + } } diff --git a/system/src/Grav/Common/Data/BlueprintSchema.php b/system/src/Grav/Common/Data/BlueprintSchema.php index 3205c38..898450d 100644 --- a/system/src/Grav/Common/Data/BlueprintSchema.php +++ b/system/src/Grav/Common/Data/BlueprintSchema.php @@ -1,8 +1,9 @@ true ]; + /** + * @return array + */ + public function getTypes() + { + return $this->types; + } + + /** + * @param string $name + * @return array + */ + public function getType($name) + { + return $this->types[$name] ?? []; + } + /** * Validate data against blueprints. * @@ -47,35 +65,90 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface } /** - * Filter data by using blueprints. - * - * @param array $data + * @param array $data + * @param array $toggles * @return array */ - public function filter(array $data) + public function processForm(array $data, array $toggles = []) { - return $this->filterArray($data, $this->nested); + return $this->processFormRecursive($data, $toggles, $this->nested); + } + + /** + * Filter data by using blueprints. + * + * @param array $data Incoming data, for example from a form. + * @param bool $missingValuesAsNull Include missing values as nulls. + * @param bool $keepEmptyValues Include empty values. + * @return array + */ + public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false) + { + return $this->filterArray($data, $this->nested, $missingValuesAsNull, $keepEmptyValues); + } + + /** + * Flatten data by using blueprints. + * + * @param array $data Data to be flattened. + * @return array + */ + public function flattenData(array $data) + { + return $this->flattenArray($data, $this->nested, ''); } /** * @param array $data * @param array $rules - * @returns array + * @param string $prefix + * @return array + */ + protected function flattenArray(array $data, array $rules, string $prefix) + { + $array = []; + + foreach ($data as $key => $field) { + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = is_string($val) ? $this->items[$val] : null; + + if ($rule || isset($val['*'])) { + // Item has been defined in blueprints. + $array[$prefix.$key] = $field; + } elseif (is_array($field) && is_array($val)) { + // Array has been defined in blueprints. + $array += $this->flattenArray($field, $val, $prefix . $key . '.'); + } else { + // Undefined/extra item. + $array[$prefix.$key] = $field; + } + } + return $array; + } + + /** + * @param array $data + * @param array $rules + * @return array * @throws \RuntimeException - * @internal */ protected function validateArray(array $data, array $rules) { $messages = $this->checkRequired($data, $rules); foreach ($data as $key => $field) { - $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null); - $rule = is_string($val) ? $this->items[$val] : null; + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = \is_string($val) ? $this->items[$val] : null; if ($rule) { // Item has been defined in blueprints. + if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) { + // Skip validation in the ignored field. + continue; + } + $messages += Validation::validate($field, $rule); - } elseif (is_array($field) && is_array($val)) { + } elseif (\is_array($field) && \is_array($val)) { // Array has been defined in blueprints. $messages += $this->validateArray($field, $val); } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') { @@ -90,32 +163,99 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface /** * @param array $data * @param array $rules + * @param bool $missingValuesAsNull + * @param bool $keepEmptyValues * @return array - * @internal */ - protected function filterArray(array $data, array $rules) + protected function filterArray(array $data, array $rules, $missingValuesAsNull, $keepEmptyValues) { - $results = array(); + $results = []; + + if ($missingValuesAsNull) { + // First pass is to fill up all the fields with null. This is done to lock the ordering of the fields. + foreach ($rules as $key => $rule) { + if ($key && !isset($results[$key])) { + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = \is_string($val) ? $this->items[$val] : null; + + if (empty($rule['disabled']) && empty($rule['validate']['ignore'])) { + continue; + } + } + } + } + foreach ($data as $key => $field) { - $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null); - $rule = is_string($val) ? $this->items[$val] : null; + $val = $rules[$key] ?? $rules['*'] ?? null; + $rule = \is_string($val) ? $this->items[$val] : null; if ($rule) { // Item has been defined in blueprints. + if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) { + // Skip any data in the ignored field. + unset($results[$key]); + continue; + } + $field = Validation::filter($field, $rule); - } elseif (is_array($field) && is_array($val)) { + } elseif (\is_array($field) && \is_array($val)) { // Array has been defined in blueprints. - $field = $this->filterArray($field, $val); + $field = $this->filterArray($field, $val, $missingValuesAsNull, $keepEmptyValues); + } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') { - $field = null; + // Skip any extra data. + continue; } - if (isset($field) && (!is_array($field) || !empty($field))) { + if ($keepEmptyValues || (null !== $field && (!\is_array($field) || !empty($field)))) { $results[$key] = $field; } } - return $results; + return $results ?: null; + } + + /** + * @param array|null $data + * @param array $toggles + * @param array $nested + * @return array|null + */ + protected function processFormRecursive(?array $data, array $toggles, array $nested) + { + foreach ($nested as $key => $value) { + if ($key === '') { + continue; + } + if ($key === '*') { + // TODO: Add support to collections. + continue; + } + if (is_array($value)) { + // Recursively fetch the items. + $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggles[$key] ?? [], $value); + } else { + $field = $this->get($value); + // Do not add the field if: + if ( + // Not an input field + !$field + // Field has been disabled + || !empty($field['disabled']) + // Field validation is set to be ignored + || !empty($field['validate']['ignore']) + // Field is toggleable and the toggle is turned off + || (!empty($field['toggleable']) && empty($toggles[$key])) + ) { + continue; + } + if (!isset($data[$key])) { + $data[$key] = null; + } + } + } + + return $data; } /** @@ -128,10 +268,18 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface $messages = []; foreach ($fields as $name => $field) { - if (!is_string($field)) { + if (!\is_string($field)) { continue; } + $field = $this->items[$field]; + + // Skip ignored field, it will not be required. + if (!empty($field['disabled']) || !empty($field['validate']['ignore'])) { + continue; + } + + // Check if required. if (isset($field['validate']['required']) && $field['validate']['required'] === true) { @@ -142,9 +290,9 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface continue; } - $value = isset($field['label']) ? $field['label'] : $field['name']; + $value = $field['label'] ?? $field['name']; $language = Grav::instance()['language']; - $message = sprintf($language->translate('FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value)); + $message = sprintf($language->translate('GRAV.FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value)); $messages[$field['name']][] = $message; } } @@ -161,7 +309,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface { $value = $call['params']; - $default = isset($field[$property]) ? $field[$property] : null; + $default = $field[$property] ?? null; $config = Grav::instance()['config']->get($value, $default); if (null !== $config) { diff --git a/system/src/Grav/Common/Data/Blueprints.php b/system/src/Grav/Common/Data/Blueprints.php index c7090c5..1d0fcc6 100644 --- a/system/src/Grav/Common/Data/Blueprints.php +++ b/system/src/Grav/Common/Data/Blueprints.php @@ -1,8 +1,9 @@ instances[$type])) { - $this->instances[$type] = $this->loadFile($type); + $blueprint = $this->loadFile($type); + $this->instances[$type] = $blueprint; } return $this->instances[$type]; @@ -49,7 +54,7 @@ class Blueprints public function types() { if ($this->types === null) { - $this->types = array(); + $this->types = []; $grav = Grav::instance(); @@ -87,7 +92,7 @@ class Blueprints { $blueprint = new Blueprint($name); - if (is_array($this->search) || is_object($this->search)) { + if (\is_array($this->search) || \is_object($this->search)) { // Page types. $blueprint->setOverrides($this->search); $blueprint->setContext('blueprints://pages'); @@ -95,6 +100,15 @@ class Blueprints $blueprint->setContext($this->search); } - return $blueprint->load()->init(); + try { + $blueprint->load()->init(); + } catch (\RuntimeException $e) { + $log = Grav::instance()['log']; + $log->error(sprintf('Blueprint %s cannot be loaded: %s', $name, $e->getMessage())); + + throw $e; + } + + return $blueprint; } } diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php index a9ceca3..bfae2da 100644 --- a/system/src/Grav/Common/Data/Data.php +++ b/system/src/Grav/Common/Data/Data.php @@ -1,8 +1,9 @@ items = $items; $this->blueprints = $blueprints; @@ -70,14 +70,16 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface { $old = $this->get($name, null, $separator); if ($old !== null) { - if (!is_array($old)) { + if (!\is_array($old)) { throw new \RuntimeException('Value ' . $old); } - if (is_object($value)) { + + if (\is_object($value)) { $value = (array) $value; - } elseif (!is_array($value)) { + } elseif (!\is_array($value)) { throw new \RuntimeException('Value ' . $value); } + $value = $this->blueprints()->mergeData($old, $value, $name, $separator); } @@ -108,9 +110,10 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface */ public function joinDefaults($name, $value, $separator = '.') { - if (is_object($value)) { + if (\is_object($value)) { $value = (array) $value; } + $old = $this->get($name, null, $separator); if ($old !== null) { $value = $this->blueprints()->mergeData($value, $old, $name, $separator); @@ -125,16 +128,16 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface * Get value from the configuration and join it with given data. * * @param string $name Dot separated path to the requested value. - * @param array $value Value to be joined. + * @param array|object $value Value to be joined. * @param string $separator Separator, defaults to '.' * @return array * @throws \RuntimeException */ public function getJoined($name, $value, $separator = '.') { - if (is_object($value)) { + if (\is_object($value)) { $value = (array) $value; - } elseif (!is_array($value)) { + } elseif (!\is_array($value)) { throw new \RuntimeException('Value ' . $value); } @@ -145,7 +148,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface return $value; } - if (!is_array($old)) { + if (!\is_array($old)) { throw new \RuntimeException('Value ' . $old); } @@ -195,11 +198,14 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface /** * @return $this - * Filter all items by using blueprints. */ public function filter() { - $this->items = $this->blueprints()->filter($this->items); + $args = func_get_args(); + $missingValuesAsNull = (bool)(array_shift($args) ?: false); + $keepEmptyValues = (bool)(array_shift($args) ?: false); + + $this->items = $this->blueprints()->filter($this->items, $missingValuesAsNull, $keepEmptyValues); return $this; } @@ -223,7 +229,7 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface { if (!$this->blueprints){ $this->blueprints = new Blueprint; - } elseif (is_callable($this->blueprints)) { + } elseif (\is_callable($this->blueprints)) { // Lazy load blueprints. $blueprints = $this->blueprints; $this->blueprints = $blueprints(); @@ -282,6 +288,12 @@ class Data implements DataInterface, \ArrayAccess, \Countable, ExportInterface if ($storage) { $this->storage = $storage; } + return $this->storage; } + + public function jsonSerialize() + { + return $this->items; + } } diff --git a/system/src/Grav/Common/Data/DataInterface.php b/system/src/Grav/Common/Data/DataInterface.php index aa61d26..91279b2 100644 --- a/system/src/Grav/Common/Data/DataInterface.php +++ b/system/src/Grav/Common/Data/DataInterface.php @@ -1,8 +1,9 @@ translate($field['validate']['message']) - : $language->translate('FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"'; + : $language->translate('GRAV.FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"'; + // Validate type with fallback type text. + $method = 'type' . str_replace('-', '_', $type); + // If this is a YAML field validate/filter as such - if ($type != 'yaml' && isset($field['yaml']) && $field['yaml'] === true) { + if (isset($field['yaml']) && $field['yaml'] === true) { $method = 'typeYaml'; } - if (method_exists(__CLASS__, $method)) { - $success = self::$method($value, $validate, $field); - } else { - $success = true; - } + $messages = []; + $success = method_exists(__CLASS__, $method) ? self::$method($value, $validate, $field) : true; if (!$success) { $messages[$field['name']][] = $message; } // Check individual rules. foreach ($validate as $rule => $params) { - $method = 'validate' . ucfirst(strtr($rule, '-', '_')); + $method = 'validate' . ucfirst(str_replace('-', '_', $rule)); if (method_exists(__CLASS__, $method)) { $success = self::$method($value, $params); @@ -89,29 +87,27 @@ class Validation */ public static function filter($value, array $field) { - $validate = isset($field['validate']) ? (array) $field['validate'] : []; + $validate = (array)($field['filter'] ?? $field['validate'] ?? null); // If value isn't required, we will return null if empty value is given. - if (empty($validate['required']) && ($value === null || $value === '')) { + if (($value === null || $value === '') && empty($validate['required'])) { return null; } if (!isset($field['type'])) { $field['type'] = 'text'; } + $type = $field['filter']['type'] ?? $field['validate']['type'] ?? $field['type']; - - // Validate type with fallback type text. - $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type']; - $method = 'filter' . ucfirst(strtr($type, '-', '_')); + $method = 'filter' . ucfirst(str_replace('-', '_', $type)); // If this is a YAML field validate/filter as such - if ($type !== 'yaml' && isset($field['yaml']) && $field['yaml'] === true) { + if (isset($field['yaml']) && $field['yaml'] === true) { $method = 'filterYaml'; } if (!method_exists(__CLASS__, $method)) { - $method = 'filterText'; + $method = isset($field['array']) && $field['array'] === true ? 'filterArray' : 'filterText'; } return self::$method($value, $validate, $field); @@ -127,22 +123,26 @@ class Validation */ public static function typeText($value, array $params, array $field) { - if (!is_string($value) && !is_numeric($value)) { + if (!\is_string($value) && !is_numeric($value)) { return false; } $value = (string)$value; - if (isset($params['min']) && strlen($value) < $params['min']) { + if (!empty($params['trim'])) { + $value = trim($value); + } + + if (isset($params['min']) && \strlen($value) < $params['min']) { return false; } - if (isset($params['max']) && strlen($value) > $params['max']) { + if (isset($params['max']) && \strlen($value) > $params['max']) { return false; } - $min = isset($params['min']) ? $params['min'] : 0; - if (isset($params['step']) && (strlen($value) - $min) % $params['step'] == 0) { + $min = $params['min'] ?? 0; + if (isset($params['step']) && (\strlen($value) - $min) % $params['step'] === 0) { return false; } @@ -155,17 +155,30 @@ class Validation protected static function filterText($value, array $params, array $field) { + if (!\is_string($value) && !is_numeric($value)) { + return ''; + } + + if (!empty($params['trim'])) { + $value = trim($value); + } + return (string) $value; } + protected static function filterCheckbox($value, array $params, array $field) + { + return (bool) $value; + } + protected static function filterCommaList($value, array $params, array $field) { - return is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); + return \is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); } public static function typeCommaList($value, array $params, array $field) { - return is_array($value) ? true : self::typeText($value, $params, $field); + return \is_array($value) ? true : self::typeText($value, $params, $field); } protected static function filterLower($value, array $params) @@ -234,6 +247,7 @@ class Validation { // Set multiple: true so checkboxes can easily use min/max counts to control number of options required $field['multiple'] = true; + return self::typeArray((array) $value, $params, $field); } @@ -252,16 +266,10 @@ class Validation */ public static function typeCheckbox($value, array $params, array $field) { - $value = (string) $value; + $value = (string)$value; + $field_value = (string)($field['value'] ?? '1'); - if (!isset($field['value'])) { - $field['value'] = 1; - } - if (isset($value) && $value != $field['value']) { - return false; - } - - return true; + return $value === $field_value; } /** @@ -287,6 +295,10 @@ class Validation */ public static function typeToggle($value, array $params, array $field) { + if (\is_bool($value)) { + $value = (int)$value; + } + return self::typeArray((array) $value, $params, $field); } @@ -300,12 +312,12 @@ class Validation */ public static function typeFile($value, array $params, array $field) { - return self::typeArray((array) $value, $params, $field); + return self::typeArray((array)$value, $params, $field); } protected static function filterFile($value, array $params, array $field) { - return (array) $value; + return (array)$value; } /** @@ -343,12 +355,9 @@ class Validation return false; } - $min = isset($params['min']) ? $params['min'] : 0; - if (isset($params['step']) && fmod($value - $min, $params['step']) == 0) { - return false; - } + $min = $params['min'] ?? 0; - return true; + return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0); } protected static function filterNumber($value, array $params, array $field) @@ -408,10 +417,10 @@ class Validation */ public static function typeEmail($value, array $params, array $field) { - $values = !is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value; + $values = !\is_array($value) ? explode(',', preg_replace('/\s+/', '', $value)) : $value; - foreach ($values as $value) { - if (!(self::typeText($value, $params, $field) && filter_var($value, FILTER_VALIDATE_EMAIL))) { + foreach ($values as $val) { + if (!(self::typeText($val, $params, $field) && filter_var($val, FILTER_VALIDATE_EMAIL))) { return false; } } @@ -445,9 +454,11 @@ class Validation { if ($value instanceof \DateTime) { return true; - } elseif (!is_string($value)) { + } + if (!\is_string($value)) { return false; - } elseif (!isset($params['format'])) { + } + if (!isset($params['format'])) { return false !== strtotime($value); } @@ -479,10 +490,10 @@ class Validation */ public static function typeDate($value, array $params, array $field) { - $params = array($params); if (!isset($params['format'])) { $params['format'] = 'Y-m-d'; } + return self::typeDatetime($value, $params, $field); } @@ -496,10 +507,10 @@ class Validation */ public static function typeTime($value, array $params, array $field) { - $params = array($params); if (!isset($params['format'])) { $params['format'] = 'H:i'; } + return self::typeDatetime($value, $params, $field); } @@ -513,10 +524,10 @@ class Validation */ public static function typeMonth($value, array $params, array $field) { - $params = array($params); if (!isset($params['format'])) { $params['format'] = 'Y-m'; } + return self::typeDatetime($value, $params, $field); } @@ -533,6 +544,7 @@ class Validation if (!isset($params['format']) && !preg_match('/^\d{4}-W\d{2}$/u', $value)) { return false; } + return self::typeDatetime($value, $params, $field); } @@ -546,72 +558,85 @@ class Validation */ public static function typeArray($value, array $params, array $field) { - if (!is_array($value)) { + if (!\is_array($value)) { return false; } if (isset($field['multiple'])) { - if (isset($params['min']) && count($value) < $params['min']) { + if (isset($params['min']) && \count($value) < $params['min']) { return false; } - if (isset($params['max']) && count($value) > $params['max']) { + if (isset($params['max']) && \count($value) > $params['max']) { return false; } - $min = isset($params['min']) ? $params['min'] : 0; - if (isset($params['step']) && (count($value) - $min) % $params['step'] == 0) { + $min = $params['min'] ?? 0; + if (isset($params['step']) && (\count($value) - $min) % $params['step'] === 0) { return false; } } - $options = isset($field['options']) ? array_keys($field['options']) : array(); - $values = isset($field['use']) && $field['use'] == 'keys' ? array_keys($value) : $value; - if ($options && array_diff($values, $options)) { - return false; + // If creating new values is allowed, no further checks are needed. + if (!empty($field['selectize']['create'])) { + return true; } - return true; + $options = $field['options'] ?? []; + $use = $field['use'] ?? 'values'; + + if (empty($field['selectize']) || empty($field['multiple'])) { + $options = array_keys($options); + } + if ($use === 'keys') { + $value = array_keys($value); + } + + return !($options && array_diff($value, $options)); } protected static function filterArray($value, $params, $field) { $values = (array) $value; - $options = isset($field['options']) ? array_keys($field['options']) : array(); - $multi = isset($field['multiple']) ? $field['multiple'] : false; + $options = isset($field['options']) ? array_keys($field['options']) : []; + $multi = $field['multiple'] ?? false; - if (count($values) == 1 && isset($values[0]) && $values[0] == '') { + if (\count($values) === 1 && isset($values[0]) && $values[0] === '') { return null; } if ($options) { - $useKey = isset($field['use']) && $field['use'] == 'keys'; - foreach ($values as $key => $value) { - $values[$key] = $useKey ? (bool) $value : $value; + $useKey = isset($field['use']) && $field['use'] === 'keys'; + foreach ($values as $key => $val) { + $values[$key] = $useKey ? (bool) $val : $val; } } if ($multi) { - foreach ($values as $key => $value) { - if (is_array($value)) { - $value = implode(',', $value); - $values[$key] = array_map('trim', explode(',', $value)); + foreach ($values as $key => $val) { + if (\is_array($val)) { + $val = implode(',', $val); + $values[$key] = array_map('trim', explode(',', $val)); } else { - $values[$key] = trim($value); + $values[$key] = trim($val); } } } if (isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty'])) { - foreach ($values as $key => $value) { - foreach ($value as $inner_key => $inner_value) { - if ($inner_value == '') { - unset($value[$inner_key]); + foreach ($values as $key => $val) { + if ($val === '') { + unset($values[$key]); + } elseif (\is_array($val)) { + foreach ($val as $inner_key => $inner_value) { + if ($inner_value === '') { + unset($val[$inner_key]); + } } } - $values[$key] = $value; + $values[$key] = $val; } } @@ -620,7 +645,7 @@ class Validation public static function typeList($value, array $params, array $field) { - if (!is_array($value)) { + if (!\is_array($value)) { return false; } @@ -628,7 +653,7 @@ class Validation foreach ($value as $key => $item) { foreach ($field['fields'] as $subKey => $subField) { $subKey = trim($subKey, '.'); - $subValue = isset($item[$subKey]) ? $item[$subKey] : null; + $subValue = $item[$subKey] ?? null; self::validate($subValue, $subField); } } @@ -644,7 +669,7 @@ class Validation public static function filterYaml($value, $params) { - if (!is_string($value)) { + if (!\is_string($value)) { return $value; } @@ -670,6 +695,23 @@ class Validation return $value; } + /** + * Input value which can be ignored. + * + * @param mixed $value Value to be validated. + * @param array $params Validation parameters. + * @param array $field Blueprint for the field. + * @return bool True if validation succeeded. + */ + public static function typeUnset($value, array $params, array $field) + { + return true; + } + + public static function filterUnset($value, array $params, array $field) + { + return null; + } // HTML5 attributes (min, max and range are handled inside the types) @@ -677,9 +719,9 @@ class Validation { if (is_scalar($value)) { return (bool) $params !== true || $value !== ''; - } else { - return (bool) $params !== true || !empty($value); } + + return (bool) $params !== true || !empty($value); } public static function validatePattern($value, $params) @@ -702,12 +744,12 @@ class Validation public static function typeBool($value, $params) { - return is_bool($value) || $value == 1 || $value == 0; + return \is_bool($value) || $value == 1 || $value == 0; } public static function validateBool($value, $params) { - return is_bool($value) || $value == 1 || $value == 0; + return \is_bool($value) || $value == 1 || $value == 0; } protected static function filterBool($value, $params) @@ -722,7 +764,7 @@ class Validation public static function validateFloat($value, $params) { - return is_float(filter_var($value, FILTER_VALIDATE_FLOAT)); + return \is_float(filter_var($value, FILTER_VALIDATE_FLOAT)); } protected static function filterFloat($value, $params) @@ -737,20 +779,17 @@ class Validation public static function validateInt($value, $params) { - return is_numeric($value) && (int) $value == $value; + return is_numeric($value) && (int)$value == $value; } protected static function filterInt($value, $params) { - return (int) $value; + return (int)$value; } public static function validateArray($value, $params) { - return is_array($value) - || ($value instanceof \ArrayAccess - && $value instanceof \Traversable - && $value instanceof \Countable); + return \is_array($value) || ($value instanceof \ArrayAccess && $value instanceof \Traversable && $value instanceof \Countable); } public static function filterItem_List($value, $params) diff --git a/system/src/Grav/Common/Data/ValidationException.php b/system/src/Grav/Common/Data/ValidationException.php index 9272894..8bdb057 100644 --- a/system/src/Grav/Common/Data/ValidationException.php +++ b/system/src/Grav/Common/Data/ValidationException.php @@ -1,8 +1,9 @@ messages = $messages; $language = Grav::instance()['language']; - $this->message = $language->translate('FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message; + $this->message = $language->translate('GRAV.FORM.VALIDATION_FAIL', null, true) . ' ' . $this->message; foreach ($messages as $variable => &$list) { $list = array_unique($list); diff --git a/system/src/Grav/Common/Debugger.php b/system/src/Grav/Common/Debugger.php index 8fee09f..3afce37 100644 --- a/system/src/Grav/Common/Debugger.php +++ b/system/src/Grav/Common/Debugger.php @@ -1,18 +1,29 @@ init() gets called. $this->enabled = true; - $this->debugbar = new StandardDebugBar(); - $this->debugbar['time']->addMeasure('Loading', $this->debugbar['time']->getRequestStartTime(), microtime(true)); + $debugbar = new DebugBar(); + $debugbar->addCollector(new PhpInfoCollector()); + $debugbar->addCollector(new MessagesCollector()); + $debugbar->addCollector(new RequestDataCollector()); + $debugbar->addCollector(new TimeDataCollector($_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME)); + + $debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME); + $debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $currentTime); + $debugbar['time']->addMeasure('Debugger', $currentTime, microtime(true)); + + $this->debugbar = $debugbar; // Set deprecation collector. $this->setErrorHandler(); @@ -60,21 +91,28 @@ class Debugger */ public function init() { + if ($this->initialized) { + return $this; + } + $this->grav = Grav::instance(); $this->config = $this->grav['config']; // Enable/disable debugger based on configuration. - $this->enabled = $this->config->get('system.debugger.enabled'); + $this->enabled = (bool)$this->config->get('system.debugger.enabled'); if ($this->enabled()) { + $this->initialized = true; $plugins_config = (array)$this->config->get('plugins'); ksort($plugins_config); - - $this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config')); - $this->debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins')); + $debugbar = $this->debugbar; + $debugbar->addCollector(new MemoryCollector()); + $debugbar->addCollector(new ExceptionsCollector()); + $debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config')); + $debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins')); $this->addMessage('Grav v' . GRAV_VERSION); } @@ -86,12 +124,12 @@ class Debugger * * @param bool $state If null, the method returns the enabled value. If set, the method sets the enabled state * - * @return null + * @return bool */ public function enabled($state = null) { if ($state !== null) { - $this->enabled = $state; + $this->enabled = (bool)$state; } return $this->enabled; @@ -147,7 +185,7 @@ class Debugger /** * Adds a data collector * - * @param $collector + * @param DataCollectorInterface $collector * * @return $this * @throws \DebugBar\DebugBarException @@ -162,9 +200,9 @@ class Debugger /** * Returns a data collector * - * @param $collector + * @param DataCollectorInterface $collector * - * @return \DebugBar\DataCollector\DataCollectorInterface + * @return DataCollectorInterface * @throws \DebugBar\DebugBarException */ public function getCollector($collector) @@ -229,14 +267,14 @@ class Debugger /** * Start a timer with an associated name and description * - * @param $name + * @param string $name * @param string|null $description * * @return $this */ public function startTimer($name, $description = null) { - if ($name[0] === '_' || $this->enabled()) { + if (strpos($name, '_') === 0 || $this->enabled()) { $this->debugbar['time']->startMeasure($name, $description); $this->timers[] = $name; } @@ -253,7 +291,7 @@ class Debugger */ public function stopTimer($name) { - if (in_array($name, $this->timers, true) && ($name[0] === '_' || $this->enabled())) { + if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) { $this->debugbar['time']->stopMeasure($name); } @@ -263,7 +301,7 @@ class Debugger /** * Dump variables into the Messages tab of the Debug Bar * - * @param $message + * @param mixed $message * @param string $label * @param bool $isString * @@ -286,7 +324,7 @@ class Debugger */ public function addException(\Exception $e) { - if ($this->enabled()) { + if ($this->initialized && $this->enabled()) { $this->debugbar['exceptions']->addException($e); } @@ -321,57 +359,183 @@ class Debugger return true; } - $backtrace = debug_backtrace(false); + // Figure out error scope from the error. + $scope = 'unknown'; + if (stripos($errstr, 'grav') !== false) { + $scope = 'grav'; + } elseif (strpos($errfile, '/twig/') !== false) { + $scope = 'twig'; + } elseif (stripos($errfile, '/yaml/') !== false) { + $scope = 'yaml'; + } elseif (strpos($errfile, '/vendor/') !== false) { + $scope = 'vendor'; + } + + // Clean up backtrace to make it more useful. + $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); // Skip current call. array_shift($backtrace); + // Find yaml file where the error happened. + if ($scope === 'yaml') { + foreach ($backtrace as $current) { + if (isset($current['args'])) { + foreach ($current['args'] as $arg) { + if ($arg instanceof \SplFileInfo) { + $arg = $arg->getPathname(); + } + if (\is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) { + $errfile = $arg; + $errline = 0; + + break 2; + } + } + } + } + } + + // Filter arguments. + $cut = 0; + $previous = null; + foreach ($backtrace as $i => &$current) { + if (isset($current['args'])) { + $args = []; + foreach ($current['args'] as $arg) { + if (\is_string($arg)) { + $arg = "'" . $arg . "'"; + if (mb_strlen($arg) > 100) { + $arg = 'string'; + } + } elseif (\is_bool($arg)) { + $arg = $arg ? 'true' : 'false'; + } elseif (\is_scalar($arg)) { + $arg = $arg; + } elseif (\is_object($arg)) { + $arg = get_class($arg) . ' $object'; + } elseif (\is_array($arg)) { + $arg = '$array'; + } else { + $arg = '$object'; + } + + $args[] = $arg; + } + $current['args'] = $args; + } + + $object = $current['object'] ?? null; + unset($current['object']); + + $reflection = null; + if ($object instanceof TemplateWrapper) { + $reflection = new \ReflectionObject($object); + $property = $reflection->getProperty('template'); + $property->setAccessible(true); + $object = $property->getValue($object); + } + + if ($object instanceof Template) { + $file = $current['file'] ?? null; + + if (preg_match('`(Template.php|TemplateWrapper.php)$`', $file)) { + $current = null; + continue; + } + + $debugInfo = $object->getDebugInfo(); + + $line = 1; + if (!$reflection) { + foreach ($debugInfo as $codeLine => $templateLine) { + if ($codeLine <= $current['line']) { + $line = $templateLine; + break; + } + } + } + + $src = $object->getSourceContext(); + //$code = preg_split('/\r\n|\r|\n/', $src->getCode()); + //$current['twig']['twig'] = trim($code[$line - 1]); + $current['twig']['file'] = $src->getPath(); + $current['twig']['line'] = $line; + + $prevFile = $previous['file'] ?? null; + if ($prevFile && $file === $prevFile) { + $prevLine = $previous['line']; + + $line = 1; + foreach ($debugInfo as $codeLine => $templateLine) { + if ($codeLine <= $prevLine) { + $line = $templateLine; + break; + } + } + + //$previous['twig']['twig'] = trim($code[$line - 1]); + $previous['twig']['file'] = $src->getPath(); + $previous['twig']['line'] = $line; + } + + $cut = $i; + } elseif ($object instanceof ProcessorInterface) { + $cut = $cut ?: $i; + break; + } + + $previous = &$backtrace[$i]; + } + unset($current); + + if ($cut) { + $backtrace = array_slice($backtrace, 0, $cut + 1); + } + $backtrace = array_values(array_filter($backtrace)); + // Skip vendor libraries and the method where error was triggered. - while ($current = array_shift($backtrace)) { - if (isset($current['file']) && strpos($current['file'], 'vendor') !== false) { + foreach ($backtrace as $i => $current) { + if (!isset($current['file'])) { + continue; + } + if (strpos($current['file'], '/vendor/') !== false) { + $cut = $i + 1; continue; } if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) { - $current = array_shift($backtrace); + $cut = $i + 1; + continue; } break; } - // Add back last call. - array_unshift($backtrace, $current); - - // Filter arguments. - foreach ($backtrace as &$current) { - if (isset($current['args'])) { - $args = []; - foreach ($current['args'] as $arg) { - if (\is_string($arg)) { - $args[] = "'" . $arg . "'"; - } elseif (\is_bool($arg)) { - $args[] = $arg ? 'true' : 'false'; - } elseif (\is_scalar($arg)) { - $args[] = $arg; - } elseif (\is_object($arg)) { - $args[] = get_class($arg) . ' $object'; - } elseif (\is_array($arg)) { - $args[] = '$array'; - } else { - $args[] = '$object'; - } - } - $current['args'] = $args; - } + if ($cut) { + $backtrace = array_slice($backtrace, $cut); } - unset($current); + $backtrace = array_values(array_filter($backtrace)); - $this->deprecations[] = [ + $current = reset($backtrace); + + // If the issue happened inside twig file, change the file and line to match that file. + $file = $current['twig']['file'] ?? ''; + if ($file) { + $errfile = $file; + $errline = $current['twig']['line'] ?? 0; + } + + $deprecation = [ + 'scope' => $scope, 'message' => $errstr, 'file' => $errfile, 'line' => $errline, 'trace' => $backtrace, + 'count' => 1 ]; + $this->deprecations[] = $deprecation; + // Do not pass forward. return true; } @@ -396,38 +560,37 @@ class Debugger protected function getDepracatedMessage($deprecated) { - $scope = 'unknown'; - if (stripos($deprecated['message'], 'grav') !== false) { - $scope = 'grav'; - } elseif (!isset($deprecated['file'])) { - $scope = 'unknown'; - } elseif (stripos($deprecated['file'], 'twig') !== false) { - $scope = 'twig'; - } elseif (stripos($deprecated['file'], 'yaml') !== false) { - $scope = 'yaml'; - } elseif (stripos($deprecated['file'], 'vendor') !== false) { - $scope = 'vendor'; - } + $scope = $deprecated['scope']; $trace = []; - foreach ($deprecated['trace'] as $current) { - $class = isset($current['class']) ? $current['class'] : ''; - $type = isset($current['type']) ? $current['type'] : ''; - $function = $this->getFunction($current); - if (isset($current['file'])) { - $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']); + if (isset($deprecated['trace'])) { + foreach ($deprecated['trace'] as $current) { + $class = $current['class'] ?? ''; + $type = $current['type'] ?? ''; + $function = $this->getFunction($current); + if (isset($current['file'])) { + $current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']); + } + + unset($current['class'], $current['type'], $current['function'], $current['args']); + + if (isset($current['twig'])) { + $trace[] = $current['twig']; + } else { + $trace[] = ['call' => $class . $type . $function] + $current; + } } - - unset($current['class'], $current['type'], $current['function'], $current['args']); - - $trace[] = ['call' => $class . $type . $function] + $current; } + $array = [ + 'message' => $deprecated['message'], + 'file' => $deprecated['file'], + 'line' => $deprecated['line'], + 'trace' => $trace + ]; + return [ - [ - 'message' => $deprecated['message'], - 'trace' => $trace - ], + array_filter($array), $scope ]; } @@ -438,6 +601,6 @@ class Debugger return ''; } - return $trace['function'] . '(' . implode(', ', $trace['args']) . ')'; + return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')'; } } diff --git a/system/src/Grav/Common/Errors/BareHandler.php b/system/src/Grav/Common/Errors/BareHandler.php index 25c0e0a..52effc6 100644 --- a/system/src/Grav/Common/Errors/BareHandler.php +++ b/system/src/Grav/Common/Errors/BareHandler.php @@ -1,8 +1,9 @@ get('system.errors'); - $jsonRequest = $_SERVER && isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] == 'application/json'; + $jsonRequest = $_SERVER && isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json'; // Setup Whoops-based error handler $system = new SystemFacade; - $whoops = new \Whoops\Run($system); + $whoops = new Whoops\Run($system); $verbosity = 1; @@ -49,17 +50,8 @@ class Errors break; } - if (method_exists('Whoops\Util\Misc', 'isAjaxRequest')) { //Whoops 2.0 - if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) { - $whoops->pushHandler(new Whoops\Handler\JsonResponseHandler); - } - } elseif (function_exists('Whoops\isAjaxRequest')) { //Whoops 2.0.0-alpha - if (Whoops\isAjaxRequest() || $jsonRequest) { - $whoops->pushHandler(new Whoops\Handler\JsonResponseHandler); - } - } else { //Whoops 1.x - $json_page = new Whoops\Handler\JsonResponseHandler; - $json_page->onlyForAjaxRequests(true); + if (Whoops\Util\Misc::isAjaxRequest() || $jsonRequest) { + $whoops->pushHandler(new Whoops\Handler\JsonResponseHandler); } if (isset($config['log']) && $config['log']) { @@ -70,7 +62,7 @@ class Errors } catch (\Exception $e) { echo $e; } - }, 'log'); + }); } $whoops->register(); diff --git a/system/src/Grav/Common/Errors/SimplePageHandler.php b/system/src/Grav/Common/Errors/SimplePageHandler.php index 829f596..311d722 100644 --- a/system/src/Grav/Common/Errors/SimplePageHandler.php +++ b/system/src/Grav/Common/Errors/SimplePageHandler.php @@ -1,8 +1,9 @@ searchPaths[] = __DIR__ . "/Resources"; + $this->searchPaths[] = __DIR__ . '/Resources'; } /** @@ -31,8 +32,8 @@ class SimplePageHandler extends Handler $inspector = $this->getInspector(); $helper = new TemplateHelper(); - $templateFile = $this->getResource("layout.html.php"); - $cssFile = $this->getResource("error.css"); + $templateFile = $this->getResource('layout.html.php'); + $cssFile = $this->getResource('error.css'); $code = $inspector->getException()->getCode(); if ( ($code >= 400) && ($code < 600) ) @@ -46,9 +47,9 @@ class SimplePageHandler extends Handler } $vars = array( - "stylesheet" => file_get_contents($cssFile), - "code" => $code, - "message" => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING), + 'stylesheet' => file_get_contents($cssFile), + 'code' => $code, + 'message' => filter_var(rawurldecode($message), FILTER_SANITIZE_STRING), ); $helper->setVariables($vars); @@ -58,7 +59,7 @@ class SimplePageHandler extends Handler } /** - * @param $resource + * @param string $resource * * @return string * @throws \RuntimeException @@ -74,7 +75,7 @@ class SimplePageHandler extends Handler // Search through available search paths, until we find the // resource we're after: foreach ($this->searchPaths as $path) { - $fullPath = $path . "/$resource"; + $fullPath = "{$path}/{$resource}"; if (is_file($fullPath)) { // Cache the result: diff --git a/system/src/Grav/Common/Errors/SystemFacade.php b/system/src/Grav/Common/Errors/SystemFacade.php index 5b73a2b..02ef0cf 100644 --- a/system/src/Grav/Common/Errors/SystemFacade.php +++ b/system/src/Grav/Common/Errors/SystemFacade.php @@ -1,8 +1,9 @@ ['.DS_Store'], + 'exclude_paths' => [] + ]; + + protected $archive_file; + + public static function create($compression) + { + if ($compression === 'zip') { + return new ZipArchiver(); + } + + return new ZipArchiver(); + } + + public function setArchive($archive_file) + { + $this->archive_file = $archive_file; + return $this; + } + + public function setOptions($options) + { + // Set infinite PHP execution time if possible. + if (function_exists('set_time_limit') && !Utils::isFunctionDisabled('set_time_limit')) { + set_time_limit(0); + } + + $this->options = $options + $this->options; + return $this; + } + + public abstract function compress($folder, callable $status = null); + + public abstract function extract($destination, callable $status = null); + + public abstract function addEmptyFolders($folders, callable $status = null); + + protected function getArchiveFiles($rootPath) + { + $exclude_paths = $this->options['exclude_paths']; + $exclude_files = $this->options['exclude_files']; + $dirItr = new \RecursiveDirectoryIterator($rootPath, \RecursiveDirectoryIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::UNIX_PATHS); + $filterItr = new RecursiveDirectoryFilterIterator($dirItr, $rootPath, $exclude_paths, $exclude_files); + $files = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST); + + return $files; + } + +} diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index 0d69100..f17410c 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -1,8 +1,9 @@ getFilename()[0] === '.') { + if (strpos($file->getFilename(), '.') === 0) { continue; } if (!$folders && $file->isDir()) { @@ -255,7 +257,7 @@ abstract class Folder if (isset($filters['value'])) { $filter = $filters['value']; if (is_callable($filter)) { - $filePath = call_user_func($filter, $file); + $filePath = $filter($file); } else { $filePath = preg_replace($filter, '', $filePath); } @@ -316,7 +318,7 @@ 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. @@ -416,23 +418,27 @@ abstract class Folder */ public static function create($folder) { - if (is_dir($folder)) { + // Silence error for open_basedir; should fail in mkdir instead. + if (@is_dir($folder)) { return; } $success = @mkdir($folder, 0777, true); if (!$success) { - $error = error_get_last(); - throw new \RuntimeException($error['message']); + // Take yet another look, make sure that the folder doesn't exist. + clearstatcache(true, $folder); + if (!@is_dir($folder)) { + throw new \RuntimeException(sprintf('Unable to create directory: %s', $folder)); + } } } /** * Recursive copy of one directory to another * - * @param $src - * @param $dest + * @param string $src + * @param string $dest * * @return bool * @throws \RuntimeException @@ -448,7 +454,7 @@ abstract class Folder // If the destination directory does not exist create it if (!is_dir($dest)) { - static::mkdir($dest); + static::create($dest); } // Open the source directory to read in files @@ -475,7 +481,7 @@ abstract class Folder protected static function doDelete($folder, $include_target = true) { // Special case for symbolic links. - if (is_link($folder)) { + if ($include_target && is_link($folder)) { return @unlink($folder); } diff --git a/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php b/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php new file mode 100644 index 0000000..c662fc4 --- /dev/null +++ b/system/src/Grav/Common/Filesystem/RecursiveDirectoryFilterIterator.php @@ -0,0 +1,67 @@ +current(); + $filename = $file->getFilename(); + $relative_filename = str_replace($this::$root . '/', '', $file->getPathname()); + + if ($file->isDir()) { + if (in_array($relative_filename, $this::$ignore_folders, true)) { + return false; + } + if (!in_array($filename, $this::$ignore_files, true)) { + return true; + } + } elseif ($file->isFile() && !in_array($filename, $this::$ignore_files, true)) { + return true; + } + return false; + } + + public function getChildren() + { + /** @var RecursiveDirectoryFilterIterator $iterator */ + $iterator = $this->getInnerIterator(); + + return new self($iterator->getChildren(), $this::$root, $this::$ignore_folders, $this::$ignore_files); + } +} diff --git a/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php index 3972074..eb493db 100644 --- a/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php +++ b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php @@ -1,8 +1,9 @@ get('system.pages.ignore_folders'); + + if (empty($ignore_folders)) { + $ignore_folders = Grav::instance()['config']->get('system.pages.ignore_folders'); } + + $this::$ignore_folders = $ignore_folders; } /** @@ -34,12 +39,9 @@ class RecursiveFolderFilterIterator extends \RecursiveFilterIterator */ public function accept() { - /** @var $current \SplFileInfo */ + /** @var \SplFileInfo $current */ $current = $this->current(); - if ($current->isDir() && !in_array($current->getFilename(), $this::$folder_ignores, true)) { - return true; - } - return false; + return $current->isDir() && !in_array($current->getFilename(), $this::$ignore_folders, true); } } diff --git a/system/src/Grav/Common/Filesystem/ZipArchiver.php b/system/src/Grav/Common/Filesystem/ZipArchiver.php new file mode 100644 index 0000000..91212f7 --- /dev/null +++ b/system/src/Grav/Common/Filesystem/ZipArchiver.php @@ -0,0 +1,111 @@ +open($this->archive_file); + + if ($archive === true) { + Folder::create($destination); + + if (!$zip->extractTo($destination)) { + throw new \RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination); + } + + $zip->close(); + return $this; + } + + throw new \RuntimeException('ZipArchiver: Failed to open ' . $this->archive_file); + } + + public function compress($source, callable $status = null) + { + if (!extension_loaded('zip')) { + throw new \InvalidArgumentException('ZipArchiver: Zip PHP module not installed...'); + } + + if (!file_exists($source)) { + throw new \InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...'); + } + + $zip = new \ZipArchive(); + if (!$zip->open($this->archive_file, \ZipArchive::CREATE)) { + 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([ + 'type' => 'count', + 'steps' => iterator_count($files), + ]); + + foreach ($files as $file) { + $filePath = $file->getPathname(); + $relativePath = ltrim(substr($filePath, strlen($rootPath)), '/'); + + if ($file->isDir()) { + $zip->addEmptyDir($relativePath); + } else { + $zip->addFile($filePath, $relativePath); + } + + $status && $status([ + 'type' => 'progress', + ]); + } + + $status && $status([ + 'type' => 'message', + 'message' => 'Compressing...' + ]); + + $zip->close(); + + return $this; + } + + public function addEmptyFolders($folders, callable $status = null) + { + if (!extension_loaded('zip')) { + throw new \InvalidArgumentException('ZipArchiver: Zip PHP module not installed...'); + } + + $zip = new \ZipArchive(); + if (!$zip->open($this->archive_file)) { + throw new \InvalidArgumentException('ZipArchiver: ' . $this->archive_file . ' cannot be opened...'); + } + + $status && $status([ + 'type' => 'message', + 'message' => 'Adding empty folders...' + ]); + + foreach($folders as $folder) { + $zip->addEmptyDir($folder); + $status && $status([ + 'type' => 'progress', + ]); + } + + $zip->close(); + + return $this; + } +} diff --git a/system/src/Grav/Common/Form/FormFlash.php b/system/src/Grav/Common/Form/FormFlash.php new file mode 100644 index 0000000..b67313b --- /dev/null +++ b/system/src/Grav/Common/Form/FormFlash.php @@ -0,0 +1,101 @@ +files as $field => $files) { + if (strpos($field, '/')) { + continue; + } + foreach ($files as $file) { + if (\is_array($file)) { + $file['tmp_name'] = $this->getTmpDir() . '/' . $file['tmp_name']; + $fields[$field][$file['path'] ?? $file['name']] = $file; + } + } + } + + return $fields; + } + + /** + * @param string $field + * @param string $filename + * @param array $upload + * @return bool + * @deprecated 1.6 For backwards compatibility only, do not use + */ + public function uploadFile(string $field, string $filename, array $upload): bool + { + if (!$this->uniqueId) { + return false; + } + + $tmp_dir = $this->getTmpDir(); + Folder::create($tmp_dir); + + $tmp_file = $upload['file']['tmp_name']; + $basename = basename($tmp_file); + + if (!move_uploaded_file($tmp_file, $tmp_dir . '/' . $basename)) { + return false; + } + + $upload['file']['tmp_name'] = $basename; + $upload['file']['name'] = $filename; + + $this->addFileInternal($field, $filename, $upload['file']); + + return true; + } + + /** + * @param string $field + * @param string $filename + * @param array $upload + * @param array $crop + * @return bool + * @deprecated 1.6 For backwards compatibility only, do not use + */ + public function cropFile(string $field, string $filename, array $upload, array $crop): bool + { + if (!$this->uniqueId) { + return false; + } + + $tmp_dir = $this->getTmpDir(); + Folder::create($tmp_dir); + + $tmp_file = $upload['file']['tmp_name']; + $basename = basename($tmp_file); + + if (!move_uploaded_file($tmp_file, $tmp_dir . '/' . $basename)) { + return false; + } + + $upload['file']['tmp_name'] = $basename; + $upload['file']['name'] = $filename; + + $this->addFileInternal($field, $filename, $upload['file'], $crop); + + return true; + } +} diff --git a/system/src/Grav/Common/GPM/AbstractCollection.php b/system/src/Grav/Common/GPM/AbstractCollection.php index 3430a6a..a9e1ac9 100644 --- a/system/src/Grav/Common/GPM/AbstractCollection.php +++ b/system/src/Grav/Common/GPM/AbstractCollection.php @@ -1,8 +1,9 @@ items as $name => $package) { - $items[$name] = $package->toArray(); - } - - return json_encode($items); + return json_encode($this->toArray()); } public function toArray() diff --git a/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php index 78d93bc..bc4ecab 100644 --- a/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php +++ b/system/src/Grav/Common/GPM/Common/AbstractPackageCollection.php @@ -1,8 +1,9 @@ $item) { + foreach (self::$cache[$method] as $name => $item) { $this->append([$name => $item]); } } diff --git a/system/src/Grav/Common/GPM/Common/Package.php b/system/src/Grav/Common/GPM/Common/Package.php index fe4618a..40d3759 100644 --- a/system/src/Grav/Common/GPM/Common/Package.php +++ b/system/src/Grav/Common/GPM/Common/Package.php @@ -1,8 +1,9 @@ data = $package; if ($type) { @@ -22,28 +27,44 @@ class Package { } } - public function getData() { + /** + * @return Data + */ + public function getData() + { return $this->data; } - public function __get($key) { + public function __get($key) + { return $this->data->get($key); } - public function __isset($key) { - return isset($this->data->$key); + public function __set($key, $value) + { + return $this->data->set($key, $value); } - public function __toString() { + public function __isset($key) + { + return isset($this->data->{$key}); + } + + public function __toString() + { return $this->toJson(); } - public function toJson() { + public function toJson() + { return $this->data->toJson(); } - public function toArray() { + /** + * @return array + */ + public function toArray() + { return $this->data->toArray(); } - } diff --git a/system/src/Grav/Common/GPM/GPM.php b/system/src/Grav/Common/GPM/GPM.php index 696ba65..6c9e7b0 100644 --- a/system/src/Grav/Common/GPM/GPM.php +++ b/system/src/Grav/Common/GPM/GPM.php @@ -1,8 +1,9 @@ cache = []; $this->installed = new Local\Packages(); try { $this->repository = new Remote\Packages($refresh, $callback); @@ -94,7 +97,7 @@ class GPM extends Iterator /** * Returns the amount of locally installed packages - * @return integer Amount of installed packages + * @return int Amount of installed packages */ public function countInstalled() { @@ -144,7 +147,7 @@ class GPM extends Iterator /** * Checks if a Plugin is installed * @param string $slug The slug of the Plugin - * @return boolean True if the Plugin has been installed. False otherwise + * @return bool True if the Plugin has been installed. False otherwise */ public function isPluginInstalled($slug) { @@ -178,7 +181,7 @@ class GPM extends Iterator /** * Checks if a Theme is installed * @param string $slug The slug of the Theme - * @return boolean True if the Theme has been installed. False otherwise + * @return bool True if the Theme has been installed. False otherwise */ public function isThemeInstalled($slug) { @@ -187,7 +190,7 @@ class GPM extends Iterator /** * Returns the amount of updates available - * @return integer Amount of available updates + * @return int Amount of available updates */ public function countUpdates() { @@ -242,13 +245,12 @@ class GPM extends Iterator continue; } - $local_version = $plugin->version ? $plugin->version : 'Unknown'; + $local_version = $plugin->version ?: 'Unknown'; $remote_version = $repository[$slug]->version; if (version_compare($local_version, $remote_version) < 0) { $repository[$slug]->available = $remote_version; $repository[$slug]->version = $local_version; - $repository[$slug]->name = $repository[$slug]->name; $repository[$slug]->type = $repository[$slug]->release_type; $items[$slug] = $repository[$slug]; } @@ -262,7 +264,7 @@ class GPM extends Iterator /** * Get the latest release of a package from the GPM * - * @param $package_name + * @param string $package_name * * @return string|null */ @@ -285,7 +287,7 @@ class GPM extends Iterator /** * Check if a Plugin or Theme is updatable * @param string $slug The slug of the package - * @return boolean True if updatable. False otherwise or if not found + * @return bool True if updatable. False otherwise or if not found */ public function isUpdatable($slug) { @@ -295,7 +297,7 @@ class GPM extends Iterator /** * Checks if a Plugin is updatable * @param string $plugin The slug of the Plugin - * @return boolean True if the Plugin is updatable. False otherwise + * @return bool True if the Plugin is updatable. False otherwise */ public function isPluginUpdatable($plugin) { @@ -322,7 +324,7 @@ class GPM extends Iterator continue; } - $local_version = $plugin->version ? $plugin->version : 'Unknown'; + $local_version = $plugin->version ?: 'Unknown'; $remote_version = $repository[$slug]->version; if (version_compare($local_version, $remote_version) < 0) { @@ -341,7 +343,7 @@ class GPM extends Iterator /** * Checks if a Theme is Updatable * @param string $theme The slug of the Theme - * @return boolean True if the Theme is updatable. False otherwise + * @return bool True if the Theme is updatable. False otherwise */ public function isThemeUpdatable($theme) { @@ -351,7 +353,7 @@ class GPM extends Iterator /** * Get the release type of a package (stable / testing) * - * @param $package_name + * @param string $package_name * * @return string|null */ @@ -374,9 +376,9 @@ class GPM extends Iterator /** * Returns true if the package latest release is stable * - * @param $package_name + * @param string $package_name * - * @return boolean + * @return bool */ public function isStableRelease($package_name) { @@ -386,9 +388,9 @@ class GPM extends Iterator /** * Returns true if the package latest release is testing * - * @param $package_name + * @param string $package_name * - * @return boolean + * @return bool */ public function isTestingRelease($package_name) { @@ -503,8 +505,8 @@ class GPM extends Iterator /** * Download the zip package via the URL * - * @param $package_file - * @param $tmp + * @param string $package_file + * @param string $tmp * @return null|string */ public static function downloadPackage($package_file, $tmp) @@ -519,7 +521,7 @@ class GPM extends Iterator $output = Response::get($package_file, []); if ($output) { - Folder::mkdir($tmp); + Folder::create($tmp); file_put_contents($tmp . DS . $filename, $output); return $tmp . DS . $filename; } @@ -530,8 +532,8 @@ class GPM extends Iterator /** * Copy the local zip package to tmp * - * @param $package_file - * @param $tmp + * @param string $package_file + * @param string $tmp * @return null|string */ public static function copyPackage($package_file, $tmp) @@ -540,7 +542,7 @@ class GPM extends Iterator if (file_exists($package_file)) { $filename = basename($package_file); - Folder::mkdir($tmp); + Folder::create($tmp); copy(realpath($package_file), $tmp . DS . $filename); return $tmp . DS . $filename; } @@ -551,7 +553,7 @@ class GPM extends Iterator /** * Try to guess the package type from the source files * - * @param $source + * @param string $source * @return bool|string */ public static function getPackageType($source) @@ -564,44 +566,46 @@ class GPM extends Iterator file_exists($source . 'system/config/system.yaml') ) { return 'grav'; - } else { - // must have a blueprint - if (!file_exists($source . 'blueprints.yaml')) { - return false; - } + } - // either theme or plugin - $name = basename($source); - if (Utils::contains($name, 'theme')) { - return 'theme'; - } elseif (Utils::contains($name, 'plugin')) { - return 'plugin'; - } - foreach (glob($source . "*.php") as $filename) { - $contents = file_get_contents($filename); - if (preg_match($theme_regex, $contents)) { - return 'theme'; - } elseif (preg_match($plugin_regex, $contents)) { - return 'plugin'; - } - } + // must have a blueprint + if (!file_exists($source . 'blueprints.yaml')) { + return false; + } - // Assume it's a theme + // either theme or plugin + $name = basename($source); + if (Utils::contains($name, 'theme')) { return 'theme'; } + if (Utils::contains($name, 'plugin')) { + return 'plugin'; + } + foreach (glob($source . '*.php') as $filename) { + $contents = file_get_contents($filename); + if (preg_match($theme_regex, $contents)) { + return 'theme'; + } + if (preg_match($plugin_regex, $contents)) { + return 'plugin'; + } + } + + // Assume it's a theme + return 'theme'; } /** * Try to guess the package name from the source files * - * @param $source + * @param string $source * @return bool|string */ public static function getPackageName($source) { $ignore_yaml_files = ['blueprints', 'languages']; - foreach (glob($source . "*.yaml") as $filename) { + foreach (glob($source . '*.yaml') as $filename) { $name = strtolower(basename($filename, '.yaml')); if (in_array($name, $ignore_yaml_files)) { continue; @@ -614,7 +618,7 @@ class GPM extends Iterator /** * Find/Parse the blueprint file * - * @param $source + * @param string $source * @return array|bool */ public static function getBlueprints($source) @@ -634,15 +638,15 @@ class GPM extends Iterator /** * Get the install path for a name and a particular type of package * - * @param $type - * @param $name + * @param string $type + * @param string $name * @return string */ public static function getInstallPath($type, $name) { $locator = Grav::instance()['locator']; - if ($type == 'theme') { + if ($type === 'theme') { $install_path = $locator->findResource('themes://', false) . DS . $name; } else { $install_path = $locator->findResource('plugins://', false) . DS . $name; @@ -692,7 +696,7 @@ class GPM extends Iterator } $not_found = new \stdClass(); - $not_found->name = $inflector->camelize($search); + $not_found->name = $inflector::camelize($search); $not_found->slug = $search; $not_found->package_type = $type; $not_found->install_path = str_replace('%name%', $search, $this->install_paths[$type]); @@ -726,7 +730,7 @@ class GPM extends Iterator $dependency = $dependency['name']; } - if ($dependency == $slug) { + if ($dependency === $slug) { $dependent_packages[] = $package_name; } } @@ -740,8 +744,8 @@ class GPM extends Iterator /** * Get the required version of a dependency of a package * - * @param $package_slug - * @param $dependency_slug + * @param string $package_slug + * @param string $dependency_slug * * @return mixed */ @@ -766,7 +770,7 @@ class GPM extends Iterator * @param array $ignore_packages_list * * @return bool - * @throws \Exception + * @throws \RuntimeException */ public function checkNoOtherPackageNeedsThisDependencyInALowerVersion( $slug, @@ -789,8 +793,8 @@ class GPM extends Iterator $compatible = $this->checkNextSignificantReleasesAreCompatible($version, $other_dependency_version); if (!$compatible) { - if (!in_array($dependent_package, $ignore_packages_list)) { - throw new \Exception("Package $slug is required in an older version by package $dependent_package. This package needs a newer version, and because of this it cannot be installed. The $dependent_package package must be updated to use a newer release of $slug.", + if (!in_array($dependent_package, $ignore_packages_list, true)) { + throw new \RuntimeException("Package $slug is required in an older version by package $dependent_package. This package needs a newer version, and because of this it cannot be installed. The $dependent_package package must be updated to use a newer release of $slug.", 2); } } @@ -804,7 +808,7 @@ class GPM extends Iterator /** * Check the passed packages list can be updated * - * @param $packages_names_list + * @param array $packages_names_list * * @throws \Exception */ @@ -833,36 +837,36 @@ class GPM extends Iterator { $dependencies = $this->calculateMergedDependenciesOfPackages($packages); foreach ($dependencies as $dependency_slug => $dependencyVersionWithOperator) { - if (in_array($dependency_slug, $packages)) { + if (\in_array($dependency_slug, $packages, true)) { unset($dependencies[$dependency_slug]); continue; } // Check PHP version - if ($dependency_slug == 'php') { + if ($dependency_slug === 'php') { $current_php_version = phpversion(); if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator), $current_php_version) === 1 ) { //Needs a Grav update first - throw new \Exception("One of the packages require PHP " . $dependencies['php'] . ". Please update PHP to resolve this"); - } else { - unset($dependencies[$dependency_slug]); - continue; + throw new \RuntimeException("One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this"); } + + unset($dependencies[$dependency_slug]); + continue; } //First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell. - if ($dependency_slug == 'grav') { + if ($dependency_slug === 'grav') { if (version_compare($this->calculateVersionNumberFromDependencyVersion($dependencyVersionWithOperator), GRAV_VERSION) === 1 ) { //Needs a Grav update first - throw new \Exception("One of the packages require Grav " . $dependencies['grav'] . ". Please update Grav to the latest release."); - } else { - unset($dependencies[$dependency_slug]); - continue; + throw new \RuntimeException("One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release."); } + + unset($dependencies[$dependency_slug]); + continue; } if ($this->isPluginInstalled($dependency_slug)) { @@ -888,7 +892,7 @@ class GPM extends Iterator $currentlyInstalledVersion); if (!$compatible) { - throw new \Exception('Dependency ' . $dependency_slug . ' is required in an older version than the one installed. This package must be updated. Please get in touch with its developer.', + throw new \RuntimeException('Dependency ' . $dependency_slug . ' is required in an older version than the one installed. This package must be updated. Please get in touch with its developer.', 2); } } @@ -899,7 +903,7 @@ class GPM extends Iterator if ($this->firstVersionIsLower($latestRelease, $dependencyVersion)) { //throw an exception if a required version cannot be found in the GPM yet - throw new \Exception('Dependency ' . $package_yaml['name'] . ' is required in version ' . $dependencyVersion . ' which is higher than the latest release, ' . $latestRelease . '. Try running `bin/gpm -f index` to force a refresh of the GPM cache', + throw new \RuntimeException('Dependency ' . $package_yaml['name'] . ' is required in version ' . $dependencyVersion . ' which is higher than the latest release, ' . $latestRelease . '. Try running `bin/gpm -f index` to force a refresh of the GPM cache', 1); } @@ -950,7 +954,7 @@ class GPM extends Iterator private function firstVersionIsLower($firstVersion, $secondVersion) { - return version_compare($firstVersion, $secondVersion) == -1; + return version_compare($firstVersion, $secondVersion) === -1; } /** @@ -1005,7 +1009,7 @@ class GPM extends Iterator $current_package_version_number = $this->calculateVersionNumberFromDependencyVersion($current_package_version_information); if (!$current_package_version_number) { - throw new \Exception('Bad format for version of dependency ' . $current_package_name . ' for package ' . $packageName, + throw new \RuntimeException('Bad format for version of dependency ' . $current_package_name . ' for package ' . $packageName, 1); } @@ -1021,7 +1025,7 @@ class GPM extends Iterator if (!$currently_stored_version_is_in_next_significant_release_format && !$current_package_version_is_in_next_significant_release_format) { //Comparing versions equals or higher, a simple version_compare is enough if (version_compare($currently_stored_version_number, - $current_package_version_number) == -1 + $current_package_version_number) === -1 ) { //Current package version is higher $dependencies[$current_package_name] = $current_package_version_information; } @@ -1029,7 +1033,7 @@ class GPM extends Iterator $compatible = $this->checkNextSignificantReleasesAreCompatible($currently_stored_version_number, $current_package_version_number); if (!$compatible) { - throw new \Exception('Dependency ' . $current_package_name . ' is required in two incompatible versions', + throw new \RuntimeException('Dependency ' . $current_package_name . ' is required in two incompatible versions', 2); } } @@ -1076,17 +1080,20 @@ class GPM extends Iterator */ public function calculateVersionNumberFromDependencyVersion($version) { - if ($version == '*') { + if ($version === '*') { return null; - } elseif ($version == '') { - return null; - } elseif ($this->versionFormatIsNextSignificantRelease($version)) { - return trim(substr($version, 1)); - } elseif ($this->versionFormatIsEqualOrHigher($version)) { - return trim(substr($version, 2)); - } else { - return $version; } + if ($version === '') { + return null; + } + if ($this->versionFormatIsNextSignificantRelease($version)) { + return trim(substr($version, 1)); + } + if ($this->versionFormatIsEqualOrHigher($version)) { + return trim(substr($version, 2)); + } + + return $version; } /** @@ -1094,13 +1101,13 @@ class GPM extends Iterator * * Example: returns true for $version: '~2.0' * - * @param $version + * @param string $version * * @return bool */ - public function versionFormatIsNextSignificantRelease($version) + public function versionFormatIsNextSignificantRelease($version): bool { - return substr($version, 0, 1) == '~'; + return strpos($version, '~') === 0; } /** @@ -1108,13 +1115,13 @@ class GPM extends Iterator * * Example: returns true for $version: '>=2.0' * - * @param $version + * @param string $version * * @return bool */ - public function versionFormatIsEqualOrHigher($version) + public function versionFormatIsEqualOrHigher($version): bool { - return substr($version, 0, 2) == '>='; + return strpos($version, '>=') === 0; } /** @@ -1130,17 +1137,17 @@ class GPM extends Iterator * * @return bool */ - public function checkNextSignificantReleasesAreCompatible($version1, $version2) + public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool { $version1array = explode('.', $version1); $version2array = explode('.', $version2); - if (count($version1array) > count($version2array)) { + if (\count($version1array) > \count($version2array)) { list($version1array, $version2array) = [$version2array, $version1array]; } $i = 0; - while ($i < count($version1array) - 1) { + while ($i < \count($version1array) - 1) { if ($version1array[$i] != $version2array[$i]) { return false; } diff --git a/system/src/Grav/Common/GPM/Installer.php b/system/src/Grav/Common/GPM/Installer.php index f16cdcc..cd51163 100644 --- a/system/src/Grav/Common/GPM/Installer.php +++ b/system/src/Grav/Common/GPM/Installer.php @@ -1,8 +1,9 @@ 'user/themes/antimatter'] * @param string $extracted The local path to the extacted ZIP package + * @param bool $keepExtracted True if you want to keep the original files * @return bool True if everything went fine, False otherwise. */ - public static function install($zip, $destination, $options = [], $extracted = null) + public static function install($zip, $destination, $options = [], $extracted = null, $keepExtracted = false) { $destination = rtrim($destination, DS); $options = array_merge(self::$options, $options); @@ -88,15 +90,15 @@ class Installer return false; } - if (self::lastErrorCode() == self::IS_LINK && $options['ignore_symlinks'] || - self::lastErrorCode() == self::EXISTS && !$options['overwrite'] + if ((self::lastErrorCode() === self::IS_LINK && $options['ignore_symlinks']) || + (self::lastErrorCode() === self::EXISTS && !$options['overwrite']) ) { return false; } // Create a tmp location $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); - $tmp = $tmp_dir . '/Grav-' . uniqid(); + $tmp = $tmp_dir . '/Grav-' . uniqid('', false); if (!$extracted) { $extracted = self::unZip($zip, $tmp); @@ -111,7 +113,6 @@ class Installer return false; } - $is_install = true; $installer = self::loadInstaller($extracted, $is_install); @@ -140,7 +141,7 @@ class Installer self::moveInstall($extracted, $install_path); } } else { - self::sophisticatedInstall($extracted, $install_path, $options['ignores']); + self::sophisticatedInstall($extracted, $install_path, $options['ignores'], $keepExtracted); } Folder::delete($tmp); @@ -165,8 +166,8 @@ class Installer /** * Unzip a file to somewhere * - * @param $zip_file - * @param $destination + * @param string $zip_file + * @param string $destination * @return bool|string */ public static function unZip($zip_file, $destination) @@ -175,7 +176,7 @@ class Installer $archive = $zip->open($zip_file); if ($archive === true) { - Folder::mkdir($destination); + Folder::create($destination); $unzip = $zip->extractTo($destination); @@ -189,9 +190,8 @@ class Installer $package_folder_name = preg_replace('#\./$#', '', $zip->getNameIndex(0)); $zip->close(); - $extracted_folder = $destination . '/' . $package_folder_name; - return $extracted_folder; + return $destination . '/' . $package_folder_name; } self::$error = self::ZIP_EXTRACT_ERROR; @@ -216,7 +216,7 @@ class Installer $install_file = $installer_file_folder . DS . 'install.php'; if (file_exists($install_file)) { - require_once($install_file); + require_once $install_file; } else { return null; } @@ -253,8 +253,8 @@ class Installer } /** - * @param $source_path - * @param $install_path + * @param string $source_path + * @param string $install_path * * @return bool */ @@ -270,8 +270,8 @@ class Installer } /** - * @param $source_path - * @param $install_path + * @param string $source_path + * @param string $install_path * * @return bool */ @@ -279,24 +279,26 @@ class Installer { if (empty($source_path)) { throw new \RuntimeException("Directory $source_path is missing"); - } else { - Folder::rcopy($source_path, $install_path); } + Folder::rcopy($source_path, $install_path); + return true; } /** - * @param $source_path - * @param $install_path + * @param string $source_path + * @param string $install_path + * @param array $ignores + * @param bool $keep_source * * @return bool */ - public static function sophisticatedInstall($source_path, $install_path, $ignores = []) + public static function sophisticatedInstall($source_path, $install_path, $ignores = [], $keep_source = false) { foreach (new \DirectoryIterator($source_path) as $file) { - if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores)) { + if ($file->isLink() || $file->isDot() || \in_array($file->getFilename(), $ignores, true)) { continue; } @@ -304,7 +306,11 @@ class Installer if ($file->isDir()) { Folder::delete($path); - Folder::move($file->getPathname(), $path); + if ($keep_source) { + Folder::copy($file->getPathname(), $path); + } else { + Folder::move($file->getPathname(), $path); + } if ($file->getFilename() === 'bin') { foreach (glob($path . DS . '*') as $bin_file) { @@ -326,7 +332,7 @@ class Installer * @param string $path The slug of the package(s) * @param array $options Options to use for uninstalling * - * @return boolean True if everything went fine, False otherwise. + * @return bool True if everything went fine, False otherwise. */ public static function uninstall($path, $options = []) { @@ -368,7 +374,7 @@ class Installer * @param string $destination The directory to run validations at * @param array $exclude An array of constants to exclude from the validation * - * @return boolean True if validation passed. False otherwise + * @return bool True if validation passed. False otherwise */ public static function isValidDestination($destination, $exclude = []) { @@ -385,11 +391,11 @@ class Installer self::$error = self::NOT_DIRECTORY; } - if (count($exclude) && in_array(self::$error, $exclude)) { + if (\count($exclude) && \in_array(self::$error, $exclude, true)) { return true; } - return !(self::$error); + return !self::$error; } /** @@ -397,7 +403,7 @@ class Installer * * @param string $target The local path to the Grav Instance * - * @return boolean True if is a Grav Instance. False otherwise + * @return bool True if is a Grav Instance. False otherwise */ public static function isGravInstance($target) { @@ -469,23 +475,23 @@ class Installer if (self::$error_zip) { switch(self::$error_zip) { case \ZipArchive::ER_EXISTS: - $msg .= "File already exists."; + $msg .= 'File already exists.'; break; case \ZipArchive::ER_INCONS: - $msg .= "Zip archive inconsistent."; + $msg .= 'Zip archive inconsistent.'; break; case \ZipArchive::ER_MEMORY: - $msg .= "Malloc failure."; + $msg .= 'Memory allocation failure.'; break; case \ZipArchive::ER_NOENT: - $msg .= "No such file."; + $msg .= 'No such file.'; break; case \ZipArchive::ER_NOZIP: - $msg .= "Not a zip archive."; + $msg .= 'Not a zip archive.'; break; case \ZipArchive::ER_OPEN: @@ -493,16 +499,20 @@ class Installer break; case \ZipArchive::ER_READ: - $msg .= "Read error."; + $msg .= 'Read error.'; break; case \ZipArchive::ER_SEEK: - $msg .= "Seek error."; + $msg .= 'Seek error.'; break; } } break; + case self::INVALID_SOURCE: + $msg = 'Invalid source file'; + break; + default: $msg = 'Unknown Error'; break; @@ -513,7 +523,7 @@ class Installer /** * Returns the last error code of the occurred error - * @return integer The code of the last error + * @return int|string The code of the last error */ public static function lastErrorCode() { diff --git a/system/src/Grav/Common/GPM/Licenses.php b/system/src/Grav/Common/GPM/Licenses.php index 6ca596a..14c9258 100644 --- a/system/src/Grav/Common/GPM/Licenses.php +++ b/system/src/Grav/Common/GPM/Licenses.php @@ -1,8 +1,9 @@ content(); + $data = (array)$licenses->content(); $slug = strtolower($slug); if ($license && !self::validate($license)) { return false; } - if (!is_string($license)) { + if (!\is_string($license)) { if (isset($data['licenses'][$slug])) { unset($data['licenses'][$slug]); } else { @@ -66,33 +67,29 @@ class Licenses /** * Returns the license for a Premium package * - * @param $slug + * @param string $slug * - * @return string + * @return array|string */ public static function get($slug = null) { $licenses = self::getLicenseFile(); - $data = $licenses->content(); + $data = (array)$licenses->content(); $licenses->free(); $slug = strtolower($slug); if (!$slug) { - return isset($data['licenses']) ? $data['licenses'] : []; + return $data['licenses'] ?? []; } - if (!isset($data['licenses']) || !isset($data['licenses'][$slug])) { - return ''; - } - - return $data['licenses'][$slug]; + return $data['licenses'][$slug] ?? ''; } /** * Validates the License format * - * @param $license + * @param string $license * * @return bool */ @@ -106,7 +103,7 @@ class Licenses } /** - * Get's the License File object + * Get the License File object * * @return \RocketTheme\Toolbox\File\FileInterface */ @@ -114,7 +111,7 @@ class Licenses { if (!isset(self::$file)) { - $path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml'; + $path = Grav::instance()['locator']->findResource('user-data://') . '/licenses.yaml'; if (!file_exists($path)) { touch($path); } diff --git a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php index b976208..be565c9 100644 --- a/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php +++ b/system/src/Grav/Common/GPM/Local/AbstractPackageCollection.php @@ -1,8 +1,9 @@ $data) { $data->set('slug', $name); $this->items[$name] = new Package($data, $this->type); diff --git a/system/src/Grav/Common/GPM/Local/Package.php b/system/src/Grav/Common/GPM/Local/Package.php index d0fae3a..ff797a7 100644 --- a/system/src/Grav/Common/GPM/Local/Package.php +++ b/system/src/Grav/Common/GPM/Local/Package.php @@ -1,8 +1,9 @@ settings = $package->toArray(); - $html_description = \Parsedown::instance()->line($this->description); - $this->data->set('slug', $package->slug); + $html_description = \Parsedown::instance()->line($this->__get('description')); + $this->data->set('slug', $package->__get('slug')); $this->data->set('description_html', $html_description); $this->data->set('description_plain', strip_tags($html_description)); - $this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->slug)); + $this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->__get('slug'))); } /** diff --git a/system/src/Grav/Common/GPM/Local/Packages.php b/system/src/Grav/Common/GPM/Local/Packages.php index 3120d21..14a0b0b 100644 --- a/system/src/Grav/Common/GPM/Local/Packages.php +++ b/system/src/Grav/Common/GPM/Local/Packages.php @@ -1,8 +1,9 @@ all()); } } diff --git a/system/src/Grav/Common/GPM/Local/Themes.php b/system/src/Grav/Common/GPM/Local/Themes.php index 9b557af..8607e6b 100644 --- a/system/src/Grav/Common/GPM/Local/Themes.php +++ b/system/src/Grav/Common/GPM/Local/Themes.php @@ -1,8 +1,9 @@ get('system.gpm.releases', 'stable'); diff --git a/system/src/Grav/Common/GPM/Remote/GravCore.php b/system/src/Grav/Common/GPM/Remote/GravCore.php index ab5287f..1d30120 100644 --- a/system/src/Grav/Common/GPM/Remote/GravCore.php +++ b/system/src/Grav/Common/GPM/Remote/GravCore.php @@ -1,8 +1,9 @@ fetch($refresh, $callback); $this->data = json_decode($this->raw, true); - $this->version = isset($this->data['version']) ? $this->data['version'] : '-'; - $this->date = isset($this->data['date']) ? $this->data['date'] : '-'; - $this->min_php = isset($this->data['min_php']) ? $this->data['min_php'] : null; + $this->version = $this->data['version'] ?? '-'; + $this->date = $this->data['date'] ?? '-'; + $this->min_php = $this->data['min_php'] ?? null; if (isset($this->data['assets'])) { foreach ((array)$this->data['assets'] as $slug => $data) { @@ -72,7 +73,7 @@ class GravCore extends AbstractPackageCollection $diffLog = []; foreach ((array)$this->data['changelog'] as $version => $changelog) { - preg_match("/[\w-\.]+/", $version, $cleanVersion); + preg_match("/[\w\-\.]+/", $version, $cleanVersion); if (!$cleanVersion || version_compare($diff, $cleanVersion[0], '>=')) { continue; @@ -122,7 +123,7 @@ class GravCore extends AbstractPackageCollection public function getMinPHPVersion() { // If non min set, assume current PHP version - if (is_null($this->min_php)) { + if (null === $this->min_php) { $this->min_php = phpversion(); } return $this->min_php; diff --git a/system/src/Grav/Common/GPM/Remote/Package.php b/system/src/Grav/Common/GPM/Remote/Package.php index 830f63d..196e92b 100644 --- a/system/src/Grav/Common/GPM/Remote/Package.php +++ b/system/src/Grav/Common/GPM/Remote/Package.php @@ -1,8 +1,9 @@ data; + } } diff --git a/system/src/Grav/Common/GPM/Remote/Packages.php b/system/src/Grav/Common/GPM/Remote/Packages.php index d8afd90..46bc31f 100644 --- a/system/src/Grav/Common/GPM/Remote/Packages.php +++ b/system/src/Grav/Common/GPM/Remote/Packages.php @@ -1,8 +1,9 @@ 1 ? $args : array_shift($args); $uri = $args[0]; - $options = $args[1]; - $callback = $args[2]; + $options = $args[1] ?? []; + $callback = $args[2] ?? null; $ch = curl_init($uri); @@ -343,9 +345,9 @@ class Response switch ($code) { case '404': - throw new \RuntimeException("Page not found"); + throw new \RuntimeException('Page not found'); case '401': - throw new \RuntimeException("Invalid LICENSE"); + throw new \RuntimeException('Invalid LICENSE'); default: throw new \RuntimeException("Error while trying to download (code: $code): $uri \nMessage: $error_message"); } @@ -357,9 +359,9 @@ class Response } /** - * @param $ch - * @param $options - * @param $callback + * @param resource $ch + * @param array $options + * @param bool $callback * * @return bool|mixed */ @@ -381,7 +383,7 @@ class Response return curl_exec($ch); } - $max_redirects = isset($options['curl'][CURLOPT_MAXREDIRS]) ? $options['curl'][CURLOPT_MAXREDIRS] : 5; + $max_redirects = $options['curl'][CURLOPT_MAXREDIRS] ?? 5; $options['curl'][CURLOPT_FOLLOWLOCATION] = false; // open_basedir set but no redirects to follow, we can disable followlocation and proceed normally @@ -405,8 +407,8 @@ class Response if (curl_errno($rch)) { $code = 0; } else { - $code = curl_getinfo($rch, CURLINFO_HTTP_CODE); - if ($code == 301 || $code == 302 || $code == 303) { + $code = (int)curl_getinfo($rch, CURLINFO_HTTP_CODE); + if ($code === 301 || $code === 302 || $code === 303) { preg_match('/Location:(.*?)\n/', $header, $matches); $uri = trim(array_pop($matches)); } else { diff --git a/system/src/Grav/Common/GPM/Upgrader.php b/system/src/Grav/Common/GPM/Upgrader.php index 1c995bc..5d9406d 100644 --- a/system/src/Grav/Common/GPM/Upgrader.php +++ b/system/src/Grav/Common/GPM/Upgrader.php @@ -1,8 +1,9 @@ min_php)) { + if (null === $this->min_php) { $this->min_php = $this->remote->getMinPHPVersion(); } return $this->min_php; @@ -125,7 +126,7 @@ class Upgrader */ public function isUpgradable() { - return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), "<"); + return version_compare($this->getLocalVersion(), $this->getRemoteVersion(), '<'); } /** diff --git a/system/src/Grav/Common/Getters.php b/system/src/Grav/Common/Getters.php index 6f58fba..e69116e 100644 --- a/system/src/Grav/Common/Getters.php +++ b/system/src/Grav/Common/Getters.php @@ -1,8 +1,9 @@ gettersVariable; return isset($this->{$var}[$offset]); - } else { - return isset($this->{$offset}); } + + return isset($this->{$offset}); } /** @@ -88,10 +89,10 @@ abstract class Getters implements \ArrayAccess, \Countable if ($this->gettersVariable) { $var = $this->gettersVariable; - return isset($this->{$var}[$offset]) ? $this->{$var}[$offset] : null; - } else { - return isset($this->{$offset}) ? $this->{$offset} : null; + return $this->{$var}[$offset] ?? null; } + + return $this->{$offset} ?? null; } /** @@ -128,10 +129,10 @@ abstract class Getters implements \ArrayAccess, \Countable { if ($this->gettersVariable) { $var = $this->gettersVariable; - count($this->{$var}); - } else { - count($this->toArray()); + return \count($this->{$var}); } + + return \count($this->toArray()); } /** @@ -145,16 +146,16 @@ abstract class Getters implements \ArrayAccess, \Countable $var = $this->gettersVariable; return $this->{$var}; - } else { - $properties = (array)$this; - $list = []; - foreach ($properties as $property => $value) { - if ($property[0] != "\0") { - $list[$property] = $value; - } - } - - return $list; } + + $properties = (array)$this; + $list = []; + foreach ($properties as $property => $value) { + if ($property[0] !== "\0") { + $list[$property] = $value; + } + } + + return $list; } } diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index 84260d0..009d57d 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -1,21 +1,48 @@ 'Grav\Common\Uri', - 'events' => 'RocketTheme\Toolbox\Event\EventDispatcher', - 'cache' => 'Grav\Common\Cache', - 'Grav\Common\Service\SessionServiceProvider', - 'plugins' => 'Grav\Common\Plugins', - 'themes' => 'Grav\Common\Themes', - 'twig' => 'Grav\Common\Twig\Twig', - 'taxonomy' => 'Grav\Common\Taxonomy', - 'language' => 'Grav\Common\Language\Language', - 'pages' => 'Grav\Common\Page\Pages', - 'Grav\Common\Service\TaskServiceProvider', + 'Grav\Common\Service\AccountsServiceProvider', 'Grav\Common\Service\AssetsServiceProvider', - 'Grav\Common\Service\PageServiceProvider', - 'Grav\Common\Service\OutputServiceProvider', - 'browser' => 'Grav\Common\Browser', - 'exif' => 'Grav\Common\Helpers\Exif', - 'Grav\Common\Service\StreamsServiceProvider', + 'Grav\Common\Service\BackupsServiceProvider', 'Grav\Common\Service\ConfigServiceProvider', - 'inflector' => 'Grav\Common\Inflector', - 'siteSetupProcessor' => 'Grav\Common\Processors\SiteSetupProcessor', - 'configurationProcessor' => 'Grav\Common\Processors\ConfigurationProcessor', - 'errorsProcessor' => 'Grav\Common\Processors\ErrorsProcessor', - 'debuggerInitProcessor' => 'Grav\Common\Processors\DebuggerInitProcessor', - 'initializeProcessor' => 'Grav\Common\Processors\InitializeProcessor', - 'pluginsProcessor' => 'Grav\Common\Processors\PluginsProcessor', - 'themesProcessor' => 'Grav\Common\Processors\ThemesProcessor', - 'tasksProcessor' => 'Grav\Common\Processors\TasksProcessor', - 'assetsProcessor' => 'Grav\Common\Processors\AssetsProcessor', - 'twigProcessor' => 'Grav\Common\Processors\TwigProcessor', - 'pagesProcessor' => 'Grav\Common\Processors\PagesProcessor', - 'debuggerAssetsProcessor' => 'Grav\Common\Processors\DebuggerAssetsProcessor', - 'renderProcessor' => 'Grav\Common\Processors\RenderProcessor', + 'Grav\Common\Service\ErrorServiceProvider', + 'Grav\Common\Service\FilesystemServiceProvider', + 'Grav\Common\Service\InflectorServiceProvider', + 'Grav\Common\Service\LoggerServiceProvider', + 'Grav\Common\Service\OutputServiceProvider', + 'Grav\Common\Service\PagesServiceProvider', + 'Grav\Common\Service\RequestServiceProvider', + 'Grav\Common\Service\SessionServiceProvider', + 'Grav\Common\Service\StreamsServiceProvider', + 'Grav\Common\Service\TaskServiceProvider', + 'browser' => 'Grav\Common\Browser', + 'cache' => 'Grav\Common\Cache', + 'events' => 'RocketTheme\Toolbox\Event\EventDispatcher', + 'exif' => 'Grav\Common\Helpers\Exif', + 'plugins' => 'Grav\Common\Plugins', + 'scheduler' => 'Grav\Common\Scheduler\Scheduler', + 'taxonomy' => 'Grav\Common\Taxonomy', + 'themes' => 'Grav\Common\Themes', + 'twig' => 'Grav\Common\Twig\Twig', + 'uri' => 'Grav\Common\Uri', ]; /** - * @var array All processors that are processed in $this->process() + * @var array All middleware processors that are processed in $this->process() */ - protected $processors = [ - 'siteSetupProcessor', + protected $middleware = [ 'configurationProcessor', + 'loggerProcessor', 'errorsProcessor', - 'debuggerInitProcessor', + 'debuggerProcessor', 'initializeProcessor', 'pluginsProcessor', 'themesProcessor', + 'requestProcessor', 'tasksProcessor', + 'backupsProcessor', + 'schedulerProcessor', 'assetsProcessor', 'twigProcessor', 'pagesProcessor', @@ -88,6 +108,8 @@ class Grav extends Container 'renderProcessor', ]; + protected $initialized = []; + /** * Reset the Grav instance. */ @@ -119,21 +141,116 @@ class Grav extends Container return self::$instance; } + /** + * Setup Grav instance using specific environment. + * + * Initializes Grav streams by + * + * @param string|null $environment + * @return $this + */ + public function setup(string $environment = null) + { + if (isset($this->initialized['setup'])) { + return $this; + } + + $this->initialized['setup'] = true; + + $this->measureTime('_setup', 'Site Setup', function () use ($environment) { + // Force environment if passed to the method. + if ($environment) { + Setup::$environment = $environment; + } + + $this['setup']; + $this['streams']; + }); + + return $this; + } + /** * Process a request */ public function process() { - // process all processors (e.g. config, initialize, assets, ..., render) - foreach ($this->processors as $processor) { - $processor = $this[$processor]; - $this->measureTime($processor->id, $processor->title, function () use ($processor) { - $processor->process(); - }); + if (isset($this->initialized['process'])) { + return; } + // Initialize Grav if needed. + $this->setup(); + + $this->initialized['process'] = true; + + $container = new Container( + [ + 'configurationProcessor' => function () { + return new ConfigurationProcessor($this); + }, + 'loggerProcessor' => function () { + return new LoggerProcessor($this); + }, + 'errorsProcessor' => function () { + return new ErrorsProcessor($this); + }, + 'debuggerProcessor' => function () { + return new DebuggerProcessor($this); + }, + 'initializeProcessor' => function () { + return new InitializeProcessor($this); + }, + 'backupsProcessor' => function () { + return new BackupsProcessor($this); + }, + 'pluginsProcessor' => function () { + return new PluginsProcessor($this); + }, + 'themesProcessor' => function () { + return new ThemesProcessor($this); + }, + 'schedulerProcessor' => function () { + return new SchedulerProcessor($this); + }, + 'requestProcessor' => function () { + return new RequestProcessor($this); + }, + 'tasksProcessor' => function () { + return new TasksProcessor($this); + }, + 'assetsProcessor' => function () { + return new AssetsProcessor($this); + }, + 'twigProcessor' => function () { + return new TwigProcessor($this); + }, + 'pagesProcessor' => function () { + return new PagesProcessor($this); + }, + 'debuggerAssetsProcessor' => function () { + return new DebuggerAssetsProcessor($this); + }, + 'renderProcessor' => function () { + return new RenderProcessor($this); + }, + ] + ); + + $default = function (ServerRequestInterface $request) { + return new Response(404); + }; + /** @var Debugger $debugger */ $debugger = $this['debugger']; + + $collection = new RequestHandler($this->middleware, $default, $container); + + $response = $collection->handle($this['request']); + + $this->header($response); + echo $response->getBody(); + $debugger->render(); register_shutdown_function([$this, 'shutdown']); @@ -147,7 +264,7 @@ class Grav extends Container // Initialize Locale if set and configured. if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) { $language = $this['language']->getLanguage(); - setlocale(LC_ALL, strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language); + setlocale(LC_ALL, \strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language); } elseif ($this['config']->get('system.default_locale')) { setlocale(LC_ALL, $this['config']->get('system.default_locale')); } @@ -213,53 +330,22 @@ class Grav extends Container /** * Set response header. + * + * @param ResponseInterface|null $response */ - public function header() + public function header(ResponseInterface $response = null) { - /** @var Page $page */ - $page = $this['page']; + if (null === $response) { + /** @var PageInterface $page */ + $page = $this['page']; + $response = new Response($page->httpResponseCode(), $page->httpHeaders(), ''); + } - $format = $page->templateFormat(); - - header('Content-type: ' . Utils::getMimeByExtension($format, 'text/html')); - - $cache_control = $page->cacheControl(); - - // Calculate Expires Headers if set to > 0 - $expires = $page->expires(); - - if ($expires > 0) { - $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT'; - if (!$cache_control) { - header('Cache-Control: max-age=' . $expires); + header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}"); + foreach ($response->getHeaders() as $key => $values) { + foreach ($values as $i => $value) { + header($key . ': ' . $value, $i === 0); } - header('Expires: ' . $expires_date); - } - - // Set cache-control header - if ($cache_control) { - header('Cache-Control: ' . strtolower($cache_control)); - } - - // Set the last modified time - if ($page->lastModified()) { - $last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT'; - header('Last-Modified: ' . $last_modified_date); - } - - // Calculate a Hash based on the raw file - if ($page->eTag()) { - header('ETag: "' . md5($page->raw() . $page->modified()).'"'); - } - - // Set HTTP response code - if (isset($this['page']->header()->http_response_code)) { - http_response_code($this['page']->header()->http_response_code); - } - - // Vary: Accept-Encoding - if ($this['config']->get('system.pages.vary_accept_encoding', false)) { - header('Vary: Accept-Encoding'); } } @@ -286,7 +372,7 @@ class Grav extends Container public function shutdown() { // Prevent user abort allowing onShutdown event to run without interruptions. - if (function_exists('ignore_user_abort')) { + if (\function_exists('ignore_user_abort')) { @ignore_user_abort(true); } @@ -300,7 +386,7 @@ class Grav extends Container // the connection to the client open. This will make page loads to feel much faster. // FastCGI allows us to flush all response data to the client and finish the request. - $success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false; + $success = \function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false; if (!$success) { // Unfortunately without FastCGI there is no way to force close the connection. @@ -323,7 +409,7 @@ class Grav extends Container // Get length and close the connection. header('Content-Length: ' . ob_get_length()); - header("Connection: close"); + header('Connection: close'); ob_end_flush(); @ob_flush(); @@ -337,13 +423,33 @@ class Grav extends Container /** * Magic Catch All Function - * Used to call closures like measureTime on the instance. + * + * Used to call closures. + * * Source: http://stackoverflow.com/questions/419804/closures-as-class-members */ public function __call($method, $args) { - $closure = $this->$method; - call_user_func_array($closure, $args); + $closure = $this->{$method}; + \call_user_func_array($closure, $args); + } + + /** + * Measure how long it takes to do an action. + * + * @param string $timerId + * @param string $timerTitle + * @param callable $callback + * @return mixed Returns value returned by the callable. + */ + public function measureTime(string $timerId, string $timerTitle, callable $callback) + { + $debugger = $this['debugger']; + $debugger->startTimer($timerId, $timerTitle); + $result = $callback(); + $debugger->stopTimer($timerId); + + return $result; } /** @@ -357,22 +463,15 @@ class Grav extends Container { $container = new static($values); - $container['grav'] = $container; - $container['debugger'] = new Debugger(); - $debugger = $container['debugger']; + $container['grav'] = function (Container $container) { + user_error('Calling $grav[\'grav\'] or {{ grav.grav }} is deprecated since Grav 1.6, just use $grav or {{ grav }}', E_USER_DEPRECATED); - // closure that measures time by wrapping a function into startTimer and stopTimer - // The debugger can be passed to the closure. Should be more performant - // then to get it from the container all time. - $container->measureTime = function ($timerId, $timerTitle, $callback) use ($debugger) { - $debugger->startTimer($timerId, $timerTitle); - $callback(); - $debugger->stopTimer($timerId); + return $container; }; $container->measureTime('_services', 'Services', function () use ($container) { - $container->registerServices($container); + $container->registerServices(); }); return $container; @@ -389,45 +488,20 @@ class Grav extends Container protected function registerServices() { foreach (self::$diMap as $serviceKey => $serviceClass) { - if (is_int($serviceKey)) { - $this->registerServiceProvider($serviceClass); + if (\is_int($serviceKey)) { + $this->register(new $serviceClass); } else { - $this->registerService($serviceKey, $serviceClass); + $this[$serviceKey] = function ($c) use ($serviceClass) { + return new $serviceClass($c); + }; } } } - /** - * Register a service provider with the container. - * - * @param string $serviceClass - * - * @return void - */ - protected function registerServiceProvider($serviceClass) - { - $this->register(new $serviceClass); - } - - /** - * Register a service with the container. - * - * @param string $serviceKey - * @param string $serviceClass - * - * @return void - */ - protected function registerService($serviceKey, $serviceClass) - { - $this[$serviceKey] = function ($c) use ($serviceClass) { - return new $serviceClass($c); - }; - } - /** * This attempts to find media, other files, and download them * - * @param $path + * @param string $path */ public function fallbackUrl($path) { @@ -453,7 +527,7 @@ class Grav extends Container $path_parts = pathinfo($path); - /** @var Page $page */ + /** @var PageInterface $page */ $page = $this['pages']->dispatch($path_parts['dirname'], true); if ($page) { @@ -466,8 +540,8 @@ class Grav extends Container /** @var Medium $medium */ $medium = $media[$media_file]; foreach ($uri->query(null, true) as $action => $params) { - if (in_array($action, ImageMedium::$magic_actions)) { - call_user_func_array([&$medium, $action], explode(',', $params)); + if (\in_array($action, ImageMedium::$magic_actions, true)) { + \call_user_func_array([&$medium, $action], explode(',', $params)); } } Utils::download($medium->path(), false); @@ -486,7 +560,7 @@ class Grav extends Container if ($extension) { $download = true; - if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []))) { + if (\in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) { $download = false; } Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download); diff --git a/system/src/Grav/Common/GravTrait.php b/system/src/Grav/Common/GravTrait.php index a17a170..a82c23a 100644 --- a/system/src/Grav/Common/GravTrait.php +++ b/system/src/Grav/Common/GravTrait.php @@ -1,15 +1,16 @@ ', '?' 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G' @@ -22,27 +23,31 @@ class Base32 { 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' - ); + ]; /** * Encode in Base32 * - * @param $bytes + * @param string $bytes * @return string */ - public static function encode( $bytes ) { - $i = 0; $index = 0; $digit = 0; + public static function encode($bytes) + { + $i = 0; $index = 0; $base32 = ''; - $bytes_len = strlen($bytes); - while( $i < $bytes_len ) { - $currByte = ord($bytes{$i}); + $bytesLen = \strlen($bytes); + + while ($i < $bytesLen) { + $currByte = \ord($bytes[$i]); + /* Is the current digit going to span a byte boundary? */ - if( $index > 3 ) { - if( ($i + 1) < $bytes_len ) { - $nextByte = ord($bytes{$i+1}); + if ($index > 3) { + if (($i + 1) < $bytesLen) { + $nextByte = \ord($bytes[$i+1]); } else { $nextByte = 0; } + $digit = $currByte & (0xFF >> $index); $index = ($index + 5) % 8; $digit <<= $index; @@ -51,9 +56,12 @@ class Base32 { } else { $digit = ($currByte >> (8 - ($index + 5))) & 0x1F; $index = ($index + 5) % 8; - if( $index === 0 ) $i++; + if ($index === 0) { + $i++; + } } - $base32 .= self::$base32Chars{$digit}; + + $base32 .= self::$base32Chars[$digit]; } return $base32; } @@ -61,30 +69,42 @@ class Base32 { /** * Decode in Base32 * - * @param $base32 + * @param string $base32 * @return string */ - public static function decode( $base32 ) { - $bytes = array(); - $base32_len = strlen($base32); - for( $i=$base32_len*5/8-1; $i>=0; --$i ) { + public static function decode($base32) + { + $bytes = []; + $base32Len = \strlen($base32); + $base32LookupLen = \count(self::$base32Lookup); + + for ($i = $base32Len * 5 / 8 - 1; $i >= 0; --$i) { $bytes[] = 0; } - for( $i = 0, $index = 0, $offset = 0; $i < $base32_len; $i++ ) { - $lookup = ord($base32{$i}) - ord('0'); + + for ($i = 0, $index = 0, $offset = 0; $i < $base32Len; $i++) { + $lookup = \ord($base32[$i]) - \ord('0'); + /* Skip chars outside the lookup table */ - if( $lookup < 0 || $lookup >= count(self::$base32Lookup) ) { + if ($lookup < 0 || $lookup >= $base32LookupLen) { continue; } + $digit = self::$base32Lookup[$lookup]; + /* If this digit is not in the table, ignore it */ - if( $digit == 0xFF ) continue; - if( $index <= 3 ) { + if ($digit === 0xFF) { + continue; + } + + if ($index <= 3) { $index = ($index + 5) % 8; - if( $index == 0) { + if ($index === 0) { $bytes[$offset] |= $digit; $offset++; - if( $offset >= count($bytes) ) break; + if ($offset >= \count($bytes)) { + break; + } } else { $bytes[$offset] |= $digit << (8 - $index); } @@ -92,12 +112,18 @@ class Base32 { $index = ($index + 5) % 8; $bytes[$offset] |= ($digit >> $index); $offset++; - if ($offset >= count($bytes) ) break; + if ($offset >= \count($bytes)) { + break; + } $bytes[$offset] |= $digit << (8 - $index); } } + $bites = ''; - foreach( $bytes as $byte ) $bites .= chr($byte); + foreach ($bytes as $byte) { + $bites .= \chr($byte); + } + return $bites; } } diff --git a/system/src/Grav/Common/Helpers/Excerpts.php b/system/src/Grav/Common/Helpers/Excerpts.php index 53faaf4..f07c3aa 100644 --- a/system/src/Grav/Common/Helpers/Excerpts.php +++ b/system/src/Grav/Common/Helpers/Excerpts.php @@ -1,31 +1,28 @@ ` - * @param Page $page The current page object - * @return string Returns final HTML string + * @param string $html HTML tag e.g. `` + * @param PageInterface|null $page Page, defaults to the current page object + * @return string Returns final HTML string */ - public static function processImageHtml($html, Page $page) + public static function processImageHtml($html, PageInterface $page = null) { $excerpt = static::getExcerptFromHtml($html, 'img'); @@ -79,7 +76,7 @@ class Excerpts /** * Rebuild HTML tag from an excerpt array * - * @param $excerpt + * @param array $excerpt * @return string */ public static function getHtmlFromExcerpt($excerpt) @@ -110,253 +107,44 @@ class Excerpts /** * Process a Link excerpt * - * @param $excerpt - * @param Page $page + * @param array $excerpt + * @param PageInterface|null $page Page, defaults to the current page object * @param string $type * @return mixed */ - public static function processLinkExcerpt($excerpt, Page $page, $type = 'link') + public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link') { - $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); + $excerpts = new ExcerptsObject($page); - $url_parts = static::parseUrl($url); - - // If there is a query, then parse it and build action calls. - if (isset($url_parts['query'])) { - $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) { - $parts = explode('=', $item, 2); - $value = isset($parts[1]) ? rawurldecode($parts[1]) : true; - $carry[$parts[0]] = $value; - - return $carry; - }, []); - - // Valid attributes supported. - $valid_attributes = ['rel', 'target', 'id', 'class', 'classes']; - - // Unless told to not process, go through actions. - if (array_key_exists('noprocess', $actions)) { - unset($actions['noprocess']); - } else { - // Loop through actions for the image and call them. - foreach ($actions as $attrib => $value) { - $key = $attrib; - - if (in_array($attrib, $valid_attributes, true)) { - // support both class and classes. - if ($attrib === 'classes') { - $attrib = 'class'; - } - $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); - unset($actions[$key]); - } - } - } - - $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986); - } - - // If no query elements left, unset query. - if (empty($url_parts['query'])) { - unset ($url_parts['query']); - } - - // Set path to / if not set. - if (empty($url_parts['path'])) { - $url_parts['path'] = ''; - } - - // If scheme isn't http(s).. - if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) { - // Handle custom streams. - if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) { - $url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}"); - unset($url_parts['stream'], $url_parts['scheme']); - } - - $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); - return $excerpt; - } - - // Handle paths and such. - $url_parts = Uri::convertUrl($page, $url_parts, $type); - - // Build the URL from the component parts and set it on the element. - $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); - - return $excerpt; + return $excerpts->processLinkExcerpt($excerpt, $type); } /** * Process an image excerpt * * @param array $excerpt - * @param Page $page - * @return mixed + * @param PageInterface|null $page Page, defaults to the current page object + * @return array */ - public static function processImageExcerpt(array $excerpt, Page $page) + public static function processImageExcerpt(array $excerpt, PageInterface $page = null) { - $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); - $url_parts = static::parseUrl($url); + $excerpts = new ExcerptsObject($page); - $media = null; - $filename = null; - - if (!empty($url_parts['stream'])) { - $filename = $url_parts['scheme'] . '://' . (isset($url_parts['path']) ? $url_parts['path'] : ''); - - $media = $page->media(); - - } else { - // File is also local if scheme is http(s) and host matches. - $local_file = isset($url_parts['path']) - && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true)) - && (empty($url_parts['host']) || $url_parts['host'] === Grav::instance()['uri']->host()); - - if ($local_file) { - $filename = basename($url_parts['path']); - $folder = dirname($url_parts['path']); - - // Get the local path to page media if possible. - if ($folder === $page->url(false, false, false)) { - // Get the media objects for this page. - $media = $page->media(); - } else { - // see if this is an external page to this one - $base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/'); - $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/'); - - /** @var Page $ext_page */ - $ext_page = Grav::instance()['pages']->dispatch($page_route, true); - if ($ext_page) { - $media = $ext_page->media(); - } else { - Grav::instance()->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media])); - } - } - } - } - - // If there is a media file that matches the path referenced.. - if ($media && $filename && isset($media[$filename])) { - // Get the medium object. - /** @var Medium $medium */ - $medium = $media[$filename]; - - // Process operations - $medium = static::processMediaActions($medium, $url_parts); - $element_excerpt = $excerpt['element']['attributes']; - - $alt = isset($element_excerpt['alt']) ? $element_excerpt['alt'] : ''; - $title = isset($element_excerpt['title']) ? $element_excerpt['title'] : ''; - $class = isset($element_excerpt['class']) ? $element_excerpt['class'] : ''; - $id = isset($element_excerpt['id']) ? $element_excerpt['id'] : ''; - - $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true); - - } else { - // Not a current page media file, see if it needs converting to relative. - $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts); - } - - return $excerpt; + return $excerpts->processImageExcerpt($excerpt); } /** * Process media actions * - * @param $medium - * @param $url - * @return mixed + * @param Medium $medium + * @param string|array $url + * @param PageInterface|null $page Page, defaults to the current page object + * @return Medium */ - public static function processMediaActions($medium, $url) + public static function processMediaActions($medium, $url, PageInterface $page = null) { - if (!is_array($url)) { - $url_parts = parse_url($url); - } else { - $url_parts = $url; - } + $excerpts = new ExcerptsObject($page); - $actions = []; - - // if there is a query, then parse it and build action calls - if (isset($url_parts['query'])) { - $actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) { - $parts = explode('=', $item, 2); - $value = isset($parts[1]) ? $parts[1] : null; - $carry[] = ['method' => $parts[0], 'params' => $value]; - - return $carry; - }, []); - } - - if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) { - $actions[] = ['method' => 'fixOrientation', 'params' => '']; - } - $defaults = Grav::instance()['config']->get('system.images.defaults'); - if (is_array($defaults) && count($defaults)) { - foreach ($defaults as $method => $params) { - $actions[] = [ - 'method' => $method, - 'params' => $params, - ]; - } - } - - // loop through actions for the image and call them - foreach ($actions as $action) { - $matches = []; - - if (preg_match('/\[(.*)\]/', $action['params'], $matches)) { - $args = [explode(',', $matches[1])]; - } else { - $args = explode(',', $action['params']); - } - - $medium = call_user_func_array([$medium, $action['method']], $args); - } - - if (isset($url_parts['fragment'])) { - $medium->urlHash($url_parts['fragment']); - } - - return $medium; - } - - /** - * Variation of parse_url() which works also with local streams. - * - * @param string $url - * @return array|bool - */ - protected static function parseUrl($url) - { - $url_parts = Utils::multibyteParseUrl($url); - - if (isset($url_parts['scheme'])) { - /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; - - // Special handling for the streams. - if ($locator->schemeExists($url_parts['scheme'])) { - if (isset($url_parts['host'])) { - // Merge host and path into a path. - $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : ''); - unset($url_parts['host']); - } - - $url_parts['stream'] = true; - } - } - - return $url_parts; - } - - protected static function resolveStream($url) - { - /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; - - return $locator->isStream($url) ? ($locator->findResource($url, false) ?: $locator->findResource($url, false, true)) : $url; + return $excerpts->processMediaActions($medium, $url); } } diff --git a/system/src/Grav/Common/Helpers/Exif.php b/system/src/Grav/Common/Helpers/Exif.php index ee0c4ca..15aff66 100644 --- a/system/src/Grav/Common/Helpers/Exif.php +++ b/system/src/Grav/Common/Helpers/Exif.php @@ -1,15 +1,15 @@ .*)\] (?P\w+).(?P\w+): (?P.*[^ ]+) (?P[^ ]+) (?P[^ ]+)/'; + + /** + * Get the objects of a tailed file + * + * @param string $filepath + * @param int $lines + * @param bool $desc + * @return array + */ + public function objectTail($filepath, $lines = 1, $desc = true) + { + $data = $this->tail($filepath, $lines); + $tailed_log = explode(PHP_EOL, $data); + $line_objects = []; + + foreach ($tailed_log as $line) { + $line_objects[] = $this->parse($line); + } + + return $desc ? $line_objects : array_reverse($line_objects); + } + + /** + * Optimized way to get just the last few entries of a log file + * + * @param string $filepath + * @param int $lines + * @return bool|string + */ + public function tail($filepath, $lines = 1) { + + $f = @fopen($filepath, "rb"); + if ($f === false) return false; + + else $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096)); + + fseek($f, -1, SEEK_END); + if (fread($f, 1) != "\n") $lines -= 1; + + // Start reading + $output = ''; + $chunk = ''; + // While we would like more + while (ftell($f) > 0 && $lines >= 0) { + // Figure out how far back we should jump + $seek = min(ftell($f), $buffer); + // 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; + // Jump back to where we started reading + fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); + // Decrease our line counter + $lines -= substr_count($chunk, "\n"); + } + // While we have too many lines + // (Because of buffer size we might have read too many) + while ($lines++ < 0) { + // Find first newline and remove all text before that + $output = substr($output, strpos($output, "\n") + 1); + } + // Close file and return + fclose($f); + + return trim($output); + } + + /** + * Helper class to get level color + * + * @param string $level + * @return mixed|string + */ + public static function levelColor($level) + { + $colors = [ + 'DEBUG' => 'green', + 'INFO' => 'cyan', + 'NOTICE' => 'yellow', + 'WARNING' => 'yellow', + 'ERROR' => 'red', + 'CRITICAL' => 'red', + 'ALERT' => 'red', + 'EMERGENCY' => 'magenta' + ]; + return $colors[$level] ?? 'white'; + } + + /** + * Parse a monolog row into array bits + * + * @param string $line + * @return array + */ + public function parse($line) + { + if( !is_string($line) || strlen($line) === 0) { + return array(); + } + preg_match($this->pattern, $line, $data); + if (!isset($data['date'])) { + return array(); + } + + preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches); + if (is_array($matches) && isset($matches[1])) { + $data['message'] = trim($matches[1]); + $data['trace'] = trim($matches[2]); + } + + return array( + 'date' => \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']), + 'logger' => $data['logger'], + 'level' => $data['level'], + 'message' => $data['message'], + 'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null, + 'context' => json_decode($data['context'], true), + 'extra' => json_decode($data['extra'], true) + ); + } + + /** + * Parse text of trace into an array of lines + * + * @param string $trace + * @param int $rows + * @return array + */ + public static function parseTrace($trace, $rows = 10) + { + $lines = array_filter(preg_split('/#\d*/m', $trace)); + return array_slice($lines, 0, $rows); + } + +} diff --git a/system/src/Grav/Common/Helpers/Truncator.php b/system/src/Grav/Common/Helpers/Truncator.php index 474aae1..d116bb3 100644 --- a/system/src/Grav/Common/Helpers/Truncator.php +++ b/system/src/Grav/Common/Helpers/Truncator.php @@ -1,8 +1,9 @@ getElementsByTagName("body")->item(0); + $doc = self::htmlToDomDocument($html); + $container = $doc->getElementsByTagName('div')->item(0); + $container = $container->parentNode->removeChild($container); // Iterate over words. - $words = new DOMWordsIterator($body); + $words = new DOMWordsIterator($container); $truncated = false; foreach ($words as $word) { @@ -65,7 +65,7 @@ class Truncator { $words[$offset][1] + strlen($words[$offset][0]) ); - self::removeProceedingNodes($curNode, $body); + self::removeProceedingNodes($curNode, $container); if (!empty($ellipsis)) { self::insertEllipsis($curNode, $ellipsis); @@ -80,32 +80,31 @@ class Truncator { // Return original HTML if not truncated. if ($truncated) { - return self::innerHTML($body); - } else { - return $html; + $html = self::getCleanedHtml($doc, $container); } + + return $html; } /** * Safely truncates HTML by a given number of letters. * @param string $html Input HTML. - * @param integer $limit Limit to how many letters we preserve. + * @param int $limit Limit to how many letters we preserve. * @param string $ellipsis String to use as ellipsis (if any). * @return string Safe truncated HTML. */ - public static function truncateLetters($html, $limit = 0, $ellipsis = "") + public static function truncateLetters($html, $limit = 0, $ellipsis = '') { if ($limit <= 0) { return $html; } - $dom = self::htmlToDomDocument($html); - - // Grab the body of our DOM. - $body = $dom->getElementsByTagName('body')->item(0); + $doc = self::htmlToDomDocument($html); + $container = $doc->getElementsByTagName('div')->item(0); + $container = $container->parentNode->removeChild($container); // Iterate over letters. - $letters = new DOMLettersIterator($body); + $letters = new DOMLettersIterator($container); $truncated = false; foreach ($letters as $letter) { @@ -114,7 +113,7 @@ class Truncator { $currentText = $letters->currentTextPosition(); $currentText[0]->nodeValue = mb_substr($currentText[0]->nodeValue, 0, $currentText[1] + 1); - self::removeProceedingNodes($currentText[0], $body); + self::removeProceedingNodes($currentText[0], $container); if (!empty($ellipsis)) { self::insertEllipsis($currentText[0], $ellipsis); @@ -128,10 +127,10 @@ class Truncator { // Return original HTML if not truncated. if ($truncated) { - return self::innerHTML($body); - } else { - return $html; + $html = self::getCleanedHtml($doc, $container); } + + return $html; } /** @@ -142,7 +141,7 @@ class Truncator { public static function htmlToDomDocument($html) { if (!$html) { - $html = '

'; + $html = ''; } // Transform multibyte entities which otherwise display incorrectly. @@ -154,7 +153,7 @@ class Truncator { // Instantiate new DOMDocument object, and then load in UTF-8 HTML. $dom = new DOMDocument(); $dom->encoding = 'UTF-8'; - $dom->loadHTML($html); + $dom->loadHTML("
$html
"); return $dom; } @@ -187,6 +186,27 @@ class Truncator { } } + /** + * Clean extra code + * + * @param DOMDocument $doc + * @param $container + * @return string + */ + private static function getCleanedHTML(DOMDocument $doc, $container) + { + while ($doc->firstChild) { + $doc->removeChild($doc->firstChild); + } + + while ($container->firstChild ) { + $doc->appendChild($container->firstChild); + } + + $html = trim($doc->saveHTML()); + return $html; + } + /** * Inserts an ellipsis * @param DOMNode|DOMElement $domNode Element to insert after. @@ -214,21 +234,102 @@ class Truncator { } /** - * Returns the innerHTML of a particular DOMElement * - * @param $element - * @return string */ - private static function innerHTML($element) { - $innerHTML = ''; - $children = $element->childNodes; - foreach ($children as $child) - { - $tmp_dom = new DOMDocument(); - $tmp_dom->appendChild($tmp_dom->importNode($child, true)); - $innerHTML.=trim($tmp_dom->saveHTML()); + public function truncate( + $text, + $length = 100, + $ending = '...', + $exact = false, + $considerHtml = true + ) { + if ($considerHtml) { + // if the plain text is shorter than the maximum length, return the whole text + if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) { + return $text; + } + // splits all html-tags to scanable lines + preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); + $total_length = strlen($ending); + $open_tags = array(); + $truncate = ''; + foreach ($lines as $line_matchings) { + // if there is any html-tag in this line, handle it and add it (uncounted) to the output + if (!empty($line_matchings[1])) { + // if it's an "empty element" with or without xhtml-conform closing slash + if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is', $line_matchings[1])) { + // do nothing + // if tag is a closing tag + } else if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) { + // delete tag from $open_tags list + $pos = array_search($tag_matchings[1], $open_tags); + if ($pos !== false) { + unset($open_tags[$pos]); + } + // if tag is an opening tag + } else if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) { + // add tag to the beginning of $open_tags list + array_unshift($open_tags, strtolower($tag_matchings[1])); + } + // add html-tag to $truncate'd text + $truncate .= $line_matchings[1]; + } + // calculate the length of the plain text part of the line; handle entities as one character + $content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ', $line_matchings[2])); + if ($total_length+$content_length> $length) { + // the number of characters which are left + $left = $length - $total_length; + $entities_length = 0; + // search for html entities + if (preg_match_all('/&[0-9a-z]{2,8};|&#[0-9]{1,7};|[0-9a-f]{1,6};/i', $line_matchings[2], $entities, PREG_OFFSET_CAPTURE)) { + // calculate the real length of all entities in the legal range + foreach ($entities[0] as $entity) { + if ($entity[1]+1-$entities_length <= $left) { + $left--; + $entities_length += strlen($entity[0]); + } else { + // no more characters left + break; + } + } + } + $truncate .= substr($line_matchings[2], 0, $left+$entities_length); + // maximum lenght is reached, so get off the loop + break; + } else { + $truncate .= $line_matchings[2]; + $total_length += $content_length; + } + // if the maximum length is reached, get off the loop + if($total_length>= $length) { + break; + } + } + } else { + if (strlen($text) <= $length) { + return $text; + } else { + $truncate = substr($text, 0, $length - strlen($ending)); + } } - return $innerHTML; + // if the words shouldn't be cut in the middle... + if (!$exact) { + // ...search the last occurance of a space... + $spacepos = strrpos($truncate, ' '); + if (isset($spacepos)) { + // ...and cut the text in this position + $truncate = substr($truncate, 0, $spacepos); + } + } + // add the defined ending to the text + $truncate .= $ending; + if($considerHtml) { + // close all unclosed html-tags + foreach ($open_tags as $tag) { + $truncate .= ''; + } + } + return $truncate; } } diff --git a/system/src/Grav/Common/Helpers/YamlLinter.php b/system/src/Grav/Common/Helpers/YamlLinter.php new file mode 100644 index 0000000..b7e608d --- /dev/null +++ b/system/src/Grav/Common/Helpers/YamlLinter.php @@ -0,0 +1,89 @@ +get('system.pages.theme'); + $theme_path = 'themes://' . $current_theme . '/blueprints'; + + $locator->addPath('blueprints', '', [$theme_path]); + return static::recurseFolder('blueprints://'); + } + + public static function recurseFolder($path, $extensions = 'md|yaml') + { + $lint_errors = []; + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + $flags = \RecursiveDirectoryIterator::SKIP_DOTS; + if ($locator->isStream($path)) { + $directory = $locator->getRecursiveIterator($path, $flags); + } else { + $directory = new \RecursiveDirectoryIterator($path, $flags); + } + $recursive = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST); + $iterator = new \RegexIterator($recursive, '/^.+\.'.$extensions.'$/i'); + + /** @var \RecursiveDirectoryIterator $file */ + foreach ($iterator as $filepath => $file) { + try { + Yaml::parse(static::extractYaml($filepath)); + } catch (\Exception $e) { + $lint_errors[str_replace(GRAV_ROOT, '', $filepath)] = $e->getMessage(); + } + } + + return $lint_errors; + } + + protected static function extractYaml($path) + { + $extension = pathinfo($path, PATHINFO_EXTENSION); + if ($extension === 'md') { + $file = MarkdownFile::instance($path); + $contents = $file->frontmatter(); + } else { + $contents = file_get_contents($path); + } + return $contents; + } + +} diff --git a/system/src/Grav/Common/Inflector.php b/system/src/Grav/Common/Inflector.php index 61c2338..a24bbfd 100644 --- a/system/src/Grav/Common/Inflector.php +++ b/system/src/Grav/Common/Inflector.php @@ -1,36 +1,35 @@ plural)) { + if (empty(static::$plural)) { $language = Grav::instance()['language']; - $this->plural = $language->translate('INFLECTOR_PLURALS', null, true) ?: []; - $this->singular = $language->translate('INFLECTOR_SINGULAR', null, true) ?: []; - $this->uncountable = $language->translate('INFLECTOR_UNCOUNTABLE', null, true) ?: []; - $this->irregular = $language->translate('INFLECTOR_IRREGULAR', null, true) ?: []; - $this->ordinals = $language->translate('INFLECTOR_ORDINALS', null, true) ?: []; + static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true) ?: []; + static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true) ?: []; + static::$uncountable = $language->translate('GRAV.INFLECTOR_UNCOUNTABLE', null, true) ?: []; + static::$irregular = $language->translate('GRAV.INFLECTOR_IRREGULAR', null, true) ?: []; + static::$ordinals = $language->translate('GRAV.INFLECTOR_ORDINALS', null, true) ?: []; } } @@ -42,29 +41,29 @@ class Inflector * * @return string Plural noun */ - public function pluralize($word, $count = 2) + public static function pluralize($word, $count = 2) { - $this->init(); + static::init(); - if ($count == 1) { + if ((int)$count === 1) { return $word; } $lowercased_word = strtolower($word); - foreach ($this->uncountable as $_uncountable) { - if (substr($lowercased_word, (-1 * strlen($_uncountable))) == $_uncountable) { + foreach (static::$uncountable as $_uncountable) { + if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) { return $word; } } - foreach ($this->irregular as $_plural => $_singular) { + foreach (static::$irregular as $_plural => $_singular) { if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) { return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word); } } - foreach ($this->plural as $rule => $replacement) { + foreach (static::$plural as $rule => $replacement) { if (preg_match($rule, $word)) { return preg_replace($rule, $replacement, $word); } @@ -82,28 +81,28 @@ class Inflector * * @return string Singular noun. */ - public function singularize($word, $count = 1) + public static function singularize($word, $count = 1) { - $this->init(); + static::init(); - if ($count != 1) { + if ((int)$count !== 1) { return $word; } $lowercased_word = strtolower($word); - foreach ($this->uncountable as $_uncountable) { - if (substr($lowercased_word, (-1 * strlen($_uncountable))) == $_uncountable) { + foreach (static::$uncountable as $_uncountable) { + if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) { return $word; } } - foreach ($this->irregular as $_plural => $_singular) { + foreach (static::$irregular as $_plural => $_singular) { if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) { return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word); } } - foreach ($this->singular as $rule => $replacement) { + foreach (static::$singular as $rule => $replacement) { if (preg_match($rule, $word)) { return preg_replace($rule, $replacement, $word); } @@ -129,11 +128,11 @@ class Inflector * * @return string Text formatted as title */ - public function titleize($word, $uppercase = '') + public static function titleize($word, $uppercase = '') { - $uppercase = $uppercase == 'first' ? 'ucfirst' : 'ucwords'; + $uppercase = $uppercase === 'first' ? 'ucfirst' : 'ucwords'; - return $uppercase($this->humanize($this->underscorize($word))); + return $uppercase(static::humanize(static::underscorize($word))); } /** @@ -149,7 +148,7 @@ class Inflector * * @return string UpperCamelCasedWord */ - public function camelize($word) + public static function camelize($word) { return str_replace(' ', '', ucwords(preg_replace('/[^A-Z^a-z^0-9]+/', ' ', $word))); } @@ -166,7 +165,7 @@ class Inflector * * @return string Underscored word */ - public function underscorize($word) + public static function underscorize($word) { $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', $word); $regex2 = preg_replace('/([a-zd])([A-Z])/', '\1_\2', $regex1); @@ -187,7 +186,7 @@ class Inflector * * @return string hyphenized word */ - public function hyphenize($word) + public static function hyphenize($word) { $regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word); $regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', $regex1); @@ -213,9 +212,9 @@ class Inflector * * @return string Human-readable word */ - public function humanize($word, $uppercase = '') + public static function humanize($word, $uppercase = '') { - $uppercase = $uppercase == 'all' ? 'ucwords' : 'ucfirst'; + $uppercase = $uppercase === 'all' ? 'ucwords' : 'ucfirst'; return $uppercase(str_replace('_', ' ', preg_replace('/_id$/', '', $word))); } @@ -233,9 +232,9 @@ class Inflector * * @return string Returns a lowerCamelCasedWord */ - public function variablize($word) + public static function variablize($word) { - $word = $this->camelize($word); + $word = static::camelize($word); return strtolower($word[0]) . substr($word, 1); } @@ -252,9 +251,9 @@ class Inflector * * @return string plural_table_name */ - public function tableize($class_name) + public static function tableize($class_name) { - return $this->pluralize($this->underscorize($class_name)); + return static::pluralize(static::underscorize($class_name)); } /** @@ -269,9 +268,9 @@ class Inflector * * @return string SingularClassName */ - public function classify($table_name) + public static function classify($table_name) { - return $this->camelize($this->singularize($table_name)); + return static::camelize(static::singularize($table_name)); } /** @@ -279,31 +278,27 @@ class Inflector * * This method converts 13 to 13th, 2 to 2nd ... * - * @param integer $number Number to get its ordinal value + * @param int $number Number to get its ordinal value * * @return string Ordinal representation of given string. */ - public function ordinalize($number) + public static function ordinalize($number) { - $this->init(); + static::init(); - if (in_array(($number % 100), range(11, 13))) { - return $number . $this->ordinals['default']; - } else { - switch (($number % 10)) { - case 1: - return $number . $this->ordinals['first']; - break; - case 2: - return $number . $this->ordinals['second']; - break; - case 3: - return $number . $this->ordinals['third']; - break; - default: - return $number . $this->ordinals['default']; - break; - } + if (\in_array($number % 100, range(11, 13), true)) { + return $number . static::$ordinals['default']; + } + + switch ($number % 10) { + case 1: + return $number . static::$ordinals['first']; + case 2: + return $number . static::$ordinals['second']; + case 3: + return $number . static::$ordinals['third']; + default: + return $number . static::$ordinals['default']; } } @@ -314,7 +309,7 @@ class Inflector * * @return int */ - public function monthize($days) + public static function monthize($days) { $now = new \DateTime(); $end = new \DateTime(); @@ -325,7 +320,7 @@ class Inflector // handle years if ($diff->y > 0) { - $diff->m = $diff->m + 12 * $diff->y; + $diff->m += 12 * $diff->y; } return $diff->m; diff --git a/system/src/Grav/Common/Iterator.php b/system/src/Grav/Common/Iterator.php index 99fc0c4..5f6dc7e 100644 --- a/system/src/Grav/Common/Iterator.php +++ b/system/src/Grav/Common/Iterator.php @@ -1,8 +1,9 @@ items[$key])) ? $this->items[$key] : null; + return $this->items[$key] ?? null; } /** @@ -43,8 +44,8 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable public function __clone() { foreach ($this as $key => $value) { - if (is_object($value)) { - $this->$key = clone $this->$key; + if (\is_object($value)) { + $this->{$key} = clone $this->{$key}; } } } @@ -62,7 +63,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable /** * Remove item from the list. * - * @param $key + * @param string $key */ public function remove($key) { @@ -90,7 +91,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable { $items = array_keys($this->items); - return (isset($items[$key])) ? $this->offsetGet($items[$key]) : false; + return isset($items[$key]) ? $this->offsetGet($items[$key]) : false; } /** @@ -175,7 +176,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable */ public function slice($offset, $length = null) { - $this->items = array_slice($this->items, $offset, $length); + $this->items = \array_slice($this->items, $offset, $length); return $this; } @@ -189,8 +190,9 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable */ public function random($num = 1) { - if ($num > count($this->items)) { - $num = count($this->items); + $count = \count($this->items); + if ($num > $count) { + $num = $count; } $this->items = array_intersect_key($this->items, array_flip((array)array_rand($this->items, $num))); @@ -227,8 +229,8 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable { foreach ($this->items as $key => $value) { if ( - ($callback && !call_user_func($callback, $value, $key)) || - (!$callback && !(bool)$value) + (!$callback && !(bool)$value) || + ($callback && !$callback($value, $key)) ) { unset($this->items[$key]); } @@ -251,7 +253,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable */ public function sort(callable $callback = null, $desc = false) { - if (!$callback || !is_callable($callback)) { + if (!$callback || !\is_callable($callback)) { return $this; } diff --git a/system/src/Grav/Common/Language/Language.php b/system/src/Grav/Common/Language/Language.php index 076d103..a625146 100644 --- a/system/src/Grav/Common/Language/Language.php +++ b/system/src/Grav/Common/Language/Language.php @@ -1,8 +1,9 @@ default = reset($this->languages); + $default = $this->config->get('system.languages.default_lang'); + if (isset($default) && $this->validate($default)) { + $this->default = $default; + } else { + $this->default = reset($this->languages); + } + + $this->page_extensions = null; if (empty($this->languages)) { $this->enabled = false; @@ -75,7 +88,7 @@ class Language /** * Sets the current supported languages manually * - * @param $langs + * @param array $langs */ public function setLanguages($langs) { @@ -91,18 +104,24 @@ class Language public function getAvailable() { $languagesArray = $this->languages; //Make local copy + + $languagesArray = array_map(function($value) { + return preg_quote($value); + }, $languagesArray); + sort($languagesArray); + return implode('|', array_reverse($languagesArray)); } /** * Gets language, active if set, else default * - * @return mixed + * @return string */ public function getLanguage() { - return $this->active ? $this->active : $this->default; + return $this->active ?: $this->default; } /** @@ -118,7 +137,7 @@ class Language /** * Sets default language manually * - * @param $lang + * @param string $lang * * @return bool */ @@ -136,7 +155,7 @@ class Language /** * Gets current active language * - * @return mixed + * @return string */ public function getActive() { @@ -146,9 +165,9 @@ class Language /** * Sets active language manually * - * @param $lang + * @param string $lang * - * @return bool + * @return string|bool */ public function setActive($lang) { @@ -164,9 +183,9 @@ class Language /** * Sets the active language based on the first part of the URL * - * @param $uri + * @param string $uri * - * @return mixed + * @return string */ public function setActiveFromUri($uri) { @@ -189,29 +208,24 @@ class Language } } else { // Try getting language from the session, else no active. - if (isset($this->grav['session']) && $this->grav['session']->isStarted() - && $this->config->get('system.languages.session_store_active', true)) { + if (isset($this->grav['session']) && $this->grav['session']->isStarted() && + $this->config->get('system.languages.session_store_active', true)) { $this->active = $this->grav['session']->active_language ?: null; } // if still null, try from http_accept_language header - if ($this->active === null && $this->config->get('system.languages.http_accept_language')) { - $preferred = $this->getBrowserLanguages(); - foreach ($preferred as $lang) { - if ($this->validate($lang)) { - $this->active = $lang; - break; - } + if ($this->active === null && + $this->config->get('system.languages.http_accept_language') && + $accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? false) { + + $negotiator = new LanguageNegotiator(); + $best_language = $negotiator->getBest($accept, $this->languages); + + if ($best_language instanceof AcceptLanguage) { + $this->active = $best_language->getType(); + } else { + $this->active = $this->getDefault(); } - // Repeat if not found, try base language only - fixes Safari sending the language code always - // with a locale (e.g. it-it or fr-fr). - foreach ($preferred as $lang) { - $lang = substr($lang, 0, 2); - if ($this->validate($lang)) { - $this->active = $lang; - break; - } - } } } } @@ -220,9 +234,9 @@ class Language } /** - * Get's a URL prefix based on configuration + * Get a URL prefix based on configuration * - * @param null $lang + * @param string|null $lang * @return string */ public function getLanguageURLPrefix($lang = null) @@ -238,7 +252,7 @@ class Language /** * Test to see if language is default and language should be included in the URL * - * @param null $lang + * @param string|null $lang * @return bool */ public function isIncludeDefaultLanguage($lang = null) @@ -248,11 +262,7 @@ class Language $lang = $this->getLanguage(); } - if ($this->default == $lang && $this->config->get('system.languages.include_default_lang') === false) { - return false; - } else { - return true; - } + return !($this->default === $lang && $this->config->get('system.languages.include_default_lang') === false); } /** @@ -276,7 +286,7 @@ class Language public function getFallbackPageExtensions($file_ext = null) { if (empty($this->page_extensions)) { - if (empty($file_ext)) { + if (!$file_ext) { $file_ext = CONTENT_EXT; } @@ -288,12 +298,19 @@ class Language if ($this->active) { $active_extension = '.' . $this->active . $file_ext; - $key = array_search($active_extension, $valid_lang_extensions); - unset($valid_lang_extensions[$key]); - array_unshift($valid_lang_extensions, $active_extension); - } + $key = \array_search($active_extension, $valid_lang_extensions, true); - $this->page_extensions = array_merge($valid_lang_extensions, (array)$file_ext); + // Default behavior is to find any language other than active + if ($this->config->get('system.languages.pages_fallback_only')) { + $slice = \array_slice($valid_lang_extensions, 0, $key+1); + $valid_lang_extensions = array_reverse($slice); + } else { + unset($valid_lang_extensions[$key]); + array_unshift($valid_lang_extensions, $active_extension); + } + } + $valid_lang_extensions[] = $file_ext; + $this->page_extensions = $valid_lang_extensions; } else { $this->page_extensions = (array)$file_ext; } @@ -313,7 +330,8 @@ class Language * $this->grav['pages']->init(); * ``` */ - public function resetFallbackPageExtensions() { + public function resetFallbackPageExtensions() + { $this->page_extensions = null; } @@ -330,7 +348,7 @@ class Language if ($this->active) { $active_extension = $this->active; - $key = array_search($active_extension, $fallback_languages); + $key = \array_search($active_extension, $fallback_languages, true); unset($fallback_languages[$key]); array_unshift($fallback_languages, $active_extension); } @@ -346,23 +364,19 @@ class Language /** * Ensures the language is valid and supported * - * @param $lang + * @param string $lang * * @return bool */ public function validate($lang) { - if (in_array($lang, $this->languages)) { - return true; - } - - return false; + return \in_array($lang, $this->languages, true); } /** * Translate a key and possibly arguments into a string using current lang and fallbacks * - * @param mixed $args The first argument is the lookup key value + * @param string|array $args The first argument is the lookup key value * Other arguments can be passed and replaced in the translation with sprintf syntax * @param array $languages * @param bool $array_support @@ -372,7 +386,7 @@ class Language */ public function translate($args, array $languages = null, $array_support = false, $html_out = false) { - if (is_array($args)) { + if (\is_array($args)) { $lookup = array_shift($args); } else { $lookup = $args; @@ -396,28 +410,28 @@ class Language $translation = $this->getTranslation($lang, $lookup, $array_support); if ($translation) { - if (count($args) >= 1) { + if (\count($args) >= 1) { return vsprintf($translation, $args); - } else { - return $translation; } + + return $translation; } } } if ($html_out) { return '' . $lookup . ''; - } else { - return $lookup; } + + return $lookup; } /** * Translate Array * - * @param $key - * @param $index - * @param null $languages + * @param string $key + * @param string $index + * @param array|null $languages * @param bool $html_out * * @return string @@ -447,9 +461,9 @@ class Language if ($html_out) { return '' . $key . '[' . $index . ']'; - } else { - return $key . '[' . $index . ']'; } + + return $key . '[' . $index . ']'; } /** @@ -475,11 +489,13 @@ class Language * Get the browser accepted languages * * @param array $accept_langs - * * @return array + * @deprecated 1.6 No longer used - using content negotiation. */ public function getBrowserLanguages($accept_langs = []) { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, no longer used', E_USER_DEPRECATED); + if (empty($this->http_accept_language)) { if (empty($accept_langs) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $accept_langs = $_SERVER['HTTP_ACCEPT_LANGUAGE']; @@ -487,6 +503,8 @@ class Language return $accept_langs; } + $langs = []; + foreach (explode(',', $accept_langs) as $k => $pref) { // split $pref again by ';q=' // and decorate the language entries by inverted position @@ -504,4 +522,16 @@ class Language return $this->http_accept_language; } + /** + * Accessible wrapper to LanguageCodes + * + * @param string $code + * @param string $type + * @return bool + */ + public function getLanguageCode($code, $type = 'name') + { + return LanguageCodes::get($code, $type); + } + } diff --git a/system/src/Grav/Common/Language/LanguageCodes.php b/system/src/Grav/Common/Language/LanguageCodes.php index 063d41d..780df7c 100644 --- a/system/src/Grav/Common/Language/LanguageCodes.php +++ b/system/src/Grav/Common/Language/LanguageCodes.php @@ -1,8 +1,9 @@ init($page, $defaults); + if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + + $this->init($excerpts, $defaults); } } diff --git a/system/src/Grav/Common/Markdown/ParsedownExtra.php b/system/src/Grav/Common/Markdown/ParsedownExtra.php index 481c52f..a562d8d 100644 --- a/system/src/Grav/Common/Markdown/ParsedownExtra.php +++ b/system/src/Grav/Common/Markdown/ParsedownExtra.php @@ -1,13 +1,17 @@ $defaults]; + } + $excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } + parent::__construct(); - $this->init($page, $defaults); + $this->init($excerpts, $defaults); } } diff --git a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php index 59be69b..aa76cdf 100644 --- a/system/src/Grav/Common/Markdown/ParsedownGravTrait.php +++ b/system/src/Grav/Common/Markdown/ParsedownGravTrait.php @@ -1,22 +1,21 @@ page = $page; - $this->BlockTypes['{'] [] = 'TwigTag'; - $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; - - if ($defaults === null) { - $defaults = Grav::instance()['config']->get('system.pages.markdown'); + if (!$excerpts || $excerpts instanceof PageInterface) { + // Deprecated in Grav 1.6.10 + if ($defaults) { + $defaults = ['markdown' => $defaults]; + } + $this->excerpts = new Excerpts($excerpts, $defaults); + user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED); + } else { + $this->excerpts = $excerpts; } - $this->setBreaksEnabled($defaults['auto_line_breaks']); - $this->setUrlsLinked($defaults['auto_url_links']); - $this->setMarkupEscaped($defaults['escape_markup']); - $this->setSpecialChars($defaults['special_chars']); + $this->BlockTypes['{'][] = 'TwigTag'; + $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; - $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this])); + $defaults = $this->excerpts->getConfig(); + if (isset($defaults['markdown']['auto_line_breaks'])) { + $this->setBreaksEnabled($defaults['markdown']['auto_line_breaks']); + } + if (isset($defaults['markdown']['auto_url_links'])) { + $this->setUrlsLinked($defaults['markdown']['auto_url_links']); + } + if (isset($defaults['markdown']['escape_markup'])) { + $this->setMarkupEscaped($defaults['markdown']['escape_markup']); + } + if (isset($defaults['markdown']['special_chars'])) { + $this->setSpecialChars($defaults['markdown']['special_chars']); + } + + $this->excerpts->fireInitializedEvent($this); + } + + /** + * @return Excerpts + */ + public function getExcerpts() + { + return $this->excerpts; } /** * Be able to define a new Block type or override an existing one * - * @param $type - * @param $tag + * @param string $type + * @param string $tag * @param bool $continuable * @param bool $completable - * @param $index + * @param int|null $index */ public function addBlockType($type, $tag, $continuable = false, $completable = false, $index = null) { @@ -87,9 +107,9 @@ trait ParsedownGravTrait /** * Be able to define a new Inline type or override an existing one * - * @param $type - * @param $tag - * @param $index + * @param string $type + * @param string $tag + * @param int|null $index */ public function addInlineType($type, $tag, $index = null) { @@ -107,13 +127,14 @@ trait ParsedownGravTrait /** * Overrides the default behavior to allow for plugin-provided blocks to be continuable * - * @param $Type + * @param string $Type * * @return bool */ protected function isBlockContinuable($Type) { - $continuable = \in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue'); + $continuable = \in_array($Type, $this->continuable_blocks, true) + || method_exists($this, 'block' . $Type . 'Continue'); return $continuable; } @@ -121,13 +142,14 @@ trait ParsedownGravTrait /** * Overrides the default behavior to allow for plugin-provided blocks to be completable * - * @param $Type + * @param string $Type * * @return bool */ protected function isBlockCompletable($Type) { - $completable = \in_array($Type, $this->completable_blocks) || method_exists($this, 'block' . $Type . 'Complete'); + $completable = \in_array($Type, $this->completable_blocks, true) + || method_exists($this, 'block' . $Type . 'Complete'); return $completable; } @@ -148,7 +170,7 @@ trait ParsedownGravTrait /** * Setter for special chars * - * @param $special_chars + * @param array $special_chars * * @return $this */ @@ -209,7 +231,7 @@ trait ParsedownGravTrait // if this is an image process it if (isset($excerpt['element']['attributes']['src'])) { - $excerpt = Excerpts::processImageExcerpt($excerpt, $this->page); + $excerpt = $this->excerpts->processImageExcerpt($excerpt); } return $excerpt; @@ -217,11 +239,7 @@ trait ParsedownGravTrait protected function inlineLink($excerpt) { - if (isset($excerpt['type'])) { - $type = $excerpt['type']; - } else { - $type = 'link'; - } + $type = $excerpt['type'] ?? 'link'; // do some trickery to get around Parsedown requirement for valid URL if its Twig in there if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) { @@ -237,13 +255,15 @@ trait ParsedownGravTrait // if this is a link if (isset($excerpt['element']['attributes']['href'])) { - $excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type); + $excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type); } return $excerpt; } - // For extending this class via plugins + /** + * For extending this class via plugins + */ public function __call($method, $args) { if (isset($this->{$method}) === true) { diff --git a/system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php b/system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php index 06f71a5..9f91663 100644 --- a/system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php +++ b/system/src/Grav/Common/Media/Interfaces/MediaCollectionInterface.php @@ -1,9 +1,23 @@ findResource('user://'); if (strpos($folder, $user) === 0) { - return 'user://' . substr($folder, strlen($user)+1); + return 'user://' . substr($folder, \strlen($user)+1); } return null; @@ -55,14 +67,14 @@ trait MediaTrait */ public function getMedia() { - $cache = $this->getMediaCache(); - if ($this->media === null) { + $cache = $this->getMediaCache(); + // Use cached media if possible. $cacheKey = md5('media' . $this->getCacheKey()); - if (!$media = $cache->fetch($cacheKey)) { + if (!$media = $cache->get($cacheKey)) { $media = new Media($this->getMediaFolder(), $this->getMediaOrder()); - $cache->save($cacheKey, $media); + $cache->set($cacheKey, $media); } $this->media = $media; } @@ -80,7 +92,7 @@ trait MediaTrait { $cache = $this->getMediaCache(); $cacheKey = md5('media' . $this->getCacheKey()); - $cache->save($cacheKey, $media); + $cache->set($cacheKey, $media); $this->media = $media; @@ -95,14 +107,19 @@ trait MediaTrait $cache = $this->getMediaCache(); $cacheKey = md5('media' . $this->getCacheKey()); $cache->delete($cacheKey); + + $this->media = null; } /** - * @return Cache + * @return CacheInterface */ protected function getMediaCache() { - return Grav::instance()['cache']; + /** @var Cache $cache */ + $cache = Grav::instance()['cache']; + + return $cache->getSimpleCache(); } /** diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php index 5113e81..51de16b 100644 --- a/system/src/Grav/Common/Page/Collection.php +++ b/system/src/Grav/Common/Page/Collection.php @@ -1,8 +1,9 @@ items[$page->path()] = ['slug' => $page->slug()]; @@ -66,8 +68,8 @@ class Collection extends Iterator /** * Add a page with path and slug * - * @param $path - * @param $slug + * @param string $path + * @param string $slug * @return $this */ public function add($path, $slug) @@ -100,6 +102,7 @@ class Collection extends Iterator foreach($collection as $page) { $this->addPage($page); } + return $this; } @@ -117,6 +120,7 @@ class Collection extends Iterator $this->items = array_uintersect($array1, $array2, function($val1, $val2) { return strcmp($val1['slug'], $val2['slug']); }); + return $this; } @@ -130,13 +134,14 @@ class Collection extends Iterator public function setParams(array $params) { $this->params = array_merge($this->params, $params); + return $this; } /** * Returns current page. * - * @return Page + * @return PageInterface */ public function current() { @@ -166,13 +171,13 @@ class Collection extends Iterator */ public function offsetGet($offset) { - return !empty($this->items[$offset]) ? $this->pages->get($offset) : null; + return $this->pages->get($offset) ?: null; } /** * Split collection into array of smaller collections. * - * @param $size + * @param int $size * @return array|Collection[] */ public function batch($size) @@ -190,19 +195,19 @@ class Collection extends Iterator /** * Remove item from the list. * - * @param Page|string|null $key + * @param PageInterface|string|null $key * * @return $this * @throws \InvalidArgumentException */ public function remove($key = null) { - if ($key instanceof Page) { + if ($key instanceof PageInterface) { $key = $key->path(); - } elseif (is_null($key)) { - $key = key($this->items); + } elseif (null === $key) { + $key = (string)key($this->items); } - if (!is_string($key)) { + if (!\is_string($key)) { throw new \InvalidArgumentException('Invalid argument $key.'); } @@ -233,15 +238,11 @@ class Collection extends Iterator * * @param string $path * - * @return boolean True if item is first. + * @return bool True if item is first. */ public function isFirst($path) { - if ($this->items && $path == array_keys($this->items)[0]) { - return true; - } else { - return false; - } + return $this->items && $path === array_keys($this->items)[0]; } /** @@ -249,15 +250,11 @@ class Collection extends Iterator * * @param string $path * - * @return boolean True if item is last. + * @return bool True if item is last. */ public function isLast($path) { - if ($this->items && $path == array_keys($this->items)[count($this->items) - 1]) { - return true; - } else { - return false; - } + return $this->items && $path === array_keys($this->items)[\count($this->items) - 1]; } /** @@ -265,7 +262,7 @@ class Collection extends Iterator * * @param string $path * - * @return Page The previous item. + * @return PageInterface The previous item. */ public function prevSibling($path) { @@ -277,7 +274,7 @@ class Collection extends Iterator * * @param string $path * - * @return Page The next item. + * @return PageInterface The next item. */ public function nextSibling($path) { @@ -288,9 +285,9 @@ class Collection extends Iterator * Returns the adjacent sibling based on a direction. * * @param string $path - * @param integer $direction either -1 or +1 + * @param int $direction either -1 or +1 * - * @return Page The sibling item. + * @return PageInterface|Collection The sibling item. */ public function adjacentSibling($path, $direction = 1) { @@ -312,11 +309,11 @@ class Collection extends Iterator * * @param string $path the path the item * - * @return Integer the index of the current page. + * @return int the index of the current page. */ public function currentPosition($path) { - return array_search($path, array_keys($this->items)); + return \array_search($path, \array_keys($this->items), true); } /** @@ -325,14 +322,14 @@ class Collection extends Iterator * Dates can be passed in as text that strtotime() can process * http://php.net/manual/en/function.strtotime.php * - * @param $startDate + * @param string $startDate * @param bool $endDate - * @param $field + * @param string|null $field * * @return $this * @throws \Exception */ - public function dateRange($startDate, $endDate = false, $field = false) + public function dateRange($startDate, $endDate = false, $field = null) { $start = Utils::date2timestamp($startDate); $end = $endDate ? Utils::date2timestamp($endDate) : false; @@ -350,6 +347,7 @@ class Collection extends Iterator } $this->items = $date_range; + return $this; } @@ -518,7 +516,7 @@ class Collection extends Iterator /** * Creates new collection with only pages of the specified type * - * @param $type + * @param string $type * * @return Collection The collection */ @@ -528,7 +526,7 @@ class Collection extends Iterator foreach ($this->items as $path => $slug) { $page = $this->pages->get($path); - if ($page !== null && $page->template() == $type) { + if ($page !== null && $page->template() === $type) { $items[$path] = $slug; } } @@ -541,7 +539,7 @@ class Collection extends Iterator /** * Creates new collection with only pages of one of the specified types * - * @param $types + * @param string[] $types * * @return Collection The collection */ @@ -551,7 +549,7 @@ class Collection extends Iterator foreach ($this->items as $path => $slug) { $page = $this->pages->get($path); - if ($page !== null && in_array($page->template(), $types)) { + if ($page !== null && \in_array($page->template(), $types, true)) { $items[$path] = $slug; } } @@ -564,7 +562,7 @@ class Collection extends Iterator /** * Creates new collection with only pages of one of the specified access levels * - * @param $accessLevels + * @param array $accessLevels * * @return Collection The collection */ @@ -576,19 +574,19 @@ class Collection extends Iterator $page = $this->pages->get($path); if ($page !== null && isset($page->header()->access)) { - if (is_array($page->header()->access)) { + if (\is_array($page->header()->access)) { //Multiple values for access $valid = false; foreach ($page->header()->access as $index => $accessLevel) { - if (is_array($accessLevel)) { + if (\is_array($accessLevel)) { foreach ($accessLevel as $innerIndex => $innerAccessLevel) { - if (in_array($innerAccessLevel, $accessLevels)) { + if (\in_array($innerAccessLevel, $accessLevels)) { $valid = true; } } } else { - if (in_array($index, $accessLevels)) { + if (\in_array($index, $accessLevels)) { $valid = true; } } @@ -598,7 +596,7 @@ class Collection extends Iterator } } else { //Single value for access - if (in_array($page->header()->access, $accessLevels)) { + if (\in_array($page->header()->access, $accessLevels)) { $items[$path] = $slug; } } diff --git a/system/src/Grav/Common/Page/Header.php b/system/src/Grav/Common/Page/Header.php index 7df862b..e3854ae 100644 --- a/system/src/Grav/Common/Page/Header.php +++ b/system/src/Grav/Common/Page/Header.php @@ -1,8 +1,9 @@ true) for example + * + * @param array $var an Array of name value pairs where the name is the process and value is true or false + * + * @return array an Array of name value pairs where the name is the process and value is true or false + */ + public function process($var = null); + + /** + * Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses + * the parent folder from the path + * + * @param string $var the slug, e.g. 'my-blog' + * + * @return string the slug + */ + public function slug($var = null); + + /** + * Get/set order number of this page. + * + * @param int $var + * + * @return int|bool + */ + public function order($var = null); + + /** + * Gets and sets the identifier for this Page object. + * + * @param string $var the identifier + * + * @return string the identifier + */ + public function id($var = null); + + /** + * Gets and sets the modified timestamp. + * + * @param int $var modified unix timestamp + * + * @return int modified unix timestamp + */ + public function modified($var = null); + + /** + * Gets and sets the option to show the last_modified header for the page. + * + * @param boolean $var show last_modified header + * + * @return boolean show last_modified header + */ + public function lastModified($var = null); + + /** + * Get/set the folder. + * + * @param string $var Optional path + * + * @return string|null + */ + public function folder($var = null); + + /** + * Gets and sets the date for this Page object. This is typically passed in via the page headers + * + * @param string $var string representation of a date + * + * @return int unix timestamp representation of the date + */ + public function date($var = null); + + /** + * Gets and sets the date format for this Page object. This is typically passed in via the page headers + * using typical PHP date string structure - http://php.net/manual/en/function.date.php + * + * @param string $var string representation of a date format + * + * @return string string representation of a date format + */ + public function dateformat($var = null); + + /** + * Gets and sets the taxonomy array which defines which taxonomies this page identifies itself with. + * + * @param array $var an array of taxonomies + * + * @return array an array of taxonomies + */ + public function taxonomy($var = null); + + /** + * Gets the configured state of the processing method. + * + * @param string $process the process, eg "twig" or "markdown" + * + * @return bool whether or not the processing method is enabled for this Page + */ + public function shouldProcess($process); + + /** + * Returns whether or not this Page object has a .md file associated with it or if its just a directory. + * + * @return bool True if its a page with a .md file associated + */ + public function isPage(); + + /** + * Returns whether or not this Page object is a directory or a page. + * + * @return bool True if its a directory + */ + public function isDir(); + + /** + * Returns whether the page exists in the filesystem. + * + * @return bool + */ + public function exists(); +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageInterface.php b/system/src/Grav/Common/Page/Interfaces/PageInterface.php index 14d6cdc..24bf50c 100644 --- a/system/src/Grav/Common/Page/Interfaces/PageInterface.php +++ b/system/src/Grav/Common/Page/Interfaces/PageInterface.php @@ -1,9 +1,19 @@ save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * + * @return $this + */ + public function move(PageInterface $parent); + + /** + * Prepare a copy from the page. Copies also everything that's under the current page. + * + * Returns a new Page object for the copy. + * You need to call $this->save() in order to perform the move. + * + * @param PageInterface $parent New parent page. + * + * @return $this + */ + public function copy(PageInterface $parent); + + /** + * Get blueprints for the page. + * + * @return Blueprint + */ + public function blueprints(); + + /** + * Get the blueprint name for this page. Use the blueprint form field if set + * + * @return string + */ + public function blueprintName(); + + /** + * Validate page header. + * + * @throws Exception + */ + public function validate(); + + /** + * Filter page header from illegal contents. + */ + public function filter(); + + /** + * Get unknown header variables. + * + * @return array + */ + public function extra(); + + /** + * Convert page to an array. + * + * @return array + */ + public function toArray(); + + /** + * Convert page to YAML encoded string. + * + * @return string + */ + public function toYaml(); + + /** + * Convert page to JSON encoded string. + * + * @return string + */ + public function toJson(); + + /** + * Returns normalized list of name => form pairs. + * + * @return array + */ + public function forms(); + + /** + * @param array $new + */ + public function addForms(array $new); + + /** + * Gets and sets the name field. If no name field is set, it will return 'default.md'. + * + * @param string $var The name of this page. + * + * @return string The name of this page. + */ + public function name($var = null); + + /** + * Returns child page type. + * + * @return string + */ + public function childType(); + + /** + * Gets and sets the template field. This is used to find the correct Twig template file to render. + * If no field is set, it will return the name without the .md extension + * + * @param string $var the template name + * + * @return string the template name + */ + public function template($var = null); + + /** + * Allows a page to override the output render format, usually the extension provided + * in the URL. (e.g. `html`, `json`, `xml`, etc). + * + * @param null $var + * + * @return null + */ + public function templateFormat($var = null); + + /** + * Gets and sets the extension field. + * + * @param null $var + * + * @return null|string + */ + public function extension($var = null); + + /** + * Gets and sets the expires field. If not set will return the default + * + * @param int $var The new expires value. + * + * @return int The expires value + */ + public function expires($var = null); + + /** + * Gets and sets the cache-control property. If not set it will return the default value (null) + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control for more details on valid options + * + * @param null $var + * @return null + */ + public function cacheControl($var = null); + + public function ssl($var = null); + + /** + * Returns the state of the debugger override etting for this page + * + * @return mixed + */ + public function debugger(); + + /** + * Function to merge page metadata tags and build an array of Metadata objects + * that can then be rendered in the page. + * + * @param array $var an Array of metadata values to set + * + * @return array an Array of metadata values for the page + */ + public function metadata($var = null); + + /** + * Gets and sets the option to show the etag header for the page. + * + * @param bool $var show etag header + * + * @return bool show etag header + */ + public function eTag($var = null); + + /** + * Gets and sets the path to the .md file for this Page object. + * + * @param string $var the file path + * + * @return string|null the file path + */ + public function filePath($var = null); + + /** + * Gets the relative path to the .md file + * + * @return string The relative file path + */ + public function filePathClean(); + + /** + * Gets and sets the order by which any sub-pages should be sorted. + * + * @param string $var the order, either "asc" or "desc" + * + * @return string the order, either "asc" or "desc" + * @deprecated 1.6 + */ + public function orderDir($var = null); + + /** + * Gets and sets the order by which the sub-pages should be sorted. + * + * default - is the order based on the file system, ie 01.Home before 02.Advark + * title - is the order based on the title set in the pages + * date - is the order based on the date set in the pages + * folder - is the order based on the name of the folder with any numerics omitted + * + * @param string $var supported options include "default", "title", "date", and "folder" + * + * @return string supported options include "default", "title", "date", and "folder" + * @deprecated 1.6 + */ + public function orderBy($var = null); + + /** + * Gets the manual order set in the header. + * + * @param string $var supported options include "default", "title", "date", and "folder" + * + * @return array + * @deprecated 1.6 + */ + public function orderManual($var = null); + + /** + * Gets and sets the maxCount field which describes how many sub-pages should be displayed if the + * sub_pages header property is set for this page object. + * + * @param int $var the maximum number of sub-pages + * + * @return int the maximum number of sub-pages + * @deprecated 1.6 + */ + public function maxCount($var = null); + + /** + * Gets and sets the modular var that helps identify this page is a modular child + * + * @param bool $var true if modular_twig + * + * @return bool true if modular_twig + */ + public function modular($var = null); + + /** + * Gets and sets the modular_twig var that helps identify this page as a modular child page that will need + * twig processing handled differently from a regular page. + * + * @param bool $var true if modular_twig + * + * @return bool true if modular_twig + */ + public function modularTwig($var = null); + + /** + * Returns children of this page. + * + * @return \Grav\Common\Page\Collection + */ + public function children(); + + /** + * Check to see if this item is the first in an array of sub-pages. + * + * @return bool True if item is first. + */ + public function isFirst(); + + /** + * Check to see if this item is the last in an array of sub-pages. + * + * @return bool True if item is last + */ + public function isLast(); + + /** + * Gets the previous sibling based on current position. + * + * @return PageInterface the previous Page item + */ + public function prevSibling(); + + /** + * Gets the next sibling based on current position. + * + * @return PageInterface the next Page item + */ + public function nextSibling(); + + /** + * Returns the adjacent sibling based on a direction. + * + * @param int $direction either -1 or +1 + * + * @return PageInterface|bool the sibling page + */ + public function adjacentSibling($direction = 1); + + /** + * Helper method to return an ancestor page. + * + * @param bool $lookup Name of the parent folder + * + * @return PageInterface page you were looking for if it exists + */ + public function ancestor($lookup = null); + + /** + * Helper method to return an ancestor page to inherit from. The current + * page object is returned. + * + * @param string $field Name of the parent folder + * + * @return PageInterface + */ + public function inherited($field); + + /** + * Helper method to return an ancestor field only to inherit from. The + * first occurrence of an ancestor field will be returned if at all. + * + * @param string $field Name of the parent folder + * + * @return array + */ + public function inheritedField($field); + + /** + * Helper method to return a page. + * + * @param string $url the url of the page + * @param bool $all + * + * @return PageInterface page you were looking for if it exists + */ + public function find($url, $all = false); + + /** + * Get a collection of pages in the current context. + * + * @param string|array $params + * @param bool $pagination + * + * @return Collection + * @throws \InvalidArgumentException + */ + public function collection($params = 'content', $pagination = true); + + /** + * @param string|array $value + * @param bool $only_published + * @return mixed + * @internal + */ + public function evaluate($value, $only_published = true); + + /** + * Returns whether or not the current folder exists + * + * @return bool + */ + public function folderExists(); + + /** + * Gets the Page Unmodified (original) version of the page. + * + * @return PageInterface The original version of the page. + */ + public function getOriginal(); + + /** + * Gets the action. + * + * @return string The Action string. + */ + public function getAction(); +} diff --git a/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php new file mode 100644 index 0000000..c3afeab --- /dev/null +++ b/system/src/Grav/Common/Page/Interfaces/PageRoutableInterface.php @@ -0,0 +1,188 @@ +page = $page ?? Grav::instance()['page'] ?? null; + + // Add defaults to the configuration. + if (null === $config || !isset($config['markdown'], $config['images'])) { + $c = Grav::instance()['config']; + $config = $config ?? []; + $config += [ + 'markdown' => $c->get('system.pages.markdown', []), + 'images' => $c->get('system.images', []) + ]; + } + + $this->config = $config; + } + + public function getPage(): PageInterface + { + return $this->page; + } + + public function getConfig(): array + { + return $this->config; + } + + public function fireInitializedEvent($markdown): void + { + $grav = Grav::instance(); + + $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page])); + } + + /** + * Process a Link excerpt + * + * @param array $excerpt + * @param string $type + * @return array + */ + public function processLinkExcerpt(array $excerpt, string $type = 'link'): array + { + $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); + + $url_parts = $this->parseUrl($url); + + // If there is a query, then parse it and build action calls. + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = isset($parts[1]) ? rawurldecode($parts[1]) : true; + $carry[$parts[0]] = $value; + + return $carry; + }, + [] + ); + + // Valid attributes supported. + $valid_attributes = ['rel', 'target', 'id', 'class', 'classes']; + + // Unless told to not process, go through actions. + if (array_key_exists('noprocess', $actions)) { + unset($actions['noprocess']); + } else { + // Loop through actions for the image and call them. + foreach ($actions as $attrib => $value) { + $key = $attrib; + + if (in_array($attrib, $valid_attributes, true)) { + // support both class and classes. + if ($attrib === 'classes') { + $attrib = 'class'; + } + $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value); + unset($actions[$key]); + } + } + } + + $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986); + } + + // If no query elements left, unset query. + if (empty($url_parts['query'])) { + unset ($url_parts['query']); + } + + // Set path to / if not set. + if (empty($url_parts['path'])) { + $url_parts['path'] = ''; + } + + // If scheme isn't http(s).. + if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) { + // Handle custom streams. + if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) { + $grav = Grav::instance(); + $url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}"); + unset($url_parts['stream'], $url_parts['scheme']); + } + + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + // Handle paths and such. + $url_parts = Uri::convertUrl($this->page, $url_parts, $type); + + // Build the URL from the component parts and set it on the element. + $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts); + + return $excerpt; + } + + /** + * Process an image excerpt + * + * @param array $excerpt + * @return array + */ + public function processImageExcerpt(array $excerpt): array + { + $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); + $url_parts = $this->parseUrl($url); + + $media = null; + $filename = null; + + if (!empty($url_parts['stream'])) { + $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? ''); + + $media = $this->page->getMedia(); + + } else { + $grav = Grav::instance(); + + // File is also local if scheme is http(s) and host matches. + $local_file = isset($url_parts['path']) + && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true)) + && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host()); + + if ($local_file) { + $filename = basename($url_parts['path']); + $folder = dirname($url_parts['path']); + + // Get the local path to page media if possible. + if ($this->page && $folder === $this->page->url(false, false, false)) { + // Get the media objects for this page. + $media = $this->page->getMedia(); + } else { + // see if this is an external page to this one + $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/'); + $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/'); + + /** @var PageInterface $ext_page */ + $ext_page = $grav['pages']->dispatch($page_route, true); + if ($ext_page) { + $media = $ext_page->getMedia(); + } else { + $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media])); + } + } + } + } + + // If there is a media file that matches the path referenced.. + if ($media && $filename && isset($media[$filename])) { + // Get the medium object. + /** @var Medium $medium */ + $medium = $media[$filename]; + + // Process operations + $medium = $this->processMediaActions($medium, $url_parts); + $element_excerpt = $excerpt['element']['attributes']; + + $alt = $element_excerpt['alt'] ?? ''; + $title = $element_excerpt['title'] ?? ''; + $class = $element_excerpt['class'] ?? ''; + $id = $element_excerpt['id'] ?? ''; + + $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true); + + } else { + // Not a current page media file, see if it needs converting to relative. + $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts); + } + + return $excerpt; + } + + /** + * Process media actions + * + * @param Medium $medium + * @param string|array $url + * @return Medium|Link + */ + public function processMediaActions($medium, $url) + { + $url_parts = is_string($url) ? $this->parseUrl($url) : $url; + $actions = []; + + // if there is a query, then parse it and build action calls + if (isset($url_parts['query'])) { + $actions = array_reduce( + explode('&', $url_parts['query']), + static function ($carry, $item) { + $parts = explode('=', $item, 2); + $value = $parts[1] ?? null; + $carry[] = ['method' => $parts[0], 'params' => $value]; + + return $carry; + }, + [] + ); + } + + $config = $this->getConfig(); + if (!empty($config['images']['auto_fix_orientation'])) { + $actions[] = ['method' => 'fixOrientation', 'params' => '']; + } + + $defaults = $config['images']['defaults'] ?? []; + if (count($defaults)) { + foreach ($defaults as $method => $params) { + $actions[] = [ + 'method' => $method, + 'params' => $params, + ]; + } + } + + // loop through actions for the image and call them + foreach ($actions as $action) { + $matches = []; + + if (preg_match('/\[(.*)\]/', $action['params'], $matches)) { + $args = [explode(',', $matches[1])]; + } else { + $args = explode(',', $action['params']); + } + + $medium = call_user_func_array([$medium, $action['method']], $args); + } + + if (isset($url_parts['fragment'])) { + $medium->urlHash($url_parts['fragment']); + } + + return $medium; + } + + /** + * Variation of parse_url() which works also with local streams. + * + * @param string $url + * @return array|bool + */ + protected function parseUrl(string $url) + { + $url_parts = Utils::multibyteParseUrl($url); + + if (isset($url_parts['scheme'])) { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + // Special handling for the streams. + if ($locator->schemeExists($url_parts['scheme'])) { + if (isset($url_parts['host'])) { + // Merge host and path into a path. + $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : ''); + unset($url_parts['host']); + } + + $url_parts['stream'] = true; + } + } + + return $url_parts; + } + + /** + * @param string $url + * @return bool|string + */ + protected function resolveStream(string $url) + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + if ($locator->isStream($url)) { + return $locator->findResource($url, false) ?: $locator->findResource($url, false, true); + } + + return $url; + } +} diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php index e714f7b..88b2ced 100644 --- a/system/src/Grav/Common/Page/Media.php +++ b/system/src/Grav/Common/Page/Media.php @@ -1,8 +1,9 @@ path = $path; + $this->setPath($path); $this->media_order = $media_order; $this->__wakeup(); - $this->init(); + if ($load) { + $this->init(); + } } /** @@ -72,37 +75,40 @@ class Media extends AbstractMedia */ protected function init() { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; $config = Grav::instance()['config']; + $locator = Grav::instance()['locator']; $exif_reader = isset(Grav::instance()['exif']) ? Grav::instance()['exif']->getReader() : false; $media_types = array_keys(Grav::instance()['config']->get('media.types')); // Handle special cases where page doesn't exist in filesystem. - if (!is_dir($this->path)) { + if (!is_dir($this->getPath())) { return; } - $iterator = new \FilesystemIterator($this->path, \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS); + $iterator = new \FilesystemIterator($this->getPath(), \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS); $media = []; /** @var \DirectoryIterator $info */ foreach ($iterator as $path => $info) { // Ignore folders and Markdown files. - if (!$info->isFile() || $info->getExtension() === 'md' || $info->getFilename()[0] === '.') { + if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) { continue; } // Find out what type we're dealing with list($basename, $ext, $type, $extra) = $this->getFileParts($info->getFilename()); - if (!in_array(strtolower($ext), $media_types)) { + if (!\in_array(strtolower($ext), $media_types, true)) { continue; } if ($type === 'alternative') { - $media["{$basename}.{$ext}"][$type][$extra] = [ 'file' => $path, 'size' => $info->getSize() ]; + $media["{$basename}.{$ext}"][$type][$extra] = ['file' => $path, 'size' => $info->getSize()]; } else { - $media["{$basename}.{$ext}"][$type] = [ 'file' => $path, 'size' => $info->getSize() ]; + $media["{$basename}.{$ext}"][$type] = ['file' => $path, 'size' => $info->getSize()]; } } @@ -147,7 +153,7 @@ class Media extends AbstractMedia if (file_exists($meta_path)) { $types['meta']['file'] = $meta_path; - } elseif ($file_path && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif') && $exif_reader) { + } elseif ($file_path && $exif_reader && $medium->get('mime') === 'image/jpeg' && empty($types['meta']) && $config->get('system.media.auto_metadata_exif')) { $meta = $exif_reader->read($file_path); @@ -155,7 +161,11 @@ class Media extends AbstractMedia $meta_data = $meta->getData(); $meta_trimmed = array_diff_key($meta_data, array_flip($this->standard_exif)); if ($meta_trimmed) { - $file = File::instance($meta_path); + if ($locator->isStream($meta_path)) { + $file = File::instance($locator->findResource($meta_path, true, true)); + } else { + $file = File::instance($meta_path); + } $file->save(Yaml::dump($meta_trimmed)); $types['meta']['file'] = $meta_path; } @@ -202,12 +212,11 @@ class Media extends AbstractMedia } /** - * Enable accessing the media path - * - * @return mixed + * @return string + * @deprecated 1.6 Use $this->getPath() instead. */ public function path() { - return $this->path; + return $this->getPath(); } } diff --git a/system/src/Grav/Common/Page/Medium/AbstractMedia.php b/system/src/Grav/Common/Page/Medium/AbstractMedia.php index 58f9886..f67cd65 100644 --- a/system/src/Grav/Common/Page/Medium/AbstractMedia.php +++ b/system/src/Grav/Common/Page/Medium/AbstractMedia.php @@ -1,30 +1,55 @@ path; + } + + public function setPath(?string $path) + { + $this->path = $path; + } + /** * Get medium by filename. * @@ -48,73 +73,78 @@ abstract class AbstractMedia extends Getters implements MediaCollectionInterface } /** - * @param mixed $offset + * Set file modification timestamps (query params) for all the media files. * - * @return mixed + * @param string|int|null $timestamp + * @return $this */ - public function offsetGet($offset) + public function setTimestamps($timestamp = null) { - $object = parent::offsetGet($offset); + /** @var Medium $instance */ + foreach ($this->items as $instance) { + $instance->setTimestamp($timestamp); + } - // It would be nice if previous image modification would not affect the later ones. - //$object = $object ? clone($object) : null; - - return $object; + return $this; } /** * Get a list of all media. * - * @return array|MediaObjectInterface[] + * @return MediaObjectInterface[] */ public function all() { - $this->instances = $this->orderMedia($this->instances); + $this->items = $this->orderMedia($this->items); - return $this->instances; + return $this->items; } /** * Get a list of all image media. * - * @return array|MediaObjectInterface[] + * @return MediaObjectInterface[] */ public function images() { $this->images = $this->orderMedia($this->images); + return $this->images; } /** * Get a list of all video media. * - * @return array|MediaObjectInterface[] + * @return MediaObjectInterface[] */ public function videos() { $this->videos = $this->orderMedia($this->videos); + return $this->videos; } /** * Get a list of all audio media. * - * @return array|MediaObjectInterface[] + * @return MediaObjectInterface[] */ public function audios() { $this->audios = $this->orderMedia($this->audios); + return $this->audios; } /** * Get a list of all file media. * - * @return array|MediaObjectInterface[] + * @return MediaObjectInterface[] */ public function files() { $this->files = $this->orderMedia($this->files); + return $this->files; } @@ -122,9 +152,12 @@ abstract class AbstractMedia extends Getters implements MediaCollectionInterface * @param string $name * @param MediaObjectInterface $file */ - protected function add($name, $file) + public function add($name, $file) { - $this->instances[$name] = $file; + if (!$file) { + return; + } + $this->offsetSet($name, $file); switch ($file->type) { case 'image': $this->images[$name] = $file; @@ -143,13 +176,14 @@ abstract class AbstractMedia extends Getters implements MediaCollectionInterface /** * Order the media based on the page's media_order * - * @param $media + * @param array $media * @return array */ protected function orderMedia($media) { if (null === $this->media_order) { - $page = Grav::instance()['pages']->get($this->path); + /** @var Page $page */ + $page = Grav::instance()['pages']->get($this->getPath()); if ($page && isset($page->header()->media_order)) { $this->media_order = array_map('trim', explode(',', $page->header()->media_order)); diff --git a/system/src/Grav/Common/Page/Medium/AudioMedium.php b/system/src/Grav/Common/Page/Medium/AudioMedium.php index aae3597..74ef746 100644 --- a/system/src/Grav/Common/Page/Medium/AudioMedium.php +++ b/system/src/Grav/Common/Page/Medium/AudioMedium.php @@ -1,8 +1,9 @@ attributes['controls'] = true; - } - else - { + } else { unset($this->attributes['controls']); } + return $this; } /** * Allows to set the preload behaviour * - * @param $preload + * @param string $preload * @return $this */ public function preload($preload) { - $validPreloadAttrs = array('auto','metadata','none'); + $validPreloadAttrs = ['auto', 'metadata', 'none']; - if (in_array($preload, $validPreloadAttrs)) - { + if (\in_array($preload, $validPreloadAttrs, true)) { $this->attributes['preload'] = $preload; } + return $this; } @@ -70,13 +69,14 @@ class AudioMedium extends Medium * Allows to set the controlsList behaviour * Separate multiple values with a hyphen * - * @param $controlsList + * @param string $controlsList * @return $this */ public function controlsList($controlsList) { $controlsList = str_replace('-', ' ', $controlsList); $this->attributes['controlsList'] = $controlsList; + return $this; } @@ -88,14 +88,12 @@ class AudioMedium extends Medium */ public function muted($status = false) { - if($status) - { + if($status) { $this->attributes['muted'] = true; - } - else - { + } else { unset($this->attributes['muted']); } + return $this; } @@ -107,14 +105,12 @@ class AudioMedium extends Medium */ public function loop($status = false) { - if($status) - { + if($status) { $this->attributes['loop'] = true; - } - else - { + } else { unset($this->attributes['loop']); } + return $this; } @@ -126,14 +122,12 @@ class AudioMedium extends Medium */ public function autoplay($status = false) { - if($status) - { + if($status) { $this->attributes['autoplay'] = true; - } - else - { + } else { unset($this->attributes['autoplay']); } + return $this; } @@ -148,6 +142,7 @@ class AudioMedium extends Medium parent::reset(); $this->attributes['controls'] = true; + return $this; } } diff --git a/system/src/Grav/Common/Page/Medium/GlobalMedia.php b/system/src/Grav/Common/Page/Medium/GlobalMedia.php index 74adc08..9fb30e9 100644 --- a/system/src/Grav/Common/Page/Medium/GlobalMedia.php +++ b/system/src/Grav/Common/Page/Medium/GlobalMedia.php @@ -1,8 +1,9 @@ guessType(); @@ -49,21 +51,16 @@ class ImageFile extends Image } // Computes the hash - $this->hash = $this->getHash($type, $quality); + $this->hash = $this->getHash($type, $quality, $extras); - // Generates the cache file - $cacheFile = ''; + // Seo friendly image names + $seofriendly = Grav::instance()['config']->get('system.images.seofriendly', false); - if (!$this->prettyName || $this->prettyPrefix) { - $cacheFile .= $this->hash; - } - - if ($this->prettyPrefix) { - $cacheFile .= '-'; - } - - if ($this->prettyName) { - $cacheFile .= $this->prettyName; + if ($seofriendly) { + $mini_hash = substr($this->hash, 0, 4) . substr($this->hash, -4); + $cacheFile = "{$this->prettyName}-{$mini_hash}"; + } else { + $cacheFile = "{$this->hash}-{$this->prettyName}"; } $cacheFile .= '.' . $type; @@ -107,4 +104,42 @@ class ImageFile extends Image return $this->getFilename($file); } + + /** + * Gets the hash. + * @param string $type + * @param int $quality + * @param [] $extras + * @return null + */ + public function getHash($type = 'guess', $quality = 80, $extras = []) + { + if (null === $this->hash) { + $this->generateHash($type, $quality, $extras); + } + + return $this->hash; + } + + /** + * Generates the hash. + * @param string $type + * @param int $quality + * @param array $extras + */ + public function generateHash($type = 'guess', $quality = 80, $extras = []) + { + $inputInfos = $this->source->getInfos(); + + $datas = array( + $inputInfos, + $this->serializeOperations(), + $type, + $quality, + $extras + ); + + $this->hash = sha1(serialize($datas)); + } + } diff --git a/system/src/Grav/Common/Page/Medium/ImageMedium.php b/system/src/Grav/Common/Page/Medium/ImageMedium.php index 72f23b4..1925bef 100644 --- a/system/src/Grav/Common/Page/Medium/ImageMedium.php +++ b/system/src/Grav/Common/Page/Medium/ImageMedium.php @@ -1,8 +1,9 @@ get('filepath')) === 0) { + $path = $this->get('filepath'); + if (!$path || !file_exists($path) || !filesize($path)) { return; } - $image_info = getimagesize($this->get('filepath')); + $image_info = getimagesize($path); + $this->def('width', $image_info[0]); $this->def('height', $image_info[1]); $this->def('mime', $image_info['mime']); @@ -119,7 +122,7 @@ class ImageMedium extends Medium /** * Add meta file for the medium. * - * @param $filepath + * @param string $filepath * @return $this */ public function addMetaFile($filepath) @@ -167,8 +170,7 @@ class ImageMedium extends Medium { /** @var UniformResourceLocator $locator */ $locator = Grav::instance()['locator']; - $image_path = $locator->findResource('cache://images', true); - $image_dir = $locator->findResource('cache://images', false); + $image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true); $saved_image_path = $this->saveImage(); $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path); @@ -178,6 +180,7 @@ class ImageMedium extends Medium } if (Utils::startsWith($output, $image_path)) { + $image_dir = $locator->findResource('cache://images', false); $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output); } @@ -185,7 +188,7 @@ class ImageMedium extends Medium $this->reset(); } - return trim(Grav::instance()['base_url'] . '/' . ltrim($output . $this->querystring() . $this->urlHash(), '/'), '\\'); + return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\'); } /** @@ -229,9 +232,9 @@ class ImageMedium extends Medium } /** - * Allows the ability to override the Inmage's Pretty name stored in cache + * Allows the ability to override the image's pretty name stored in cache * - * @param $name + * @param string $name */ public function setImagePrettyName($name) { @@ -260,11 +263,12 @@ class ImageMedium extends Medium * widths. Existing image alternatives won't be overwritten. * * @param int|int[] $min_width - * @param int [$max_width=2500] - * @param int [$step=200] + * @param int $max_width + * @param int $step * @return $this */ - public function derivatives($min_width, $max_width = 2500, $step = 200) { + public function derivatives($min_width, $max_width = 2500, $step = 200) + { if (!empty($this->alternatives)) { $max = max(array_keys($this->alternatives)); $base = $this->alternatives[$max]; @@ -331,7 +335,7 @@ class ImageMedium extends Medium * Parsedown element for source display mode * * @param array $attributes - * @param boolean $reset + * @param bool $reset * @return array */ public function sourceParsedownElement(array $attributes, $reset = true) @@ -344,7 +348,7 @@ class ImageMedium extends Medium $attributes['sizes'] = $this->sizes(); } - return [ 'name' => 'img', 'attributes' => $attributes ]; + return ['name' => 'img', 'attributes' => $attributes]; } /** @@ -358,7 +362,7 @@ class ImageMedium extends Medium if ($this->image) { $this->image(); - $this->querystring(''); + $this->medium_querystring = []; $this->filter(); $this->clearAlternatives(); } @@ -374,7 +378,7 @@ class ImageMedium extends Medium /** * Turn the current Medium into a Link * - * @param boolean $reset + * @param bool $reset * @param array $attributes * @return Link */ @@ -394,7 +398,7 @@ class ImageMedium extends Medium * * @param int $width * @param int $height - * @param boolean $reset + * @param bool $reset * @return Link */ public function lightbox($width = null, $height = null, $reset = true) @@ -404,7 +408,7 @@ class ImageMedium extends Medium } if ($width && $height) { - $this->cropResize($width, $height); + $this->__call('cropResize', [$width, $height]); } return parent::lightbox($width, $height, $reset); @@ -414,7 +418,7 @@ class ImageMedium extends Medium * Sets or gets the quality of the image * * @param int $quality 0-100 quality - * @return Medium + * @return int|$this */ public function quality($quality = null) { @@ -424,6 +428,7 @@ class ImageMedium extends Medium } $this->quality = $quality; + return $this; } @@ -443,6 +448,7 @@ class ImageMedium extends Medium } $this->format = $format; + return $this; } @@ -457,6 +463,7 @@ class ImageMedium extends Medium if ($sizes) { $this->sizes = $sizes; + return $this; } @@ -477,10 +484,12 @@ class ImageMedium extends Medium */ public function width($value = 'auto') { - if (!$value || $value === 'auto') + if (!$value || $value === 'auto') { $this->attributes['width'] = $this->get('width'); - else + } else { $this->attributes['width'] = $value; + } + return $this; } @@ -498,10 +507,12 @@ class ImageMedium extends Medium */ public function height($value = 'auto') { - if (!$value || $value === 'auto') + if (!$value || $value === 'auto') { $this->attributes['height'] = $this->get('height'); - else + } else { $this->attributes['height'] = $value; + } + return $this; } @@ -608,7 +619,7 @@ class ImageMedium extends Medium $this->image->merge(ImageFile::open($overlay)); } - return $this->image->cacheFile($this->format, $this->quality); + return $this->image->cacheFile($this->format, $this->quality, false, [$this->get('width'), $this->get('height'), $this->get('modified')]); } /** diff --git a/system/src/Grav/Common/Page/Medium/Link.php b/system/src/Grav/Common/Page/Medium/Link.php index 15aac7b..6978635 100644 --- a/system/src/Grav/Common/Page/Medium/Link.php +++ b/system/src/Grav/Common/Page/Medium/Link.php @@ -1,8 +1,9 @@ get('system.media.enable_media_timestamp', true)) { - $this->querystring('&' . Grav::instance()['cache']->getKey()); + $this->timestamp = Grav::instance()['cache']->getKey(); } $this->def('mime', 'application/octet-stream'); @@ -112,6 +127,49 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface return false; } + /** + * Get file modification time for the medium. + * + * @return int|null + */ + public function modified() + { + $path = $this->get('filepath'); + + if (!file_exists($path)) { + return null; + } + + return filemtime($path) ?: null; + } + + /** + * @return int + */ + public function size() + { + $path = $this->get('filepath'); + + if (!file_exists($path)) { + return 0; + } + + return filesize($path) ?: 0; + } + + /** + * Set querystring to file modification timestamp (or value provided as a parameter). + * + * @param string|int|null $timestamp + * @return $this + */ + public function setTimestamp($timestamp = null) + { + $this->timestamp = (string)($timestamp ?? $this->modified()); + + return $this; + } + /** * Returns an array containing just the metadata * @@ -125,7 +183,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface /** * Add meta file for the medium. * - * @param $filepath + * @param string $filepath */ public function addMetaFile($filepath) { @@ -136,7 +194,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface /** * Add alternative Medium to this Medium. * - * @param $ratio + * @param int|float $ratio * @param Medium $alternative */ public function addAlternative($ratio, Medium $alternative) @@ -184,11 +242,18 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface */ public function relativePath($reset = true) { + $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath')); + + $locator = Grav::instance()['locator']; + if ($locator->isStream($output)) { + $output = $locator->findResource($output, false); + } + if ($reset) { $this->reset(); } - return str_replace(GRAV_ROOT, '', $this->get('filepath')); + return str_replace(GRAV_ROOT, '', $output); } /** @@ -210,40 +275,60 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface $this->reset(); } - return trim(Grav::instance()['base_url'] . '/' . ltrim($output . $this->querystring() . $this->urlHash(), '/'), '\\'); + return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\'); } /** * Get/set querystring for the file's url * * @param string $querystring - * @param boolean $withQuestionmark + * @param bool $withQuestionmark * @return string */ public function querystring($querystring = null, $withQuestionmark = true) { - if (!is_null($querystring)) { - $this->set('querystring', ltrim($querystring, '?&')); - + if (null !== $querystring) { + $this->medium_querystring[] = ltrim($querystring, '?&'); foreach ($this->alternatives as $alt) { $alt->querystring($querystring, $withQuestionmark); } } - $querystring = $this->get('querystring', ''); - - if ($withQuestionmark && !empty($querystring)) { - return '?' . $querystring; - } else { - return $querystring; + if (empty($this->medium_querystring)) { + return ''; } + + // join the strings + $querystring = implode('&', $this->medium_querystring); + // explode all strings + $query_parts = explode('&', $querystring); + // Join them again now ensure the elements are unique + $querystring = implode('&', array_unique($query_parts)); + + return $withQuestionmark ? ('?' . $querystring) : $querystring; + } + + /** + * Get the URL with full querystring + * + * @param string $url + * @return string + */ + public function urlQuerystring($url) + { + $querystring = $this->querystring(); + if (isset($this->timestamp) && !Utils::contains($querystring, $this->timestamp)) { + $querystring = empty($querystring) ? ('?' . $this->timestamp) : ($querystring . '&' . $this->timestamp); + } + + return ltrim($url . $querystring . $this->urlHash(), '/'); } /** * Get/set hash for the file's url * * @param string $hash - * @param boolean $withHash + * @param bool $withHash * @return string */ public function urlHash($hash = null, $withHash = true) @@ -254,11 +339,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface $hash = $this->get('urlHash', ''); - if ($withHash && !empty($hash)) { - return '#' . $hash; - } else { - return $hash; - } + return $withHash && !empty($hash) ? '#' . $hash : $hash; } /** @@ -268,7 +349,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface * @param string $alt * @param string $class * @param string $id - * @param boolean $reset + * @param bool $reset * @return array */ public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true) @@ -332,6 +413,8 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface case 'source': $element = $this->sourceParsedownElement($attributes, false); break; + default: + $element = []; } if ($reset) { @@ -347,7 +430,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface * Parsedown element for source display mode * * @param array $attributes - * @param boolean $reset + * @param bool $reset * @return array */ protected function sourceParsedownElement(array $attributes, $reset = true) @@ -359,7 +442,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface * Parsedown element for text display mode * * @param array $attributes - * @param boolean $reset + * @param bool $reset * @return array */ protected function textParsedownElement(array $attributes, $reset = true) @@ -434,7 +517,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface */ public function thumbnail($type = 'auto') { - if ($type !== 'auto' && !in_array($type, $this->thumbnailTypes)) { + if ($type !== 'auto' && !\in_array($type, $this->thumbnailTypes, true)) { return $this; } @@ -451,7 +534,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface /** * Turn the current Medium into a Link * - * @param boolean $reset + * @param bool $reset * @param array $attributes * @return Link */ @@ -475,7 +558,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface * * @param int $width * @param int $height - * @param boolean $reset + * @param bool $reset * @return Link */ public function lightbox($width = null, $height = null, $reset = true) @@ -500,7 +583,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface { $classes = func_get_args(); if (!empty($classes)) { - $this->attributes['class'] = implode(',', (array)$classes); + $this->attributes['class'] = implode(',', $classes); } return $this; @@ -510,7 +593,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface * Add an id to the element from Markdown or Twig * Example: ![Example](myimg.png?id=primary-img) * - * @param $id + * @param string $id * @return $this */ public function id($id) @@ -545,7 +628,7 @@ class Medium extends Data implements RenderableInterface, MediaObjectInterface public function __call($method, $args) { $qs = $method; - if (count($args) > 1 || (count($args) == 1 && !empty($args[0]))) { + if (\count($args) > 1 || (\count($args) === 1 && !empty($args[0]))) { $qs .= '=' . implode(',', array_map(function ($a) { if (is_array($a)) { $a = '[' . implode(',', $a) . ']'; diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php index b5b197e..dc372d8 100644 --- a/system/src/Grav/Common/Page/Medium/MediumFactory.php +++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php @@ -1,8 +1,9 @@ get("media.types." . strtolower($ext)); - if (!$media_params) { + $media_params = $config->get('media.types.' . strtolower($ext)); + if (!\is_array($media_params)) { return null; } $params += $media_params; // Add default settings for undefined variables. - $params += $config->get('media.types.defaults'); + $params += (array)$config->get('media.types.defaults'); + $params += [ + 'type' => 'file', + 'thumb' => 'media/thumb.png', + 'mime' => 'application/octet-stream', + 'filepath' => $file, + 'filename' => $filename, + 'basename' => $basename, + 'extension' => $ext, + 'path' => $path, + 'modified' => filemtime($file), + 'thumbnails' => [] + ]; + + $locator = Grav::instance()['locator']; + + $file = $locator->findResource("image://{$params['thumb']}"); + if ($file) { + $params['thumbnails']['default'] = $file; + } + + return static::fromArray($params); + } + + /** + * Create Medium from an uploaded file + * + * @param FormFlashFile $uploadedFile + * @param array $params + * @return Medium + */ + public static function fromUploadedFile(FormFlashFile $uploadedFile, array $params = []) + { + $parts = pathinfo($uploadedFile->getClientFilename()); + $filename = $parts['basename']; + $ext = $parts['extension']; + $basename = $parts['filename']; + $file = $uploadedFile->getTmpFile(); + $path = dirname($file); + + $config = Grav::instance()['config']; + + $media_params = $config->get('media.types.' . strtolower($ext)); + if (!\is_array($media_params)) { + return null; + } + + $params += $media_params; + + // Add default settings for undefined variables. + $params += (array)$config->get('media.types.defaults'); $params += [ 'type' => 'file', 'thumb' => 'media/thumb.png', @@ -75,28 +127,22 @@ class MediumFactory */ public static function fromArray(array $items = [], Blueprint $blueprint = null) { - $type = isset($items['type']) ? $items['type'] : null; + $type = $items['type'] ?? null; switch ($type) { case 'image': return new ImageMedium($items, $blueprint); - break; case 'thumbnail': return new ThumbnailImageMedium($items, $blueprint); - break; case 'animated': case 'vector': return new StaticImageMedium($items, $blueprint); - break; case 'video': return new VideoMedium($items, $blueprint); - break; case 'audio': return new AudioMedium($items, $blueprint); - break; default: return new Medium($items, $blueprint); - break; } } diff --git a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php index aaf6fde..c6d75bc 100644 --- a/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php +++ b/system/src/Grav/Common/Page/Medium/ParsedownHtmlTrait.php @@ -1,21 +1,23 @@ parsedownElement($title, $alt, $class, $id, $reset); if (!$this->parsedown) { - $this->parsedown = new Parsedown(null, null); + $this->parsedown = new Parsedown(new Excerpts()); } return $this->parsedown->elementToHtml($element); diff --git a/system/src/Grav/Common/Page/Medium/RenderableInterface.php b/system/src/Grav/Common/Page/Medium/RenderableInterface.php index 35b0378..50a6e67 100644 --- a/system/src/Grav/Common/Page/Medium/RenderableInterface.php +++ b/system/src/Grav/Common/Page/Medium/RenderableInterface.php @@ -1,8 +1,9 @@ url($reset); - return [ 'name' => 'img', 'attributes' => $attributes ]; + return ['name' => 'img', 'attributes' => $attributes]; } } diff --git a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php index 4e7d619..8d6d43f 100644 --- a/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php +++ b/system/src/Grav/Common/Page/Medium/StaticResizeTrait.php @@ -1,8 +1,9 @@ bubble('thumbnail', [$type], false); + return $this->bubble('getThumbnail', [], false); } /** * Turn the current Medium into a Link * - * @param boolean $reset + * @param bool $reset * @param array $attributes * @return Link */ @@ -103,7 +105,7 @@ class ThumbnailImageMedium extends ImageMedium * * @param int $width * @param int $height - * @param boolean $reset + * @param bool $reset * @return Link */ public function lightbox($width = null, $height = null, $reset = true) @@ -116,7 +118,7 @@ class ThumbnailImageMedium extends ImageMedium * * @param string $method * @param array $arguments - * @param boolean $testLinked + * @param bool $testLinked * @return Medium */ protected function bubble($method, array $arguments = [], $testLinked = true) diff --git a/system/src/Grav/Common/Page/Medium/VideoMedium.php b/system/src/Grav/Common/Page/Medium/VideoMedium.php index 23271bf..c0b488a 100644 --- a/system/src/Grav/Common/Page/Medium/VideoMedium.php +++ b/system/src/Grav/Common/Page/Medium/VideoMedium.php @@ -1,8 +1,9 @@ attributes['autoplay'] = true; + if ($status) { + $this->attributes['autoplay'] = ''; } else { unset($this->attributes['autoplay']); } @@ -94,6 +95,23 @@ class VideoMedium extends Medium return $this; } + /** + * Allows ability to set the preload option + * + * @param null $status + * @return $this + */ + public function preload($status = null) + { + if ($status) { + $this->attributes['preload'] = $status; + } else { + unset($this->attributes['preload']); + } + + return $this; + } + /** * Allows to set the playsinline attribute * diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 0e57b03..1730359 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -1,14 +1,14 @@ extension('.' . $file->getExtension()); + } else { + $this->extension($extension); + } + + // extract page language from page extension + $language = trim(basename($this->extension(), 'md'), '.') ?: null; + $this->language($language); + $this->hide_home_route = $config->get('system.home.hide_in_urls', false); $this->home_route = $this->adjustRouteCase($config->get('system.home.alias')); - $this->filePath($file->getPathName()); + $this->filePath($file->getPathname()); $this->modified($file->getMTime()); $this->id($this->modified() . md5($this->filePath())); $this->routable(true); @@ -142,16 +158,6 @@ class Page implements PageInterface $this->published(); $this->urlExtension(); - // some extension logic - if (empty($extension)) { - $this->extension('.' . $file->getExtension()); - } else { - $this->extension($extension); - } - - // extract page language from page extension - $language = trim(basename($this->extension(), 'md'), '.') ?: null; - $this->language($language); return $this; } @@ -193,7 +199,7 @@ class Page implements PageInterface $aPage = new Page(); $aPage->init(new \SplFileInfo($path), $language . '.md'); - $route = isset($aPage->header()->routes['default']) ? $aPage->header()->routes['default'] : $aPage->rawRoute(); + $route = $aPage->header()->routes['default'] ?? $aPage->rawRoute(); if (!$route) { $route = $aPage->route(); } @@ -218,7 +224,7 @@ class Page implements PageInterface */ public function untranslatedLanguages($includeUnpublished = false) { - $filename = substr($this->name, 0, -(strlen($this->extension()))); + $filename = substr($this->name, 0, -strlen($this->extension())); $config = Grav::instance()['config']; $languages = $config->get('system.languages.supported', []); $untranslatedLanguages = []; @@ -340,9 +346,9 @@ class Page implements PageInterface $this->processFrontmatter(); } } - } catch (ParseException $e) { + } catch (\Exception $e) { $file->raw(Grav::instance()['language']->translate([ - 'FRONTMATTER_ERROR_PAGE', + 'GRAV.FRONTMATTER_ERROR_PAGE', $this->slug(), $file->filename(), $e->getMessage(), @@ -360,10 +366,10 @@ class Page implements PageInterface if ($var) { if (isset($this->header->slug)) { - $this->slug(($this->header->slug)); + $this->slug($this->header->slug); } if (isset($this->header->routes)) { - $this->routes = (array)($this->header->routes); + $this->routes = (array)$this->header->routes; } if (isset($this->header->title)) { $this->title = trim($this->header->title); @@ -408,12 +414,10 @@ class Page implements PageInterface $this->markdown_extra = (bool)$this->header->markdown_extra; } if (isset($this->header->taxonomy)) { - foreach ((array)$this->header->taxonomy as $taxonomy => $taxitems) { - $this->taxonomy[$taxonomy] = (array)$taxitems; - } + $this->taxonomy($this->header->taxonomy); } if (isset($this->header->max_count)) { - $this->max_count = intval($this->header->max_count); + $this->max_count = (int)$this->header->max_count; } if (isset($this->header->process)) { foreach ((array)$this->header->process as $process => $status) { @@ -430,7 +434,7 @@ class Page implements PageInterface $this->unpublishDate($this->header->unpublish_date); } if (isset($this->header->expires)) { - $this->expires = intval($this->header->expires); + $this->expires = (int)$this->header->expires; } if (isset($this->header->cache_control)) { $this->cache_control = $this->header->cache_control; @@ -450,6 +454,9 @@ class Page implements PageInterface if (isset($this->header->debugger)) { $this->debugger = (bool)$this->header->debugger; } + if (isset($this->header->append_url_extension)) { + $this->url_extension = $this->header->append_url_extension; + } } return $this->header; @@ -458,7 +465,7 @@ class Page implements PageInterface /** * Get page language * - * @param $var + * @param string $var * * @return mixed */ @@ -474,20 +481,73 @@ class Page implements PageInterface /** * Modify a header value directly * - * @param $key - * @param $value + * @param string $key + * @param mixed $value */ public function modifyHeader($key, $value) { $this->header->{$key} = $value; } + /** + * @return int + */ + public function httpResponseCode() + { + return (int)($this->header()->http_response_code ?? 200); + } + + public function httpHeaders() + { + $headers = []; + + $grav = Grav::instance(); + $format = $this->templateFormat(); + $cache_control = $this->cacheControl(); + $expires = $this->expires(); + + // Set Content-Type header + $headers['Content-Type'] = Utils::getMimeByExtension($format, 'text/html'); + + // Calculate Expires Headers if set to > 0 + if ($expires > 0) { + $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT'; + if (!$cache_control) { + $headers['Cache-Control'] = 'max-age=' . $expires; + } + $headers['Expires'] = $expires_date; + } + + // Set Cache-Control header + if ($cache_control) { + $headers['Cache-Control'] = strtolower($cache_control); + } + + // Set Last-Modified header + if ($this->lastModified()) { + $last_modified_date = gmdate('D, d M Y H:i:s', $this->modified()) . ' GMT'; + $headers['Last-Modified'] = $last_modified_date; + } + + // Calculate ETag based on the raw file + if ($this->eTag()) { + $headers['ETag'] = '"' . md5($this->raw() . $this->modified()).'"'; + } + + // Set Vary: Accept-Encoding header + if ($grav['config']->get('system.pages.vary_accept_encoding', false)) { + $headers['Vary'] = 'Accept-Encoding'; + } + + return $headers; + } + /** * Get the summary. * * @param int $size Max summary size. * - * @param boolean $textOnly Only count text size. + * @param bool $textOnly Only count text size. * * @return string */ @@ -551,7 +611,7 @@ class Page implements PageInterface return mb_strimwidth($content, 0, $size, '...', 'utf-8'); } - $summary = Utils::truncateHTML($content, $size); + $summary = Utils::truncateHtml($content, $size); return html_entity_decode($summary); } @@ -613,13 +673,13 @@ class Page implements PageInterface $process_markdown = $this->shouldProcess('markdown'); $process_twig = $this->shouldProcess('twig') || $this->modularTwig(); - $cache_enable = isset($this->header->cache_enable) ? $this->header->cache_enable : $config->get('system.cache.enabled', + $cache_enable = $this->header->cache_enable ?? $config->get('system.cache.enabled', true); - $twig_first = isset($this->header->twig_first) ? $this->header->twig_first : $config->get('system.pages.twig_first', + $twig_first = $this->header->twig_first ?? $config->get('system.pages.twig_first', true); // never cache twig means it's always run after content - $never_cache_twig = isset($this->header->never_cache_twig) ? $this->header->never_cache_twig : $config->get('system.pages.never_cache_twig', + $never_cache_twig = $this->header->never_cache_twig ?? $config->get('system.pages.never_cache_twig', false); // if no cached-content run everything @@ -687,6 +747,8 @@ class Page implements PageInterface $this->content = str_replace("

{$delimiter}

", '', $this->content); } + // Fire event when Page::content() is called + Grav::instance()->fireEvent('onPageContent', new Event(['page' => $this])); } return $this->content; @@ -709,8 +771,8 @@ class Page implements PageInterface /** * Add an entry to the page's contentMeta array * - * @param $name - * @param $value + * @param string $name + * @param string $value */ public function addContentMeta($name, $value) { @@ -720,9 +782,9 @@ class Page implements PageInterface /** * Return the whole contentMeta array as it currently stands * - * @param null $name + * @param string|null $name * - * @return mixed + * @return string */ public function getContentMeta($name = null) { @@ -740,9 +802,9 @@ class Page implements PageInterface /** * Sets the whole content meta array in one shot * - * @param $content_meta + * @param array $content_meta * - * @return mixed + * @return array */ public function setContentMeta($content_meta) { @@ -757,23 +819,31 @@ class Page implements PageInterface /** @var Config $config */ $config = Grav::instance()['config']; - $defaults = (array)$config->get('system.pages.markdown'); + $markdownDefaults = (array)$config->get('system.pages.markdown'); if (isset($this->header()->markdown)) { - $defaults = array_merge($defaults, $this->header()->markdown); + $markdownDefaults = array_merge($markdownDefaults, $this->header()->markdown); } // pages.markdown_extra is deprecated, but still check it... - if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { + if (!isset($markdownDefaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED); - $defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); + $markdownDefaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); } + $extra = $markdownDefaults['extra'] ?? false; + $defaults = [ + 'markdown' => $markdownDefaults, + 'images' => $config->get('system.images', []) + ]; + + $excerpts = new Excerpts($this, $defaults); + // Initialize the preferred variant of Parsedown - if ($defaults['extra']) { - $parsedown = new ParsedownExtra($this, $defaults); + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); } else { - $parsedown = new Parsedown($this, $defaults); + $parsedown = new Parsedown($excerpts); } $this->content = $parsedown->text($this->content); @@ -812,13 +882,11 @@ class Page implements PageInterface /** * Needed by the onPageContentProcessed event to set the raw page content * - * @param $content + * @param string $content */ public function setRawContent($content) { - $content = $content === null ? '': $content; - - $this->content = $content; + $this->content = $content ?? ''; } /** @@ -835,7 +903,9 @@ class Page implements PageInterface return $this->raw_content; } if ($name === 'route') { - return $this->parent()->rawRoute(); + $parent = $this->parent(); + + return $parent ? $parent->rawRoute() : ''; } if ($name === 'order') { $order = $this->order(); @@ -904,9 +974,9 @@ class Page implements PageInterface /** * Gets and Sets the Page raw content * - * @param null $var + * @param string|null $var * - * @return null + * @return string */ public function rawMarkdown($var = null) { @@ -962,11 +1032,11 @@ class Page implements PageInterface * * You need to call $this->save() in order to perform the move. * - * @param Page $parent New parent page. + * @param PageInterface $parent New parent page. * * @return $this */ - public function move(Page $parent) + public function move(PageInterface $parent) { if (!$this->_original) { $clone = clone $this; @@ -976,10 +1046,10 @@ class Page implements PageInterface $this->_action = 'move'; if ($this->route() === $parent->route()) { - throw new Exception('Failed: Cannot set page parent to self'); + throw new \RuntimeException('Failed: Cannot set page parent to self'); } if (Utils::startsWith($parent->rawRoute(), $this->rawRoute())) { - throw new Exception('Failed: Cannot set page parent to a child of current page'); + throw new \RuntimeException('Failed: Cannot set page parent to a child of current page'); } $this->parent($parent); @@ -1006,11 +1076,11 @@ class Page implements PageInterface * Returns a new Page object for the copy. * You need to call $this->save() in order to perform the move. * - * @param Page $parent New parent page. + * @param PageInterface $parent New parent page. * * @return $this */ - public function copy($parent) + public function copy(PageInterface $parent) { $this->move($parent); $this->_action = 'copy'; @@ -1062,7 +1132,7 @@ class Page implements PageInterface /** * Validate page header. * - * @throws Exception + * @throws \Exception */ public function validate() { @@ -1135,6 +1205,87 @@ class Page implements PageInterface return $this->id(); } + /** + * Returns normalized list of name => form pairs. + * + * @return array + */ + public function forms() + { + if (null === $this->forms) { + $header = $this->header(); + + // Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin) + $grav = Grav::instance(); + $grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $this, 'header' => $header])); + + $rules = $header->rules ?? null; + if (!\is_array($rules)) { + $rules = []; + } + + $forms = []; + + // First grab page.header.form + $form = $this->normalizeForm($header->form ?? null, null, $rules); + if ($form) { + $forms[$form['name']] = $form; + } + + // Append page.header.forms (override singular form if it clashes) + $headerForms = $header->forms ?? null; + if (\is_array($headerForms)) { + foreach ($headerForms as $name => $form) { + $form = $this->normalizeForm($form, $name, $rules); + if ($form) { + $forms[$form['name']] = $form; + } + } + } + + $this->forms = $forms; + } + + return $this->forms; + } + + /** + * @param array $new + */ + public function addForms(array $new) + { + // Initialize forms. + $this->forms(); + + foreach ($new as $form) { + $form = $this->normalizeForm($form); + if ($form) { + $this->forms[$form['name']] = $form; + } + } + } + + protected function normalizeForm($form, $name = null, array $rules = []) + { + if (!\is_array($form)) { + return null; + } + + // Ignore numeric indexes on name. + if (!$name || (string)(int)$name === (string)$name) { + $name = null; + } + + $name = $name ?? $form['name'] ?? $this->slug(); + + $formRules = $form['rules'] ?? null; + if (!\is_array($formRules)) { + $formRules = []; + } + + return ['name' => $name, 'rules' => $rules + $formRules] + $form; + } + /** * Gets and sets the associated media as found in the page folder. * @@ -1186,7 +1337,7 @@ class Page implements PageInterface $this->name = $var; } - return empty($this->name) ? 'default.md' : $this->name; + return $this->name ?: 'default.md'; } /** @@ -1231,12 +1382,47 @@ class Page implements PageInterface { if ($var !== null) { $this->template_format = $var; + return $this->template_format; } - if (empty($this->template_format)) { - $this->template_format = Grav::instance()['uri']->extension('html'); + if (isset($this->template_format)) { + return $this->template_format; } + // Set from URL extension set on page + $page_extension = trim($this->header->append_url_extension ?? '' , '.'); + if (!empty($page_extension)) { + $this->template_format = $page_extension; + + return $this->template_format; + } + + // Set from uri extension + $uri_extension = Grav::instance()['uri']->extension(); + if (is_string($uri_extension)) { + $this->template_format = $uri_extension; + + return $this->template_format; + } + + // Use content negotiation via the `accept:` header + $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null; + if (is_string($http_accept)) { + $negotiator = new Negotiator(); + + $supported_types = Utils::getSupportPageTypes(['html', 'json']); + $priorities = Utils::getMimeTypes($supported_types); + + $media_type = $negotiator->getBest($http_accept, $priorities); + $mimetype = $media_type instanceof Accept ? $media_type->getValue() : ''; + + $this->template_format = Utils::getExtensionByMime($mimetype); + + return $this->template_format; + } + + // Last chance set a default type + $this->template_format = 'html'; return $this->template_format; } @@ -1272,9 +1458,8 @@ class Page implements PageInterface } // if not set in the page get the value from system config - if (empty($this->url_extension)) { - $this->url_extension = trim(isset($this->header->append_url_extension) ? $this->header->append_url_extension : Grav::instance()['config']->get('system.pages.append_url_extension', - false)); + if (null === $this->url_extension) { + $this->url_extension = Grav::instance()['config']->get('system.pages.append_url_extension', ''); } return $this->url_extension; @@ -1293,7 +1478,7 @@ class Page implements PageInterface $this->expires = $var; } - return !isset($this->expires) ? Grav::instance()['config']->get('system.pages.expires') : $this->expires; + return $this->expires ?? Grav::instance()['config']->get('system.pages.expires'); } /** @@ -1309,7 +1494,7 @@ class Page implements PageInterface $this->cache_control = $var; } - return !isset($this->cache_control) ? Grav::instance()['config']->get('system.pages.cache_control') : $this->cache_control; + return $this->cache_control ?? Grav::instance()['config']->get('system.pages.cache_control'); } /** @@ -1481,11 +1666,7 @@ class Page implements PageInterface */ public function debugger() { - if (isset($this->debugger) && $this->debugger === false) { - return false; - } - - return true; + return !(isset($this->debugger) && $this->debugger === false); } /** @@ -1538,7 +1719,7 @@ class Page implements PageInterface } else { // If it this is a standard meta data type if ($value) { - if (in_array($key, $header_tag_http_equivs)) { + if (\in_array($key, $header_tag_http_equivs, true)) { $this->metadata[$key] = [ 'http_equiv' => $key, 'content' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8') @@ -1569,6 +1750,14 @@ class Page implements PageInterface return $this->metadata; } + /** + * Reset the metadata and pull from header again + */ + public function resetMetadata() + { + $this->metadata = null; + } + /** * Gets and Sets the slug for the Page. The slug is used in the URL routing. If not set it uses * the parent folder from the path @@ -1587,7 +1776,6 @@ class Page implements PageInterface $this->slug = $this->adjustRouteCase(preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder)) ?: null; } - return $this->slug; } @@ -1601,7 +1789,7 @@ class Page implements PageInterface public function order($var = null) { if ($var !== null) { - $order = !empty($var) ? sprintf('%02d.', (int)$var) : ''; + $order = $var ? sprintf('%02d.', (int)$var) : ''; $this->folder($order . preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder)); return $order; @@ -1609,7 +1797,7 @@ class Page implements PageInterface preg_match(PAGE_ORDER_PREFIX_REGEX, $this->folder, $order); - return isset($order[0]) ? $order[0] : false; + return $order[0] ?? false; } /** @@ -1670,7 +1858,7 @@ class Page implements PageInterface /** @var Config $config */ $config = $grav['config']; - // get base route (multisite base and language) + // get base route (multi-site base and language) $route = $include_base ? $pages->baseRoute() : ''; // add full route if configured to do so @@ -1743,8 +1931,7 @@ class Page implements PageInterface */ public function unsetRouteSlug() { - unset($this->route); - unset($this->slug); + unset($this->route, $this->slug); } /** @@ -1761,7 +1948,8 @@ class Page implements PageInterface } if (empty($this->raw_route)) { - $baseRoute = $this->parent ? (string)$this->parent()->rawRoute() : null; + $parent = $this->parent(); + $baseRoute = $parent ? (string)$parent->rawRoute() : null; $slug = $this->adjustRouteCase(preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder)); @@ -1866,9 +2054,9 @@ class Page implements PageInterface /** * Gets and sets the option to show the etag header for the page. * - * @param boolean $var show etag header + * @param bool $var show etag header * - * @return boolean show etag header + * @return bool show etag header */ public function eTag($var = null) { @@ -1885,9 +2073,9 @@ class Page implements PageInterface /** * Gets and sets the option to show the last_modified header for the page. * - * @param boolean $var show last_modified header + * @param bool $var show last_modified header * - * @return boolean show last_modified header + * @return bool show last_modified header */ public function lastModified($var = null) { @@ -1916,7 +2104,7 @@ class Page implements PageInterface // Folder of the page. $this->folder = basename(dirname($var)); // Path to the page. - $this->path = dirname(dirname($var)); + $this->path = dirname($var, 2); } return $this->path . '/' . $this->folder . '/' . ($this->name ?: ''); @@ -1929,9 +2117,7 @@ class Page implements PageInterface */ public function filePathClean() { - $path = str_replace(ROOT_DIR, '', $this->filePath()); - - return $path; + return str_replace(ROOT_DIR, '', $this->filePath()); } /** @@ -1939,9 +2125,7 @@ class Page implements PageInterface */ public function relativePagePath() { - $path = str_replace('/' . $this->name(), '', $this->filePathClean()); - - return $path; + return str_replace('/' . $this->name(), '', $this->filePathClean()); } /** @@ -2023,12 +2207,16 @@ class Page implements PageInterface * @param string $var the order, either "asc" or "desc" * * @return string the order, either "asc" or "desc" + * @deprecated 1.6 */ public function orderDir($var = null) { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + if ($var !== null) { $this->order_dir = $var; } + if (empty($this->order_dir)) { $this->order_dir = 'asc'; } @@ -2047,9 +2235,12 @@ class Page implements PageInterface * @param string $var supported options include "default", "title", "date", and "folder" * * @return string supported options include "default", "title", "date", and "folder" + * @deprecated 1.6 */ public function orderBy($var = null) { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + if ($var !== null) { $this->order_by = $var; } @@ -2063,9 +2254,12 @@ class Page implements PageInterface * @param string $var supported options include "default", "title", "date", and "folder" * * @return array + * @deprecated 1.6 */ public function orderManual($var = null) { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + if ($var !== null) { $this->order_manual = $var; } @@ -2080,9 +2274,12 @@ class Page implements PageInterface * @param int $var the maximum number of sub-pages * * @return int the maximum number of sub-pages + * @deprecated 1.6 */ public function maxCount($var = null) { + //user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + if ($var !== null) { $this->max_count = (int)$var; } @@ -2105,6 +2302,14 @@ class Page implements PageInterface public function taxonomy($var = null) { if ($var !== null) { + // make sure first level are arrays + array_walk($var, function(&$value) { + $value = (array) $value; + }); + // make sure all values are strings + array_walk_recursive($var, function(&$value) { + $value = (string) $value; + }); $this->taxonomy = $var; } @@ -2156,17 +2361,17 @@ class Page implements PageInterface */ public function shouldProcess($process) { - return isset($this->process[$process]) ? (bool)$this->process[$process] : false; + return (bool)($this->process[$process] ?? false); } /** * Gets and Sets the parent object for this page * - * @param Page $var the parent page object + * @param PageInterface $var the parent page object * - * @return Page|null the parent page object if it exists. + * @return PageInterface|null the parent page object if it exists. */ - public function parent(Page $var = null) + public function parent(PageInterface $var = null) { if ($var) { $this->parent = $var->path(); @@ -2183,7 +2388,7 @@ class Page implements PageInterface /** * Gets the top parent object for this page * - * @return Page|null the top parent page object if it exists. + * @return PageInterface|null the top parent page object if it exists. */ public function topParent() { @@ -2208,7 +2413,7 @@ class Page implements PageInterface /** * Returns children of this page. * - * @return \Grav\Common\Page\Collection + * @return Collection */ public function children() { @@ -2222,11 +2427,12 @@ class Page implements PageInterface /** * Check to see if this item is the first in an array of sub-pages. * - * @return boolean True if item is first. + * @return bool True if item is first. */ public function isFirst() { - $collection = $this->parent()->collection('content', false); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; if ($collection instanceof Collection) { return $collection->isFirst($this->path()); } @@ -2237,11 +2443,12 @@ class Page implements PageInterface /** * Check to see if this item is the last in an array of sub-pages. * - * @return boolean True if item is last + * @return bool True if item is last */ public function isLast() { - $collection = $this->parent()->collection('content', false); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; if ($collection instanceof Collection) { return $collection->isLast($this->path()); } @@ -2252,7 +2459,7 @@ class Page implements PageInterface /** * Gets the previous sibling based on current position. * - * @return Page the previous Page item + * @return PageInterface the previous Page item */ public function prevSibling() { @@ -2262,7 +2469,7 @@ class Page implements PageInterface /** * Gets the next sibling based on current position. * - * @return Page the next Page item + * @return PageInterface the next Page item */ public function nextSibling() { @@ -2272,13 +2479,14 @@ class Page implements PageInterface /** * Returns the adjacent sibling based on a direction. * - * @param integer $direction either -1 or +1 + * @param int $direction either -1 or +1 * - * @return Page|bool the sibling page + * @return PageInterface|bool the sibling page */ public function adjacentSibling($direction = 1) { - $collection = $this->parent()->collection('content', false); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; if ($collection instanceof Collection) { return $collection->adjacentSibling($this->path(), $direction); } @@ -2289,18 +2497,17 @@ class Page implements PageInterface /** * Returns the item in the current position. * - * @param string $path the path the item - * - * @return Integer the index of the current page. + * @return int the index of the current page. */ public function currentPosition() { - $collection = $this->parent()->collection('content', false); + $parent = $this->parent(); + $collection = $parent ? $parent->collection('content', false) : null; if ($collection instanceof Collection) { return $collection->currentPosition($this->path()); } - return true; + return 1; } /** @@ -2313,14 +2520,7 @@ class Page implements PageInterface $uri_path = rtrim(urldecode(Grav::instance()['uri']->path()), '/') ?: '/'; $routes = Grav::instance()['pages']->routes(); - if (isset($routes[$uri_path])) { - if ($routes[$uri_path] === $this->path()) { - return true; - } - - } - - return false; + return isset($routes[$uri_path]) && $routes[$uri_path] === $this->path(); } /** @@ -2337,7 +2537,7 @@ class Page implements PageInterface $routes = Grav::instance()['pages']->routes(); if (isset($routes[$uri_path])) { - /** @var Page $child_page */ + /** @var PageInterface $child_page */ $child_page = $pages->dispatch($uri->route())->parent(); if ($child_page) { while (!$child_page->root()) { @@ -2360,9 +2560,8 @@ class Page implements PageInterface public function home() { $home = Grav::instance()['config']->get('system.home.alias'); - $is_home = ($this->route() === $home || $this->rawRoute() === $home); - return $is_home; + return $this->route() === $home || $this->rawRoute() === $home; } /** @@ -2372,20 +2571,15 @@ class Page implements PageInterface */ public function root() { - if (!$this->parent && !$this->name && !$this->visible) { - return true; - } - - return false; + return !$this->parent && !$this->name && !$this->visible; } /** * Helper method to return an ancestor page. * - * @param string $url The url of the page * @param bool $lookup Name of the parent folder * - * @return \Grav\Common\Page\Page page you were looking for if it exists + * @return PageInterface page you were looking for if it exists */ public function ancestor($lookup = null) { @@ -2401,7 +2595,7 @@ class Page implements PageInterface * * @param string $field Name of the parent folder * - * @return Page + * @return PageInterface */ public function inherited($field) { @@ -2440,7 +2634,7 @@ class Page implements PageInterface /** @var Pages $pages */ $inherited = $pages->inherited($this->route, $field); - $inheritedParams = (array)$inherited->value('header.' . $field); + $inheritedParams = $inherited ? (array)$inherited->value('header.' . $field) : []; $currentParams = (array)$this->value('header.' . $field); if ($inheritedParams && is_array($inheritedParams)) { $currentParams = array_replace_recursive($inheritedParams, $currentParams); @@ -2455,7 +2649,7 @@ class Page implements PageInterface * @param string $url the url of the page * @param bool $all * - * @return \Grav\Common\Page\Page page you were looking for if it exists + * @return PageInterface page you were looking for if it exists */ public function find($url, $all = false) { @@ -2469,7 +2663,7 @@ class Page implements PageInterface * Get a collection of pages in the current context. * * @param string|array $params - * @param boolean $pagination + * @param bool $pagination * * @return Collection * @throws \InvalidArgumentException @@ -2505,7 +2699,7 @@ class Page implements PageInterface /** @var Config $config */ $config = Grav::instance()['config']; - $process_taxonomy = isset($params['url_taxonomy_filters']) ? $params['url_taxonomy_filters'] : $config->get('system.pages.url_taxonomy_filters'); + $process_taxonomy = $params['url_taxonomy_filters'] ?? $config->get('system.pages.url_taxonomy_filters'); if ($process_taxonomy) { foreach ((array)$config->get('site.taxonomies') as $taxonomy) { @@ -2520,8 +2714,7 @@ class Page implements PageInterface } foreach ($items as $item) { $item = rawurldecode($item); - if (empty($page->taxonomy[$taxonomy]) || !in_array(htmlspecialchars_decode($item, - ENT_QUOTES), $page->taxonomy[$taxonomy]) + if (empty($page->taxonomy[$taxonomy]) || !\in_array(htmlspecialchars_decode($item, ENT_QUOTES), $page->taxonomy[$taxonomy], true) ) { $collection->remove($page->path()); } @@ -2537,12 +2730,9 @@ class Page implements PageInterface // remove any inclusive sets from filer: $sets = ['published', 'visible', 'modular', 'routable']; foreach ($sets as $type) { - if (isset($params['filter'][$type]) && isset($params['filter']['non-'.$type])) { - if ($params['filter'][$type] && $params['filter']['non-'.$type]) { - unset ($params['filter'][$type]); - unset ($params['filter']['non-'.$type]); - } - + $var = "non-{$type}"; + if (isset($params['filter'][$type], $params['filter'][$var]) && $params['filter'][$type] && $params['filter'][$var]) { + unset ($params['filter'][$type], $params['filter'][$var]); } } @@ -2602,17 +2792,17 @@ class Page implements PageInterface } if (isset($params['dateRange'])) { - $start = isset($params['dateRange']['start']) ? $params['dateRange']['start'] : 0; - $end = isset($params['dateRange']['end']) ? $params['dateRange']['end'] : false; - $field = isset($params['dateRange']['field']) ? $params['dateRange']['field'] : false; + $start = $params['dateRange']['start'] ?? 0; + $end = $params['dateRange']['end'] ?? false; + $field = $params['dateRange']['field'] ?? false; $collection->dateRange($start, $end, $field); } if (isset($params['order'])) { - $by = isset($params['order']['by']) ? $params['order']['by'] : 'default'; - $dir = isset($params['order']['dir']) ? $params['order']['dir'] : 'asc'; - $custom = isset($params['order']['custom']) ? $params['order']['custom'] : null; - $sort_flags = isset($params['order']['sort_flags']) ? $params['order']['sort_flags'] : null; + $by = $params['order']['by'] ?? 'default'; + $dir = $params['order']['dir'] ?? 'asc'; + $custom = $params['order']['custom'] ?? null; + $sort_flags = $params['order']['sort_flags'] ?? null; if (is_array($sort_flags)) { $sort_flags = array_map('constant', $sort_flags); //transform strings to constant value @@ -2625,7 +2815,7 @@ class Page implements PageInterface } /** @var Grav $grav */ - $grav = Grav::instance()['grav']; + $grav = Grav::instance(); // New Custom event to handle things like pagination. $grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection])); @@ -2634,7 +2824,7 @@ class Page implements PageInterface if ($pagination) { $params = $collection->params(); - $limit = isset($params['limit']) ? $params['limit'] : 0; + $limit = $params['limit'] ?? 0; $start = !empty($params['pagination']) ? ($uri->currentPage() - 1) * $limit : 0; if ($limit && $collection->count() > $limit) { @@ -2649,7 +2839,6 @@ class Page implements PageInterface * @param string|array $value * @param bool $only_published * @return mixed - * @internal */ public function evaluate($value, $only_published = true) { @@ -2666,9 +2855,9 @@ class Page implements PageInterface $result = []; foreach ((array)$value as $key => $val) { if (is_int($key)) { - $result = $result + $this->evaluate($val)->toArray(); + $result = $result + $this->evaluate($val, $only_published)->toArray(); } else { - $result = $result + $this->evaluate([$key => $val])->toArray(); + $result = $result + $this->evaluate([$key => $val], $only_published)->toArray(); } } @@ -2749,7 +2938,7 @@ class Page implements PageInterface case 'page': case 'self': $results = new Collection(); - $results = $results->addPage($page)->nonModular(); + $results = $results->addPage($page); break; case 'descendants': @@ -2865,7 +3054,7 @@ class Page implements PageInterface /** * Reorders all siblings according to a defined order * - * @param $new_order + * @param array|null $new_order */ protected function doReorder($new_order) { @@ -2878,28 +3067,32 @@ class Page implements PageInterface $this->_original->path($this->path()); - $siblings = $this->parent()->children(); - $siblings->order('slug', 'asc', $new_order); + $parent = $this->parent(); + $siblings = $parent ? $parent->children() : null; - $counter = 0; + if ($siblings) { + $siblings->order('slug', 'asc', $new_order); - // Reorder all moved pages. - foreach ($siblings as $slug => $page) { - $order = (int)trim($page->order(), '.'); - $counter++; + $counter = 0; - if ($order) { - if ($page->path() === $this->path() && $this->folderExists()) { - // Handle current page; we do want to change ordering number, but nothing else. - $this->order($counter); - $this->save(false); - } else { - // Handle all the other pages. - $page = $pages->get($page->path()); - if ($page && $page->folderExists() && !$page->_action) { - $page = $page->move($this->parent()); - $page->order($counter); - $page->save(false); + // Reorder all moved pages. + foreach ($siblings as $slug => $page) { + $order = (int)trim($page->order(), '.'); + $counter++; + + if ($order) { + if ($page->path() === $this->path() && $this->folderExists()) { + // Handle current page; we do want to change ordering number, but nothing else. + $this->order($counter); + $this->save(false); + } else { + // Handle all the other pages. + $page = $pages->get($page->path()); + if ($page && $page->folderExists() && !$page->_action) { + $page = $page->move($this->parent()); + $page->order($counter); + $page->save(false); + } } } } @@ -2911,7 +3104,7 @@ class Page implements PageInterface * * @internal * - * @throws Exception + * @throws \Exception */ protected function doRelocation() { @@ -2961,18 +3154,13 @@ class Page implements PageInterface { $case_insensitive = Grav::instance()['config']->get('system.force_lowercase_urls'); - if ($case_insensitive) { - return mb_strtolower($route); - } else { - return $route; - } + return $case_insensitive ? mb_strtolower($route) : $route; } /** * Gets the Page Unmodified (original) version of the page. * - * @return Page - * The original version of the page. + * @return PageInterface The original version of the page. */ public function getOriginal() { @@ -2982,8 +3170,7 @@ class Page implements PageInterface /** * Gets the action. * - * @return string - * The Action string. + * @return string The Action string. */ public function getAction() { diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index 0c527d1..cd0dcc0 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -1,8 +1,9 @@ grav[$type] . $this->baseRoute($lang); } @@ -218,11 +231,20 @@ class Pages return $this->baseUrl($lang, $absolute) . Uri::filterPath($route); } + public function setCheckMethod($method) + { + $this->check_method = strtolower($method); + } + /** * Class initialization. Must be called before using this class. */ public function init() { + if ($this->initialized) { + return; + } + $config = $this->grav['config']; $this->ignore_files = $config->get('system.pages.ignore_files'); $this->ignore_folders = $config->get('system.pages.ignore_folders'); @@ -232,6 +254,10 @@ class Pages $this->children = []; $this->routes = []; + if (!$this->check_method) { + $this->setCheckMethod($config->get('system.cache.check.method', 'file')); + } + $this->buildPages(); } @@ -254,7 +280,7 @@ class Pages /** * Returns a list of all pages. * - * @return array|Page[] + * @return array|PageInterface[] */ public function instances() { @@ -274,10 +300,10 @@ class Pages /** * Adds a page and assigns a route to it. * - * @param Page $page Page to be added. + * @param PageInterface $page Page to be added. * @param string $route Optional route (uses route from the object if not set). */ - public function addPage(Page $page, $route = null) + public function addPage(PageInterface $page, $route = null) { if (!isset($this->instances[$page->path()])) { $this->instances[$page->path()] = $page; @@ -294,13 +320,13 @@ class Pages /** * Sort sub-pages in a page. * - * @param Page $page + * @param PageInterface $page * @param string $order_by * @param string $order_dir * * @return array */ - public function sort(Page $page, $order_by = null, $order_dir = null, $sort_flags = null) + public function sort(PageInterface $page, $order_by = null, $order_dir = null, $sort_flags = null) { if ($order_by === null) { $order_by = $page->orderBy(); @@ -310,7 +336,7 @@ class Pages } $path = $page->path(); - $children = isset($this->children[$path]) ? $this->children[$path] : []; + $children = $this->children[$path] ?? []; if (!$children) { return $children; @@ -331,9 +357,10 @@ class Pages /** * @param Collection $collection - * @param $orderBy + * @param string|int $orderBy * @param string $orderDir - * @param null $orderManual + * @param array|null $orderManual + * @param int|null $sort_flags * * @return array * @internal @@ -365,12 +392,12 @@ class Pages * * @param string $path The filesystem full path of the page * - * @return Page + * @return PageInterface * @throws \Exception */ public function get($path) { - return isset($this->instances[(string)$path]) ? $this->instances[(string)$path] : null; + return $this->instances[(string)$path] ?? null; } /** @@ -382,7 +409,7 @@ class Pages */ public function children($path) { - $children = isset($this->children[(string)$path]) ? $this->children[(string)$path] : []; + $children = $this->children[(string)$path] ?? []; return new Collection($children, [], $this); } @@ -393,7 +420,7 @@ class Pages * @param string $route The relative URL of the page * @param string $path The relative path of the ancestor folder * - * @return Page|null + * @return PageInterface|null */ public function ancestor($route, $path = null) { @@ -403,8 +430,10 @@ class Pages if ($page && $page->path() === $path) { return $page; } - if ($page && !$page->parent()->root()) { - return $this->ancestor($page->parent()->route(), $path); + + $parent = $page ? $page->parent() : null; + if ($parent && !$parent->root()) { + return $this->ancestor($parent->route(), $path); } } @@ -417,7 +446,7 @@ class Pages * @param string $route The relative route of the page * @param string $field The field name of the ancestor to query for * - * @return Page|null + * @return PageInterface|null */ public function inherited($route, $field = null) { @@ -425,11 +454,12 @@ class Pages $page = $this->dispatch($route, true); - if ($page && $page->parent()->value('header.' . $field) !== null) { - return $page->parent(); + $parent = $page ? $page->parent() : null; + if ($parent && $parent->value('header.' . $field) !== null) { + return $parent; } - if ($page && !$page->parent()->root()) { - return $this->inherited($page->parent()->route(), $field); + if ($parent && !$parent->root()) { + return $this->inherited($parent->route(), $field); } } @@ -442,7 +472,7 @@ class Pages * @param string $route The relative URL of the page * @param bool $all * - * @return Page|null + * @return PageInterface|null */ public function find($route, $all = false) { @@ -456,7 +486,7 @@ class Pages * @param bool $all * * @param bool $redirect - * @return Page|null + * @return PageInterface|null * @throws \Exception */ public function dispatch($route, $all = false, $redirect = true) @@ -498,13 +528,13 @@ class Pages $source_url = $uri->uri(false); // Try Regex style redirects - $site_redirects = $config->get("site.redirects"); + $site_redirects = $config->get('site.redirects'); if (is_array($site_redirects)) { foreach ((array)$site_redirects as $pattern => $replace) { $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#'; try { $found = preg_replace($pattern, $replace, $source_url); - if ($found != $source_url) { + if ($found !== $source_url) { $this->grav->redirectLangSafe($found); } } catch (ErrorException $e) { @@ -514,7 +544,7 @@ class Pages } // Try Regex style routes - $site_routes = $config->get("site.routes"); + $site_routes = $config->get('site.routes'); if (is_array($site_routes)) { foreach ((array)$site_routes as $pattern => $replace) { $pattern = '#^' . str_replace('/', '\/', ltrim($pattern, '^')) . '#'; @@ -538,12 +568,13 @@ class Pages /** * Get root page. * - * @return Page + * @return PageInterface */ public function root() { /** @var UniformResourceLocator $locator */ $locator = $this->grav['locator']; + return $this->instances[rtrim($locator->findResource('page://'), DS)]; } @@ -577,15 +608,15 @@ class Pages /** * Get all pages * - * @param \Grav\Common\Page\Page $current + * @param PageInterface $current * * @return \Grav\Common\Page\Collection */ - public function all(Page $current = null) + public function all(PageInterface $current = null) { $all = new Collection(); - /** @var Page $current */ + /** @var PageInterface $current */ $current = $current ?: $this->root(); if (!$current->root()) { @@ -647,7 +678,7 @@ class Pages /** * Get list of route/title of all pages. * - * @param Page $current + * @param PageInterface $current * @param int $level * @param bool $rawRoutes * @@ -658,7 +689,7 @@ class Pages * @param bool $limitLevels * @return array */ - public function getList(Page $current = null, $level = 0, $rawRoutes = false, $showAll = true, $showFullpath = false, $showSlug = false, $showModular = false, $limitLevels = false) + public function getList(PageInterface $current = null, $level = 0, $rawRoutes = false, $showAll = true, $showFullpath = false, $showSlug = false, $showModular = false, $limitLevels = false) { if (!$current) { if ($level) { @@ -710,7 +741,6 @@ class Pages public static function getTypes() { if (!self::$types) { - $grav = Grav::instance(); $scanBlueprintsAndTemplates = function () use ($grav) { @@ -748,6 +778,18 @@ class Pages $scanBlueprintsAndTemplates(); } + // Register custom paths to the locator. + $locator = $grav['locator']; + foreach (self::$types as $type => $paths) { + foreach ($paths as $k => $path) { + if (strpos($path, 'blueprints://') === 0) { + unset($paths[$k]); + } + } + if ($paths) { + $locator->addPath('blueprints', "pages/$type.yaml", $paths); + } + } } return self::$types; @@ -788,7 +830,7 @@ class Pages /** @var Admin $admin */ $admin = Grav::instance()['admin']; - /** @var Page $page */ + /** @var PageInterface $page */ $page = $admin->getPage($admin->route); if ($page && $page->modular()) { @@ -811,19 +853,18 @@ class Pages $accessLevels = []; foreach ($this->all() as $page) { if (isset($page->header()->access)) { - if (is_array($page->header()->access)) { + if (\is_array($page->header()->access)) { foreach ($page->header()->access as $index => $accessLevel) { - if (is_array($accessLevel)) { + if (\is_array($accessLevel)) { foreach ($accessLevel as $innerIndex => $innerAccessLevel) { - array_push($accessLevels, $innerIndex); + $accessLevels[] = $innerIndex; } } else { - array_push($accessLevels, $index); + $accessLevels[] = $index; } } } else { - - array_push($accessLevels, $page->header()->access); + $accessLevels[] = $page->header()->access; } } } @@ -924,7 +965,7 @@ class Pages $taxonomy = $this->grav['taxonomy']; // how should we check for last modified? Default is by file - switch (strtolower($config->get('system.cache.check.method', 'file'))) { + switch ($this->check_method) { case 'none': case 'off': $hash = 0; @@ -962,7 +1003,7 @@ class Pages /** * Accessible method to manually reset the pages cache * - * @param $pages_dir + * @param string $pages_dir */ public function resetPages($pages_dir) { @@ -985,13 +1026,13 @@ class Pages * Recursive function to load & build page relationships. * * @param string $directory - * @param Page|null $parent + * @param PageInterface|null $parent * - * @return Page + * @return PageInterface * @throws \RuntimeException * @internal */ - protected function recurse($directory, Page $parent = null) + protected function recurse($directory, PageInterface $parent = null) { $directory = rtrim($directory, DS); $page = new Page; @@ -1002,13 +1043,10 @@ class Pages /** @var Language $language */ $language = $this->grav['language']; - // stuff to do at root page - if ($parent === null) { - - // Fire event for memory and time consuming plugins... - if ($config->get('system.pages.events.page')) { - $this->grav->fireEvent('onBuildPagesInitialized'); - } + // Stuff to do at root page + // Fire event for memory and time consuming plugins... + if ($parent === null && $config->get('system.pages.events.page')) { + $this->grav->fireEvent('onBuildPagesInitialized'); } $page->path($directory); @@ -1040,7 +1078,7 @@ class Pages $folders = []; $page_found = null; - $page_extension = ''; + $page_extension = '.md'; $last_modified = 0; $iterator = new \FilesystemIterator($directory); @@ -1049,7 +1087,7 @@ class Pages $filename = $file->getFilename(); // Ignore all hidden files if set. - if ($this->ignore_hidden && $filename && $filename[0] === '.') { + if ($this->ignore_hidden && $filename && strpos($filename, '.') === 0) { continue; } @@ -1123,9 +1161,15 @@ class Pages } } - // Set routability to false if no page found + if (!$content_exists) { + // Set routability to false if no page found $page->routable(false); + + // Hide empty folders if option set + if ($config->get('system.pages.hide_empty_folders')) { + $page->visible(false); + } } // Override the modified time if modular @@ -1154,14 +1198,14 @@ class Pages */ protected function buildRoutes() { - /** @var $taxonomy Taxonomy */ + /** @var Taxonomy $taxonomy */ $taxonomy = $this->grav['taxonomy']; // Get the home route $home = self::resetHomeRoute(); // Build routes and taxonomy map. - /** @var $page Page */ + /** @var PageInterface $page */ foreach ($this->instances as $page) { if (!$page->root()) { // process taxonomy @@ -1175,7 +1219,7 @@ class Pages $this->routes[$route] = $page_path; // add raw route - if ($raw_route != $route) { + if ($raw_route !== $route) { $this->routes[$raw_route] = $page_path; } @@ -1196,9 +1240,10 @@ class Pages } // Alias and set default route to home page. - if ($home && isset($this->routes['/' . $home])) { - $this->routes['/'] = $this->routes['/' . $home]; - $this->get($this->routes['/' . $home])->route('/'); + $homeRoute = '/' . $home; + if ($home && isset($this->routes[$homeRoute])) { + $this->routes['/'] = $this->routes[$homeRoute]; + $this->get($this->routes[$homeRoute])->route('/'); } } @@ -1206,8 +1251,8 @@ class Pages * @param string $path * @param array $pages * @param string $order_by - * @param array $manual - * @param int $sort_flags + * @param array|null $manual + * @param int|null $sort_flags * * @throws \RuntimeException * @internal @@ -1227,7 +1272,7 @@ class Pages } foreach ($pages as $key => $info) { - $child = isset($this->instances[$key]) ? $this->instances[$key] : null; + $child = $this->instances[$key] ?? null; if (!$child) { throw new \RuntimeException("Page does not exist: {$key}"); } @@ -1324,7 +1369,7 @@ class Pages foreach ($list as $key => $dummy) { $info = $pages[$key]; - $order = array_search($info['slug'], $manual); + $order = \array_search($info['slug'], $manual, true); if ($order === false) { $order = $i++; } diff --git a/system/src/Grav/Common/Page/Types.php b/system/src/Grav/Common/Page/Types.php index 436d4f8..03e3f6e 100644 --- a/system/src/Grav/Common/Page/Types.php +++ b/system/src/Grav/Common/Page/Types.php @@ -1,8 +1,9 @@ systemBlueprints) { - $blueprint = isset($this->systemBlueprints[$type]) ? $this->systemBlueprints[$type] : $this->systemBlueprints['default']; + $blueprint = $this->systemBlueprints[$type] ?? $this->systemBlueprints['default']; } if ($blueprint) { @@ -43,7 +44,7 @@ class Types implements \ArrayAccess, \Iterator, \Countable public function scanBlueprints($uri) { - if (!is_string($uri)) { + if (!\is_string($uri)) { throw new \InvalidArgumentException('First parameter must be URI'); } @@ -63,7 +64,7 @@ class Types implements \ArrayAccess, \Iterator, \Countable public function scanTemplates($uri) { - if (!is_string($uri)) { + if (!\is_string($uri)) { throw new \InvalidArgumentException('First parameter must be URI'); } @@ -96,9 +97,10 @@ class Types implements \ArrayAccess, \Iterator, \Countable if (strpos($name, '/')) { continue; } - $list[$name] = ucfirst(strtr($name, '_', ' ')); + $list[$name] = ucfirst(str_replace('_', ' ', $name)); } ksort($list); + return $list; } @@ -109,9 +111,10 @@ class Types implements \ArrayAccess, \Iterator, \Countable if (strpos($name, 'modular/') !== 0) { continue; } - $list[$name] = trim(ucfirst(strtr(basename($name), '_', ' '))); + $list[$name] = ucfirst(trim(str_replace('_', ' ', basename($name)))); } ksort($list); + return $list; } @@ -133,8 +136,6 @@ class Types implements \ArrayAccess, \Iterator, \Countable $options['value'] = 'Url'; } - $list = Folder::all($uri, $options); - - return $list; + return Folder::all($uri, $options); } } diff --git a/system/src/Grav/Common/Plugin.php b/system/src/Grav/Common/Plugin.php index ec5b346..8729a51 100644 --- a/system/src/Grav/Common/Plugin.php +++ b/system/src/Grav/Common/Plugin.php @@ -1,20 +1,21 @@ grav['events']; foreach ($events as $eventName => $params) { - if (is_string($params)) { + if (\is_string($params)) { $dispatcher->addListener($eventName, [$this, $params]); - } elseif (is_string($params[0])) { - $dispatcher->addListener($eventName, [$this, $params[0]], isset($params[1]) ? $params[1] : 0); + } elseif (\is_string($params[0])) { + $dispatcher->addListener($eventName, [$this, $params[0]], $this->getPriority($params, $eventName)); } else { foreach ($params as $listener) { - $dispatcher->addListener($eventName, [$this, $listener[0]], isset($listener[1]) ? $listener[1] : 0); + $dispatcher->addListener($eventName, [$this, $listener[0]], $this->getPriority($listener, $eventName)); } } } } + /** + * @param array $params + * @param string $eventName + */ + private function getPriority($params, $eventName) + { + $grav = Grav::instance(); + $override = implode('.', ["priorities", $this->name, $eventName, $params[0]]); + if ($grav['config']->get($override) !== null) + { + return $grav['config']->get($override); + } elseif (isset($params[1])) { + return $params[1]; + } + return 0; + } + /** * @param array $events */ @@ -158,9 +186,9 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess $dispatcher = $this->grav['events']; foreach ($events as $eventName => $params) { - if (is_string($params)) { + if (\is_string($params)) { $dispatcher->removeListener($eventName, [$this, $params]); - } elseif (is_string($params[0])) { + } elseif (\is_string($params[0])) { $dispatcher->removeListener($eventName, [$this, $params[0]]); } else { foreach ($params as $listener) { @@ -173,7 +201,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess /** * Whether or not an offset exists. * - * @param mixed $offset An offset to check for. + * @param string $offset An offset to check for. * @return bool Returns TRUE on success or FALSE on failure. */ public function offsetExists($offset) @@ -189,7 +217,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess /** * Returns the value at specified offset. * - * @param mixed $offset The offset to retrieve. + * @param string $offset The offset to retrieve. * @return mixed Can return all value types. */ public function offsetGet($offset) @@ -199,30 +227,30 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess if ($offset === 'title') { $offset = 'name'; } - return isset($this->blueprint[$offset]) ? $this->blueprint[$offset] : null; + return $this->blueprint[$offset] ?? null; } /** * Assigns a value to the specified offset. * - * @param mixed $offset The offset to assign the value to. + * @param string $offset The offset to assign the value to. * @param mixed $value The value to set. - * @throws LogicException + * @throws \LogicException */ public function offsetSet($offset, $value) { - throw new LogicException(__CLASS__ . ' blueprints cannot be modified.'); + throw new \LogicException(__CLASS__ . ' blueprints cannot be modified.'); } /** * Unsets an offset. * - * @param mixed $offset The offset to unset. - * @throws LogicException + * @param string $offset The offset to unset. + * @throws \LogicException */ public function offsetUnset($offset) { - throw new LogicException(__CLASS__ . ' blueprints cannot be modified.'); + throw new \LogicException(__CLASS__ . ' blueprints cannot be modified.'); } /** @@ -248,7 +276,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess /** * Merge global and page configurations. * - * @param Page $page The page to merge the configurations with the + * @param PageInterface $page The page to merge the configurations with the * plugin settings. * @param mixed $deep false = shallow|true = recursive|merge = recursive+unique * @param array $params Array of additional configuration options to @@ -257,7 +285,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess * * @return Data */ - protected function mergeConfig(Page $page, $deep = false, $params = [], $type = 'plugins') + protected function mergeConfig(PageInterface $page, $deep = false, $params = [], $type = 'plugins') { $class_name = $this->name; $class_name_merged = $class_name . '.merged'; @@ -265,10 +293,10 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess $page_header = $page->header(); $header = []; - if (!isset($page_header->$class_name_merged) && isset($page_header->$class_name)) { + if (!isset($page_header->{$class_name_merged}) && isset($page_header->{$class_name})) { // Get default plugin configurations and retrieve page header configuration - $config = $page_header->$class_name; - if (is_bool($config)) { + $config = $page_header->{$class_name}; + if (\is_bool($config)) { // Overwrite enabled option with boolean value in page header $config = ['enabled' => $config]; } @@ -277,8 +305,8 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess // Create new config object and set it on the page object so it's cached for next time $page->modifyHeader($class_name_merged, new Data($header)); - } else if (isset($page_header->$class_name_merged)) { - $merged = $page_header->$class_name_merged; + } else if (isset($page_header->{$class_name_merged})) { + $merged = $page_header->{$class_name_merged}; $header = $merged->toArray(); } if (empty($header)) { @@ -294,12 +322,12 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess /** * Merge arrays based on deepness * - * @param bool $deep - * @param $array1 - * @param $array2 - * @return array|mixed + * @param string|bool $deep + * @param array $array1 + * @param array $array2 + * @return array */ - private function mergeArrays($deep = false, $array1, $array2) + private function mergeArrays($deep, $array1, $array2) { if ($deep === 'merge') { return Utils::arrayMergeRecursiveUnique($array1, $array2); @@ -316,7 +344,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess * * @param string $plugin_name The name of the plugin whose config it should store. * - * @return true + * @return bool */ public static function saveConfig($plugin_name) { @@ -338,7 +366,7 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess /** * Simpler getter for the plugin blueprint * - * @return mixed + * @return Blueprint */ public function getBlueprint() { diff --git a/system/src/Grav/Common/Plugins.php b/system/src/Grav/Common/Plugins.php index 2a9d523..358727f 100644 --- a/system/src/Grav/Common/Plugins.php +++ b/system/src/Grav/Common/Plugins.php @@ -1,8 +1,9 @@ name; - $result = self::get($name); + + try { + $result = self::get($name); + } catch (\Exception $e) { + $exception = new \RuntimeException(sprintf('Plugin %s: %s', $name, $e->getMessage()), $e->getCode(), $e); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage("Plugin {$name} cannot be loaded, please check Exceptions tab", 'error'); + $debugger->addException($exception); + + continue; + } if ($result) { $list[$name] = $result; @@ -167,7 +181,7 @@ class Plugins extends Iterator return null; } - $obj = new Data($file->content(), $blueprint); + $obj = new Data((array)$file->content(), $blueprint); // Override with user configuration. $obj->merge(Grav::instance()['config']->get('plugins.' . $name) ?: []); @@ -184,24 +198,31 @@ class Plugins extends Iterator $grav = Grav::instance(); $locator = $grav['locator']; - $filePath = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); - if (!is_file($filePath)) { + $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); + + if (is_file($file)) { + // Local variables available in the file: $grav, $config, $name, $file + $class = include_once $file; + + $pluginClassFormat = [ + 'Grav\\Plugin\\' . ucfirst($name). 'Plugin', + 'Grav\\Plugin\\' . Inflector::camelize($name) . 'Plugin' + ]; + + foreach ($pluginClassFormat as $pluginClass) { + if (class_exists($pluginClass)) { + $class = new $pluginClass($name, $grav); + break; + } + } + } else { $grav['log']->addWarning( sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name) ); return null; } - require_once $filePath; - - $pluginClassName = 'Grav\\Plugin\\' . ucfirst($name) . 'Plugin'; - if (!class_exists($pluginClassName)) { - $pluginClassName = 'Grav\\Plugin\\' . $grav['inflector']->camelize($name) . 'Plugin'; - if (!class_exists($pluginClassName)) { - throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.", $name)); - } - } - return new $pluginClassName($name, $grav); + return $class; } } diff --git a/system/src/Grav/Common/Processors/AssetsProcessor.php b/system/src/Grav/Common/Processors/AssetsProcessor.php index 6ca952d..8b46401 100644 --- a/system/src/Grav/Common/Processors/AssetsProcessor.php +++ b/system/src/Grav/Common/Processors/AssetsProcessor.php @@ -1,21 +1,30 @@ startTimer(); $this->container['assets']->init(); $this->container->fireEvent('onAssetsInitialized'); + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/BackupsProcessor.php b/system/src/Grav/Common/Processors/BackupsProcessor.php new file mode 100644 index 0000000..d2a822c --- /dev/null +++ b/system/src/Grav/Common/Processors/BackupsProcessor.php @@ -0,0 +1,30 @@ +startTimer(); + $backups = $this->container['backups']; + $backups->init(); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/ConfigurationProcessor.php b/system/src/Grav/Common/Processors/ConfigurationProcessor.php index 0347853..83ceb84 100644 --- a/system/src/Grav/Common/Processors/ConfigurationProcessor.php +++ b/system/src/Grav/Common/Processors/ConfigurationProcessor.php @@ -1,21 +1,30 @@ startTimer(); $this->container['config']->init(); $this->container['plugins']->setup(); + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php b/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php index 643e8d1..11a39e8 100644 --- a/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php +++ b/system/src/Grav/Common/Processors/DebuggerAssetsProcessor.php @@ -1,20 +1,31 @@ startTimer(); $this->container['debugger']->addAssets(); + $this->stopTimer(); + + return $handler->handle($request); + } } diff --git a/system/src/Grav/Common/Processors/DebuggerInitProcessor.php b/system/src/Grav/Common/Processors/DebuggerInitProcessor.php deleted file mode 100644 index e3b4e1d..0000000 --- a/system/src/Grav/Common/Processors/DebuggerInitProcessor.php +++ /dev/null @@ -1,20 +0,0 @@ -container['debugger']->init(); - } -} diff --git a/system/src/Grav/Common/Processors/DebuggerProcessor.php b/system/src/Grav/Common/Processors/DebuggerProcessor.php new file mode 100644 index 0000000..892af55 --- /dev/null +++ b/system/src/Grav/Common/Processors/DebuggerProcessor.php @@ -0,0 +1,29 @@ +startTimer(); + $this->container['debugger']->init(); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/ErrorsProcessor.php b/system/src/Grav/Common/Processors/ErrorsProcessor.php index 7c7685d..b0ef6ca 100644 --- a/system/src/Grav/Common/Processors/ErrorsProcessor.php +++ b/system/src/Grav/Common/Processors/ErrorsProcessor.php @@ -1,20 +1,29 @@ startTimer(); $this->container['errors']->resetHandlers(); + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php b/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php new file mode 100644 index 0000000..fd97e9e --- /dev/null +++ b/system/src/Grav/Common/Processors/Events/RequestHandlerEvent.php @@ -0,0 +1,78 @@ +offsetGet('request'); + } + + /** + * @return Route + */ + public function getRoute(): Route + { + return $this->getRequest()->getAttribute('route'); + } + + /** + * @return RequestHandler + */ + public function getHandler(): RequestHandler + { + return $this->offsetGet('handler'); + } + + /** + * @return ResponseInterface|null + */ + public function getResponse(): ?ResponseInterface + { + return $this->offsetGet('response'); + } + + /** + * @param ResponseInterface $response + * @return $this + */ + public function setResponse(ResponseInterface $response): self + { + $this->offsetSet('response', $response); + $this->stopPropagation(); + + return $this; + } + + /** + * @param string $name + * @param MiddlewareInterface $middleware + * @return RequestHandlerEvent + */ + public function addMiddleware(string $name, MiddlewareInterface $middleware): self + { + /** @var RequestHandler $handler */ + $handler = $this['handler']; + $handler->addMiddleware($name, $middleware); + + return $this; + } +} diff --git a/system/src/Grav/Common/Processors/InitializeProcessor.php b/system/src/Grav/Common/Processors/InitializeProcessor.php index 6bf8cdd..fe2869c 100644 --- a/system/src/Grav/Common/Processors/InitializeProcessor.php +++ b/system/src/Grav/Common/Processors/InitializeProcessor.php @@ -1,8 +1,9 @@ startTimer(); + /** @var Config $config */ $config = $this->container['config']; $config->debug(); @@ -31,13 +38,24 @@ class InitializeProcessor extends ProcessorBase implements ProcessorInterface } // Initialize the timezone. - if ($config->get('system.timezone')) { - date_default_timezone_set($this->container['config']->get('system.timezone')); + $timezone = $config->get('system.timezone'); + if ($timezone) { + date_default_timezone_set($timezone); } // FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS. if (isset($this->container['session']) && $config->get('system.session.initialize', true)) { - $this->container['session']->init(); + // TODO: remove in 2.0. + $this->container['accounts']; + + try { + $this->container['session']->init(); + } catch (SessionException $e) { + $this->container['session']->init(); + $message = 'Session corruption detected, restarting session...'; + $this->addMessage($message); + $this->container['messages']->add($message, 'error'); + } } /** @var Uri $uri */ @@ -46,10 +64,17 @@ class InitializeProcessor extends ProcessorBase implements ProcessorInterface // Redirect pages with trailing slash if configured to do so. $path = $uri->path() ?: '/'; - if ($path !== '/' && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($path, '/')) { - $this->container->redirectLangSafe(rtrim($path, '/')); + if ($path !== '/' + && $config->get('system.pages.redirect_trailing_slash', false) + && Utils::endsWith($path, '/')) { + + $redirect = (string) $uri::getCurrentRoute()->toString(); + $this->container->redirect($redirect); } $this->container->setLocale(); + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/LoggerProcessor.php b/system/src/Grav/Common/Processors/LoggerProcessor.php new file mode 100644 index 0000000..af3c169 --- /dev/null +++ b/system/src/Grav/Common/Processors/LoggerProcessor.php @@ -0,0 +1,50 @@ +startTimer(); + + $grav = $this->container; + + /** @var Config $config */ + $config = $grav['config']; + + switch ($config->get('system.log.handler', 'file')) { + case 'syslog': + $log = $grav['log']; + $log->popHandler(); + + $facility = $config->get('system.log.syslog.facility', 'local6'); + $logHandler = new SyslogHandler('grav', $facility); + $formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%"); + $logHandler->setFormatter($formatter); + + $log->pushHandler($logHandler); + break; + } + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/PagesProcessor.php b/system/src/Grav/Common/Processors/PagesProcessor.php index b2828f7..ede22e5 100644 --- a/system/src/Grav/Common/Processors/PagesProcessor.php +++ b/system/src/Grav/Common/Processors/PagesProcessor.php @@ -1,23 +1,29 @@ startTimer(); + // Dump Cache state $this->container['debugger']->addMessage($this->container['cache']->getCacheStatus()); @@ -25,20 +31,41 @@ class PagesProcessor extends ProcessorBase implements ProcessorInterface $this->container->fireEvent('onPagesInitialized', new Event(['pages' => $this->container['pages']])); $this->container->fireEvent('onPageInitialized', new Event(['page' => $this->container['page']])); - /** @var Page $page */ + /** @var PageInterface $page */ $page = $this->container['page']; if (!$page->routable()) { // If no page found, fire event - $event = $this->container->fireEvent('onPageNotFound', new Event(['page' => $page])); + $event = new Event(['page' => $page]); + $event->page = null; + $event = $this->container->fireEvent('onPageNotFound', $event); if (isset($event->page)) { unset ($this->container['page']); - $this->container['page'] = $event->page; + $this->container['page'] = $page = $event->page; } else { throw new \RuntimeException('Page Not Found', 404); } + + $this->addMessage("Routed to page {$page->rawRoute()} (type: {$page->template()}) [Not Found fallback]"); + } else { + $this->addMessage("Routed to page {$page->rawRoute()} (type: {$page->template()})"); + + $task = $this->container['task']; + $action = $this->container['action']; + if ($task) { + $event = new Event(['task' => $task, 'page' => $page]); + $this->container->fireEvent('onPageTask', $event); + $this->container->fireEvent('onPageTask.' . $task, $event); + } elseif ($action) { + $event = new Event(['action' => $action, 'page' => $page]); + $this->container->fireEvent('onPageAction', $event); + $this->container->fireEvent('onPageAction.' . $action, $event); + } } + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/PluginsProcessor.php b/system/src/Grav/Common/Processors/PluginsProcessor.php index ee56393..9d2943b 100644 --- a/system/src/Grav/Common/Processors/PluginsProcessor.php +++ b/system/src/Grav/Common/Processors/PluginsProcessor.php @@ -1,21 +1,32 @@ startTimer(); + // TODO: remove in 2.0. + $this->container['accounts']; $this->container['plugins']->init(); $this->container->fireEvent('onPluginsInitialized'); + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/ProcessorBase.php b/system/src/Grav/Common/Processors/ProcessorBase.php index 056c86d..5ce7bfd 100644 --- a/system/src/Grav/Common/Processors/ProcessorBase.php +++ b/system/src/Grav/Common/Processors/ProcessorBase.php @@ -1,25 +1,48 @@ container = $container; } + protected function startTimer($id = null, $title = null) + { + /** @var Debugger $debugger */ + $debugger = $this->container['debugger']; + $debugger->startTimer($id ?? $this->id, $title ?? $this->title); + } + + protected function stopTimer($id = null) + { + /** @var Debugger $debugger */ + $debugger = $this->container['debugger']; + $debugger->stopTimer($id ?? $this->id); + } + + protected function addMessage($message, $label = 'info', $isString = true) + { + /** @var Debugger $debugger */ + $debugger = $this->container['debugger']; + $debugger->addMessage($message, $label, $isString); + } } diff --git a/system/src/Grav/Common/Processors/ProcessorInterface.php b/system/src/Grav/Common/Processors/ProcessorInterface.php index 0e4b169..a8e4aaa 100644 --- a/system/src/Grav/Common/Processors/ProcessorInterface.php +++ b/system/src/Grav/Common/Processors/ProcessorInterface.php @@ -1,14 +1,16 @@ startTimer(); + $container = $this->container; $output = $container['output']; - if ($output instanceof \Psr\Http\Message\ResponseInterface) { - // Support for custom output providers like Slim Framework. - } else { - // Use internal Grav output. - $container->output = $output; - $container->fireEvent('onOutputGenerated'); - - // Set the header type - $container->header(); - - echo $container->output; - - // remove any output - $container->output = ''; - - $this->container->fireEvent('onOutputRendered'); + if ($output instanceof ResponseInterface) { + return $output; } + + ob_start(); + + // Use internal Grav output. + $container->output = $output; + $container->fireEvent('onOutputGenerated'); + + echo $container->output; + + // remove any output + $container->output = ''; + + $this->container->fireEvent('onOutputRendered'); + + $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/Processors/RequestProcessor.php b/system/src/Grav/Common/Processors/RequestProcessor.php new file mode 100644 index 0000000..97564e6 --- /dev/null +++ b/system/src/Grav/Common/Processors/RequestProcessor.php @@ -0,0 +1,54 @@ +startTimer(); + + $header = $request->getHeaderLine('Content-Type'); + $type = trim(strstr($header, ';', true) ?: $header); + if ($type === 'application/json') { + $request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true)); + } + + $uri = $request->getUri(); + $ext = mb_strtolower(pathinfo($uri->getPath(), PATHINFO_EXTENSION)); + + $request = $request + ->withAttribute('grav', $this->container) + ->withAttribute('time', $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME) + ->withAttribute('route', Uri::getCurrentRoute()->withExtension($ext)) + ->withAttribute('referrer', $this->container['uri']->referrer()); + + $event = new RequestHandlerEvent(['request' => $request, 'handler' => $handler]); + /** @var RequestHandlerEvent $event */ + $event = $this->container->fireEvent('onRequestHandlerInit', $event); + $response = $event->getResponse(); + $this->stopTimer(); + + if ($response) { + return $response; + } + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/SchedulerProcessor.php b/system/src/Grav/Common/Processors/SchedulerProcessor.php new file mode 100644 index 0000000..64f3fba --- /dev/null +++ b/system/src/Grav/Common/Processors/SchedulerProcessor.php @@ -0,0 +1,31 @@ +startTimer(); + $scheduler = $this->container['scheduler']; + $this->container->fireEvent('onSchedulerInitialized', new Event(['scheduler' => $scheduler])); + $this->stopTimer(); + + return $handler->handle($request); + } +} diff --git a/system/src/Grav/Common/Processors/SiteSetupProcessor.php b/system/src/Grav/Common/Processors/SiteSetupProcessor.php deleted file mode 100644 index 1f3f8af..0000000 --- a/system/src/Grav/Common/Processors/SiteSetupProcessor.php +++ /dev/null @@ -1,21 +0,0 @@ -container['setup']->init(); - $this->container['streams']; - } -} diff --git a/system/src/Grav/Common/Processors/TasksProcessor.php b/system/src/Grav/Common/Processors/TasksProcessor.php index d1e7a2f..aaf5cdf 100644 --- a/system/src/Grav/Common/Processors/TasksProcessor.php +++ b/system/src/Grav/Common/Processors/TasksProcessor.php @@ -1,23 +1,61 @@ startTimer(); + $task = $this->container['task']; - if ($task) { - $this->container->fireEvent('onTask.' . $task); + $action = $this->container['action']; + if ($task || $action) { + $attributes = $request->getAttribute('controller'); + + $controllerClass = $attributes['class'] ?? null; + if ($controllerClass) { + /** @var RequestHandlerInterface $controller */ + $controller = new $controllerClass($attributes['path'] ?? '', $attributes['params'] ?? []); + try { + $response = $controller->handle($request); + + if ($response->getStatusCode() === 418) { + $response = $handler->handle($request); + } + + $this->stopTimer(); + + return $response; + + } catch (NotFoundException $e) { + // Task not found: Let it pass through. + } + } + + if ($task) { + $this->container->fireEvent('onTask.' . $task); + } elseif ($action) { + $this->container->fireEvent('onAction.' . $action); + } } + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/ThemesProcessor.php b/system/src/Grav/Common/Processors/ThemesProcessor.php index c9ea013..d1c6ae5 100644 --- a/system/src/Grav/Common/Processors/ThemesProcessor.php +++ b/system/src/Grav/Common/Processors/ThemesProcessor.php @@ -1,20 +1,29 @@ startTimer(); $this->container['themes']->init(); + $this->stopTimer(); + + return $handler->handle($request); } } diff --git a/system/src/Grav/Common/Processors/TwigProcessor.php b/system/src/Grav/Common/Processors/TwigProcessor.php index 392824f..4ed247d 100644 --- a/system/src/Grav/Common/Processors/TwigProcessor.php +++ b/system/src/Grav/Common/Processors/TwigProcessor.php @@ -1,21 +1,29 @@ startTimer(); $this->container['twig']->init(); - } + $this->stopTimer(); + return $handler->handle($request); + } } diff --git a/system/src/Grav/Common/Scheduler/Cron.php b/system/src/Grav/Common/Scheduler/Cron.php new file mode 100644 index 0000000..3a42b29 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Cron.php @@ -0,0 +1,595 @@ + modified for Grav integration + * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved. + * @license MIT License; see LICENSE file for details. + */ + +namespace Grav\Common\Scheduler; + +/* + * Usage examples : + * ---------------- + * + * $cron = new Cron('10-30/5 12 * * *'); + * + * var_dump($cron->getMinutes()); + * // array(5) { + * // [0]=> int(10) + * // [1]=> int(15) + * // [2]=> int(20) + * // [3]=> int(25) + * // [4]=> int(30) + * // } + * + * var_dump($cron->getText('fr')); + * // string(32) "Chaque jour à 12:10,15,20,25,30" + * + * var_dump($cron->getText('en')); + * // string(30) "Every day at 12:10,15,20,25,30" + * + * var_dump($cron->getType()); + * // string(3) "day" + * + * var_dump($cron->getCronHours()); + * // string(2) "12" + * + * var_dump($cron->matchExact(new \DateTime('2012-07-01 13:25:10'))); + * // bool(false) + * + * var_dump($cron->matchExact(new \DateTime('2012-07-01 12:15:20'))); + * // bool(true) + * + * var_dump($cron->matchWithMargin(new \DateTime('2012-07-01 12:32:50'), -3, 5)); + * // bool(true) + */ +class Cron +{ + public const TYPE_UNDEFINED = ''; + public const TYPE_MINUTE = 'minute'; + public const TYPE_HOUR = 'hour'; + public const TYPE_DAY = 'day'; + public const TYPE_WEEK = 'week'; + public const TYPE_MONTH = 'month'; + public const TYPE_YEAR = 'year'; + /** + * + * @var array + */ + protected $texts = [ + 'fr' => [ + 'empty' => '-tout-', + 'name_minute' => 'minute', + 'name_hour' => 'heure', + 'name_day' => 'jour', + 'name_week' => 'semaine', + 'name_month' => 'mois', + 'name_year' => 'année', + 'text_period' => 'Chaque %s', + 'text_mins' => 'à %s minutes', + 'text_time' => 'à %s:%s', + 'text_dow' => 'le %s', + 'text_month' => 'de %s', + 'text_dom' => 'le %s', + 'weekdays' => ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'], + 'months' => ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'], + ], + 'en' => [ + 'empty' => '-all-', + 'name_minute' => 'minute', + 'name_hour' => 'hour', + 'name_day' => 'day', + 'name_week' => 'week', + 'name_month' => 'month', + 'name_year' => 'year', + 'text_period' => 'Every %s', + 'text_mins' => 'at %s minutes past the hour', + 'text_time' => 'at %s:%s', + 'text_dow' => 'on %s', + 'text_month' => 'of %s', + 'text_dom' => 'on the %s', + 'weekdays' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], + 'months' => ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'], + ], + ]; + + /** + * min hour dom month dow + * @var string + */ + protected $cron = ''; + /** + * + * @var array + */ + protected $minutes = []; + /** + * + * @var array + */ + protected $hours = []; + /** + * + * @var array + */ + protected $months = []; + /** + * 0-7 : sunday, monday, ... saturday, sunday + * @var array + */ + protected $dow = []; + /** + * + * @var array + */ + protected $dom = []; + + /** + * + * @param string|null $cron + */ + public function __construct($cron = null) + { + if (null !== $cron) { + $this->setCron($cron); + } + } + + /** + * + * @return string + */ + public function getCron() + { + return implode(' ', [ + $this->getCronMinutes(), + $this->getCronHours(), + $this->getCronDaysOfMonth(), + $this->getCronMonths(), + $this->getCronDaysOfWeek(), + ]); + } + + /** + * + * @param string $lang 'fr' or 'en' + * @return string + */ + public function getText($lang) + { + // check lang + if (!isset($this->texts[$lang])) { + return $this->getCron(); + } + + $texts = $this->texts[$lang]; + // check type + + $type = $this->getType(); + if ($type === self::TYPE_UNDEFINED) { + return $this->getCron(); + } + + // init + $elements = []; + $elements[] = sprintf($texts['text_period'], $texts['name_' . $type]); + + // hour + if ($type === self::TYPE_HOUR) { + $elements[] = sprintf($texts['text_mins'], $this->getCronMinutes()); + } + + // week + if ($type === self::TYPE_WEEK) { + $dow = $this->getCronDaysOfWeek(); + foreach ($texts['weekdays'] as $i => $wd) { + $dow = str_replace((string) ($i + 1), $wd, $dow); + } + $elements[] = sprintf($texts['text_dow'], $dow); + } + + // month + year + if (\in_array($type, [self::TYPE_MONTH, self::TYPE_YEAR], true)) { + $elements[] = sprintf($texts['text_dom'], $this->getCronDaysOfMonth()); + } + + // year + if ($type === self::TYPE_YEAR) { + $months = $this->getCronMonths(); + for ($i = count($texts['months']) - 1; $i >= 0; $i--) { + $months = str_replace((string) ($i + 1), $texts['months'][$i], $months); + } + $elements[] = sprintf($texts['text_month'], $months); + } + + // day + week + month + year + if (\in_array($type, [self::TYPE_DAY, self::TYPE_WEEK, self::TYPE_MONTH, self::TYPE_YEAR], true)) { + $elements[] = sprintf($texts['text_time'], $this->getCronHours(), $this->getCronMinutes()); + } + + return str_replace('*', $texts['empty'], implode(' ', $elements)); + } + + /** + * + * @return string + */ + public function getType() + { + $mask = preg_replace('/[^\* ]/', '-', $this->getCron()); + $mask = preg_replace('/-+/', '-', $mask); + $mask = preg_replace('/[^-\*]/', '', $mask); + + if ($mask === '*****') { + return self::TYPE_MINUTE; + } + + if ($mask === '-****') { + return self::TYPE_HOUR; + } + + if (substr($mask, -3) === '***') { + return self::TYPE_DAY; + } + + if (substr($mask, -3) === '-**') { + return self::TYPE_MONTH; + } + + if (substr($mask, -3) === '**-') { + return self::TYPE_WEEK; + } + + if (substr($mask, -2) === '-*') { + return self::TYPE_YEAR; + } + + return self::TYPE_UNDEFINED; + } + + /** + * + * @param string $cron + * @return Cron + */ + public function setCron($cron) + { + // sanitize + $cron = trim($cron); + $cron = preg_replace('/\s+/', ' ', $cron); + // explode + $elements = explode(' ', $cron); + if (\count($elements) !== 5) { + throw new \RuntimeException('Bad number of elements'); + } + + $this->cron = $cron; + $this->setMinutes($elements[0]); + $this->setHours($elements[1]); + $this->setDaysOfMonth($elements[2]); + $this->setMonths($elements[3]); + $this->setDaysOfWeek($elements[4]); + + return $this; + } + + /** + * + * @return string + */ + public function getCronMinutes() + { + return $this->arrayToCron($this->minutes); + } + + /** + * + * @return string + */ + public function getCronHours() + { + return $this->arrayToCron($this->hours); + } + + /** + * + * @return string + */ + public function getCronDaysOfMonth() + { + return $this->arrayToCron($this->dom); + } + + /** + * + * @return string + */ + public function getCronMonths() + { + return $this->arrayToCron($this->months); + } + + /** + * + * @return string + */ + public function getCronDaysOfWeek() + { + return $this->arrayToCron($this->dow); + } + + /** + * + * @return array + */ + public function getMinutes() + { + return $this->minutes; + } + + /** + * + * @return array + */ + public function getHours() + { + return $this->hours; + } + + /** + * + * @return array + */ + public function getDaysOfMonth() + { + return $this->dom; + } + + /** + * + * @return array + */ + public function getMonths() + { + return $this->months; + } + + /** + * + * @return array + */ + public function getDaysOfWeek() + { + return $this->dow; + } + + /** + * + * @param string|array $minutes + * @return Cron + */ + public function setMinutes($minutes) + { + $this->minutes = $this->cronToArray($minutes, 0, 59); + + return $this; + } + + /** + * + * @param string|array $hours + * @return Cron + */ + public function setHours($hours) + { + $this->hours = $this->cronToArray($hours, 0, 23); + + return $this; + } + + /** + * + * @param string|array $months + * @return Cron + */ + public function setMonths($months) + { + $this->months = $this->cronToArray($months, 1, 12); + + return $this; + } + + /** + * + * @param string|array $dow + * @return Cron + */ + public function setDaysOfWeek($dow) + { + $this->dow = $this->cronToArray($dow, 0, 7); + + return $this; + } + + /** + * + * @param string|array $dom + * @return Cron + */ + public function setDaysOfMonth($dom) + { + $this->dom = $this->cronToArray($dom, 1, 31); + + return $this; + } + + /** + * + * @param mixed $date + * @param int $min + * @param int $hour + * @param int $day + * @param int $month + * @param int $weekday + * @return \DateTime + */ + protected function parseDate($date, &$min, &$hour, &$day, &$month, &$weekday) + { + if (is_numeric($date) && (int)$date == $date) { + $date = new \DateTime('@' . $date); + } + elseif (is_string($date)) { + $date = new \DateTime('@' . strtotime($date)); + } + if ($date instanceof \DateTime) { + $min = (int)$date->format('i'); + $hour = (int)$date->format('H'); + $day = (int)$date->format('d'); + $month = (int)$date->format('m'); + $weekday = (int)$date->format('w'); // 0-6 + } + else { + throw new \RuntimeException('Date format not supported'); + } + + return new \DateTime($date->format('Y-m-d H:i:sP')); + } + + /** + * + * @param int|string|\DateTime $date + */ + public function matchExact($date) + { + $date = $this->parseDate($date, $min, $hour, $day, $month, $weekday); + + return + (empty($this->minutes) || \in_array($min, $this->minutes, true)) && + (empty($this->hours) || \in_array($hour, $this->hours, true)) && + (empty($this->dom) || \in_array($day, $this->dom, true)) && + (empty($this->months) || \in_array($month, $this->months, true)) && + (empty($this->dow) || \in_array($weekday, $this->dow, true) || ($weekday == 0 && \in_array(7, $this->dow, true)) || ($weekday == 7 && \in_array(0, $this->dow, true)) + ); + } + + /** + * + * @param int|string|\DateTime $date + * @param int $minuteBefore + * @param int $minuteAfter + */ + public function matchWithMargin($date, $minuteBefore = 0, $minuteAfter = 0) + { + if ($minuteBefore > 0) { + throw new \RuntimeException('MinuteBefore parameter cannot be positive !'); + } + if ($minuteAfter < 0) { + throw new \RuntimeException('MinuteAfter parameter cannot be negative !'); + } + + $date = $this->parseDate($date, $min, $hour, $day, $month, $weekday); + $interval = new \DateInterval('PT1M'); // 1 min + if ($minuteBefore !== 0) { + $date->sub(new \DateInterval('PT' . abs($minuteBefore) . 'M')); + } + $n = $minuteAfter - $minuteBefore + 1; + for ($i = 0; $i < $n; $i++) { + if ($this->matchExact($date)) { + return true; + } + $date->add($interval); + } + + return false; + } + + /** + * + * @param array $array + * @return string + */ + protected function arrayToCron($array) + { + $n = \count($array); + if (!\is_array($array) || $n === 0) { + return '*'; + } + + $cron = [$array[0]]; + $s = $c = $array[0]; + for ($i = 1; $i < $n; $i++) { + if ($array[$i] == $c + 1) { + $c = $array[$i]; + $cron[\count($cron) - 1] = $s . '-' . $c; + } + else { + $s = $c = $array[$i]; + $cron[] = $c; + } + } + + return implode(',', $cron); + } + + /** + * + * @param array|string $string + * @param int $min + * @param int $max + * @return array + */ + protected function cronToArray($string, $min, $max) + { + $array = []; + if (\is_array($string)) { + foreach ($string as $val) { + if (is_numeric($val) && (int)$val == $val && $val >= $min && $val <= $max) { + $array[] = (int)$val; + } + } + } elseif ($string !== '*') { + while ($string !== '') { + // test "*/n" expression + if (preg_match('/^\*\/([0-9]+),?/', $string, $m)) { + for ($i = max(0, $min); $i <= min(59, $max); $i += $m[1]) { + $array[] = (int)$i; + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "a-b/n" expression + if (preg_match('/^([0-9]+)-([0-9]+)\/([0-9]+),?/', $string, $m)) { + for ($i = max($m[1], $min); $i <= min($m[2], $max); $i += $m[3]) { + $array[] = (int)$i; + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "a-b" expression + if (preg_match('/^([0-9]+)-([0-9]+),?/', $string, $m)) { + for ($i = max($m[1], $min); $i <= min($m[2], $max); $i++) { + $array[] = (int)$i; + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "c" expression + if (preg_match('/^([0-9]+),?/', $string, $m)) { + if ($m[1] >= $min && $m[1] <= $max) { + $array[] = (int)$m[1]; + } + $string = substr($string, strlen($m[0])); + continue; + } + + // something goes wrong in the expression + return []; + } + } + sort($array); + + return $array; + } +} diff --git a/system/src/Grav/Common/Scheduler/IntervalTrait.php b/system/src/Grav/Common/Scheduler/IntervalTrait.php new file mode 100644 index 0000000..b382c9d --- /dev/null +++ b/system/src/Grav/Common/Scheduler/IntervalTrait.php @@ -0,0 +1,399 @@ +at = $expression; + $this->executionTime = CronExpression::factory($expression); + + return $this; + } + + /** + * Set the execution time to every minute. + * + * @return self + */ + public function everyMinute() + { + return $this->at('* * * * *'); + } + + /** + * Set the execution time to every hour. + * + * @param int|string $minute + * @return self + */ + public function hourly($minute = 0) + { + $c = $this->validateCronSequence($minute); + + return $this->at("{$c['minute']} * * * *"); + } + + /** + * Set the execution time to once a day. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function daily($hour = 0, $minute = 0) + { + if (\is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = $parts[1] ?? '0'; + } + $c = $this->validateCronSequence($minute, $hour); + + return $this->at("{$c['minute']} {$c['hour']} * * *"); + } + + /** + * Set the execution time to once a week. + * + * @param int|string $weekday + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function weekly($weekday = 0, $hour = 0, $minute = 0) + { + if (\is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = $parts[1] ?? '0'; + } + $c = $this->validateCronSequence($minute, $hour, null, null, $weekday); + + return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}"); + } + + /** + * Set the execution time to once a month. + * + * @param int|string $month + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0) + { + if (\is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = $parts[1] ?? '0'; + } + $c = $this->validateCronSequence($minute, $hour, $day, $month); + + return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *"); + } + + /** + * Set the execution time to every Sunday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function sunday($hour = 0, $minute = 0) + { + return $this->weekly(0, $hour, $minute); + } + + /** + * Set the execution time to every Monday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function monday($hour = 0, $minute = 0) + { + return $this->weekly(1, $hour, $minute); + } + + /** + * Set the execution time to every Tuesday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function tuesday($hour = 0, $minute = 0) + { + return $this->weekly(2, $hour, $minute); + } + + /** + * Set the execution time to every Wednesday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function wednesday($hour = 0, $minute = 0) + { + return $this->weekly(3, $hour, $minute); + } + + /** + * Set the execution time to every Thursday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function thursday($hour = 0, $minute = 0) + { + return $this->weekly(4, $hour, $minute); + } + + /** + * Set the execution time to every Friday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function friday($hour = 0, $minute = 0) + { + return $this->weekly(5, $hour, $minute); + } + + /** + * Set the execution time to every Saturday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function saturday($hour = 0, $minute = 0) + { + return $this->weekly(6, $hour, $minute); + } + + /** + * Set the execution time to every January. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function january($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(1, $day, $hour, $minute); + } + + /** + * Set the execution time to every February. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function february($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(2, $day, $hour, $minute); + } + + /** + * Set the execution time to every March. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function march($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(3, $day, $hour, $minute); + } + + /** + * Set the execution time to every April. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function april($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(4, $day, $hour, $minute); + } + + /** + * Set the execution time to every May. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function may($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(5, $day, $hour, $minute); + } + + /** + * Set the execution time to every June. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function june($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(6, $day, $hour, $minute); + } + + /** + * Set the execution time to every July. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function july($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(7, $day, $hour, $minute); + } + + /** + * Set the execution time to every August. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function august($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(8, $day, $hour, $minute); + } + + /** + * Set the execution time to every September. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function september($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(9, $day, $hour, $minute); + } + + /** + * Set the execution time to every October. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function october($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(10, $day, $hour, $minute); + } + + /** + * Set the execution time to every November. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function november($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(11, $day, $hour, $minute); + } + + /** + * Set the execution time to every December. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function december($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(12, $day, $hour, $minute); + } + + /** + * Validate sequence of cron expression. + * + * @param int|string $minute + * @param int|string $hour + * @param int|string $day + * @param int|string $month + * @param int|string $weekday + * @return array + */ + private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null) + { + return [ + 'minute' => $this->validateCronRange($minute, 0, 59), + 'hour' => $this->validateCronRange($hour, 0, 23), + 'day' => $this->validateCronRange($day, 1, 31), + 'month' => $this->validateCronRange($month, 1, 12), + 'weekday' => $this->validateCronRange($weekday, 0, 6), + ]; + } + + /** + * Validate sequence of cron expression. + * + * @param int|string $value + * @param int $min + * @param int $max + * @return mixed + */ + private function validateCronRange($value, $min, $max) + { + if ($value === null || $value === '*') { + return '*'; + } + + if (! is_numeric($value) || + ! ($value >= $min && $value <= $max) + ) { + throw new \InvalidArgumentException( + "Invalid value: it should be '*' or between {$min} and {$max}." + ); + } + + return $value; + } +} + diff --git a/system/src/Grav/Common/Scheduler/Job.php b/system/src/Grav/Common/Scheduler/Job.php new file mode 100644 index 0000000..6250f1d --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Job.php @@ -0,0 +1,522 @@ +id = Grav::instance()['inflector']->hyphenize($id); + } else { + if (is_string($command)) { + $this->id = md5($command); + } else { + /* @var object $command */ + $this->id = spl_object_hash($command); + } + } + $this->creationTime = new \DateTime('now'); + // initialize the directory path for lock files + $this->tempDir = sys_get_temp_dir(); + $this->command = $command; + $this->args = $args; + // Set enabled state + $status = Grav::instance()['config']->get('scheduler.status'); + $this->enabled = !(isset($status[$id]) && $status[$id] === 'disabled'); + } + + /** + * Get the command + * + * @return string + */ + public function getCommand() + { + return $this->command; + } + + /** + * Get the cron 'at' syntax for this job + * + * @return string + */ + public function getAt() + { + return $this->at; + } + + /** + * Get the status of this job + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } + + /** + * Get optional arguments + * + * @return string|null + */ + public function getArguments() + { + if (\is_string($this->args)) { + return $this->args; + } + + return null; + } + + public function getCronExpression() + { + return CronExpression::factory($this->at); + } + + /** + * Get the status of the last run for this job + * + * @return bool + */ + public function isSuccessful() + { + return $this->successful; + } + + /** + * Get the Job id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Check if the Job is due to run. + * It accepts as input a DateTime used to check if + * the job is due. Defaults to job creation time. + * It also default the execution time if not previously defined. + * + * @param \DateTime $date + * @return bool + */ + public function isDue(\DateTime $date = null) + { + // The execution time is being defaulted if not defined + if (!$this->executionTime) { + $this->at('* * * * *'); + } + + $date = $date ?? $this->creationTime; + + return $this->executionTime->isDue($date); + } + + /** + * Check if the Job is overlapping. + * + * @return bool + */ + public function isOverlapping() + { + return $this->lockFile && + file_exists($this->lockFile) && + call_user_func($this->whenOverlapping, filemtime($this->lockFile)) === false; + } + + /** + * Force the Job to run in foreground. + * + * @return $this + */ + public function inForeground() + { + $this->runInBackground = false; + + return $this; + } + + /** + * Sets/Gets an option backlink + * + * @param string $link + * + * @return null|string + */ + public function backlink($link = null) + { + if ($link) { + $this->backlink = $link; + } + return $this->backlink; + } + + + /** + * Check if the Job can run in background. + * + * @return bool + */ + public function runInBackground() + { + return !(is_callable($this->command) || $this->runInBackground === false); + } + + /** + * This will prevent the Job from overlapping. + * It prevents another instance of the same Job of + * being executed if the previous is still running. + * The job id is used as a filename for the lock file. + * + * @param string $tempDir The directory path for the lock files + * @param callable $whenOverlapping A callback to ignore job overlapping + * @return self + */ + public function onlyOne($tempDir = null, callable $whenOverlapping = null) + { + if ($tempDir === null || !is_dir($tempDir)) { + $tempDir = $this->tempDir; + } + $this->lockFile = implode('/', [ + trim($tempDir), + trim($this->id) . '.lock', + ]); + if ($whenOverlapping) { + $this->whenOverlapping = $whenOverlapping; + } else { + $this->whenOverlapping = function () { + return false; + }; + } + + return $this; + } + + /** + * Configure the job. + * + * @param array $config + * @return self + */ + public function configure(array $config = []) + { + // Check if config has defined a tempDir + if (isset($config['tempDir']) && is_dir($config['tempDir'])) { + $this->tempDir = $config['tempDir']; + } + + return $this; + } + + /** + * Truth test to define if the job should run if due. + * + * @param callable $fn + * @return self + */ + public function when(callable $fn) + { + $this->truthTest = $fn(); + + return $this; + } + + /** + * Run the job. + * + * @return bool + */ + public function run() + { + // If the truthTest failed, don't run + if ($this->truthTest !== true) { + return false; + } + + // If overlapping, don't run + if ($this->isOverlapping()) { + return false; + } + + // Write lock file if necessary + $this->createLockFile(); + + // Call before if required + if (is_callable($this->before)) { + call_user_func($this->before); + } + + // If command is callable... + if (is_callable($this->command)) { + $this->output = $this->exec(); + } else { + $args = \is_string($this->args) ? $this->args : implode(' ', $this->args); + $command = $this->command . ' ' . $args; + $process = new Process($command); + + $this->process = $process; + + if ($this->runInBackground()) { + $process->start(); + } else { + $process->run(); + $this->finalize(); + } + } + + return true; + } + + /** + * Finish up processing the job + * + * @return void + */ + public function finalize() + { + /** @var Process $process */ + $process = $this->process; + + if ($process) { + $process->wait(); + + if ($process->isSuccessful()) { + $this->successful = true; + $this->output = $process->getOutput(); + } else { + $this->successful = false; + $this->output = $process->getErrorOutput(); + } + + $this->postRun(); + + unset($this->process); + } + } + + /** + * Things to run after job has run + */ + private function postRun() + { + 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); + } + } + + // Send output to email + $this->emailOutput(); + + // Call any callback defined + if (is_callable($this->after)) { + call_user_func($this->after, $this->output, $this->returnCode); + } + + $this->removeLockFile(); + } + + /** + * Create the job lock file. + * + * @param mixed $content + * @return void + */ + private function createLockFile($content = null) + { + if ($this->lockFile) { + if ($content === null || !\is_string($content)) { + $content = $this->getId(); + } + file_put_contents($this->lockFile, $content); + } + } + + /** + * Remove the job lock file. + * + * @return void + */ + private function removeLockFile() + { + if ($this->lockFile && file_exists($this->lockFile)) { + unlink($this->lockFile); + } + } + + /** + * Execute a callable job. + * + * @throws \RuntimeException + * @return string + */ + private function exec() + { + $return_data = ''; + ob_start(); + try { + $return_data = call_user_func_array($this->command, $this->args); + $this->successful = true; + } catch (\RuntimeException $e) { + $this->successful = false; + } + $this->output = ob_get_clean() . (is_string($return_data) ? $return_data : ''); + + $this->postRun(); + } + + /** + * Set the file/s where to write the output of the job. + * + * @param string|array $filename + * @param bool $append + * @return self + */ + public function output($filename, $append = false) + { + $this->outputTo = is_array($filename) ? $filename : [$filename]; + $this->outputMode = $append === false ? 'overwrite' : 'append'; + + return $this; + } + + /** + * Get the job output. + * + * @return mixed + */ + public function getOutput() + { + return $this->output; + } + + /** + * Set the emails where the output should be sent to. + * The Job should be set to write output to a file + * for this to work. + * + * @param string|array $email + * @return self + */ + public function email($email) + { + if (!is_string($email) && !is_array($email)) { + throw new \InvalidArgumentException('The email can be only string or array'); + } + + $this->emailTo = is_array($email) ? $email : [$email]; + // Force the job to run in foreground + $this->inForeground(); + + return $this; + } + + /** + * Email the output of the job, if any. + * + * @return bool + */ + private function emailOutput() + { + if (!count($this->outputTo) || !count($this->emailTo)) { + return false; + } + + if (is_callable('Grav\Plugin\Email\Utils::sendEmail')) { + $subject ='Grav Scheduled Job [' . $this->getId() . ']'; + $content = "

Output from Job ID: {$this->getId()}

\n

Command: {$this->getCommand()}


\n".$this->getOutput()."\n
"; + $to = $this->emailTo; + + \Grav\Plugin\Email\Utils::sendEmail($subject, $content, $to); + } + + return true; + } + + /** + * Set function to be called before job execution + * Job object is injected as a parameter to callable function. + * + * @param callable $fn + * @return self + */ + public function before(callable $fn) + { + $this->before = $fn; + + return $this; + } + + /** + * Set a function to be called after job execution. + * By default this will force the job to run in foreground + * because the output is injected as a parameter of this + * function, but it could be avoided by passing true as a + * second parameter. The job will run in background if it + * meets all the other criteria. + * + * @param callable $fn + * @param bool $runInBackground + * @return self + */ + public function then(callable $fn, $runInBackground = false) + { + $this->after = $fn; + // Force the job to run in foreground + if ($runInBackground === false) { + $this->inForeground(); + } + + return $this; + } +} + diff --git a/system/src/Grav/Common/Scheduler/Scheduler.php b/system/src/Grav/Common/Scheduler/Scheduler.php new file mode 100644 index 0000000..df5602e --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Scheduler.php @@ -0,0 +1,361 @@ +get('scheduler.defaults', []); + $this->config = $config; + + $this->status_path = Grav::instance()['locator']->findResource('user-data://scheduler', true, true); + if (!file_exists($this->status_path)) { + Folder::create($this->status_path); + } + + } + + /** + * Load saved jobs from config/scheduler.yaml file + */ + public function loadSavedJobs() + { + $this->saved_jobs = []; + $saved_jobs = (array) Grav::instance()['config']->get('scheduler.custom_jobs', []); + + foreach ($saved_jobs as $id => $j) { + $args = $j['args'] ?? []; + $id = Grav::instance()['inflector']->hyphenize($id); + $job = $this->addCommand($j['command'], $args, $id); + + if (isset($j['at'])) { + $job->at($j['at']); + } + + if (isset($j['output'])) { + $mode = isset($j['output_mode']) && $j['output_mode'] === 'append' ? true : false; + $job->output($j['output'], $mode); + } + + if (isset($j['email'])) { + $job->email($j['email']); + } + + // store in saved_jobs + $this->saved_jobs[] = $job; + } + + return $this; + } + + /** + * Get the queued jobs as background/foreground + * + * @param bool $all + * @return array + */ + public function getQueuedJobs($all = false) + { + $background = []; + $foreground = []; + foreach ($this->jobs as $job) { + if ($all || $job->getEnabled()) { + if ($job->runInBackground()) { + $background[] = $job; + } else { + $foreground[] = $job; + } + } + + } + return [$background, $foreground]; + } + + /** + * Get all jobs if they are disabled or not as one array + * + * @return array + */ + public function getAllJobs() + { + list($background, $foreground) = $this->loadSavedJobs()->getQueuedJobs(true); + + return array_merge($background, $foreground); + } + + /** + * Queues a PHP function execution. + * + * @param callable $fn The function to execute + * @param array $args Optional arguments to pass to the php script + * @param string $id Optional custom identifier + * @return Job + */ + public function addFunction(callable $fn, $args = [], $id = null) + { + $job = new Job($fn, $args, $id); + $this->queueJob($job->configure($this->config)); + + return $job; + } + + /** + * Queue a raw shell command. + * + * @param string $command The command to execute + * @param array $args Optional arguments to pass to the command + * @param string $id Optional custom identifier + * @return Job + */ + public function addCommand($command, $args = [], $id = null) + { + $job = new Job($command, $args, $id); + $this->queueJob($job->configure($this->config)); + + return $job; + } + + /** + * Run the scheduler. + * + * @param \DateTime|null $runTime Optional, run at specific moment + */ + public function run(\DateTime $runTime = null) + { + $this->loadSavedJobs(); + + list($background, $foreground) = $this->getQueuedJobs(false); + $alljobs = array_merge($background, $foreground); + + if (null === $runTime) { + $runTime = new \DateTime('now'); + } + + // Star processing jobs + foreach ($alljobs as $job) { + if ($job->isDue($runTime)) { + $job->run(); + $this->jobs_run[] = $job; + } + } + + // Finish handling any background jobs + foreach($background as $job) { + $job->finalize(); + } + + // Store states + $this->saveJobStates(); + } + + /** + * Reset all collected data of last run. + * + * Call before run() if you call run() multiple times. + */ + public function resetRun() + { + // Reset collected data of last run + $this->executed_jobs = []; + $this->failed_jobs = []; + $this->output_schedule = []; + + return $this; + } + + /** + * Get the scheduler verbose output. + * + * @param string $type Allowed: text, html, array + * @return mixed The return depends on the requested $type + */ + public function getVerboseOutput($type = 'text') + { + switch ($type) { + case 'text': + return implode("\n", $this->output_schedule); + case 'html': + return implode('
', $this->output_schedule); + case 'array': + return $this->output_schedule; + default: + throw new \InvalidArgumentException('Invalid output type'); + } + } + + /** + * Remove all queued Jobs. + */ + public function clearJobs() + { + $this->jobs = []; + + return $this; + } + + /** + * Helper to get the full Cron command + * + * @return string + */ + public function getCronCommand() + { + $phpBinaryFinder = new PhpExecutableFinder(); + $php = $phpBinaryFinder->find(); + $command = 'cd ' . str_replace(' ', '\ ', GRAV_ROOT) . ';' . $php . ' bin/grav scheduler'; + + return "(crontab -l; echo \"* * * * * {$command} 1>> /dev/null 2>&1\") | crontab -"; + } + + /** + * Helper to determine if cron job is setup + * + * @return int + */ + public function isCrontabSetup() + { + $process = new Process('crontab -l'); + $process->run(); + + if ($process->isSuccessful()) { + $output = $process->getOutput(); + + return preg_match('$bin\/grav schedule$', $output) ? 1 : 0; + } + + $error = $process->getErrorOutput(); + + return Utils::startsWith($error, 'crontab: no crontab') ? 0 : 2; + } + + /** + * Get the Job states file + * + * @return \RocketTheme\Toolbox\File\FileInterface|YamlFile + */ + public function getJobStates() + { + return YamlFile::instance($this->status_path . '/status.yaml'); + } + + /** + * Save job states to statys file + */ + private function saveJobStates() + { + $now = time(); + $new_states = []; + + foreach ($this->jobs_run as $job) { + if ($job->isSuccessful()) { + $new_states[$job->getId()] = ['state' => 'success', 'last-run' => $now]; + $this->pushExecutedJob($job); + } else { + $new_states[$job->getId()] = ['state' => 'failure', 'last-run' => $now, 'error' => $job->getOutput()]; + $this->pushFailedJob($job); + } + } + + $saved_states = $this->getJobStates(); + $saved_states->save(array_merge($saved_states->content(), $new_states)); + } + + /** + * Queue a job for execution in the correct queue. + * + * @param Job $job + * @return void + */ + private function queueJob(Job $job) + { + $this->jobs[] = $job; + + // Store jobs + } + + /** + * Add an entry to the scheduler verbose output array. + * + * @param string $string + * @return void + */ + private function addSchedulerVerboseOutput($string) + { + $now = '[' . (new \DateTime('now'))->format('c') . '] '; + $this->output_schedule[] = $now . $string; + // Print to stdoutput in light gray + // echo "\033[37m{$string}\033[0m\n"; + } + + /** + * Push a succesfully executed job. + * + * @param Job $job + * @return Job + */ + private function pushExecutedJob(Job $job) + { + $this->executed_jobs[] = $job; + $command = $job->getCommand(); + $args = $job->getArguments(); + // If callable, log the string Closure + if (is_callable($command)) { + $command = \is_string($command) ? $command : 'Closure'; + } + $this->addSchedulerVerboseOutput("Success: {$command} {$args}"); + + return $job; + } + + /** + * Push a failed job. + * + * @param Job $job + * @return Job + */ + private function pushFailedJob(Job $job) + { + $this->failed_jobs[] = $job; + $command = $job->getCommand(); + // If callable, log the string Closure + if (is_callable($command)) { + $command = \is_string($command) ? $command : 'Closure'; + } + $output = trim($job->getOutput()); + $this->addSchedulerVerboseOutput("Error: {$command}{$output}"); + + return $job; + } +} diff --git a/system/src/Grav/Common/Security.php b/system/src/Grav/Common/Security.php index 8fa00e1..0f61097 100644 --- a/system/src/Grav/Common/Security.php +++ b/system/src/Grav/Common/Security.php @@ -1,17 +1,20 @@ routes(); @@ -20,11 +23,11 @@ class Security $list = []; -// // This needs Symfony 4.1 to work -// $status && $status([ -// 'type' => 'count', -// 'steps' => count($routes), -// ]); + // This needs Symfony 4.1 to work + $status && $status([ + 'type' => 'count', + 'steps' => count($routes), + ]); foreach ($routes as $path) { @@ -43,7 +46,12 @@ class Security $results = Security::detectXssFromArray($data); if (!empty($results)) { - $list[$page->filePathClean()] = $results; + if ($route) { + $list[$page->route()] = $results; + } else { + $list[$page->filePathClean()] = $results; + } + } } catch (\Exception $e) { @@ -85,7 +93,7 @@ class Security * their content. * * @param string $string The string to run XSS detection logic on - * @return boolean|string Type of XSS vector if the given `$string` may contain XSS, false otherwise. + * @return bool|string Type of XSS vector if the given `$string` may contain XSS, false otherwise. * * Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138 */ @@ -118,9 +126,8 @@ class Security $config = Grav::instance()['config']; - $dangerous_tags = $config->get('security.xss_dangerous_tags'); - $dangerous_tags = array_map('preg_quote', array_map("trim", $dangerous_tags)); - + $dangerous_tags = array_map('preg_quote', array_map("trim", $config->get('security.xss_dangerous_tags'))); + $invalid_protocols = array_map('preg_quote', array_map("trim", $config->get('security.xss_invalid_protocols'))); $enabled_rules = $config->get('security.xss_enabled'); // Set the patterns we'll test against @@ -129,7 +136,7 @@ class Security 'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])(\son|\sxmlns)[a-z].*=>?#iUu', // Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols - 'invalid_protocols' => '#((java|live|vb)script|mocha|feed|data):.*?#iUu', + 'invalid_protocols' => '#(' . implode('|', $invalid_protocols) . '):.*?#iUu', // Match -moz-bindings 'moz_binding' => '#-moz-binding[a-z\x00-\x20]*:#u', @@ -138,7 +145,7 @@ class Security 'html_inline_styles' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(style=[^>]*(url\:|x\:expression).*)>?#iUu', // Match potentially dangerous tags - 'dangerous_tags' => '#]*>?#ui' + 'dangerous_tags' => '#]*>?#ui' ]; diff --git a/system/src/Grav/Common/Service/AccountsServiceProvider.php b/system/src/Grav/Common/Service/AccountsServiceProvider.php new file mode 100644 index 0000000..e0778be --- /dev/null +++ b/system/src/Grav/Common/Service/AccountsServiceProvider.php @@ -0,0 +1,129 @@ +get('system.accounts.type', 'data')); + if ($type === 'flex') { + /** @var Debugger $debugger */ + $debugger = $container['debugger']; + $debugger->addMessage('User Accounts: Flex Directory'); + return $this->flexAccounts($container); + } + + return $this->dataAccounts($container); + }; + + $container['users'] = $container->factory(function (Container $container) { + user_error('Grav::instance()[\'users\'] is deprecated since Grav 1.6, use Grav::instance()[\'accounts\'] instead', E_USER_DEPRECATED); + + return $container['accounts']; + }); + } + + protected function dataAccounts(Container $container) + { + if (!defined('GRAV_USER_INSTANCE')) { + define('GRAV_USER_INSTANCE', 'DATA'); + } + + // Use User class for backwards compatibility. + return new DataUser\UserCollection(User::class); + } + + protected function flexAccounts(Container $container) + { + if (!defined('GRAV_USER_INSTANCE')) { + define('GRAV_USER_INSTANCE', 'FLEX'); + } + + /** @var Config $config */ + $config = $container['config']; + + $options = [ + 'enabled' => true, + 'data' => [ + 'object' => User::class, // Use User class for backwards compatibility. + 'collection' => FlexUser\UserCollection::class, + 'index' => FlexUser\UserIndex::class, + 'storage' => $this->getFlexStorage($config->get('system.accounts.storage', 'file')), + 'search' => [ + 'options' => [ + 'contains' => 1 + ], + 'fields' => [ + 'key', + 'email' + ] + ] + ] + ] + ($config->get('plugins.flex-objects.object') ?: []); + + $directory = new FlexDirectory('accounts', 'blueprints://user/accounts.yaml', $options); + + /** @var EventDispatcher $dispatcher */ + $dispatcher = $container['events']; + $dispatcher->addListener('onFlexInit', function (Event $event) use ($directory) { + /** @var Flex $flex */ + $flex = $event['flex']; + $flex->addDirectory($directory); + }); + + return $directory->getIndex(); + } + + protected function getFlexStorage($config) + { + if (\is_array($config)) { + return $config; + } + + if ($config === 'folder') { + return [ + 'class' => FlexUser\Storage\UserFolderStorage::class, + 'options' => [ + 'formatter' => ['class' => YamlFormatter::class], + 'folder' => 'account://', + 'pattern' => '{FOLDER}/{KEY:2}/{KEY}/user.yaml', + 'key' => 'username', + 'indexed' => true + ], + ]; + } + + return [ + 'class' => FlexUser\Storage\UserFileStorage::class, + 'options' => [ + 'formatter' => ['class' => YamlFormatter::class], + 'folder' => 'account://', + 'pattern' => '{FOLDER}/{KEY}.yaml', + 'key' => 'storage_key', + 'indexed' => true + ], + ]; + } +} diff --git a/system/src/Grav/Common/Service/AssetsServiceProvider.php b/system/src/Grav/Common/Service/AssetsServiceProvider.php index 9618013..004da9c 100644 --- a/system/src/Grav/Common/Service/AssetsServiceProvider.php +++ b/system/src/Grav/Common/Service/AssetsServiceProvider.php @@ -1,8 +1,9 @@ setup(); + + return $backups; + }; + } +} diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php index 379c042..c874441 100644 --- a/system/src/Grav/Common/Service/ConfigServiceProvider.php +++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php @@ -1,8 +1,9 @@ init(); + + return $setup; }; $container['blueprints'] = function ($c) { @@ -45,11 +50,10 @@ class ConfigServiceProvider implements ServiceProviderInterface $container['languages'] = function ($c) { return static::languages($c); }; - } - public static function setup(Container $container) - { - return new Setup($container); + $container['language'] = function ($c) { + return new Language($c); + }; } public static function blueprints(Container $container) @@ -93,12 +97,15 @@ class ConfigServiceProvider implements ServiceProviderInterface $paths = $locator->findResources('plugins://'); $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths); - $config = new CompiledConfig($cache, $files, GRAV_ROOT); - $config->setBlueprints(function() use ($container) { + $compiled = new CompiledConfig($cache, $files, GRAV_ROOT); + $compiled->setBlueprints(function() use ($container) { return $container['blueprints']; }); - return $config->name("master-{$setup->environment}")->load(); + $config = $compiled->name("master-{$setup->environment}")->load(); + $config->environment = $setup->environment; + + return $config; } public static function languages(Container $container) @@ -133,8 +140,8 @@ class ConfigServiceProvider implements ServiceProviderInterface /** * Find specific paths in plugins * - * @param $plugins - * @param $folder_path + * @param array $plugins + * @param string $folder_path * @return array */ private static function pluginFolderPaths($plugins, $folder_path) diff --git a/system/src/Grav/Common/Service/ErrorServiceProvider.php b/system/src/Grav/Common/Service/ErrorServiceProvider.php index 139de54..fce69ff 100644 --- a/system/src/Grav/Common/Service/ErrorServiceProvider.php +++ b/system/src/Grav/Common/Service/ErrorServiceProvider.php @@ -1,8 +1,9 @@ findResource('log://grav.log', true, true); - $log->pushHandler(new StreamHandler($log_file, Logger::DEBUG)); return $log; diff --git a/system/src/Grav/Common/Service/OutputServiceProvider.php b/system/src/Grav/Common/Service/OutputServiceProvider.php index 89322ee..a9b2877 100644 --- a/system/src/Grav/Common/Service/OutputServiceProvider.php +++ b/system/src/Grav/Common/Service/OutputServiceProvider.php @@ -1,14 +1,15 @@ processSite($page->templateFormat()); diff --git a/system/src/Grav/Common/Service/PageServiceProvider.php b/system/src/Grav/Common/Service/PagesServiceProvider.php similarity index 92% rename from system/src/Grav/Common/Service/PageServiceProvider.php rename to system/src/Grav/Common/Service/PagesServiceProvider.php index ccfa580..0a37e83 100644 --- a/system/src/Grav/Common/Service/PageServiceProvider.php +++ b/system/src/Grav/Common/Service/PagesServiceProvider.php @@ -1,8 +1,9 @@ fromGlobals(); + }; + + $container['route'] = $container->factory(function() { + return clone Uri::getCurrentRoute(); + }); + } +} diff --git a/system/src/Grav/Common/Service/SchedulerServiceProvider.php b/system/src/Grav/Common/Service/SchedulerServiceProvider.php new file mode 100644 index 0000000..1df4bbc --- /dev/null +++ b/system/src/Grav/Common/Service/SchedulerServiceProvider.php @@ -0,0 +1,24 @@ +get('plugins.admin.enabled')) { - $base = '/' . trim($config->get('plugins.admin.route'), '/'); + $admin_base = '/' . trim($config->get('plugins.admin.route'), '/'); // Uri::route() is not processed yet, let's quickly get what we need. $current_route = str_replace(Uri::filterPath($uri->rootUrl(false)), '', parse_url($uri->url(true), PHP_URL_PATH)); + // Test to see if path starts with a supported language + admin base + $lang = Utils::pathPrefixedByLangCode($current_route); + $lang_admin_base = '/' . $lang . $admin_base; + // Check no language, simple language prefix (en) and region specific language prefix (en-US). - $pos = strpos($current_route, $base); - if ($pos === 0 || $pos === 3 || $pos === 6) { + if (Utils::startsWith($current_route, $admin_base) || Utils::startsWith($current_route, $lang_admin_base)) { $cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800); $enabled = $is_admin = true; } @@ -67,8 +71,11 @@ class SessionServiceProvider implements ServiceProviderInterface $cookie_lifetime = 9999999999; } - $inflector = new Inflector(); - $session_name = $inflector->hyphenize($config->get('system.session.name', 'grav_site')) . '-' . substr(md5(GRAV_ROOT), 0, 7); + $session_prefix = $c['inflector']->hyphenize($config->get('system.session.name', 'grav-site')); + $session_uniqueness = $config->get('system.session.uniqueness', 'path') === 'path' ? substr(md5(GRAV_ROOT), 0, 7) : md5($config->get('security.salt')); + + $session_name = $session_prefix . '-' . $session_uniqueness; + if ($is_admin && $config->get('system.session.split', true)) { $session_name .= '-admin'; } diff --git a/system/src/Grav/Common/Service/StreamsServiceProvider.php b/system/src/Grav/Common/Service/StreamsServiceProvider.php index a2ebcc6..3bbbea1 100644 --- a/system/src/Grav/Common/Service/StreamsServiceProvider.php +++ b/system/src/Grav/Common/Service/StreamsServiceProvider.php @@ -1,8 +1,9 @@ initializeLocator($locator); return $locator; }; - $container['streams'] = function($c) { + $container['streams'] = function(Container $container) { /** @var Setup $setup */ - $setup = $c['setup']; + $setup = $container['setup']; /** @var UniformResourceLocator $locator */ - $locator = $c['locator']; + $locator = $container['locator']; // Set locator to both streams. Stream::setLocator($locator); diff --git a/system/src/Grav/Common/Service/TaskServiceProvider.php b/system/src/Grav/Common/Service/TaskServiceProvider.php index 40b9696..c22121a 100644 --- a/system/src/Grav/Common/Service/TaskServiceProvider.php +++ b/system/src/Grav/Common/Service/TaskServiceProvider.php @@ -1,8 +1,9 @@ param('task'); + $container['task'] = function (Grav $c) { + $task = $_POST['task'] ?? $c['uri']->param('task'); + if (null !== $task) { + $task = filter_var($task, FILTER_SANITIZE_STRING); + } + + return $task ?: null; + }; + + $container['action'] = function (Grav $c) { + $action = $_POST['action'] ?? $c['uri']->param('action'); + if (null !== $action) { + $action = filter_var($action, FILTER_SANITIZE_STRING); + } + + return $action ?: null; }; } } diff --git a/system/src/Grav/Common/Session.php b/system/src/Grav/Common/Session.php index d617f46..cd9b2c1 100644 --- a/system/src/Grav/Common/Session.php +++ b/system/src/Grav/Common/Session.php @@ -1,13 +1,16 @@ getInstance() method instead. */ public static function instance() { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getInstance() method instead', E_USER_DEPRECATED); + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getInstance() method instead', E_USER_DEPRECATED); return static::getInstance(); } @@ -31,7 +34,7 @@ class Session extends \Grav\Framework\Session\Session */ public function init() { - if ($this->autoStart) { + if ($this->autoStart && !$this->isStarted()) { $this->start(); $this->autoStart = false; @@ -53,11 +56,11 @@ class Session extends \Grav\Framework\Session\Session * Returns attributes. * * @return array Attributes - * @deprecated 1.5 Use getAll() method instead + * @deprecated 1.5 Use ->getAll() method instead. */ public function all() { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getAll() method instead', E_USER_DEPRECATED); + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getAll() method instead', E_USER_DEPRECATED); return $this->getAll(); } @@ -66,11 +69,11 @@ class Session extends \Grav\Framework\Session\Session * Checks if the session was started. * * @return Boolean - * @deprecated 1.5 Use isStarted() method instead + * @deprecated 1.5 Use ->isStarted() method instead. */ public function started() { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use isStarted() method instead', E_USER_DEPRECATED); + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->isStarted() method instead', E_USER_DEPRECATED); return $this->isStarted(); } @@ -84,7 +87,7 @@ class Session extends \Grav\Framework\Session\Session */ public function setFlashObject($name, $object) { - $this->{$name} = serialize($object); + $this->__set($name, serialize($object)); return $this; } @@ -97,9 +100,34 @@ class Session extends \Grav\Framework\Session\Session */ public function getFlashObject($name) { - $object = unserialize($this->{$name}); + $serialized = $this->__get($name); - $this->{$name} = null; + $object = \is_string($serialized) ? unserialize($serialized, ['allowed_classes' => true]) : $serialized; + + $this->__unset($name); + + if ($name === 'files-upload') { + $grav = Grav::instance(); + + // Make sure that Forms 3.0+ has been installed. + if (null === $object && isset($grav['forms'])) { + user_error( + __CLASS__ . '::' . __FUNCTION__ . '(\'files-upload\') is deprecated since Grav 1.6, use $form->getFlash()->getLegacyFiles() instead', + E_USER_DEPRECATED + ); + + /** @var Uri $uri */ + $uri = $grav['uri']; + /** @var Grav\Plugin\Form\Forms $form */ + $form = $grav['forms']->getActiveForm(); + + $sessionField = base64_encode($uri->url); + + /** @var FormFlash $flash */ + $flash = $form ? $form->getFlash() : null; + $object = $flash ? [$sessionField => $flash->getLegacyFiles()] : null; + } + } return $object; } diff --git a/system/src/Grav/Common/Taxonomy.php b/system/src/Grav/Common/Taxonomy.php index dda2844..e323647 100644 --- a/system/src/Grav/Common/Taxonomy.php +++ b/system/src/Grav/Common/Taxonomy.php @@ -1,8 +1,9 @@ taxonomy(); } - if (!$page->published() || empty($page_taxonomy)) { + if (empty($page_taxonomy) || !$page->published()) { return; } @@ -99,7 +100,7 @@ class Taxonomy } } - if (strtolower($operator) == 'or') { + if (strtolower($operator) === 'or') { foreach ($matches as $match) { $results = array_merge($results, $match); } @@ -136,14 +137,8 @@ class Taxonomy * * @return array keys of this taxonomy */ - public function getTaxonomyItemKeys($taxonomy) { - if (isset($this->taxonomy_map[$taxonomy])) { - - $results = array_keys($this->taxonomy_map[$taxonomy]); - - return $results; - } - - return []; + public function getTaxonomyItemKeys($taxonomy) + { + return isset($this->taxonomy_map[$taxonomy]) ? array_keys($this->taxonomy_map[$taxonomy]) : []; } } diff --git a/system/src/Grav/Common/Theme.php b/system/src/Grav/Common/Theme.php index 537df11..dbc589f 100644 --- a/system/src/Grav/Common/Theme.php +++ b/system/src/Grav/Common/Theme.php @@ -1,14 +1,15 @@ getFilename(); - $result = self::get($theme); + + try { + $result = $this->get($theme); + } catch (\Exception $e) { + $exception = new \RuntimeException(sprintf('Theme %s: %s', $theme, $e->getMessage()), $e->getCode(), $e); + + /** @var Debugger $debugger */ + $debugger = $this->grav['debugger']; + $debugger->addMessage("Theme {$theme} cannot be loaded, please check Exceptions tab", 'error'); + $debugger->addException($exception); + + continue; + } if ($result) { $list[$theme] = $result; @@ -143,7 +156,7 @@ class Themes extends Iterator $blueprint->set('thumbnail', $this->grav['base_url'] . '/' . $path); } - $obj = new Data($file->content(), $blueprint); + $obj = new Data((array)$file->content(), $blueprint); // Override with user configuration. $obj->merge($this->config->get('themes.' . $name) ?: []); @@ -195,8 +208,7 @@ class Themes extends Iterator foreach ($themeClassFormat as $themeClass) { if (class_exists($themeClass)) { - $themeClassName = $themeClass; - $class = new $themeClassName($grav, $config, $name); + $class = new $themeClass($grav, $config, $name); break; } } @@ -249,7 +261,7 @@ class Themes extends Iterator } } - if (in_array($scheme, $registered)) { + if (\in_array($scheme, $registered, true)) { stream_wrapper_unregister($scheme); } $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream'; @@ -289,12 +301,12 @@ class Themes extends Iterator $locator = $this->grav['locator']; if ($config->get('system.languages.translations', true)) { - $language_file = $locator->findResource("theme://languages" . YAML_EXT); + $language_file = $locator->findResource('theme://languages' . YAML_EXT); if ($language_file) { $language = CompiledYamlFile::instance($language_file)->content(); $this->grav['languages']->mergeRecursive($language); } - $languages_folder = $locator->findResource("theme://languages/"); + $languages_folder = $locator->findResource('theme://languages'); if (file_exists($languages_folder)) { $languages = []; $iterator = new \DirectoryIterator($languages_folder); diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php index 0aa12ea..7bb2bfc 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeMarkdown.php @@ -1,25 +1,36 @@ $body], [], $lineno, $tag); } /** * Compiles the node to PHP. * - * @param \Twig_Compiler A Twig_Compiler instance + * @param Compiler $compiler A Twig_Compiler instance */ - public function compile(\Twig_Compiler $compiler) + public function compile(Compiler $compiler) { $compiler ->addDebugInfo($this) @@ -30,6 +41,6 @@ class TwigNodeMarkdown extends \Twig_Node implements \Twig_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($content);' . PHP_EOL); + ->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($context, $content);' . PHP_EOL); } } diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeRender.php b/system/src/Grav/Common/Twig/Node/TwigNodeRender.php new file mode 100644 index 0000000..753adf0 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeRender.php @@ -0,0 +1,78 @@ + $object, 'layout' => $layout, 'context' => $context], [], $lineno, $tag); + } + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig_Compiler instance + * @throws \LogicException + */ + public function compile(Compiler $compiler) + { + $compiler->addDebugInfo($this); + $compiler->write('$object = ')->subcompile($this->getNode('object'))->raw(';' . PHP_EOL); + + $layout = $this->getNode('layout'); + if ($layout) { + $compiler->write('$layout = ')->subcompile($layout)->raw(';' . PHP_EOL); + } else { + $compiler->write('$layout = null;' . PHP_EOL); + } + + $context = $this->getNode('context'); + if ($context) { + $compiler->write('$attributes = ')->subcompile($context)->raw(';' . PHP_EOL); + } else { + $compiler->write('$attributes = null;' . PHP_EOL); + } + + $compiler + ->write('$html = $object->render($layout, $attributes ?? []);' . PHP_EOL) + ->write('$block = $context[\'block\'] ?? null;' . PHP_EOL) + ->write('if ($block instanceof \Grav\Framework\ContentBlock\ContentBlock && $html instanceof \Grav\Framework\ContentBlock\ContentBlock) {' . PHP_EOL) + ->indent() + ->write('$block->addBlock($html);' . PHP_EOL) + ->write('echo $html->getToken();' . PHP_EOL) + ->outdent() + ->write('} else {' . PHP_EOL) + ->indent() + ->write('echo (string)$html;' . PHP_EOL) + ->outdent() + ->write('}' . PHP_EOL) + ; + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeScript.php b/system/src/Grav/Common/Twig/Node/TwigNodeScript.php index 23f16a3..4f6f776 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeScript.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeScript.php @@ -1,33 +1,39 @@ addDebugInfo($this); + $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); + if ($this->getNode('attributes') !== null) { $compiler ->write('$attributes = ') ->subcompile($this->getNode('attributes')) ->raw(";\n") - ->write("if (\$attributes !== null && !is_array(\$attributes)) {\n") + ->write("if (!is_array(\$attributes)) {\n") ->indent() ->write("throw new UnexpectedValueException('{% {$this->tagName} with x %}: x is not an array');\n") ->outdent() @@ -60,43 +68,34 @@ class TwigNodeScript extends \Twig_Node implements \Twig_NodeCaptureInterface if ($this->getNode('group') !== null) { $compiler - ->write('$group = ') + ->write("\$attributes['group'] = ") ->subcompile($this->getNode('group')) ->raw(";\n") - ->write("if (\$group !== null && !is_string(\$group)) {\n") + ->write("if (!is_string(\$attributes['group'])) {\n") ->indent() ->write("throw new UnexpectedValueException('{% {$this->tagName} in x %}: x is not a string');\n") ->outdent() ->write("}\n"); - } else { - $compiler->write('$group = null;' . "\n"); } if ($this->getNode('priority') !== null) { $compiler - ->write('$priority = (int)(') + ->write("\$attributes['priority'] = (int)(") ->subcompile($this->getNode('priority')) ->raw(");\n"); - } else { - $compiler->write('$priority = null;' . "\n"); } - $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); - if ($this->getNode('file') !== null) { $compiler - ->write('$file = ') + ->write('$assets->addJs(') ->subcompile($this->getNode('file')) - ->write(";\n") - ->write("\$pipeline = !empty(\$attributes['pipeline']);\n") - ->write("\$loading = !empty(\$attributes['defer']) ? 'defer' : (!empty(\$attributes['async']) ? 'async' : null);\n") - ->write("\$assets->addJs(\$file, \$priority, \$pipeline, \$loading, \$group);\n"); + ->raw(", \$attributes);\n"); } else { $compiler ->write("ob_start();\n") ->subcompile($this->getNode('body')) - ->write("\$content = ob_get_clean();") - ->write("\$assets->addInlineJs(\$content, \$priority, \$group, \$attributes);\n"); + ->write('$content = ob_get_clean();' . "\n") + ->write("\$assets->addInlineJs(\$content, \$attributes);\n"); } } } diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php b/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php index ae30e43..22f3302 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeStyle.php @@ -1,30 +1,39 @@ addDebugInfo($this); + $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); + if ($this->getNode('attributes') !== null) { $compiler ->write('$attributes = ') ->subcompile($this->getNode('attributes')) ->raw(";\n") - ->write("if (\$attributes !== null && !is_array(\$attributes)) {\n") + ->write("if (!is_array(\$attributes)) {\n") ->indent() ->write("throw new UnexpectedValueException('{% {$this->tagName} with x %}: x is not an array');\n") ->outdent() @@ -57,42 +68,34 @@ class TwigNodeStyle extends \Twig_Node implements \Twig_NodeCaptureInterface if ($this->getNode('group') !== null) { $compiler - ->write('$group = ') + ->write("\$attributes['group'] = ") ->subcompile($this->getNode('group')) ->raw(";\n") - ->write("if (\$group !== null && !is_string(\$group)) {\n") + ->write("if (!is_string(\$attributes['group'])) {\n") ->indent() ->write("throw new UnexpectedValueException('{% {$this->tagName} in x %}: x is not a string');\n") ->outdent() ->write("}\n"); - } else { - $compiler->write('$group = null;' . "\n"); } if ($this->getNode('priority') !== null) { $compiler - ->write('$priority = (int)(') + ->write("\$attributes['priority'] = (int)(") ->subcompile($this->getNode('priority')) ->raw(");\n"); - } else { - $compiler->write('$priority = null;' . "\n"); } - $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); - if ($this->getNode('file') !== null) { $compiler - ->write('$file = ') + ->write('$assets->addCss(') ->subcompile($this->getNode('file')) - ->write(";\n") - ->write("\$pipeline = !empty(\$attributes['pipeline']);\n") - ->write("\$assets->addCss(\$file, \$priority, \$pipeline, \$group);\n"); + ->raw(", \$attributes);\n"); } else { $compiler ->write("ob_start();\n") ->subcompile($this->getNode('body')) - ->write("\$content = ob_get_clean();") - ->write("\$assets->addInlineCss(\$content, \$priority, \$group);\n"); + ->write('$content = ob_get_clean();' . "\n") + ->write("\$assets->addInlineCss(\$content, \$attributes);\n"); } } } diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php index a03faa5..f29ed6d 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeSwitch.php @@ -1,19 +1,31 @@ addDebugInfo($this) diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php new file mode 100644 index 0000000..5bb5899 --- /dev/null +++ b/system/src/Grav/Common/Twig/Node/TwigNodeThrow.php @@ -0,0 +1,51 @@ + $message], ['code' => $code], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler A Twig_Compiler instance + * @throws \LogicException + */ + public function compile(Compiler $compiler) + { + $compiler->addDebugInfo($this); + + $compiler + ->write('throw new \RuntimeException(') + ->subcompile($this->getNode('message')) + ->write(', ') + ->write($this->getAttribute('code') ?: 500) + ->write(");\n"); + } +} diff --git a/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php index ecfcdab..4028e74 100644 --- a/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php +++ b/system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php @@ -1,32 +1,43 @@ $try, 'catch' => $catch), array(), $lineno, $tag); + parent::__construct(['try' => $try, 'catch' => $catch], [], $lineno, $tag); } /** * Compiles the node to PHP. * - * @param \Twig_Compiler $compiler A Twig_Compiler instance + * @param Compiler $compiler A Twig_Compiler instance * @throws \LogicException */ - public function compile(\Twig_Compiler $compiler) + public function compile(Compiler $compiler) { $compiler->addDebugInfo($this); diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php index a1d4135..7a1d139 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserMarkdown.php @@ -1,14 +1,17 @@ getLine(); - $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideMarkdownEnd'), true); - $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); return new TwigNodeMarkdown($body, $lineno, $this->getTag()); } /** * Decide if current token marks end of Markdown block. * - * @param \Twig_Token $token + * @param Token $token * @return bool */ - public function decideMarkdownEnd(\Twig_Token $token) + public function decideMarkdownEnd(Token $token) { return $token->test('endmarkdown'); } diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php new file mode 100644 index 0000000..811705d --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserRender.php @@ -0,0 +1,75 @@ +getLine(); + + [$object, $layout, $context] = $this->parseArguments($token); + + return new TwigNodeRender($object, $layout, $context, $lineno, $this->getTag()); + } + + /** + * @param Token $token + * @return array + */ + protected function parseArguments(Token $token) + { + $stream = $this->parser->getStream(); + + $object = $this->parser->getExpressionParser()->parseExpression(); + + $layout = null; + if ($stream->nextIf(Token::NAME_TYPE, 'layout')) { + $stream->expect(Token::PUNCTUATION_TYPE, ':'); + $layout = $this->parser->getExpressionParser()->parseExpression(); + } + + $context = null; + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { + $context = $this->parser->getExpressionParser()->parseExpression(); + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return [$object, $layout, $context]; + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'render'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php index fd87b1b..bb04707 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserScript.php @@ -1,14 +1,18 @@ getLine(); $stream = $this->parser->getStream(); @@ -39,51 +43,51 @@ class TwigTokenParserScript extends \Twig_TokenParser $content = null; if ($file === null) { $content = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); } return new TwigNodeScript($content, $file, $group, $priority, $attributes, $lineno, $this->getTag()); } /** - * @param \Twig_Token $token + * @param Token $token * @return array */ - protected function parseArguments(\Twig_Token $token) + protected function parseArguments(Token $token) { $stream = $this->parser->getStream(); $file = null; - if (!$stream->test(\Twig_Token::NAME_TYPE) && !$stream->test(\Twig_Token::OPERATOR_TYPE) && !$stream->test(\Twig_Token::BLOCK_END_TYPE)) { + if (!$stream->test(Token::NAME_TYPE) && !$stream->test(Token::OPERATOR_TYPE) && !$stream->test(Token::BLOCK_END_TYPE)) { $file = $this->parser->getExpressionParser()->parseExpression(); } $group = null; - if ($stream->nextIf(\Twig_Token::OPERATOR_TYPE, 'in')) { + if ($stream->nextIf(Token::OPERATOR_TYPE, 'in')) { $group = $this->parser->getExpressionParser()->parseExpression(); } $priority = null; - if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'priority')) { - $stream->expect(\Twig_Token::PUNCTUATION_TYPE, ':'); + if ($stream->nextIf(Token::NAME_TYPE, 'priority')) { + $stream->expect(Token::PUNCTUATION_TYPE, ':'); $priority = $this->parser->getExpressionParser()->parseExpression(); } $attributes = null; - if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'with')) { + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { $attributes = $this->parser->getExpressionParser()->parseExpression(); } - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); return [$file, $group, $priority, $attributes]; } /** - * @param \Twig_Token $token + * @param Token $token * @return bool */ - public function decideBlockEnd(\Twig_Token $token) + public function decideBlockEnd(Token $token) { return $token->test('endscript'); } diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php index 0c09ed4..5a5beb6 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserStyle.php @@ -1,14 +1,18 @@ getLine(); $stream = $this->parser->getStream(); @@ -38,51 +42,51 @@ class TwigTokenParserStyle extends \Twig_TokenParser $content = null; if (!$file) { $content = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); } return new TwigNodeStyle($content, $file, $group, $priority, $attributes, $lineno, $this->getTag()); } /** - * @param \Twig_Token $token + * @param Token $token * @return array */ - protected function parseArguments(\Twig_Token $token) + protected function parseArguments(Token $token) { $stream = $this->parser->getStream(); $file = null; - if (!$stream->test(\Twig_Token::NAME_TYPE) && !$stream->test(\Twig_Token::OPERATOR_TYPE) && !$stream->test(\Twig_Token::BLOCK_END_TYPE)) { + if (!$stream->test(Token::NAME_TYPE) && !$stream->test(Token::OPERATOR_TYPE) && !$stream->test(Token::BLOCK_END_TYPE)) { $file = $this->parser->getExpressionParser()->parseExpression(); } $group = null; - if ($stream->nextIf(\Twig_Token::OPERATOR_TYPE, 'in')) { + if ($stream->nextIf(Token::OPERATOR_TYPE, 'in')) { $group = $this->parser->getExpressionParser()->parseExpression(); } $priority = null; - if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'priority')) { - $stream->expect(\Twig_Token::PUNCTUATION_TYPE, ':'); + if ($stream->nextIf(Token::NAME_TYPE, 'priority')) { + $stream->expect(Token::PUNCTUATION_TYPE, ':'); $priority = $this->parser->getExpressionParser()->parseExpression(); } $attributes = null; - if ($stream->nextIf(\Twig_Token::NAME_TYPE, 'with')) { + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { $attributes = $this->parser->getExpressionParser()->parseExpression(); } - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); return [$file, $group, $priority, $attributes]; } /** - * @param \Twig_Token $token + * @param Token $token * @return bool */ - public function decideBlockEnd(\Twig_Token $token) + public function decideBlockEnd(Token $token) { return $token->test('endstyle'); } diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php index 0768b30..7bd3ba9 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserSwitch.php @@ -1,8 +1,9 @@ getLine(); $stream = $this->parser->getStream(); $name = $this->parser->getExpressionParser()->parseExpression(); - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); // There can be some whitespace between the {% switch %} and first {% case %} tag. - while ($stream->getCurrent()->getType() === \Twig_Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) === '') { + while ($stream->getCurrent()->getType() === Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) === '') { $stream->next(); } - $stream->expect(\Twig_Token::BLOCK_START_TYPE); + $stream->expect(Token::BLOCK_START_TYPE); $expressionParser = $this->parser->getExpressionParser(); @@ -59,23 +64,23 @@ class TwigTokenParserSwitch extends \Twig_TokenParser while (true) { $values[] = $expressionParser->parsePrimaryExpression(); // Multiple allowed values? - if ($stream->test(\Twig_Token::OPERATOR_TYPE, 'or')) { + if ($stream->test(Token::OPERATOR_TYPE, 'or')) { $stream->next(); } else { break; } } - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse(array($this, 'decideIfFork')); - $cases[] = new \Twig_Node([ - 'values' => new \Twig_Node($values), + $cases[] = new Node([ + 'values' => new Node($values), 'body' => $body ]); break; case 'default': - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); $default = $this->parser->subparse(array($this, 'decideIfEnd')); break; @@ -84,22 +89,22 @@ class TwigTokenParserSwitch extends \Twig_TokenParser break; default: - throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1); + throw new SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1); } } - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); - return new TwigNodeSwitch($name, new \Twig_Node($cases), $default, $lineno, $this->getTag()); + return new TwigNodeSwitch($name, new Node($cases), $default, $lineno, $this->getTag()); } /** * Decide if current token marks switch logic. * - * @param \Twig_Token $token + * @param Token $token * @return bool */ - public function decideIfFork(\Twig_Token $token) + public function decideIfFork(Token $token) { return $token->test(array('case', 'default', 'endswitch')); } @@ -107,10 +112,10 @@ class TwigTokenParserSwitch extends \Twig_TokenParser /** * Decide if current token marks end of swtich block. * - * @param \Twig_Token $token + * @param Token $token * @return bool */ - public function decideIfEnd(\Twig_Token $token) + public function decideIfEnd(Token $token) { return $token->test(array('endswitch')); } diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php new file mode 100644 index 0000000..c0d6de9 --- /dev/null +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserThrow.php @@ -0,0 +1,54 @@ + + * {% throw 404 'Not Found' %} + * + */ +class TwigTokenParserThrow extends AbstractTokenParser +{ + /** + * Parses a token and returns a node. + * + * @param Token $token A Twig_Token instance + * + * @return Node A Twig_Node instance + */ + public function parse(Token $token) + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + + $code = $stream->expect(Token::NUMBER_TYPE)->getValue(); + $message = $this->parser->getExpressionParser()->parseExpression(); + $stream->expect(Token::BLOCK_END_TYPE); + + return new TwigNodeThrow($code, $message, $lineno, $this->getTag()); + } + + /** + * Gets the tag name associated with this token parser. + * + * @return string The tag name + */ + public function getTag() + { + return 'throw'; + } +} diff --git a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php index b205861..95adef3 100644 --- a/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php +++ b/system/src/Grav/Common/Twig/TokenParser/TwigTokenParserTryCatch.php @@ -1,14 +1,18 @@ */ -class TwigTokenParserTryCatch extends \Twig_TokenParser +class TwigTokenParserTryCatch extends AbstractTokenParser { /** * Parses a token and returns a node. * - * @param \Twig_Token $token A Twig_Token instance + * @param Token $token A Twig_Token instance * - * @return \Twig_Node A Twig_Node instance + * @return Node A Twig_Node instance */ - public function parse(\Twig_Token $token) + public function parse(Token $token) { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); $try = $this->parser->subparse([$this, 'decideCatch']); $stream->next(); - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); $catch = $this->parser->subparse([$this, 'decideEnd']); $stream->next(); - $stream->expect(\Twig_Token::BLOCK_END_TYPE); + $stream->expect(Token::BLOCK_END_TYPE); return new TwigNodeTryCatch($try, $catch, $lineno, $this->getTag()); } - public function decideCatch(\Twig_Token $token) + public function decideCatch(Token $token) { return $token->test(array('catch')); } - public function decideEnd(\Twig_Token $token) + public function decideEnd(Token $token) { return $token->test(array('endtry')) || $token->test(array('endcatch')); } diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php index 567b18c..5c9008f 100644 --- a/system/src/Grav/Common/Twig/Twig.php +++ b/system/src/Grav/Common/Twig/Twig.php @@ -1,8 +1,9 @@ twig)) { + if (null === $this->twig) { /** @var Config $config */ $config = $this->grav['config']; /** @var UniformResourceLocator $locator */ $locator = $this->grav['locator']; - /** @var Language $language */ $language = $this->grav['language']; @@ -98,7 +99,8 @@ class Twig $this->grav->fireEvent('onTwigTemplatePaths'); // Add Grav core templates location - $this->twig_paths = array_merge($this->twig_paths, $locator->findResources('system://templates')); + $core_templates = array_merge($locator->findResources('system://templates'), $locator->findResources('system://templates/testing')); + $this->twig_paths = array_merge($this->twig_paths, $core_templates); $this->loader = new \Twig_Loader_Filesystem($this->twig_paths); @@ -174,13 +176,16 @@ class Twig // set default date format if set in config if ($config->get('system.pages.dateformat.long')) { - $this->twig->getExtension('Twig_Extension_Core')->setDateFormat($config->get('system.pages.dateformat.long')); + /** @var \Twig_Extension_Core $extension */ + $extension = $this->twig->getExtension('Twig_Extension_Core'); + $extension->setDateFormat($config->get('system.pages.dateformat.long')); } // enable the debug extension if required if ($config->get('system.twig.debug')) { $this->twig->addExtension(new \Twig_Extension_Debug()); } $this->twig->addExtension(new TwigExtension()); + $this->twig->addExtension(new DeferredExtension()); $this->grav->fireEvent('onTwigExtensions'); @@ -209,6 +214,8 @@ class Twig 'language_codes' => new LanguageCodes, ]; } + + return $this; } /** @@ -243,15 +250,15 @@ class Twig * 1) Handles modular pages by rendering a specific page based on its modular twig template * 2) Renders individual page items for twig processing before the site rendering * - * @param Page $item The page item to render + * @param PageInterface $item The page item to render * @param string $content Optional content override * * @return string The rendered output * @throws \Twig_Error_Loader */ - public function processPage(Page $item, $content = null) + public function processPage(PageInterface $item, $content = null) { - $content = $content !== null ? $content : $item->content(); + $content = $content ?? $item->content(); // override the twig header vars for local resolution $this->grav->fireEvent('onTwigPageVariables', new Event(['page' => $item])); @@ -261,8 +268,9 @@ class Twig $twig_vars['media'] = $item->media(); $twig_vars['header'] = $item->header(); - $local_twig = clone($this->twig); + $local_twig = clone $this->twig; + $output = ''; try { // Process Modular Twig if ($item->modularTwig()) { @@ -365,7 +373,7 @@ class Twig $twig_vars['header'] = $page->header(); $twig_vars['media'] = $page->media(); $twig_vars['content'] = $content; - $ext = '.' . ($format ? $format : 'html') . TWIG_EXT; + $ext = '.' . ($format ?: 'html') . TWIG_EXT; // determine if params are set, if so disable twig cache $params = $this->grav['uri']->params(null, true); @@ -381,7 +389,7 @@ class Twig } catch (\Twig_Error_Loader $e) { $error_msg = $e->getMessage(); // Try html version of this template if initial template was NOT html - if ($ext != '.html' . TWIG_EXT) { + if ($ext !== '.html' . TWIG_EXT) { try { $page->templateFormat('html'); $output = $this->twig->render($page->template() . '.html' . TWIG_EXT, $vars + $twig_vars); @@ -398,8 +406,8 @@ class Twig /** * Wraps the Twig_Loader_Filesystem addPath method (should be used only in `onTwigLoader()` event - * @param $template_path - * @param null $namespace + * @param string $template_path + * @param string $namespace */ public function addPath($template_path, $namespace = '__main__') { @@ -408,8 +416,8 @@ class Twig /** * Wraps the Twig_Loader_Filesystem prependPath method (should be used only in `onTwigLoader()` event - * @param $template_path - * @param null $namespace + * @param string $template_path + * @param string $namespace */ public function prependPath($template_path, $namespace = '__main__') { @@ -426,18 +434,14 @@ class Twig */ public function template($template) { - if (isset($this->template)) { - return $this->template; - } else { - return $template; - } + return $this->template ?? $template; } /** * Overrides the autoescape setting * - * @param boolean $state - * @deprecated 1.5 + * @param bool $state + * @deprecated 1.5 Auto-escape should always be turned on to protect against XSS issues (can be disabled per template file). */ public function setAutoescape($state) { diff --git a/system/src/Grav/Common/Twig/TwigEnvironment.php b/system/src/Grav/Common/Twig/TwigEnvironment.php index 66ca8bf..54ef24f 100644 --- a/system/src/Grav/Common/Twig/TwigEnvironment.php +++ b/system/src/Grav/Common/Twig/TwigEnvironment.php @@ -1,8 +1,9 @@ grav = Grav::instance(); - $this->debugger = isset($this->grav['debugger']) ? $this->grav['debugger'] : null; + $this->debugger = $this->grav['debugger'] ?? null; $this->config = $this->grav['config']; } @@ -73,7 +84,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']), new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']), new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']), - new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['is_safe' => ['html']]), + new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]), new \Twig_SimpleFilter('md5', [$this, 'md5Filter']), new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']), new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']), @@ -98,9 +109,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn new \Twig_SimpleFilter('print_r', 'print_r'), new \Twig_SimpleFilter('yaml_encode', [$this, 'yamlEncodeFilter']), new \Twig_SimpleFilter('yaml_decode', [$this, 'yamlDecodeFilter']), + new \Twig_SimpleFilter('nicecron', [$this, 'niceCronFilter']), // Translations - new \Twig_SimpleFilter('t', [$this, 'translate']), + new \Twig_SimpleFilter('t', [$this, 'translate'], ['needs_environment' => true]), new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']), new \Twig_SimpleFilter('ta', [$this, 'translateArray']), @@ -110,6 +122,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn new \Twig_SimpleFilter('bool', [$this, 'boolFilter']), new \Twig_SimpleFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]), new \Twig_SimpleFilter('array', [$this, 'arrayFilter']), + + // Object Types + new \Twig_SimpleFilter('get_type', [$this, 'getTypeFunc']), + new \Twig_SimpleFilter('of_type', [$this, 'ofTypeFunc']) ]; } @@ -126,7 +142,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn new \Twig_SimpleFunction('array_key_exists', 'array_key_exists'), new \Twig_SimpleFunction('array_unique', 'array_unique'), new \Twig_SimpleFunction('array_intersect', [$this, 'arrayIntersectFunc']), - new \Twig_simpleFunction('authorize', [$this, 'authorize']), + new \Twig_SimpleFunction('authorize', [$this, 'authorize']), new \Twig_SimpleFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), new \Twig_SimpleFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), new \Twig_SimpleFunction('vardump', [$this, 'vardumpFunc']), @@ -137,7 +153,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn new \Twig_SimpleFunction('gist', [$this, 'gistFunc']), new \Twig_SimpleFunction('nonce_field', [$this, 'nonceFieldFunc']), new \Twig_SimpleFunction('pathinfo', 'pathinfo'), - new \Twig_simpleFunction('random_string', [$this, 'randomStringFunc']), + new \Twig_SimpleFunction('random_string', [$this, 'randomStringFunc']), new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']), new \Twig_SimpleFunction('regex_replace', [$this, 'regexReplace']), new \Twig_SimpleFunction('regex_filter', [$this, 'regexFilter']), @@ -157,12 +173,18 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn new \Twig_SimpleFunction('nicenumber', [$this, 'niceNumberFunc']), new \Twig_SimpleFunction('nicefilesize', [$this, 'niceFilesizeFunc']), new \Twig_SimpleFunction('nicetime', [$this, 'nicetimeFunc']), + new \Twig_SimpleFunction('cron', [$this, 'cronFunc']), new \Twig_SimpleFunction('xss', [$this, 'xssFunc']), + // Translations - new \Twig_simpleFunction('t', [$this, 'translate']), - new \Twig_simpleFunction('tl', [$this, 'translateLanguage']), - new \Twig_simpleFunction('ta', [$this, 'translateArray']), + new \Twig_SimpleFunction('t', [$this, 'translate'], ['needs_environment' => true]), + new \Twig_SimpleFunction('tl', [$this, 'translateLanguage']), + new \Twig_SimpleFunction('ta', [$this, 'translateArray']), + + // Object Types + new \Twig_SimpleFunction('get_type', [$this, 'getTypeFunc']), + new \Twig_SimpleFunction('of_type', [$this, 'ofTypeFunc']) ]; } @@ -172,6 +194,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn public function getTokenParsers() { return [ + new TwigTokenParserRender(), + new TwigTokenParserThrow(), new TwigTokenParserTryCatch(), new TwigTokenParserScript(), new TwigTokenParserStyle(), @@ -204,16 +228,13 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn public function safeEmailFilter($str) { $email = ''; - for ( $i = 0, $len = strlen( $str ); $i < $len; $i++ ) { - $j = mt_rand( 0, 1); - if ( $j === 0 ) { - $email .= '&#' . ord( $str[$i] ) . ';'; - } elseif ( $j === 1 ) { - $email .= $str[$i]; - } + for ($i = 0, $len = strlen($str); $i < $len; $i++) { + $j = random_int(0, 1); + + $email .= $j === 0 ? '&#' . ord($str[$i]) . ';' : $str[$i]; } - return str_replace( '@', '@', $email ); + return str_replace('@', '@', $email); } /** @@ -226,7 +247,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn */ public function randomizeFilter($original, $offset = 0) { - if (!is_array($original)) { + if (!\is_array($original)) { return $original; } @@ -238,7 +259,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn $random = array_slice($original, $offset); shuffle($random); - $sizeOf = count($original); + $sizeOf = \count($original); for ($x = 0; $x < $sizeOf; $x++) { if ($x < $offset) { $sorted[] = $original[$x]; @@ -261,19 +282,15 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn */ public function modulusFilter($number, $divider, $items = null) { - if (is_string($number)) { + if (\is_string($number)) { $number = strlen($number); } $remainder = $number % $divider; - if (is_array($items)) { - if (isset($items[$remainder])) { - return $items[$remainder]; - } - - return $items[0]; - } + if (\is_array($items)) { + return $items[$remainder] ?? $items[0]; + } return $remainder; } @@ -295,11 +312,11 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn * @param string $data * @param int $count * - * @return mixed + * @return string */ public function inflectorFilter($action, $data, $count = null) { - $action = $action . 'ize'; + $action .= 'ize'; $inflector = $this->grav['inflector']; @@ -308,18 +325,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn ['titleize', 'camelize', 'underscorize', 'hyphenize', 'humanize', 'ordinalize', 'monthize'], true )) { - return $inflector->$action($data); + return $inflector->{$action}($data); } if (\in_array($action, ['pluralize', 'singularize'], true)) { - if ($count) { - return $inflector->$action($data, $count); - } + return $count ? $inflector->{$action}($data, $count) : $inflector->{$action}($data); + } - return $inflector->$action($data); - } - - return $data; + return $data; } /** @@ -337,7 +350,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Return Base32 encoded string * - * @param $str + * @param string $str * @return string */ public function base32EncodeFilter($str) @@ -348,7 +361,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Return Base32 decoded string * - * @param $str + * @param string $str * @return bool|string */ public function base32DecodeFilter($str) @@ -359,7 +372,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Return Base64 encoded string * - * @param $str + * @param string $str * @return string */ public function base64EncodeFilter($str) @@ -370,7 +383,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Return Base64 decoded string * - * @param $str + * @param string $str * @return bool|string */ public function base64DecodeFilter($str) @@ -414,8 +427,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Wrapper for chunk_split() function * - * @param $value - * @param $chars + * @param string $value + * @param int $chars * @param string $split * @return string */ @@ -427,60 +440,88 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * determine if a string contains another * - * @param String $haystack - * @param String $needle + * @param string $haystack + * @param string $needle * - * @return boolean + * @return bool */ public function containsFilter($haystack, $needle) { - return (strpos($haystack, $needle) !== false); + if (empty($needle)) { + return $haystack; + } + + return (strpos($haystack, (string) $needle) !== false); + } + + /** + * Gets a human readable output for cron syntax + * + * @param $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 $date - * @param $long_strings + * @param string $date + * @param bool $long_strings * - * @return boolean + * @param bool $show_tense + * @return bool */ - public function nicetimeFunc($date, $long_strings = true) + public function nicetimeFunc($date, $long_strings = true, $show_tense = true) { if (empty($date)) { - return $this->grav['language']->translate('NICETIME.NO_DATE_PROVIDED', null, true); + return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED', null, true); } if ($long_strings) { $periods = [ - "NICETIME.SECOND", - "NICETIME.MINUTE", - "NICETIME.HOUR", - "NICETIME.DAY", - "NICETIME.WEEK", - "NICETIME.MONTH", - "NICETIME.YEAR", - "NICETIME.DECADE" + '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" + '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"]; + $lengths = ['60', '60', '24', '7', '4.35', '12', '10']; $now = time(); // check if unix timestamp - if ((string)(int)$date == $date) { + if ((string)(int)$date === (string)$date) { $unix_date = $date; } else { $unix_date = strtotime($date); @@ -488,21 +529,21 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn // check validity of date if (empty($unix_date)) { - return $this->grav['language']->translate('NICETIME.BAD_DATE', null, true); + return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE', null, true); } // is it future date or past date if ($now > $unix_date) { $difference = $now - $unix_date; - $tense = $this->grav['language']->translate('NICETIME.AGO', null, true); + $tense = $this->grav['language']->translate('GRAV.NICETIME.AGO', null, true); - } else if ($now == $unix_date) { + } elseif ($now == $unix_date) { $difference = $now - $unix_date; - $tense = $this->grav['language']->translate('NICETIME.JUST_NOW', null, false); + $tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW', null, false); } else { $difference = $unix_date - $now; - $tense = $this->grav['language']->translate('NICETIME.FROM_NOW', null, true); + $tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW', null, true); } for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) { @@ -523,38 +564,40 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } } - $periods[$j] = $this->grav['language']->translate($periods[$j], null, true); + $periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j], null, true); if ($now == $unix_date) { - return "{$tense}"; + return $tense; } - return "$difference $periods[$j] {$tense}"; + $time = "{$difference} {$periods[$j]}"; + $time .= $show_tense ? " {$tense}" : ''; + + return $time; } /** * Allow quick check of a string for XSS Vulnerabilities * - * @param $string + * @param string|array $data * @return bool|string|array */ public function xssFunc($data) { - if (is_array($data)) { - $results = Security::detectXssFromArray($data); - } else { + if (!\is_array($data)) { return Security::detectXss($data); } + $results = Security::detectXssFromArray($data); $results_parts = array_map(function($value, $key) { return $key.': \''.$value . '\''; }, array_values($results), array_keys($results)); - return implode(', ', $results_parts); + return implode(', ', $results_parts); } /** - * @param $string + * @param string $string * * @return mixed */ @@ -568,36 +611,21 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } /** - * @param $string + * @param string $string * + * @param array $context * @param bool $block Block or Line processing * @return mixed|string */ - public function markdownFunction($string, $block = true) + public function markdownFunction($context, $string, $block = true) { - $page = $this->grav['page']; - $defaults = $this->config->get('system.pages.markdown'); - - // Initialize the preferred variant of Parsedown - if ($defaults['extra']) { - $parsedown = new ParsedownExtra($page, $defaults); - } else { - $parsedown = new Parsedown($page, $defaults); - } - - if ($block) { - $string = $parsedown->text($string); - } else { - $string = $parsedown->line($string); - } - - - return $string; + $page = $context['page'] ?? null; + return Utils::processMarkdown($string, $block, $page); } /** - * @param $haystack - * @param $needle + * @param string $haystack + * @param string $needle * * @return bool */ @@ -607,8 +635,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } /** - * @param $haystack - * @param $needle + * @param string $haystack + * @param string $needle * * @return bool */ @@ -618,7 +646,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } /** - * @param $value + * @param mixed $value * @param null $default * * @return null @@ -629,7 +657,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } /** - * @param $value + * @param string $value * @param null $chars * * @return string @@ -640,7 +668,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } /** - * @param $value + * @param string $value * @param null $chars * * @return string @@ -707,37 +735,62 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } /** - * @return mixed + * @return string */ - public function translate() + public function translate(\Twig_Environment $twig) { - return $this->grav['language']->translate(func_get_args()); + // 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 $args + * @param string|array $args * @param array|null $languages * @param bool $array_support * @param bool $html_out - * @return mixed + * @return string */ public function translateLanguage($args, array $languages = null, $array_support = false, $html_out = false) { - return $this->grav['language']->translate($args, $languages, $array_support, $html_out); + /** @var Language $language */ + $language = $this->grav['language']; + + return $language->translate($args, $languages, $array_support, $html_out); } /** - * @param $key - * @param $index - * @param null $lang - * - * @return mixed + * @param string $key + * @param string $index + * @param array|null $lang + * @return string */ public function translateArray($key, $index, $lang = null) { - return $this->grav['language']->translateArray($key, $index, $lang); + /** @var Language $language */ + $language = $this->grav['language']; + + return $language->translateArray($key, $index, $lang); } /** @@ -781,14 +834,15 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn $env = new \Twig_Environment($loader); $template = $env->createTemplate($twig); + return $template->render($context); } /** * This function will evaluate a $string through the $environment, and return its results. * - * @param $context - * @param $string + * @param array $context + * @param string $string * @return mixed */ public function evaluateStringFunc($context, $string ) @@ -802,7 +856,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn * (c) 2011 Fabien Potencier * * @param \Twig_Environment $env - * @param $context + * @param string $context */ public function dump(\Twig_Environment $env, $context) { @@ -864,14 +918,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Pad a string to a certain length with another string * - * @param $input - * @param $pad_length + * @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) + 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); } @@ -892,15 +946,16 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn return array($key => $val); } - $current_array[$key] = $val; - return $current_array; - } + $current_array[$key] = $val; + + return $current_array; + } /** * Wrapper for array_intersect() method * - * @param $array1 - * @param $array2 + * @param array $array1 + * @param array $array2 * @return array */ public function arrayIntersectFunc($array1, $array2) @@ -915,7 +970,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Returns a string from a value. If the value is array, return it json encoded * - * @param $value + * @param array|string $value * * @return string */ @@ -925,8 +980,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn return json_encode($value); } - return $value; - } + return $value; + } /** * Translate a string @@ -951,10 +1006,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn */ public function authorize($action) { - /** @var User $user */ - $user = $this->grav['user']; + /** @var UserInterface|null $user */ + $user = $this->grav['user'] ?? null; - if (!$user->authenticated || (isset($user->authorized) && !$user->authorized)) { + if (!$user || !$user->authenticated || (isset($user->authorized) && !$user->authorized)) { return false; } @@ -994,8 +1049,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn * * @param string $str * @param bool $assoc - * @param int $depth - * @param int $options + * @param int $depth + * @param int $options * @return array */ public function jsonDecodeFilter($str, $assoc = false, $depth = 512, $options = 0) @@ -1023,7 +1078,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn * @param mixed $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 mixed the resulting content + * @return string|string[]|null the resulting content */ public function regexReplace($subject, $pattern, $replace, $limit = -1) { @@ -1033,12 +1088,13 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Twig wrapper for PHP's preg_grep method * - * @param $array - * @param $regex + * @param array $array + * @param string $regex * @param int $flags * @return array */ - public function regexFilter($array, $regex, $flags = 0) { + public function regexFilter($array, $regex, $flags = 0) + { return preg_grep($regex, $array, $flags); } @@ -1072,7 +1128,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn * Check if HTTP_X_REQUESTED_WITH has been set to xmlhttprequest, * in which case we may unsafely assume ajax. Non critical use only. * - * @return true if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest + * @return bool True if HTTP_X_REQUESTED_WITH exists and has been set to xmlhttprequest */ public function isAjaxFunc() { @@ -1082,16 +1138,15 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } /** - * Get's the Exif data for a file + * Get the Exif data for a file * - * @param $image + * @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']; @@ -1101,7 +1156,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn $exif_reader = $this->grav['exif']->getReader(); - if (file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { + if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { $exif_data = $exif_reader->read($image); @@ -1110,10 +1165,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn return $exif_data->getRawData(); } - return $exif_data->getData(); - } + return $exif_data->getData(); } } + } return null; } @@ -1121,7 +1176,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Simple function to read a file based on a filepath and output it * - * @param $filepath + * @param string $filepath * @return bool|string */ public function readFileFunc($filepath) @@ -1133,7 +1188,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn $filepath = $locator->findResource($filepath); } - if (file_exists($filepath)) { + if ($filepath && file_exists($filepath)) { return file_get_contents($filepath); } @@ -1143,7 +1198,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Process a folder as Media and return a media object * - * @param $media_dir + * @param string $media_dir * @return Media|null */ public function mediaDirFunc($media_dir) @@ -1155,7 +1210,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn $media_dir = $locator->findResource($media_dir); } - if (file_exists($media_dir)) { + if ($media_dir && file_exists($media_dir)) { return new Media($media_dir); } @@ -1165,7 +1220,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Dump a variable to the browser * - * @param $var + * @param mixed $var */ public function vardumpFunc($var) { @@ -1175,68 +1230,50 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Returns a nicer more readable filesize based on bytes * - * @param $bytes + * @param int $bytes * @return string */ public function niceFilesizeFunc($bytes) { - if ($bytes >= 1073741824) - { - $bytes = number_format($bytes / 1073741824, 2) . ' GB'; - } - elseif ($bytes >= 1048576) - { - $bytes = number_format($bytes / 1048576, 2) . ' MB'; - } - elseif ($bytes >= 1024) - { - $bytes = number_format($bytes / 1024, 1) . ' KB'; - } - elseif ($bytes > 1) - { - $bytes = $bytes . ' bytes'; - } - elseif ($bytes == 1) - { - $bytes = $bytes . ' byte'; - } - else - { - $bytes = '0 bytes'; - } - - return $bytes; + return Utils::prettySize($bytes); } - /** * Returns a nicer more readable number * - * @param int|float $n - * @return bool|string + * @param int|float|string $n + * @return string|bool */ public function niceNumberFunc($n) { - // first strip any formatting; - $n = 0 + str_replace(',', '', $n); + if (!\is_float($n) && !\is_int($n)) { + if (!\is_string($n) || $n === '') { + return false; + } - // is this a number? - if (!is_numeric($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'; + return round($n/1000000000000, 2).' t'; } if ($n > 1000000000) { - return round(($n/1000000000), 2).' b'; + return round($n/1000000000, 2).' b'; } if ($n > 1000000) { - return round(($n/1000000), 2).' m'; + return round($n/1000000, 2).' m'; } if ($n > 1000) { - return round(($n/1000), 2).' k'; + return round($n/1000, 2).' k'; } return number_format($n); @@ -1245,14 +1282,15 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Get a theme variable * - * @param $var + * @param string $var * @param bool $default * @return string */ public function themeVarFunc($var, $default = null) { $header = $this->grav['page']->header(); - $header_classes = isset($header->$var) ? $header->$var : null; + $header_classes = $header->{$var} ?? null; + return $header_classes ?: $this->config->get('theme.' . $var, $default); } @@ -1260,14 +1298,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn * 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 $classes + * @param string|string[] $classes * @return string */ public function bodyClassFunc($classes) { $header = $this->grav['page']->header(); - $body_classes = isset($header->body_classes) ? $header->body_classes : ''; + $body_classes = $header->body_classes ?? ''; foreach ((array)$classes as $class) { if (!empty($body_classes) && Utils::contains($body_classes, $class)) { @@ -1284,8 +1322,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Look for a page header variable in an array of pages working its way through until a value is found * - * @param $var - * @param null $pages + * @param string $var + * @param string|string[]|null $pages * @return mixed */ public function pageHeaderVarFunc($var, $pages = null) @@ -1295,20 +1333,20 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn } // Make sure pages are an array - if (!is_array($pages)) { - $pages = array($pages); + if (!\is_array($pages)) { + $pages = [$pages]; } // Loop over pages and look for header vars foreach ($pages as $page) { - if (is_string($page)) { + if (\is_string($page)) { $page = $this->grav['pages']->find($page); } if ($page) { $header = $page->header(); - if (isset($header->$var)) { - return $header->$var; + if (isset($header->{$var})) { + return $header->{$var}; } } } @@ -1319,9 +1357,9 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Dump/Encode data into YAML format * - * @param $data - * @param $inline integer number of levels of inline syntax - * @return mixed + * @param array $data + * @param int $inline integer number of levels of inline syntax + * @return string */ public function yamlEncodeFilter($data, $inline = 10) { @@ -1331,11 +1369,77 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn /** * Decode/Parse data from YAML format * - * @param $data - * @return mixed + * @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; + break; + + case 'array': + return is_array($var); + break; + + case 'bool': + return is_bool($var); + break; + + case 'class': + return is_object($var) === true && get_class($var) === $className; + break; + + case 'float': + return is_float($var); + break; + + case 'int': + return is_int($var); + break; + + case 'numeric': + return is_numeric($var); + break; + + case 'object': + return is_object($var); + break; + + case 'scalar': + return is_scalar($var); + break; + + case 'string': + return is_string($var); + break; + } + } } diff --git a/system/src/Grav/Common/Twig/WriteCacheFileTrait.php b/system/src/Grav/Common/Twig/WriteCacheFileTrait.php index c413ca6..f87ff67 100644 --- a/system/src/Grav/Common/Twig/WriteCacheFileTrait.php +++ b/system/src/Grav/Common/Twig/WriteCacheFileTrait.php @@ -1,13 +1,15 @@ createFromString($env); } else { - $this->createFromEnvironment(is_array($env) ? $env : $_SERVER); + $this->createFromEnvironment(\is_array($env) ? $env : $_SERVER); } } @@ -143,7 +144,7 @@ class Uri } else { $this->root = $this->base . $this->root_path; } - $this->uri = Utils::replaceFirstOccurrence($orig_root_path, $this->root_path, $this->uri); + $this->uri = Utils::replaceFirstOccurrence($orig_root_path, $this->root_path, $this->uri); } else { $this->root = $this->base . $this->root_path; } @@ -173,7 +174,7 @@ class Uri } // Get the path. If there's no path, make sure pathinfo() still returns dirname variable - $path = isset($bits['path']) ? $bits['path'] : '/'; + $path = $bits['path'] ?? '/'; // remove the extension if there is one set $parts = pathinfo($path); @@ -186,11 +187,9 @@ class Uri $this->extension = $parts['extension']; } - $valid_page_types = implode('|', $config->get('system.pages.types')); - // Strip the file extension for valid page types - if (preg_match('/\.(' . $valid_page_types . ')$/', $parts['basename'])) { - $path = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, $parts['dirname']), DS) . '/' . $parts['filename']; + if ($this->isValidExtension($this->extension)) { + $path = Utils::replaceLastOccurrence(".{$this->extension}", '', $path); } // set the new url @@ -213,7 +212,7 @@ class Uri /** * Return URI path. * - * @param string $id + * @param string $id * * @return string|string[] */ @@ -229,8 +228,8 @@ class Uri /** * Return route to the current URI. By default route doesn't include base path. * - * @param bool $absolute True to include full path. - * @param bool $domain True to include domain. Works only if first parameter is also true. + * @param bool $absolute True to include full path. + * @param bool $domain True to include domain. Works only if first parameter is also true. * * @return string */ @@ -242,15 +241,15 @@ class Uri /** * Return full query string or a single query attribute. * - * @param string $id Optional attribute. Get a single query attribute if set - * @param bool $raw If true and $id is not set, return the full query array. Otherwise return the query string + * @param string $id Optional attribute. Get a single query attribute if set + * @param bool $raw If true and $id is not set, return the full query array. Otherwise return the query string * * @return string|array Returns an array if $id = null and $raw = true */ public function query($id = null, $raw = false) { if ($id !== null) { - return isset($this->queries[$id]) ? $this->queries[$id] : null; + return $this->queries[$id] ?? null; } if ($raw) { @@ -267,8 +266,8 @@ class Uri /** * Return all or a single query parameter as a URI compatible string. * - * @param string $id Optional parameter name. - * @param boolean $array return the array format or not + * @param string $id Optional parameter name. + * @param boolean $array return the array format or not * * @return null|string|array */ @@ -300,7 +299,7 @@ class Uri /** * Get URI parameter. * - * @param string $id + * @param string $id * * @return bool|string */ @@ -331,7 +330,7 @@ class Uri /** * Return URL. * - * @param bool $include_host Include hostname. + * @param bool $include_host Include hostname. * * @return string */ @@ -522,7 +521,7 @@ class Uri /** * Return root URL to the site. * - * @param bool $include_host Include hostname. + * @param bool $include_host Include hostname. * * @return mixed */ @@ -542,7 +541,7 @@ class Uri */ public function currentPage() { - return isset($this->params['page']) ? $this->params['page'] : 1; + return $this->params['page'] ?? 1; } /** @@ -555,7 +554,7 @@ class Uri */ public function referrer($default = null, $attributes = null) { - $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; + $referrer = $_SERVER['HTTP_REFERER'] ?? null; // Check that referrer came from our site. $root = $this->rootUrl(true); @@ -583,15 +582,28 @@ class Uri return static::buildUrl($this->toArray()); } - public function toArray() + public function toOriginalString() { + return static::buildUrl($this->toArray(true)); + } + + public function toArray($full = false) + { + if ($full === true) { + $root_path = $this->root_path ?? ''; + $extension = isset($this->extension) && $this->isValidExtension($this->extension) ? '.' . $this->extension : ''; + $path = $root_path . $this->path . $extension; + } else { + $path = $this->path; + } + return [ 'scheme' => $this->scheme, 'host' => $this->host, 'port' => $this->port, 'user' => $this->user, 'pass' => $this->password, - 'path' => $this->path, + 'path' => $path, 'params' => $this->params, 'query' => $this->query, 'fragment' => $this->fragment @@ -672,7 +684,7 @@ class Uri */ public static function isExternal($url) { - return Utils::startsWith($url, 'http'); + return (0 === strpos($url, 'http://') || 0 === strpos($url, 'https://') || 0 === strpos($url, '//')); } /** @@ -686,12 +698,12 @@ class Uri { $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . ':' : ''; $authority = isset($parsed_url['host']) ? '//' : ''; - $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; + $host = $parsed_url['host'] ?? ''; $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; - $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; + $user = $parsed_url['user'] ?? ''; $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; $pass = ($user || $pass) ? "{$pass}@" : ''; - $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; + $path = $parsed_url['path'] ?? ''; $path = !empty($parsed_url['params']) ? rtrim($path, '/') . static::buildParams($parsed_url['params']) : $path; $query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; @@ -723,14 +735,14 @@ class Uri /** * Converts links from absolute '/' or relative (../..) to a Grav friendly format * - * @param Page $page the current page to use as reference + * @param PageInterface $page the current page to use as reference * @param string|array $url the URL as it was written in the markdown * @param string $type the type of URL, image | link * @param bool $absolute if null, will use system default, if true will use absolute links internally * @param bool $route_only only return the route, not full URL path - * @return string the more friendly formatted url + * @return string|array the more friendly formatted url */ - public static function convertUrl(Page $page, $url, $type = 'link', $absolute = false, $route_only = false) + public static function convertUrl(PageInterface $page, $url, $type = 'link', $absolute = false, $route_only = false) { $grav = Grav::instance(); @@ -766,7 +778,7 @@ class Uri $normalized_path = Utils::normalizePath($pages_dir . $url_path); } else { $page_route = ($page->home() && !empty($url_path)) ? $page->rawRoute() : $page->route(); - $normalized_url = $base_url . Utils::normalizePath($page_route . '/' . $url_path); + $normalized_url = $base_url . Utils::normalizePath(rtrim($page_route, '/') . '/' . $url_path); $normalized_path = Utils::normalizePath($page->path() . '/' . $url_path); } @@ -806,7 +818,7 @@ class Uri // get page instances and try to find one that fits $instances = $grav['pages']->instances(); if (isset($instances[$page_path])) { - /** @var Page $target */ + /** @var PageInterface $target */ $target = $instances[$page_path]; $url_bits['path'] = $base_url . rtrim($target->route(), '/') . $filename; @@ -821,7 +833,7 @@ class Uri } // handle absolute URLs - if (is_array($url) && !$external && ($absolute === true || $grav['config']->get('system.absolute_urls', false))) { + if (\is_array($url) && !$external && ($absolute === true || $grav['config']->get('system.absolute_urls', false))) { $url['scheme'] = $uri->scheme(true); $url['host'] = $uri->host(); @@ -921,7 +933,7 @@ class Uri foreach ($matches as $match) { $param = explode($delimiter, $match[1]); - if (count($param) === 2) { + if (\count($param) === 2) { $plain_var = filter_var(rawurldecode($param[1]), FILTER_SANITIZE_STRING); $params[$param[0]] = $plain_var; $uri = str_replace($match[0], '', $uri); @@ -935,14 +947,14 @@ class Uri /** * Converts links from absolute '/' or relative (../..) to a Grav friendly format * - * @param Page $page the current page to use as reference + * @param PageInterface $page the current page to use as reference * @param string $markdown_url the URL as it was written in the markdown * @param string $type the type of URL, image | link * @param null $relative if null, will use system default, if true will use relative links internally * * @return string the more friendly formatted url */ - public static function convertUrlOld(Page $page, $markdown_url, $type = 'link', $relative = null) + public static function convertUrlOld(PageInterface $page, $markdown_url, $type = 'link', $relative = null) { $grav = Grav::instance(); @@ -1022,7 +1034,7 @@ class Uri // get page instances and try to find one that fits $instances = $grav['pages']->instances(); if (isset($instances[$page_path])) { - /** @var Page $target */ + /** @var PageInterface $target */ $target = $instances[$page_path]; $url_bits['path'] = $base_url . rtrim($target->route(), '/') . $filename; @@ -1043,7 +1055,7 @@ class Uri */ public static function addNonce($url, $action, $nonceParamName = 'nonce') { - $fake = $url && $url[0] === '/'; + $fake = $url && strpos($url, '/') === 0; if ($fake) { $url = 'http://domain.com' . $url; @@ -1051,7 +1063,7 @@ class Uri $uri = new static($url); $parts = $uri->toArray(); $nonce = Utils::getNonce($action); - $parts['params'] = (isset($parts['params']) ? $parts['params'] : []) + [$nonceParamName => $nonce]; + $parts['params'] = ($parts['params'] ?? []) + [$nonceParamName => $nonce]; if ($fake) { unset($parts['scheme'], $parts['host']); @@ -1063,7 +1075,7 @@ class Uri /** * Is the passed in URL a valid URL? * - * @param $url + * @param string $url * @return bool */ public static function isValidUrl($url) @@ -1079,7 +1091,7 @@ class Uri /** * Removes extra double slashes and fixes back-slashes * - * @param $path + * @param string $path * @return mixed|string */ public static function cleanPath($path) @@ -1142,16 +1154,16 @@ class Uri $this->scheme = $env['X-FORWARDED-PROTO']; } elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) { $this->scheme = $env['HTTP_CLOUDFRONT_FORWARDED_PROTO']; - } elseif (isset($env['REQUEST_SCHEME'])) { + } elseif (isset($env['REQUEST_SCHEME']) && empty($env['HTTPS'])) { $this->scheme = $env['REQUEST_SCHEME']; } else { - $https = isset($env['HTTPS']) ? $env['HTTPS'] : ''; + $https = $env['HTTPS'] ?? ''; $this->scheme = (empty($https) || strtolower($https) === 'off') ? 'http' : 'https'; } // Build user and password. - $this->user = isset($env['PHP_AUTH_USER']) ? $env['PHP_AUTH_USER'] : null; - $this->password = isset($env['PHP_AUTH_PW']) ? $env['PHP_AUTH_PW'] : null; + $this->user = $env['PHP_AUTH_USER'] ?? null; + $this->password = $env['PHP_AUTH_PW'] ?? null; // Build host. $hostname = 'localhost'; @@ -1185,11 +1197,11 @@ class Uri } // Build path. - $request_uri = isset($env['REQUEST_URI']) ? $env['REQUEST_URI'] : ''; + $request_uri = $env['REQUEST_URI'] ?? ''; $this->path = rawurldecode(parse_url('http://example.com' . $request_uri, PHP_URL_PATH)); // Build query string. - $this->query = isset($env['QUERY_STRING']) ? $env['QUERY_STRING'] : ''; + $this->query = $env['QUERY_STRING'] ?? ''; if ($this->query === '') { $this->query = parse_url('http://example.com' . $request_uri, PHP_URL_QUERY); } @@ -1220,7 +1232,7 @@ class Uri */ protected function hasStandardPort() { - return ($this->scheme === 'http' && $this->port === 80) || ($this->scheme === 'https' && $this->port === 443); + return ($this->port === 80 || $this->port === 443); } /** @@ -1233,14 +1245,14 @@ class Uri if ($parts === false) { throw new \RuntimeException('Malformed URL: ' . $url); } - $this->scheme = isset($parts['scheme']) ? $parts['scheme'] : null; - $this->user = isset($parts['user']) ? $parts['user'] : null; - $this->password = isset($parts['pass']) ? $parts['pass'] : null; - $this->host = isset($parts['host']) ? $parts['host'] : null; + $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->path = isset($parts['path']) ? $parts['path'] : ''; - $this->query = isset($parts['query']) ? $parts['query'] : ''; - $this->fragment = isset($parts['fragment']) ? $parts['fragment'] : null; + $this->path = $parts['path'] ?? ''; + $this->query = $parts['query'] ?? ''; + $this->fragment = $parts['fragment'] ?? null; // Validate the hostname if ($this->host) { @@ -1274,7 +1286,7 @@ class Uri } /** - * Get's post from either $_POST or JSON response object + * Get post from either $_POST or JSON response object * By default returns all data, or can return a single item * * @param string $element @@ -1313,7 +1325,7 @@ class Uri * @param bool $short * @return null|string */ - private function getContentType($short = true) + public function getContentType($short = true) { if (isset($_SERVER['CONTENT_TYPE'])) { $content_type = $_SERVER['CONTENT_TYPE']; @@ -1325,6 +1337,38 @@ class Uri return null; } + /** + * Check if this is a valid Grav extension + * + * @param $extension + * @return bool + */ + public function isValidExtension($extension) + { + $valid_page_types = implode('|', Utils::getSupportPageTypes()); + + // Strip the file extension for valid page types + if (preg_match('/(' . $valid_page_types . ')/', $extension)) { + return true; + } + return false; + } + + /** + * Allow overriding of any element (be careful!) + * + * @param $data + * @return Uri + */ + public function setUriProperties($data) + { + foreach (get_object_vars($this) as $property => $default) { + if (!array_key_exists($property, $data)) continue; + $this->{$property} = $data[$property]; // assign value to object + } + return $this; + } + /** * Get the base URI with port if needed * @@ -1362,7 +1406,7 @@ class Uri /** * Process any params based in this URL, supports any valid delimiter * - * @param $uri + * @param string $uri * @param string $delimiter * * @return string diff --git a/system/src/Grav/Common/User/Authentication.php b/system/src/Grav/Common/User/Authentication.php index b46750c..2447732 100644 --- a/system/src/Grav/Common/User/Authentication.php +++ b/system/src/Grav/Common/User/Authentication.php @@ -1,8 +1,9 @@ get('user/account'); + } + + parent::__construct($items, $blueprints); + } + + /** + * @param string $offset + * @return bool + */ + public function offsetExists($offset) + { + $value = parent::offsetExists($offset); + + // Handle special case where user was logged in before 'authorized' was added to the user object. + if (false === $value && $offset === 'authorized') { + $value = $this->offsetExists('authenticated'); + } + + return $value; + } + + /** + * @param string $offset + * @return mixed + */ + public function offsetGet($offset) + { + $value = parent::offsetGet($offset); + + // Handle special case where user was logged in before 'authorized' was added to the user object. + if (null === $value && $offset === 'authorized') { + $value = $this->offsetGet('authenticated'); + $this->offsetSet($offset, $value); + } + + return $value; + } + + public function isValid(): bool + { + return $this->items !== null; + } + + /** + * Update object with data + * + * @param array $data + * @param array $files + * @return $this + */ + public function update(array $data, array $files = []) + { + // Note: $this->merge() would cause infinite loop as it calls this method. + parent::merge($data); + + return $this; + } + + /** + * Save user without the username + */ + public function save() + { + /** @var CompiledYamlFile $file */ + $file = $this->file(); + if (!$file || !$file->filename()) { + user_error(__CLASS__ . ': calling \$user = new ' . __CLASS__ . "() is deprecated since Grav 1.6, use \$grav['accounts']->load(\$username) or \$grav['accounts']->load('') instead", E_USER_DEPRECATED); + } + + if ($file) { + $username = $this->get('username'); + + if (!$file->filename()) { + $locator = Grav::instance()['locator']; + $file->filename($locator->findResource('account://' . mb_strtolower($username) . YAML_EXT, true, true)); + } + + // if plain text password, hash it and remove plain text + $password = $this->get('password'); + if ($password) { + $this->set('hashed_password', Authentication::create($password)); + $this->undef('password'); + } + + $data = $this->items; + unset($data['username'], $data['authenticated'], $data['authorized']); + + $file->save($data); + } + } + + public function getMedia() + { + if (null === $this->_media) { + // Media object should only contain avatar, nothing else. + $media = new Media($this->getMediaFolder(), $this->getMediaOrder(), false); + + $path = $this->getAvatarFile(); + if ($path && is_file($path)) { + $medium = MediumFactory::fromFile($path); + if ($medium) { + $media->add(basename($path), $medium); + } + } + + $this->_media = $media; + } + + return $this->_media; + } + + public function getMediaFolder() + { + return $this->blueprints()->fields()['avatar']['destination'] ?? 'user://accounts/avatars'; + } + + public function getMediaOrder() + { + return []; + } + + /** + * Serialize user. + */ + public function __sleep() + { + return [ + 'items', + 'storage' + ]; + } + + /** + * Unserialize user. + */ + public function __wakeup() + { + $this->gettersVariable = 'items'; + $this->nestedSeparator = '.'; + + if (null === $this->items) { + $this->items = []; + } + + // Always set blueprints. + if (null === $this->blueprints) { + $this->blueprints = (new Blueprints)->get('user/account'); + } + } + + /** + * Merge two configurations together. + * + * @param array $data + * @return $this + * @deprecated 1.6 Use `->update($data)` instead (same but with data validation & filtering, file upload support). + */ + public function merge(array $data) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED); + + return $this->update($data); + } + + /** + * Return media object for the User's avatar. + * + * @return ImageMedium|null + * @deprecated 1.6 Use ->getAvatarImage() method instead. + */ + public function getAvatarMedia() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use getAvatarImage() method instead', E_USER_DEPRECATED); + + return $this->getAvatarImage(); + } + + /** + * Return the User's avatar URL + * + * @return string + * @deprecated 1.6 Use ->getAvatarUrl() method instead. + */ + public function avatarUrl() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use getAvatarUrl() method instead', E_USER_DEPRECATED); + + return $this->getAvatarUrl(); + } + + /** + * Checks user authorization to the action. + * Ensures backwards compatibility + * + * @param string $action + * + * @return bool + * @deprecated 1.5 Use ->authorize() method instead. + */ + public function authorise($action) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use authorize() method instead', E_USER_DEPRECATED); + + return $this->authorize($action); + } + + /** + * Implements Countable interface. + * + * @return int + * @deprecated 1.6 Method makes no sense for user account. + */ + public function count() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + return parent::count(); + } + + protected function getAvatarFile(): ?string + { + $avatars = $this->get('avatar'); + if (\is_array($avatars) && $avatars) { + $avatar = array_shift($avatars); + return $avatar['path'] ?? null; + } + + return null; + } +} diff --git a/system/src/Grav/Common/User/DataUser/UserCollection.php b/system/src/Grav/Common/User/DataUser/UserCollection.php new file mode 100644 index 0000000..800322a --- /dev/null +++ b/system/src/Grav/Common/User/DataUser/UserCollection.php @@ -0,0 +1,130 @@ +className = $className; + } + + /** + * Load user account. + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $username + * @return UserInterface + */ + public function load($username): UserInterface + { + $grav = Grav::instance(); + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + // force lowercase of username + $username = mb_strtolower($username); + + $filename = 'account://' . $username . YAML_EXT; + $path = $locator->findResource($filename) ?: $locator->findResource($filename, true, true); + $file = CompiledYamlFile::instance($path); + $content = (array)$file->content() + ['username' => $username, 'state' => 'enabled']; + + $userClass = $this->className; + $callable = function() { + $blueprints = new Blueprints; + + return $blueprints->get('user/account'); + }; + + /** @var UserInterface $user */ + $user = new $userClass($content, $callable); + $user->file($file); + + return $user; + } + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + */ + public function find($query, $fields = ['username', 'email']): UserInterface + { + $fields = (array)$fields; + + $account_dir = Grav::instance()['locator']->findResource('account://'); + $files = $account_dir ? array_diff(scandir($account_dir), ['.', '..']) : []; + + // Try with username first, you never know! + if (in_array('username', $fields, true)) { + $user = $this->load($query); + unset($fields[array_search('username', $fields, true)]); + } else { + $user = $this->load(''); + } + + // If not found, try the fields + if (!$user->exists()) { + foreach ($files as $file) { + if (Utils::endsWith($file, YAML_EXT)) { + $find_user = $this->load(trim(pathinfo($file, PATHINFO_FILENAME))); + foreach ($fields as $field) { + if ($find_user[$field] === $query) { + return $find_user; + } + } + } + } + } + return $user; + } + + /** + * Remove user account. + * + * @param string $username + * + * @return bool True if the action was performed + */ + public function delete($username): bool + { + $file_path = Grav::instance()['locator']->findResource('account://' . $username . YAML_EXT); + + return $file_path && unlink($file_path); + } + + public function count(): int + { + // check for existence of a user account + $account_dir = $file_path = Grav::instance()['locator']->findResource('account://'); + $accounts = glob($account_dir . '/*.yaml') ?: []; + + return count($accounts); + } +} diff --git a/system/src/Grav/Common/User/FlexUser/Storage/UserFileStorage.php b/system/src/Grav/Common/User/FlexUser/Storage/UserFileStorage.php new file mode 100644 index 0000000..3156fc4 --- /dev/null +++ b/system/src/Grav/Common/User/FlexUser/Storage/UserFileStorage.php @@ -0,0 +1,16 @@ + false, + 'find' => false, + 'remove' => false, + 'get' => true, + 'set' => false, + 'undef' => false, + 'def' => false, + ] + parent::getCachedMethods(); + } + + public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false) + { + // User can only be authenticated via login. + unset($elements['authenticated'], $elements['authorized']); + + parent::__construct($elements, $key, $directory, $validate); + + // Define username and state if they aren't set. + $this->defProperty('username', $key); + $this->defProperty('state', 'enabled'); + } + + /** + * Get value by using dot notation for nested arrays/objects. + * + * @example $value = $this->get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = null) + { + return $this->getNestedProperty($name, $default, $separator); + } + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function set($name, $value, $separator = null) + { + $this->setNestedProperty($name, $value, $separator); + + return $this; + } + + /** + * Unset value by using dot notation for nested arrays/objects. + * + * @example $data->undef('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function undef($name, $separator = null) + { + $this->unsetNestedProperty($name, $separator); + + return $this; + } + + /** + * Set default value by using dot notation for nested arrays/objects. + * + * @example $data->def('this.is.my.nested.variable', 'default'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function def($name, $default = null, $separator = null) + { + $this->defNestedProperty($name, $default, $separator); + + return $this; + } + + /** + * Get value from a page variable (used mostly for creating edit forms). + * + * @param string $name Variable name. + * @param mixed $default + * @param string|null $separator + * @return mixed + */ + public function getFormValue(string $name, $default = null, string $separator = null) + { + $value = parent::getFormValue($name, null, $separator); + + if ($name === 'avatar') { + return $this->parseFileProperty($value); + } + + if (null === $value) { + if ($name === 'media_order') { + return implode(',', $this->getMediaOrder()); + } + } + + return $value ?? $default; + } + + /** + * @param string $property + * @param mixed $default + * @return mixed + */ + public function getProperty($property, $default = null) + { + $value = parent::getProperty($property, $default); + + if ($property === 'avatar') { + $value = $this->parseFileProperty($value); + } + + return $value; + } + + /** + * Convert object into an array. + * + * @return array + */ + public function toArray() + { + $array = $this->jsonSerialize(); + $array['avatar'] = $this->parseFileProperty($array['avatar'] ?? null); + + return $array; + } + + /** + * Convert object into YAML string. + * + * @param int $inline The level where you switch to inline YAML. + * @param int $indent The amount of spaces to use for indentation of nested nodes. + * + * @return string A YAML string representing the object. + */ + public function toYaml($inline = 5, $indent = 2) + { + $yaml = new YamlFormatter(['inline' => $inline, 'indent' => $indent]); + + return $yaml->encode($this->toArray()); + } + + /** + * Convert object into JSON string. + * + * @return string + */ + public function toJson() + { + $json = new JsonFormatter(); + + return $json->encode($this->toArray()); + } + + /** + * Join nested values together by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + * @throws \RuntimeException + */ + public function join($name, $value, $separator = null) + { + $separator = $separator ?? '.'; + $old = $this->get($name, null, $separator); + if ($old !== null) { + if (!\is_array($old)) { + throw new \RuntimeException('Value ' . $old); + } + + if (\is_object($value)) { + $value = (array) $value; + } elseif (!\is_array($value)) { + throw new \RuntimeException('Value ' . $value); + } + + $value = $this->getBlueprint()->mergeData($old, $value, $name, $separator); + } + + $this->set($name, $value, $separator); + + return $this; + } + + /** + * Get nested structure containing default values defined in the blueprints. + * + * Fields without default value are ignored in the list. + + * @return array + */ + public function getDefaults() + { + return $this->getBlueprint()->getDefaults(); + } + + /** + * Set default values by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function joinDefaults($name, $value, $separator = null) + { + if (\is_object($value)) { + $value = (array) $value; + } + + $old = $this->get($name, null, $separator); + if ($old !== null) { + $value = $this->getBlueprint()->mergeData($value, $old, $name, $separator); + } + + $this->setNestedProperty($name, $value, $separator); + + return $this; + } + + /** + * Get value from the configuration and join it with given data. + * + * @param string $name Dot separated path to the requested value. + * @param array|object $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return array + * @throws \RuntimeException + */ + public function getJoined($name, $value, $separator = null) + { + if (\is_object($value)) { + $value = (array) $value; + } elseif (!\is_array($value)) { + throw new \RuntimeException('Value ' . $value); + } + + $old = $this->get($name, null, $separator); + + if ($old === null) { + // No value set; no need to join data. + return $value; + } + + if (!\is_array($old)) { + throw new \RuntimeException('Value ' . $old); + } + + // Return joined data. + return $this->getBlueprint()->mergeData($old, $value, $name, $separator); + } + + /** + * Set default values to the configuration if variables were not set. + * + * @param array $data + * @return $this + */ + public function setDefaults(array $data) + { + $this->setElements($this->getBlueprint()->mergeData($data, $this->toArray())); + + return $this; + } + + /** + * Validate by blueprints. + * + * @return $this + * @throws \Exception + */ + public function validate() + { + $this->getBlueprint()->validate($this->toArray()); + + return $this; + } + + /** + * Filter all items by using blueprints. + * @return $this + */ + public function filter() + { + $this->setElements($this->getBlueprint()->filter($this->toArray())); + + return $this; + } + + /** + * Get extra items which haven't been defined in blueprints. + * + * @return array + */ + public function extra() + { + return $this->getBlueprint()->extra($this->toArray()); + } + + /** + * @param string $name + * @return Blueprint + */ + public function getBlueprint(string $name = '') + { + $blueprint = clone parent::getBlueprint($name); + + $blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) { + $params = (array)$call['params']; + $method = array_shift($params); + + if (method_exists($this, $method)) { + $value = $this->{$method}(...$params); + if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) { + $field[$property] = array_merge_recursive($field[$property], $value); + } else { + $field[$property] = $value; + } + } + }); + + return $blueprint->init(); + } + + /** + * Return unmodified data as raw string. + * + * NOTE: This function only returns data which has been saved to the storage. + * + * @return string + */ + public function raw() + { + $file = $this->file(); + + return $file ? $file->raw() : ''; + } + + /** + * Set or get the data storage. + * + * @param FileInterface $storage Optionally enter a new storage. + * @return FileInterface + */ + public function file(FileInterface $storage = null) + { + if (null !== $storage) { + $this->_storage = $storage; + } + + return $this->_storage; + } + + public function isValid(): bool + { + return $this->getProperty('state') !== null; + } + + /** + * Save user without the username + */ + public function save() + { + // TODO: We may want to handle this in the storage layer in the future. + $key = $this->getStorageKey(); + if (!$key || strpos($key, '@@')) { + $storage = $this->getFlexDirectory()->getStorage(); + if ($storage instanceof FileStorage) { + $this->setStorageKey($this->getKey()); + } + } + + $password = $this->getProperty('password'); + if (null !== $password) { + $this->unsetProperty('password'); + $this->unsetProperty('password1'); + $this->unsetProperty('password2'); + $this->setProperty('hashed_password', Authentication::create($password)); + } + + return parent::save(); + } + + public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): bool + { + if (null === $user) { + /** @var UserInterface $user */ + $user = Grav::instance()['user'] ?? null; + } + + if ($user instanceof User && $user->getStorageKey() === $this->getStorageKey()) { + return true; + } + + return parent::isAuthorized($action, $scope, $user); + } + + /** + * @return array + */ + public function prepareStorage(): array + { + $elements = parent::prepareStorage(); + + // Do not save authorization information. + unset($elements['authenticated'], $elements['authorized']); + + return $elements; + } + + /** + * Merge two configurations together. + * + * @param array $data + * @return $this + * @deprecated 1.6 Use `->update($data)` instead (same but with data validation & filtering, file upload support). + */ + public function merge(array $data) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED); + + $this->setElements($this->getBlueprint()->mergeData($this->toArray(), $data)); + + return $this; + } + + /** + * Return media object for the User's avatar. + * + * @return ImageMedium|null + * @deprecated 1.6 Use ->getAvatarImage() method instead. + */ + public function getAvatarMedia() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarImage() method instead', E_USER_DEPRECATED); + + return $this->getAvatarImage(); + } + + /** + * Return the User's avatar URL + * + * @return string + * @deprecated 1.6 Use ->getAvatarUrl() method instead. + */ + public function avatarUrl() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarUrl() method instead', E_USER_DEPRECATED); + + return $this->getAvatarUrl(); + } + + /** + * Checks user authorization to the action. + * Ensures backwards compatibility + * + * @param string $action + * @return bool + * @deprecated 1.5 Use ->authorize() method instead. + */ + public function authorise($action) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->authorize() method instead', E_USER_DEPRECATED); + + return $this->authorize($action); + } + + /** + * Implements Countable interface. + * + * @return int + * @deprecated 1.6 Method makes no sense for user account. + */ + public function count() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED); + + return \count($this->jsonSerialize()); + } + + /** + * Gets the associated media collection (original images). + * + * @return MediaCollectionInterface Representation of associated media. + */ + protected function getOriginalMedia() + { + return (new Media($this->getMediaFolder() . '/original', $this->getMediaOrder()))->setTimestamps(); + } + + /** + * @param array $files + */ + protected function setUpdatedMedia(array $files): void + { + $list = []; + $list_original = []; + foreach ($files as $field => $group) { + foreach ($group as $filename => $file) { + if (strpos($field, '/original')) { + // Special handling for original images. + $list_original[$filename] = $file; + continue; + } + + $list[$filename] = $file; + + if ($file) { + /** @var FormFlashFile $file */ + $data = $file->jsonSerialize(); + $path = $file->getClientFilename(); + unset($data['tmp_name'], $data['path']); + + $this->setNestedProperty("{$field}\n{$path}", $data, "\n"); + } else { + $this->unsetNestedProperty("{$field}\n{$filename}", "\n"); + } + } + } + + $this->_uploads = $list; + $this->_uploads_original = $list_original; + } + + protected function saveUpdatedMedia(): void + { + // Upload/delete original sized images. + /** @var FormFlashFile $file */ + foreach ($this->_uploads_original ?? [] as $name => $file) { + $name = 'original/' . $name; + if ($file) { + $this->uploadMediaFile($file, $name); + } else { + $this->deleteMediaFile($name); + } + } + + /** + * @var string $filename + * @var UploadedFileInterface $file + */ + foreach ($this->getUpdatedMedia() as $filename => $file) { + if ($file) { + $this->uploadMediaFile($file, $filename); + } else { + $this->deleteMediaFile($filename); + } + } + + $this->setUpdatedMedia([]); + } + + /** + * @param array $value + * @return array + */ + protected function parseFileProperty($value) + { + if (!\is_array($value)) { + return $value; + } + + $originalMedia = $this->getOriginalMedia(); + $resizedMedia = $this->getMedia(); + + $list = []; + foreach ($value as $filename => $info) { + if (!\is_array($info)) { + continue; + } + + /** @var Medium $thumbFile */ + $thumbFile = $resizedMedia[$filename]; + /** @var Medium $imageFile */ + $imageFile = $originalMedia[$filename] ?? $thumbFile; + if ($thumbFile) { + $list[$filename] = [ + 'name' => $filename, + 'type' => $info['type'], + 'size' => $info['size'], + 'image_url' => $imageFile->url(), + 'thumb_url' => $thumbFile->url(), + 'cropData' => (object)($imageFile->metadata()['upload']['crop'] ?? []) + ]; + } + } + + return $list; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return [ + 'type' => 'accounts', + 'key' => $this->getKey(), + 'elements' => $this->jsonSerialize(), + 'storage' => $this->getStorage() + ]; + } + + /** + * @param array $serialized + */ + protected function doUnserialize(array $serialized): void + { + $grav = Grav::instance(); + + /** @var UserCollection $accounts */ + $accounts = $grav['accounts']; + + $directory = $accounts->getFlexDirectory(); + if (!$directory) { + throw new \InvalidArgumentException('Internal error'); + } + + $this->setFlexDirectory($directory); + $this->setStorage($serialized['storage']); + $this->setKey($serialized['key']); + $this->setElements($serialized['elements']); + } +} diff --git a/system/src/Grav/Common/User/FlexUser/UserCollection.php b/system/src/Grav/Common/User/FlexUser/UserCollection.php new file mode 100644 index 0000000..0f7a1d1 --- /dev/null +++ b/system/src/Grav/Common/User/FlexUser/UserCollection.php @@ -0,0 +1,100 @@ +exists(). + * + * @param string $username + * @return User + */ + public function load($username): UserInterface + { + if ($username !== '') { + $key = mb_strtolower($username); + $user = $this->get($key); + if ($user) { + return $user; + } + } else { + $key = ''; + } + + $directory = $this->getFlexDirectory(); + + /** @var User $object */ + $object = $directory->createObject( + [ + 'username' => $username, + 'state' => 'enabled' + ], + $key + ); + + return $object; + } + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return User + */ + public function find($query, $fields = ['username', 'email']): UserInterface + { + foreach ((array)$fields as $field) { + if ($field === 'key') { + $user = $this->get($query); + } elseif ($field === 'storage_key') { + $user = $this->withKeyField('storage_key')->get($query); + } elseif ($field === 'flex_key') { + $user = $this->withKeyField('flex_key')->get($query); + } elseif ($field === 'username') { + $user = $this->get(mb_strtolower($query)); + } else { + $user = parent::find($query, $field); + } + if ($user) { + return $user; + } + } + + return $this->load(''); + } + + /** + * Delete user account. + * + * @param string $username + * + * @return bool True if user account was found and was deleted. + */ + public function delete($username): bool + { + $user = $this->load($username); + + $exists = $user->exists(); + if ($exists) { + $user->delete(); + } + + return $exists; + } +} diff --git a/system/src/Grav/Common/User/FlexUser/UserIndex.php b/system/src/Grav/Common/User/FlexUser/UserIndex.php new file mode 100644 index 0000000..f95f51b --- /dev/null +++ b/system/src/Grav/Common/User/FlexUser/UserIndex.php @@ -0,0 +1,133 @@ +exists(). + * + * @param string $username + * + * @return User + */ + public function load($username): UserInterface + { + if ($username !== '') { + $key = mb_strtolower($username); + $user = $this->get($key); + if ($user) { + return $user; + } + } else { + $key = ''; + } + + $directory = $this->getFlexDirectory(); + + /** @var User $object */ + $object = $directory->createObject( + [ + 'username' => $username, + 'state' => 'enabled' + ], + $key + ); + + return $object; + } + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return User + */ + public function find($query, $fields = ['username', 'email']): UserInterface + { + foreach ((array)$fields as $field) { + if ($field === 'key') { + $user = $this->get($query); + } elseif ($field === 'storage_key') { + $user = $this->withKeyField('storage_key')->get($query); + } elseif ($field === 'flex_key') { + $user = $this->withKeyField('flex_key')->get($query); + } elseif ($field === 'email') { + $user = $this->withKeyField('email')->get($query); + } elseif ($field === 'username') { + $user = $this->get(mb_strtolower($query)); + } else { + $user = $this->__call('find', [$query, $field]); + } + if ($user) { + return $user; + } + } + + return $this->load(''); + } + + protected static function updateIndexData(array &$entry, array $data) + { + $entry['key'] = mb_strtolower($entry['key']); + $entry['email'] = isset($data['email']) ? mb_strtolower($data['email']) : null; + } + + protected static function getIndexFile(FlexStorageInterface $storage) + { + // Load saved index file. + $grav = Grav::instance(); + $locator = $grav['locator']; + $filename = $locator->findResource('user-data://flex/indexes/accounts.yaml', true, true); + + return CompiledYamlFile::instance($filename); + } + + protected static function onChanges(array $entries, array $added, array $updated, array $removed) + { + $message = sprintf('Flex: User index updated, %d objects (%d added, %d updated, %d removed).', \count($entries), \count($added), \count($updated), \count($removed)); + + $grav = Grav::instance(); + + /** @var Logger $logger */ + $logger = $grav['log']; + $logger->addDebug($message); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage($message, 'debug'); + } +} diff --git a/system/src/Grav/Common/User/Group.php b/system/src/Grav/Common/User/Group.php index a251326..9815c0e 100644 --- a/system/src/Grav/Common/User/Group.php +++ b/system/src/Grav/Common/User/Group.php @@ -1,8 +1,9 @@ $group) { - $groups[$groupname] = isset($group['readableName']) ? $group['readableName'] : $groupname; + $groups[$groupname] = $group['readableName'] ?? $groupname; } return $groups; @@ -66,10 +67,10 @@ class Group extends Data { $groups = self::groups(); - $content = isset($groups[$groupname]) ? $groups[$groupname] : []; + $content = $groups[$groupname] ?? []; $content += ['groupname' => $groupname]; - $blueprints = new Blueprints; + $blueprints = new Blueprints(); $blueprint = $blueprints->get('user/group'); return new Group($content, $blueprint); @@ -85,17 +86,17 @@ class Group extends Data /** @var Config $config */ $config = $grav['config']; - $blueprints = new Blueprints; + $blueprints = new Blueprints(); $blueprint = $blueprints->get('user/group'); - $config->set("groups.{$this->groupname}", []); + $config->set("groups.{$this->get('groupname')}", []); $fields = $blueprint->fields(); foreach ($fields as $field) { if ($field['type'] === 'text') { $value = $field['name']; if (isset($this->items['data'][$value])) { - $config->set("groups.{$this->groupname}.{$value}", $this->items['data'][$value]); + $config->set("groups.{$this->get('groupname')}.{$value}", $this->items['data'][$value]); } } if ($field['type'] === 'array' || $field['type'] === 'permissions') { @@ -104,14 +105,14 @@ class Group extends Data if ($arrayValues) { foreach ($arrayValues as $arrayIndex => $arrayValue) { - $config->set("groups.{$this->groupname}.{$value}.{$arrayIndex}", $arrayValue); + $config->set("groups.{$this->get('groupname')}.{$value}.{$arrayIndex}", $arrayValue); } } } } $type = 'groups'; - $blueprints = $this->blueprints("config/{$type}"); + $blueprints = $this->blueprints(); $filename = CompiledYamlFile::instance($grav['locator']->findResource("config://{$type}.yaml")); @@ -134,7 +135,7 @@ class Group extends Data /** @var Config $config */ $config = $grav['config']; - $blueprints = new Blueprints; + $blueprints = new Blueprints(); $blueprint = $blueprints->get('user/group'); $type = 'groups'; diff --git a/system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php b/system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php new file mode 100644 index 0000000..37b9009 --- /dev/null +++ b/system/src/Grav/Common/User/Interfaces/UserCollectionInterface.php @@ -0,0 +1,40 @@ +exists(). + * + * @param string $username + * @return UserInterface + */ + public function load($username): UserInterface; + + /** + * Find a user by username, email, etc + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + */ + public function find($query, $fields = ['username', 'email']): UserInterface; + + /** + * Delete user account. + * + * @param string $username + * @return bool True if user account was found and was deleted. + */ + public function delete($username): bool; +} diff --git a/system/src/Grav/Common/User/Interfaces/UserInterface.php b/system/src/Grav/Common/User/Interfaces/UserInterface.php new file mode 100644 index 0000000..0fba676 --- /dev/null +++ b/system/src/Grav/Common/User/Interfaces/UserInterface.php @@ -0,0 +1,198 @@ +get('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return mixed Value. + */ + public function get($name, $default = null, $separator = null); + + /** + * Set value by using dot notation for nested arrays/objects. + * + * @example $data->set('this.is.my.nested.variable', $value); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value New value. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function set($name, $value, $separator = null); + + /** + * Unset value by using dot notation for nested arrays/objects. + * + * @example $data->undef('this.is.my.nested.variable'); + * + * @param string $name Dot separated path to the requested value. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function undef($name, $separator = null); + + /** + * Set default value by using dot notation for nested arrays/objects. + * + * @example $data->def('this.is.my.nested.variable', 'default'); + * + * @param string $name Dot separated path to the requested value. + * @param mixed $default Default value (or null). + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function def($name, $default = null, $separator = null); + + /** + * Join nested values together by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + * @throws \RuntimeException + */ + public function join($name, $value, $separator = '.'); + + /** + * Get nested structure containing default values defined in the blueprints. + * + * Fields without default value are ignored in the list. + + * @return array + */ + public function getDefaults(); + + /** + * Set default values by using blueprints. + * + * @param string $name Dot separated path to the requested value. + * @param mixed $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return $this + */ + public function joinDefaults($name, $value, $separator = '.'); + + /** + * Get value from the configuration and join it with given data. + * + * @param string $name Dot separated path to the requested value. + * @param array|object $value Value to be joined. + * @param string $separator Separator, defaults to '.' + * @return array + * @throws \RuntimeException + */ + public function getJoined($name, $value, $separator = '.'); + + /** + * Set default values to the configuration if variables were not set. + * + * @param array $data + * @return $this + */ + public function setDefaults(array $data); + + /** + * Update object with data + * + * @param array $data + * @param array $files + * @return $this + */ + public function update(array $data, array $files = []); + + /** + * Returns whether the data already exists in the storage. + * + * NOTE: This method does not check if the data is current. + * + * @return bool + */ + public function exists(); + + /** + * Return unmodified data as raw string. + * + * NOTE: This function only returns data which has been saved to the storage. + * + * @return string + */ + public function raw(); + + /** + * Authenticate user. + * + * If user password needs to be updated, new information will be saved. + * + * @param string $password Plaintext password. + * + * @return bool + */ + public function authenticate(string $password): bool; + + /** + * Checks user authorization to the action. + * + * @param string $action + * @param string|null $scope + * @return bool + */ + public function authorize(string $action, string $scope = null): bool; + + /** + * Return media object for the User's avatar. + * + * Note: if there's no local avatar image for the user, you should call getAvatarUrl() to get the external avatar URL. + * + * @return ImageMedium|null + */ + public function getAvatarImage(): ?ImageMedium; + + /** + * Return the User's avatar URL. + * + * @return string + */ + public function getAvatarUrl(): string; +} diff --git a/system/src/Grav/Common/User/Traits/UserTrait.php b/system/src/Grav/Common/User/Traits/UserTrait.php new file mode 100644 index 0000000..f711cc9 --- /dev/null +++ b/system/src/Grav/Common/User/Traits/UserTrait.php @@ -0,0 +1,171 @@ +get('hashed_password'); + + $isHashed = null !== $hash; + if (!$isHashed) { + // If there is no hashed password, fake verify with default hash. + $hash = Grav::instance()['config']->get('system.security.default_hash'); + } + + // Always execute verify() to protect us from timing attacks, but make the test to fail if hashed password wasn't set. + $result = Authentication::verify($password, $hash) && $isHashed; + + $plaintext_password = $this->get('password'); + if (null !== $plaintext_password) { + // Plain-text password is still stored, check if it matches. + if ($password !== $plaintext_password) { + return false; + } + + // Force hash update to get rid of plaintext password. + $result = 2; + } + + if ($result === 2) { + // Password needs to be updated, save the user. + $this->set('password', $password); + $this->undef('hashed_password'); + $this->save(); + } + + return (bool)$result; + } + + /** + * Checks user authorization to the action. + * + * @param string $action + * @param string|null $scope + * @return bool + */ + public function authorize(string $action, string $scope = null): bool + { + if (!$this->get('authenticated')) { + return false; + } + + if ($this->get('state', 'enabled') !== 'enabled') { + return false; + } + + if (null !== $scope) { + $action = $scope . '.' . $action; + } + + $config = Grav::instance()['config']; + $authorized = false; + + //Check group access level + $groups = (array)$this->get('groups'); + foreach ($groups as $group) { + $permission = $config->get("groups.{$group}.access.{$action}"); + $authorized = Utils::isPositive($permission); + if ($authorized === true) { + break; + } + } + + //Check user access level + $access = $this->get('access'); + if ($access && Utils::getDotNotation($access, $action) !== null) { + $permission = $this->get("access.{$action}"); + $authorized = Utils::isPositive($permission); + } + + return $authorized; + } + + /** + * Return media object for the User's avatar. + * + * Note: if there's no local avatar image for the user, you should call getAvatarUrl() to get the external avatar URL. + * + * @return ImageMedium|null + */ + public function getAvatarImage(): ?ImageMedium + { + $avatars = $this->get('avatar'); + if (\is_array($avatars) && $avatars) { + $avatar = array_shift($avatars); + + $media = $this->getMedia(); + $name = $avatar['name'] ?? null; + + $image = $name ? $media[$name] : null; + if ($image instanceof ImageMedium) { + return $image; + } + } + + return null; + } + + /** + * Return the User's avatar URL + * + * @return string + */ + public function getAvatarUrl(): string + { + // Try to locate avatar image. + $avatar = $this->getAvatarImage(); + if ($avatar) { + return $avatar->url(); + } + + // Try if avatar is a sting (URL). + $avatar = $this->get('avatar'); + if (\is_string($avatar)) { + return $avatar; + } + + // Try looking for provider. + $provider = $this->get('provider'); + $provider_options = $this->get($provider); + if (\is_array($provider_options)) { + if (isset($provider_options['avatar_url']) && \is_string($provider_options['avatar_url'])) { + return $provider_options['avatar_url']; + } + if (isset($provider_options['avatar']) && \is_string($provider_options['avatar'])) { + return $provider_options['avatar']; + } + } + + $email = $this->get('email'); + + // By default fall back to gravatar image. + return $email ? 'https://www.gravatar.com/avatar/' . md5(strtolower(trim($email))) : ''; + } + + abstract public function get($name, $default = null, $separator = null); + abstract public function set($name, $value, $separator = null); + abstract public function undef($name, $separator = null); + abstract public function save(); +} diff --git a/system/src/Grav/Common/User/User.php b/system/src/Grav/Common/User/User.php index c6f5dac..ba1dbdc 100644 --- a/system/src/Grav/Common/User/User.php +++ b/system/src/Grav/Common/User/User.php @@ -1,318 +1,146 @@ exists(). - * - * @param string $username - * @param bool $setConfig - * - * @return User + * @deprecated 1.6 Use $grav['accounts'] instead of static calls. In type hints, please use UserInterface. */ - public static function load($username) + class User extends FlexUser\User { - $grav = Grav::instance(); - /** @var UniformResourceLocator $locator */ - $locator = $grav['locator']; + /** + * Load user account. + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $username + * + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->load(...) instead. + */ + public static function load($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); - // force lowercase of username - $username = strtolower($username); - - $blueprints = new Blueprints; - $blueprint = $blueprints->get('user/account'); - - $file_path = $locator->findResource('account://' . $username . YAML_EXT); - $file = CompiledYamlFile::instance($file_path); - $content = (array)$file->content() + ['username' => $username, 'state' => 'enabled']; - - $user = new User($content, $blueprint); - $user->file($file); - - return $user; - } - - /** - * Find a user by username, email, etc - * - * @param string $query the query to search for - * @param array $fields the fields to search - * @return User - */ - public static function find($query, $fields = ['username', 'email']) - { - $account_dir = Grav::instance()['locator']->findResource('account://'); - $files = $account_dir ? array_diff(scandir($account_dir), ['.', '..']) : []; - - // Try with username first, you never know! - if (in_array('username', $fields, true)) { - $user = User::load($query); - unset($fields[array_search('username', $fields, true)]); - } else { - $user = User::load(''); + return static::getCollection()->load($username); } - // If not found, try the fields - if (!$user->exists()) { - foreach ($files as $file) { - if (Utils::endsWith($file, YAML_EXT)) { - $find_user = User::load(trim(pathinfo($file, PATHINFO_FILENAME))); - foreach ($fields as $field) { - if ($find_user[$field] === $query) { - return $find_user; - } - } - } - } - } - return $user; - } + /** + * Find a user by username, email, etc + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->find(...) instead. + */ + public static function find($query, $fields = ['username', 'email']) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); - /** - * Remove user account. - * - * @param string $username - * - * @return bool True if the action was performed - */ - public static function remove($username) - { - $file_path = Grav::instance()['locator']->findResource('account://' . $username . YAML_EXT); - - return $file_path && unlink($file_path); - } - - /** - * @param string $offset - * @return bool - */ - public function offsetExists($offset) - { - $value = parent::offsetExists($offset); - - // Handle special case where user was logged in before 'authorized' was added to the user object. - if (false === $value && $offset === 'authorized') { - $value = $this->offsetExists('authenticated'); + return static::getCollection()->find($query, $fields); } - return $value; - } + /** + * Remove user account. + * + * @param string $username + * @return bool True if the action was performed + * @deprecated 1.6 Use $grav['accounts']->delete(...) instead. + */ + public static function remove($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->delete() instead', E_USER_DEPRECATED); - /** - * @param string $offset - * @return mixed - */ - public function offsetGet($offset) - { - $value = parent::offsetGet($offset); - - // Handle special case where user was logged in before 'authorized' was added to the user object. - if (null === $value && $offset === 'authorized') { - $value = $this->offsetGet('authenticated'); - $this->offsetSet($offset, $value); + return static::getCollection()->delete($username); } - return $value; - } - - /** - * Authenticate user. - * - * If user password needs to be updated, new information will be saved. - * - * @param string $password Plaintext password. - * - * @return bool - */ - public function authenticate($password) - { - $save = false; - - // Plain-text is still stored - if ($this->password) { - if ($password !== $this->password) { - // Plain-text passwords do not match, we know we should fail but execute - // verify to protect us from timing attacks and return false regardless of - // the result - Authentication::verify( - $password, - Grav::instance()['config']->get('system.security.default_hash') - ); - - return false; - } - - // Plain-text does match, we can update the hash and proceed - $save = true; - - $this->hashed_password = Authentication::create($this->password); - unset($this->password); - - } - - $result = Authentication::verify($password, $this->hashed_password); - - // Password needs to be updated, save the file. - if ($result === 2) { - $save = true; - $this->hashed_password = Authentication::create($password); - } - - if ($save) { - $this->save(); - } - - return (bool)$result; - } - - /** - * Save user without the username - */ - public function save() - { - $file = $this->file(); - - if ($file) { - $username = $this->get('username'); - - if (!$file->filename()) { - $locator = Grav::instance()['locator']; - $file->filename($locator->findResource('account://') . DS . strtolower($username) . YAML_EXT); - } - - // if plain text password, hash it and remove plain text - if ($this->password) { - $this->hashed_password = Authentication::create($this->password); - unset($this->password); - } - - unset($this->username); - $file->save($this->items); - $this->set('username', $username); + /** + * @return UserCollectionInterface + */ + protected static function getCollection() + { + return Grav::instance()['accounts']; } } - +} else { /** - * Checks user authorization to the action. - * - * @param string $action - * - * @return bool + * @deprecated 1.6 Use $grav['accounts'] instead of static calls. In type hints, use UserInterface. */ - public function authorize($action) + class User extends DataUser\User { - if (empty($this->items)) { - return false; + /** + * Load user account. + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $username + * + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->load(...) instead. + */ + public static function load($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); + + return static::getCollection()->load($username); } - if (!$this->authenticated) { - return false; + /** + * Find a user by username, email, etc + * + * Always creates user object. To check if user exists, use $this->exists(). + * + * @param string $query the query to search for + * @param array $fields the fields to search + * @return UserInterface + * @deprecated 1.6 Use $grav['accounts']->find(...) instead. + */ + public static function find($query, $fields = ['username', 'email']) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->' . __FUNCTION__ . '() instead', E_USER_DEPRECATED); + + return static::getCollection()->find($query, $fields); } - if (isset($this->state) && $this->state !== 'enabled') { - return false; + /** + * Remove user account. + * + * @param string $username + * @return bool True if the action was performed + * @deprecated 1.6 Use $grav['accounts']->delete(...) instead. + */ + public static function remove($username) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use $grav[\'accounts\']->delete() instead', E_USER_DEPRECATED); + + return static::getCollection()->delete($username); } - $return = false; - - //Check group access level - $groups = $this->get('groups'); - if ($groups) { - foreach ((array)$groups as $group) { - $permission = Grav::instance()['config']->get("groups.{$group}.access.{$action}"); - $return = Utils::isPositive($permission); - if ($return === true) { - break; - } - } - } - - //Check user access level - if ($this->get('access')) { - if (Utils::getDotNotation($this->get('access'), $action) !== null) { - $permission = $this->get("access.{$action}"); - $return = Utils::isPositive($permission); - } - } - - return $return; - } - - /** - * Checks user authorization to the action. - * Ensures backwards compatibility - * - * @param string $action - * - * @deprecated use authorize() - * @return bool - */ - public function authorise($action) - { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use authorize() method instead', E_USER_DEPRECATED); - - return $this->authorize($action); - } - - /** - * Return the User's avatar URL - * - * @return string - */ - public function avatarUrl() - { - if ($this->avatar) { - $avatar = $this->avatar; - $avatar = array_shift($avatar); - return Grav::instance()['base_url'] . '/' . $avatar['path']; - } - - return 'https://www.gravatar.com/avatar/' . md5($this->email); - } - - /** - * Serialize user. - */ - public function __sleep() - { - return [ - 'items', - 'storage' - ]; - } - - /** - * Unserialize user. - */ - public function __wakeup() - { - $this->gettersVariable = 'items'; - $this->nestedSeparator = '.'; - - if (null === $this->items) { - $this->items = []; - } - - if (null === $this->blueprints) { - $blueprints = new Blueprints; - $this->blueprints = $blueprints->get('user/account'); + /** + * @return UserCollectionInterface + */ + protected static function getCollection() + { + return Grav::instance()['accounts']; } } } diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index b49abdd..56b2d89 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -1,16 +1,19 @@ get('system.absolute_urls', false)) { - $domain = true; - } + $input = (string)$input; - if (Grav::instance()['uri']->isExternal($input)) { + if (Uri::isExternal($input)) { return $input; } - $input = ltrim((string)$input, '/'); + $grav = Grav::instance(); - if (Utils::contains((string)$input, '://')) { + /** @var Uri $uri */ + $uri = $grav['uri']; + + if (static::contains((string)$input, '://')) { /** @var UniformResourceLocator $locator */ - $locator = Grav::instance()['locator']; + $locator = $grav['locator']; $parts = Uri::parseUrl($input); - if ($parts) { - $resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false); + if (is_array($parts)) { + // Make sure we always have scheme, host, port and path. + $scheme = $parts['scheme'] ?? ''; + $host = $parts['host'] ?? ''; + $port = $parts['port'] ?? ''; + $path = $parts['path'] ?? ''; - if (isset($parts['query'])) { - $resource = $resource . '?' . $parts['query']; + if ($scheme && !$port) { + // If URL has a scheme, we need to check if it's one of Grav streams. + if (!$locator->schemeExists($scheme)) { + // If scheme does not exists as a stream, assume it's external. + return str_replace(' ', '%20', $input); + } + + // Attempt to find the resource (because of parse_url() we need to put host back to path). + $resource = $locator->findResource("{$scheme}://{$host}{$path}", false); + + if ($resource === false) { + if (!$fail_gracefully) { + return false; + } + + // Return location where the file would be if it was saved. + $resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true); + } + + } elseif ($host || $port) { + // If URL doesn't have scheme but has host or port, it is external. + return str_replace(' ', '%20', $input); } + + if (!empty($resource)) { + // Add query string back. + if (isset($parts['query'])) { + $resource .= '?' . $parts['query']; + } + + // Add fragment back. + if (isset($parts['fragment'])) { + $resource .= '#' . $parts['fragment']; + } + } + } else { // Not a valid URL (can still be a stream). $resource = $locator->findResource($input, false); } - } else { + $root = $uri->rootUrl(); + + if (static::startsWith($input, $root)) { + $input = static::replaceFirstOccurrence($root, '', $input); + } + + $input = ltrim($input, '/'); + $resource = $input; } - /** @var Uri $uri */ - $uri = Grav::instance()['uri']; + if (!$fail_gracefully && $resource === false) { + return false; + } - return $resource ? rtrim($uri->rootUrl($domain), '/') . '/' . $resource : null; + $domain = $domain ?: $grav['config']->get('system.absolute_urls', false); + + return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? ''); } /** @@ -74,15 +134,18 @@ abstract class Utils * * @param string $haystack * @param string|string[] $needle + * @param bool $case_sensitive * * @return bool */ - public static function startsWith($haystack, $needle) + public static function startsWith($haystack, $needle, $case_sensitive = true) { $status = false; + $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos'; + foreach ((array)$needle as $each_needle) { - $status = $each_needle === '' || strpos($haystack, $each_needle) === 0; + $status = $each_needle === '' || $compare_func($haystack, $each_needle) === 0; if ($status) { break; } @@ -96,15 +159,19 @@ abstract class Utils * * @param string $haystack * @param string|string[] $needle + * @param bool $case_sensitive * * @return bool */ - public static function endsWith($haystack, $needle) + public static function endsWith($haystack, $needle, $case_sensitive = true) { $status = false; + $compare_func = $case_sensitive ? 'mb_strrpos' : 'mb_strripos'; + foreach ((array)$needle as $each_needle) { - $status = $each_needle === '' || substr($haystack, -strlen($each_needle)) === $each_needle; + $expectedPosition = mb_strlen($haystack) - mb_strlen($each_needle); + $status = $each_needle === '' || $compare_func($haystack, $each_needle, 0) === $expectedPosition; if ($status) { break; } @@ -118,15 +185,18 @@ abstract class Utils * * @param string $haystack * @param string|string[] $needle + * @param bool $case_sensitive * * @return bool */ - public static function contains($haystack, $needle) + public static function contains($haystack, $needle, $case_sensitive = true) { $status = false; + $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos'; + foreach ((array)$needle as $each_needle) { - $status = $each_needle === '' || strpos($haystack, $each_needle) !== false; + $status = $each_needle === '' || $compare_func($haystack, $each_needle) !== false; if ($status) { break; } @@ -135,18 +205,45 @@ abstract class Utils return $status; } + /** + * Function that can match wildcards + * + * match_wildcard('foo*', $test), // TRUE + * match_wildcard('bar*', $test), // FALSE + * match_wildcard('*bar*', $test), // TRUE + * match_wildcard('**blob**', $test), // TRUE + * match_wildcard('*a?d*', $test), // TRUE + * match_wildcard('*etc**', $test) // TRUE + * + * @param string $wildcard_pattern + * @param string $haystack + * @return false|int + */ + public static function matchWildcard($wildcard_pattern, $haystack) { + $regex = str_replace( + array("\*", "\?"), // wildcard chars + array('.*','.'), // regexp chars + preg_quote($wildcard_pattern, '/') + ); + + return preg_match('/^'.$regex.'$/is', $haystack); + } + /** * Returns the substring of a string up to a specified needle. if not found, return the whole haystack * - * @param $haystack - * @param $needle + * @param string $haystack + * @param string $needle + * @param bool $case_sensitive * * @return string */ - public static function substrToString($haystack, $needle) + public static function substrToString($haystack, $needle, $case_sensitive = true) { - if (static::contains($haystack, $needle)) { - return substr($haystack, 0, strpos($haystack, $needle)); + $compare_func = $case_sensitive ? 'mb_strpos' : 'mb_stripos'; + + if (static::contains($haystack, $needle, $case_sensitive)) { + return mb_substr($haystack, 0, $compare_func($haystack, $needle, $case_sensitive)); } return $haystack; @@ -155,30 +252,34 @@ abstract class Utils /** * Utility method to replace only the first occurrence in a string * - * @param $search - * @param $replace - * @param $subject - * @return mixed + * @param string $search + * @param string $replace + * @param string $subject + * + * @return string */ public static function replaceFirstOccurrence($search, $replace, $subject) { if (!$search) { return $subject; } - $pos = strpos($subject, $search); + + $pos = mb_strpos($subject, $search); if ($pos !== false) { - $subject = substr_replace($subject, $replace, $pos, strlen($search)); + $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search)); } + + return $subject; } /** * Utility method to replace only the last occurrence in a string * - * @param $search - * @param $replace - * @param $subject - * @return mixed + * @param string $search + * @param string $replace + * @param string $subject + * @return string */ public static function replaceLastOccurrence($search, $replace, $subject) { @@ -186,12 +287,29 @@ abstract class Utils if($pos !== false) { - $subject = substr_replace($subject, $replace, $pos, strlen($search)); + $subject = static::mb_substr_replace($subject, $replace, $pos, mb_strlen($search)); } return $subject; } + /** + * Multibyte compatible substr_replace + * + * @param string $original + * @param string $replacement + * @param int $position + * @param int $length + * @return string + */ + public static function mb_substr_replace($original, $replacement, $position, $length) + { + $startString = mb_substr($original, 0, $position, "UTF-8"); + $endString = mb_substr($original, $position + $length, mb_strlen($original), "UTF-8"); + + return $startString . $replacement . $endString; + } + /** * Merge two objects into one. * @@ -205,12 +323,41 @@ abstract class Utils return (object)array_merge((array)$obj1, (array)$obj2); } + /** + * Lowercase an entire array. Useful when combined with `in_array()` + * + * @param array $a + * @return array|false + */ + public static function arrayLower(Array $a) + { + return array_map('mb_strtolower', $a); + } + + /** + * Simple function to remove item/s in an array by value + * + * @param $search array + * @param $value string|array + * @return array + */ + public static function arrayRemoveValue(Array $search, $value) + { + foreach ((array) $value as $val) { + $key = array_search($val, $search); + if ($key !== false) { + unset($search[$key]); + } + } + return $search; + } + /** * Recursive Merge with uniqueness * - * @param $array1 - * @param $array2 - * @return mixed + * @param array $array1 + * @param array $array2 + * @return array */ public static function arrayMergeRecursiveUnique($array1, $array2) { @@ -229,6 +376,65 @@ abstract class Utils return $array1; } + /** + * Returns an array with the differences between $array1 and $array2 + * + * @param array $array1 + * @param array $array2 + * @return array + */ + public static function arrayDiffMultidimensional($array1, $array2) + { + $result = array(); + foreach ($array1 as $key => $value) { + if (!is_array($array2) || !array_key_exists($key, $array2)) { + $result[$key] = $value; + continue; + } + if (is_array($value)) { + $recursiveArrayDiff = static::ArrayDiffMultidimensional($value, $array2[$key]); + if (count($recursiveArrayDiff)) { + $result[$key] = $recursiveArrayDiff; + } + continue; + } + if ($value != $array2[$key]) { + $result[$key] = $value; + } + } + + return $result; + } + + /** + * Array combine but supports different array lengths + * + * @param array $arr1 + * @param array $arr2 + * @return array|false + */ + public static function arrayCombine($arr1, $arr2) + { + $count = min(count($arr1), count($arr2)); + + return array_combine(array_slice($arr1, 0, $count), array_slice($arr2, 0, $count)); + } + + /** + * Array is associative or not + * + * @param array $arr + * @return bool + */ + public static function arrayIsAssociative($arr) + { + if ([] === $arr) { + return false; + } + + return array_keys($arr) !== range(0, count($arr) - 1); + } + /** * Return the Grav date formats allowed * @@ -236,7 +442,7 @@ abstract class Utils */ public static function dateFormats() { - $now = new DateTime(); + $now = new \DateTime(); $date_formats = [ 'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')', @@ -253,6 +459,24 @@ abstract class Utils return $date_formats; } + /** + * Get current date/time + * + * @param string|null $default_format + * @return string + * @throws \Exception + */ + public static function dateNow($default_format = null) + { + $now = new \DateTime(); + + if (is_null($default_format)) { + $default_format = Grav::instance()['config']->get('system.pages.dateformat.default'); + } + + return $now->format($default_format); + } + /** * Truncate text by number of characters but can cut off words. * @@ -264,7 +488,7 @@ abstract class Utils * * @return string */ - public static function truncate($string, $limit = 150, $up_to_break = false, $break = " ", $pad = "…") + public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '…') { // return with no change if string is shorter than $limit if (mb_strlen($string) <= $limit) { @@ -479,50 +703,24 @@ abstract class Utils } /** - * Return the mimetype based on filename + * Get all the mimetypes for an array of extensions * - * @param string $filename Filename or path to file - * @param string $default default value - * - * @return string + * @param array $extensions + * @return array */ - public static function getMimeByFilename($filename, $default = 'application/octet-stream') + public static function getMimeTypes(array $extensions) { - return static::getMimeByExtension(pathinfo($filename, PATHINFO_EXTENSION), $default); - } - - /** - * Return the mimetype based on existing local file - * - * @param string $filename Path to the file - * - * @return string|bool - */ - public static function getMimeByLocalFile($filename, $default = 'application/octet-stream') - { - $type = false; - - // For local files we can detect type by the file content. - if (!stream_is_local($filename) || !file_exists($filename)) { - return false; - } - - // Prefer using finfo if it exists. - if (\extension_loaded('fileinfo')) { - $finfo = finfo_open(FILEINFO_SYMLINK | FILEINFO_MIME_TYPE); - $type = finfo_file($finfo, $filename); - finfo_close($finfo); - } else { - // Fall back to use getimagesize() if it is available (not recommended, but better than nothing) - $info = @getimagesize($filename); - if ($info) { - $type = $info['mime']; + $mimetypes = []; + foreach ($extensions as $extension) { + $mimetype = static::getMimeByExtension($extension, false); + if ($mimetype && !in_array($mimetype, $mimetypes)) { + $mimetypes[] = $mimetype; } } - - return $type ?: static::getMimeByFilename($filename, $default); + return $mimetypes; } + /** * Return the mimetype based on filename extension * @@ -565,6 +763,71 @@ abstract class Utils return $default; } + /** + * Get all the extensions for an array of mimetypes + * + * @param array $mimetypes + * @return array + */ + public static function getExtensions(array $mimetypes) + { + $extensions = []; + foreach ($mimetypes as $mimetype) { + $extension = static::getExtensionByMime($mimetype, false); + if ($extension && !\in_array($extension, $extensions, true)) { + $extensions[] = $extension; + } + } + + return $extensions; + } + + /** + * Return the mimetype based on filename + * + * @param string $filename Filename or path to file + * @param string $default default value + * + * @return string + */ + public static function getMimeByFilename($filename, $default = 'application/octet-stream') + { + return static::getMimeByExtension(pathinfo($filename, PATHINFO_EXTENSION), $default); + } + + /** + * Return the mimetype based on existing local file + * + * @param string $filename Path to the file + * + * @return string|bool + */ + public static function getMimeByLocalFile($filename, $default = 'application/octet-stream') + { + $type = false; + + // For local files we can detect type by the file content. + if (!stream_is_local($filename) || !file_exists($filename)) { + return false; + } + + // Prefer using finfo if it exists. + if (\extension_loaded('fileinfo')) { + $finfo = finfo_open(FILEINFO_SYMLINK | FILEINFO_MIME_TYPE); + $type = finfo_file($finfo, $filename); + finfo_close($finfo); + } else { + // Fall back to use getimagesize() if it is available (not recommended, but better than nothing) + $info = @getimagesize($filename); + if ($info) { + $type = $info['mime']; + } + } + + return $type ?: static::getMimeByFilename($filename, $default); + } + + /** * Returns true if filename is considered safe. * @@ -601,22 +864,47 @@ abstract class Utils */ public static function normalizePath($path) { - $root = ($path[0] === '/') ? '/' : ''; - - $segments = explode('/', trim($path, '/')); - $ret = []; - foreach ($segments as $segment) { - if (($segment === '.') || $segment === '') { - continue; - } - if ($segment === '..') { - array_pop($ret); - } else { - $ret[] = $segment; - } + // Resolve any streams + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($path)) { + $path = $locator->findResource($path); } - return $root . implode('/', $ret); + // Set root properly for any URLs + $root = ''; + preg_match(self::ROOTURL_REGEX, $path, $matches); + if ($matches) { + $root = $matches[1]; + $path = $matches[2]; + } + + // Strip off leading / to ensure explode is accurate + if (Utils::startsWith($path,'/')) { + $root .= '/'; + $path = ltrim($path, '/'); + } + + // If there are any relative paths (..) handle those + if (Utils::contains($path, '..')) { + $segments = explode('/', trim($path, '/')); + $ret = []; + foreach ($segments as $segment) { + if (($segment === '.') || $segment === '') { + continue; + } + if ($segment === '..') { + array_pop($ret); + } else { + $ret[] = $segment; + } + } + $path = implode('/', $ret); + } + + // Stick everything back together + $normalized = $root . $path; + return $normalized; } /** @@ -628,7 +916,7 @@ abstract class Utils */ public static function isFunctionDisabled($function) { - return in_array($function, explode(',', ini_get('disable_functions')), true); + return \in_array($function, explode(',', ini_get('disable_functions')), true); } /** @@ -640,7 +928,7 @@ abstract class Utils { $timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL); $offsets = []; - $testDate = new \DateTime; + $testDate = new \DateTime(); foreach ($timezones as $zone) { $tz = new \DateTimeZone($zone); @@ -696,7 +984,7 @@ abstract class Utils public static function arrayFlatten($array) { $flatten = array(); - foreach ($array as $key => $inner){ + foreach ($array as $key => $inner) { if (is_array($inner)) { foreach ($inner as $inner_key => $value) { $flatten[$inner_key] = $value; @@ -705,26 +993,75 @@ abstract class Utils $flatten[$key] = $inner; } } + return $flatten; } + /** + * Flatten a multi-dimensional associative array into dot notation + * + * @param array $array + * @param string $prepend + * @return array + */ + public static function arrayFlattenDotNotation($array, $prepend = '') + { + $results = array(); + foreach ($array as $key => $value) { + if (is_array($value)) { + $results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend.$key.'.')); + } else { + $results[$prepend.$key] = $value; + } + } + + return $results; + } + + /** + * Opposite of flatten, convert flat dot notation array to multi dimensional array + * + * @param array $array + * @param string $separator + * @return array + */ + public static function arrayUnflattenDotNotation($array, $separator = '.') + { + $newArray = []; + foreach ($array as $key => $value) { + $dots = explode($separator, $key); + if (\count($dots) > 1) { + $last = &$newArray[$dots[0]]; + foreach ($dots as $k => $dot) { + if ($k === 0) { + continue; + } + $last = &$last[$dot]; + } + $last = $value; + } else { + $newArray[$key] = $value; + } + } + + return $newArray; + } + /** * Checks if the passed path contains the language code prefix * * @param string $string The path * - * @return bool + * @return bool|string Either false or the language + * */ public static function pathPrefixedByLangCode($string) { - if (strlen($string) <= 3) { - return false; - } - $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []); + $parts = explode('/', trim($string, '/')); - if ($string[0] === '/' && $string[3] === '/' && in_array(substr($string, 1, 2), $languages_enabled)) { - return true; + if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) { + return $parts[0]; } return false; @@ -745,9 +1082,9 @@ abstract class Utils // try to use DateTime and default format if ($dateformat) { - $datetime = DateTime::createFromFormat($dateformat, $date); + $datetime = \DateTime::createFromFormat($dateformat, $date); } else { - $datetime = new DateTime($date); + $datetime = new \DateTime($date); } // fallback to strtotime() if DateTime approach failed @@ -764,11 +1101,11 @@ abstract class Utils * @param null $default * @return mixed * - * @deprecated Use getDotNotation() method instead + * @deprecated 1.5 Use ->getDotNotation() method instead. */ public static function resolve(array $array, $path, $default = null) { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDotNotation() method instead', E_USER_DEPRECATED); + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->getDotNotation() method instead', E_USER_DEPRECATED); return static::getDotNotation($array, $path, $default); } @@ -797,12 +1134,9 @@ abstract class Utils */ private static function generateNonceString($action, $previousTick = false) { - $username = ''; - if (isset(Grav::instance()['user'])) { - $user = Grav::instance()['user']; - $username = $user->username; - } + $grav = Grav::instance(); + $username = isset($grav['user']) ? $grav['user']->username : ''; $token = session_id(); $i = self::nonceTick(); @@ -810,7 +1144,7 @@ abstract class Utils $i--; } - return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt')); + return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . $grav['config']->get('security.salt')); } /** @@ -871,12 +1205,8 @@ abstract class Utils //Nonce generated 12-24 hours ago $previousTick = true; - if ($nonce === self::getNonce($action, $previousTick)) { - return true; - } - //Invalid nonce - return false; + return $nonce === self::getNonce($action, $previousTick); } /** @@ -896,8 +1226,8 @@ abstract class Utils /** * Get a portion of an array (passed by reference) with dot-notation key * - * @param $array - * @param $key + * @param array $array + * @param string|int $key * @param null $default * @return mixed */ @@ -926,10 +1256,10 @@ abstract class Utils * Set portion of array (passed by reference) for a dot-notation key * and set the value * - * @param $array - * @param $key - * @param $value - * @param bool $merge + * @param array $array + * @param string|int $key + * @param mixed $value + * @param bool $merge * * @return mixed */ @@ -960,7 +1290,6 @@ abstract class Utils $array[$key] = array_merge($array[$key], $value); } - return $array; } @@ -1005,13 +1334,13 @@ abstract class Utils /** * Sort an array by a key value in the array * - * @param $array - * @param $array_key + * @param mixed $array + * @param string|int $array_key * @param int $direction * @param int $sort_flags * @return array */ - public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR ) + public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR) { $output = []; @@ -1029,14 +1358,14 @@ abstract class Utils } /** - * Get's path based on a token + * Get path based on a token * - * @param $path - * @param Page|null $page + * @param string $path + * @param PageInterface|null $page * @return string * @throws \RuntimeException */ - public static function getPagePathFromToken($path, $page = null) + public static function getPagePathFromToken($path, PageInterface $page = null) { $path_parts = pathinfo($path); $grav = Grav::instance(); @@ -1089,6 +1418,8 @@ abstract class Utils $post_max_size = static::parseSize(ini_get('post_max_size')); if ($post_max_size > 0) { $max_size = $post_max_size; + } else { + $max_size = 0; } $upload_max = static::parseSize(ini_get('upload_max_filesize')); @@ -1100,28 +1431,71 @@ abstract class Utils return $max_size; } + /** + * Convert bytes to the unit specified by the $to parameter. + * + * @param int $bytes The filesize in Bytes. + * @param string $to The unit type to convert to. Accepts K, M, or G for Kilobytes, Megabytes, or Gigabytes, respectively. + * @param int $decimal_places The number of decimal places to return. + * + * @return int Returns only the number of units, not the type letter. Returns 0 if the $to unit type is out of scope. + * + */ + public static function convertSize($bytes, $to, $decimal_places = 1) + { + $formulas = array( + 'K' => number_format($bytes / 1024, $decimal_places), + 'M' => number_format($bytes / 1048576, $decimal_places), + 'G' => number_format($bytes / 1073741824, $decimal_places) + ); + return $formulas[$to] ?? 0; + } + + /** + * Return a pretty size based on bytes + * + * @param int $bytes + * @param int $precision + * @return string + */ + public static function prettySize($bytes, $precision = 2) + { + $units = array('B', 'KB', 'MB', 'GB', 'TB'); + + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); + $pow = min($pow, count($units) - 1); + + // Uncomment one of the following alternatives + $bytes /= 1024 ** $pow; + // $bytes /= (1 << (10 * $pow)); + + return round($bytes, $precision) . ' ' . $units[$pow]; + } + /** * Parse a readable file size and return a value in bytes * - * @param $size + * @param string|int $size * @return int */ public static function parseSize($size) { $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); $size = preg_replace('/[^0-9\.]/', '', $size); + if ($unit) { - return (int)($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); + $size = $size * pow(1024, stripos('bkmgtpezy', $unit[0])); } - return (int)$size; + return (int) abs(round($size)); } /** * Multibyte-safe Parse URL function * - * @param $url - * @return mixed + * @param string $url + * @return array * @throws \InvalidArgumentException */ public static function multibyteParseUrl($url) @@ -1146,4 +1520,100 @@ abstract class Utils return $parts; } + + /** + * Process a string as markdown + * + * @param string $string + * + * @param bool $block Block or Line processing + * @param null $page + * @return string + * @throws \Exception + */ + public static function processMarkdown($string, $block = true, $page = null) + { + $grav = Grav::instance(); + $page = $page ?? $grav['page'] ?? null; + $defaults = [ + 'markdown' => $grav['config']->get('system.pages.markdown', []), + 'images' => $grav['config']->get('system.images', []) + ]; + $extra = $defaults['markdown']['extra'] ?? false; + + $excerpts = new Excerpts($page, $defaults); + + // Initialize the preferred variant of Parsedown + if ($extra) { + $parsedown = new ParsedownExtra($excerpts); + } else { + $parsedown = new Parsedown($excerpts); + } + + if ($block) { + $string = $parsedown->text($string); + } else { + $string = $parsedown->line($string); + } + + return $string; + } + + /** + * Find the subnet of an ip with CIDR prefix size + * + * @param string $ip + * @param int $prefix + * + * @return string + */ + public static function getSubnet($ip, $prefix = 64) + { + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + + // Packed representation of IP + $ip = inet_pton($ip); + + // Maximum netmask length = same as packed address + $len = 8*strlen($ip); + if ($prefix > $len) $prefix = $len; + + $mask = str_repeat('f', $prefix>>2); + + switch($prefix & 3) + { + case 3: $mask .= 'e'; break; + case 2: $mask .= 'c'; break; + case 1: $mask .= '8'; break; + } + $mask = str_pad($mask, $len>>2, '0'); + + // Packed representation of netmask + $mask = pack('H*', $mask); + // Bitwise - Take all bits that are both 1 to generate subnet + $subnet = inet_ntop($ip & $mask); + + return $subnet; + } + + /** + * Wrapper to ensure html, htm in the front of the supported page types + * + * @param array|null $defaults + * @return array|mixed + */ + public static function getSupportPageTypes(array $defaults = null) + { + $types = Grav::instance()['config']->get('system.pages.types', $defaults); + + // remove html/htm + $types = static::arrayRemoveValue($types, ['html', 'htm']); + + // put them back at the front + $types = array_merge(['html', 'htm'], $types); + + return $types; + } } diff --git a/system/src/Grav/Common/Yaml.php b/system/src/Grav/Common/Yaml.php index 2c43f73..ad6d81b 100644 --- a/system/src/Grav/Common/Yaml.php +++ b/system/src/Grav/Common/Yaml.php @@ -1,8 +1,9 @@ setName("backup") + ->setName('backup') ->addArgument( - 'destination', + 'id', InputArgument::OPTIONAL, - 'Where to store the backup (/backup is default)' + 'The ID of the backup profile to perform without prompting' ) - ->setDescription("Creates a backup of the Grav instance") - ->setHelp('The backup creates a zipped backup. Optionally can be saved in a different destination.'); + ->setDescription('Creates a backup of the Grav instance') + ->setHelp('The backup creates a zipped backup.'); $this->source = getcwd(); } - /** - * @return int|null|void - */ protected function serve() { $this->progress = new ProgressBar($this->output); - $this->progress->setFormat('Archiving %current% files [%bar%] %elapsed:6s% %memory:6s%'); + $this->progress->setFormat('Archiving %current% files [%bar%] %percent:3s%% %elapsed:6s% %message%'); + $this->progress->setBarWidth(100); Grav::instance()['config']->init(); - $destination = ($this->input->getArgument('destination')) ? $this->input->getArgument('destination') : null; - $log = JsonFile::instance(Grav::instance()['locator']->findResource("log://backup.log", true, true)); - $backup = ZipBackup::backup($destination, [$this, 'output']); + $io = new SymfonyStyle($this->input, $this->output); + $io->title('Grav Backup'); - $log->content([ - 'time' => time(), - 'location' => $backup - ]); - $log->save(); + /** @var Backups $backups */ + $backups = Grav::instance()['backups']; + $backups_list = $backups->getBackupProfiles(); + $backups_names = $backups->getBackupNames(); - $this->output->writeln(''); - $this->output->writeln(''); + $id = null; + $inline_id = $this->input->getArgument('id'); + if (null !== $inline_id && is_numeric($inline_id)) { + $id = $inline_id; + } + + if (null === $id) { + if (\count($backups_list) > 1) { + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion( + 'Choose a backup?', + $backups_names, + 0 + ); + $question->setErrorMessage('Option %s is invalid.'); + $backup_name = $helper->ask($this->input, $this->output, $question); + $id = array_search($backup_name, $backups_names, true); + + $io->newLine(); + $io->note('Selected backup: ' . $backup_name); + } else { + $id = 0; + } + } + + $backup = $backups->backup($id, [$this, 'outputProgress']); + + $io->newline(2); + $io->success('Backup Successfully Created: ' . $backup); } /** - * @param $args + * @param array $args */ - public function output($args) + public function outputProgress($args) { switch ($args['type']) { + case 'count': + $steps = $args['steps']; + $freq = (int)($steps > 100 ? round($steps / 100) : $steps); + $this->progress->setMaxSteps($steps); + $this->progress->setRedrawFrequency($freq); + $this->progress->setMessage('Adding files...'); + break; case 'message': - $this->output->writeln($args['message']); + $this->progress->setMessage($args['message']); + $this->progress->display(); break; case 'progress': - if ($args['complete']) { + if (isset($args['complete']) && $args['complete']) { $this->progress->finish(); } else { $this->progress->advance(); diff --git a/system/src/Grav/Console/Cli/CleanCommand.php b/system/src/Grav/Console/Cli/CleanCommand.php index 32e2b75..a905973 100644 --- a/system/src/Grav/Console/Cli/CleanCommand.php +++ b/system/src/Grav/Console/Cli/CleanCommand.php @@ -1,8 +1,9 @@ setName("clean") - ->setDescription("Handles cleaning chores for Grav distribution") + ->setName('clean') + ->setDescription('Handles cleaning chores for Grav distribution') ->setHelp('The clean clean extraneous folders and data'); } /** * @param InputInterface $input * @param OutputInterface $output - * - * @return int|null|void */ protected function execute(InputInterface $input, OutputInterface $output) { diff --git a/system/src/Grav/Console/Cli/ClearCacheCommand.php b/system/src/Grav/Console/Cli/ClearCacheCommand.php index cb5ffe8..8ef6f4f 100644 --- a/system/src/Grav/Console/Cli/ClearCacheCommand.php +++ b/system/src/Grav/Console/Cli/ClearCacheCommand.php @@ -1,8 +1,9 @@ setName('clear-cache') - ->setAliases(['clearcache']) + ->setName('cache') + ->setAliases(['clearcache', 'cache-clear']) ->setDescription('Clears Grav cache') + ->addOption('invalidate', null, InputOption::VALUE_NONE, 'Invalidate cache, but do not remove any files') + ->addOption('purge', null, InputOption::VALUE_NONE, 'If set purge old caches') ->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches') ->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*') ->addOption('images-only', null, InputOption::VALUE_NONE, 'If set will remove only images/*') ->addOption('cache-only', null, InputOption::VALUE_NONE, 'If set will remove only cache/*') ->addOption('tmp-only', null, InputOption::VALUE_NONE, 'If set will remove only tmp/*') - ->setHelp('The clear-cache deletes all cache files'); + + ->setHelp('The cache command allows you to interact with Grav cache'); } - /** - * @return int|null|void - */ protected function serve() { $this->cleanPaths(); @@ -45,25 +43,37 @@ class ClearCacheCommand extends ConsoleCommand private function cleanPaths() { $this->output->writeln(''); - $this->output->writeln('Clearing cache'); - $this->output->writeln(''); - if ($this->input->getOption('all')) { - $remove = 'all'; - } elseif ($this->input->getOption('assets-only')) { - $remove = 'assets-only'; - } elseif ($this->input->getOption('images-only')) { - $remove = 'images-only'; - } elseif ($this->input->getOption('cache-only')) { - $remove = 'cache-only'; - } elseif ($this->input->getOption('tmp-only')) { - $remove = 'tmp-only'; + + if ($this->input->getOption('purge')) { + $this->output->writeln('Purging old cache'); + $this->output->writeln(''); + + $msg = Cache::purgeJob(); + $this->output->writeln($msg); } else { - $remove = 'standard'; - } + $this->output->writeln('Clearing cache'); + $this->output->writeln(''); - foreach (Cache::clearCache($remove) as $result) { - $this->output->writeln($result); + if ($this->input->getOption('all')) { + $remove = 'all'; + } elseif ($this->input->getOption('assets-only')) { + $remove = 'assets-only'; + } elseif ($this->input->getOption('images-only')) { + $remove = 'images-only'; + } elseif ($this->input->getOption('cache-only')) { + $remove = 'cache-only'; + } elseif ($this->input->getOption('tmp-only')) { + $remove = 'tmp-only'; + } elseif ($this->input->getOption('invalidate')) { + $remove = 'invalidate'; + } else { + $remove = 'standard'; + } + + foreach (Cache::clearCache($remove) as $result) { + $this->output->writeln($result); + } } } } diff --git a/system/src/Grav/Console/Cli/ComposerCommand.php b/system/src/Grav/Console/Cli/ComposerCommand.php index 4f9c18f..ffb4e99 100644 --- a/system/src/Grav/Console/Cli/ComposerCommand.php +++ b/system/src/Grav/Console/Cli/ComposerCommand.php @@ -1,8 +1,9 @@ setHelp('The composer command updates the composer vendor dependencies needed by Grav'); } - /** - * @return int|null|void - */ protected function serve() { $action = $this->input->getOption('install') ? 'install' : ($this->input->getOption('update') ? 'update' : 'install'); diff --git a/system/src/Grav/Console/Cli/InstallCommand.php b/system/src/Grav/Console/Cli/InstallCommand.php index 198e1d3..05f281e 100644 --- a/system/src/Grav/Console/Cli/InstallCommand.php +++ b/system/src/Grav/Console/Cli/InstallCommand.php @@ -1,8 +1,9 @@ setName("install") + ->setName('install') ->addOption( 'symlink', 's', @@ -50,17 +43,14 @@ class InstallCommand extends ConsoleCommand InputArgument::OPTIONAL, 'Where to install the required bits (default to current project)' ) - ->setDescription("Installs the dependencies needed by Grav. Optionally can create symbolic links") + ->setDescription('Installs the dependencies needed by Grav. Optionally can create symbolic links') ->setHelp('The install command installs the dependencies needed by Grav. Optionally can create symbolic links'); } - /** - * @return int|null|void - */ protected function serve() { $dependencies_file = '.dependencies'; - $this->destination = ($this->input->getArgument('destination')) ? $this->input->getArgument('destination') : ROOT_DIR; + $this->destination = $this->input->getArgument('destination') ?: ROOT_DIR; // fix trailing slash $this->destination = rtrim($this->destination, DS) . DS; diff --git a/system/src/Grav/Console/Cli/LogViewerCommand.php b/system/src/Grav/Console/Cli/LogViewerCommand.php new file mode 100644 index 0000000..f9e93a3 --- /dev/null +++ b/system/src/Grav/Console/Cli/LogViewerCommand.php @@ -0,0 +1,85 @@ +setName('logviewer') + ->addOption( + 'file', + 'f', + InputOption::VALUE_OPTIONAL, + 'custom log file location (default = grav.log)' + ) + ->addOption( + 'lines', + 'l', + InputOption::VALUE_OPTIONAL, + 'number of lines (default = 10)' + ) + ->setDescription('Display the last few entries of Grav log') + ->setHelp("Display the last few entries of Grav log"); + } + + protected function serve() + { + $grav = Grav::instance(); + $grav->setup(); + + $file = $this->input->getOption('file') ?? 'grav.log'; + $lines = $this->input->getOption('lines') ?? 20; + $verbose = $this->input->getOption('verbose', false); + + $io = new SymfonyStyle($this->input, $this->output); + + $io->title('Log Viewer'); + + $io->writeln(sprintf('viewing last %s entries in %s', $lines, $file)); + $io->newLine(); + + $viewer = new LogViewer(); + + $logfile = $grav['locator']->findResource("log://" . $file); + + if ($logfile) { + $rows = $viewer->objectTail($logfile, $lines, true); + foreach ($rows as $log) { + $date = $log['date']; + $level_color = LogViewer::levelColor($log['level']); + + if ($date instanceof \DateTime) { + $output = "{$log['date']->format('Y-m-d h:i:s')} [<{$level_color}>{$log['level']}]"; + if ($log['trace'] && $verbose) { + $output .= " {$log['message']}\n"; + foreach ((array) $log['trace'] as $index => $tracerow) { + $output .= "{$index}${tracerow}\n"; + } + } else { + $output .= " {$log['message']}"; + } + $io->writeln($output); + } + } + } else { + $io->error('cannot find the log file: logs/' . $file); + } + + } +} diff --git a/system/src/Grav/Console/Cli/NewProjectCommand.php b/system/src/Grav/Console/Cli/NewProjectCommand.php index 5a8f3d1..90583f0 100644 --- a/system/src/Grav/Console/Cli/NewProjectCommand.php +++ b/system/src/Grav/Console/Cli/NewProjectCommand.php @@ -1,8 +1,9 @@ setHelp("The new-project command is a combination of the `setup` and `install` commands.\nCreates a new Grav instance and performs the installation of all the required dependencies."); } - /** - * @return int|null|void - */ protected function serve() { $sandboxCommand = $this->getApplication()->find('sandbox'); @@ -60,6 +55,5 @@ class NewProjectCommand extends ConsoleCommand $sandboxCommand->run($sandboxArguments, $this->output); $installCommand->run($installArguments, $this->output); - } } diff --git a/system/src/Grav/Console/Cli/SandboxCommand.php b/system/src/Grav/Console/Cli/SandboxCommand.php index 284878e..f961412 100644 --- a/system/src/Grav/Console/Cli/SandboxCommand.php +++ b/system/src/Grav/Console/Cli/SandboxCommand.php @@ -1,8 +1,9 @@ '/.gitignore', + '/.editorconfig' => '/.editorconfig', '/CHANGELOG.md' => '/CHANGELOG.md', '/LICENSE.txt' => '/LICENSE.txt', '/README.md' => '/README.md', @@ -60,18 +56,12 @@ class SandboxCommand extends ConsoleCommand '/webserver-configs' => '/webserver-configs', ]; - /** - * @var string - */ - + /** @var string */ protected $default_file = "---\ntitle: HomePage\n---\n# HomePage\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque porttitor eu felis sed ornare. Sed a mauris venenatis, pulvinar velit vel, dictum enim. Phasellus ac rutrum velit. Nunc lorem purus, hendrerit sit amet augue aliquet, iaculis ultricies nisl. Suspendisse tincidunt euismod risus, quis feugiat arcu tincidunt eget. Nulla eros mi, commodo vel ipsum vel, aliquet congue odio. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque velit orci, laoreet at adipiscing eu, interdum quis nibh. Nunc a accumsan purus."; protected $source; protected $destination; - /** - * - */ protected function configure() { $this @@ -92,9 +82,6 @@ class SandboxCommand extends ConsoleCommand $this->source = getcwd(); } - /** - * @return int|null|void - */ protected function serve() { $this->destination = $this->input->getArgument('destination'); @@ -121,9 +108,6 @@ class SandboxCommand extends ConsoleCommand $this->perms(); } - /** - * - */ private function createDirectories() { $this->output->writeln(''); @@ -131,14 +115,14 @@ class SandboxCommand extends ConsoleCommand $dirs_created = false; if (!file_exists($this->destination)) { - mkdir($this->destination, 0777, true); + Folder::create($this->destination); } foreach ($this->directories as $dir) { if (!file_exists($this->destination . $dir)) { $dirs_created = true; $this->output->writeln(' ' . $dir . ''); - mkdir($this->destination . $dir, 0777, true); + Folder::create($this->destination . $dir); } } @@ -147,9 +131,6 @@ class SandboxCommand extends ConsoleCommand } } - /** - * - */ private function copy() { $this->output->writeln(''); @@ -157,7 +138,7 @@ class SandboxCommand extends ConsoleCommand foreach ($this->mappings as $source => $target) { - if ((int)$source == $source) { + if ((string)(int)$source === (string)$source) { $source = $target; } @@ -169,9 +150,6 @@ class SandboxCommand extends ConsoleCommand } } - /** - * - */ private function symlink() { $this->output->writeln(''); @@ -179,7 +157,7 @@ class SandboxCommand extends ConsoleCommand foreach ($this->mappings as $source => $target) { - if ((int)$source == $source) { + if ((string)(int)$source === (string)$source) { $source = $target; } @@ -197,9 +175,6 @@ class SandboxCommand extends ConsoleCommand } } - /** - * - */ private function initFiles() { $this->check(); @@ -210,7 +185,7 @@ class SandboxCommand extends ConsoleCommand // Copy files if they do not exist foreach ($this->files as $source => $target) { - if ((int)$source == $source) { + if ((string)(int)$source === (string)$source) { $source = $target; } @@ -229,9 +204,6 @@ class SandboxCommand extends ConsoleCommand } } - /** - * - */ private function pages() { $this->output->writeln(''); @@ -241,7 +213,7 @@ class SandboxCommand extends ConsoleCommand $pages_dir = $this->destination . '/user/pages'; $pages_files = array_diff(scandir($pages_dir), ['..', '.']); - if (count($pages_files) == 0) { + if (\count($pages_files) === 0) { $destination = $this->source . '/user/pages'; Folder::rcopy($destination, $pages_dir); $this->output->writeln(' ' . $destination . ' -> Created'); @@ -249,9 +221,6 @@ class SandboxCommand extends ConsoleCommand } } - /** - * - */ private function perms() { $this->output->writeln(''); @@ -269,9 +238,6 @@ class SandboxCommand extends ConsoleCommand $this->output->writeln(""); } - /** - * - */ private function check() { $success = true; diff --git a/system/src/Grav/Console/Cli/SchedulerCommand.php b/system/src/Grav/Console/Cli/SchedulerCommand.php new file mode 100644 index 0000000..eaf45cd --- /dev/null +++ b/system/src/Grav/Console/Cli/SchedulerCommand.php @@ -0,0 +1,179 @@ +setName('scheduler') + ->addOption( + 'install', + 'i', + InputOption::VALUE_NONE, + 'Show Install Command' + ) + ->addOption( + 'jobs', + 'j', + InputOption::VALUE_NONE, + 'Show Jobs Summary' + ) + ->addOption( + 'details', + 'd', + InputOption::VALUE_NONE, + 'Show Job Details' + ) + ->setDescription('Run the Grav Scheduler. Best when integrated with system cron') + ->setHelp("Running without any options will force the Scheduler to run through it's jobs and process them"); + } + + protected function serve() + { +// error_reporting(1); + $grav = Grav::instance(); + $grav->setup(); + + $grav['uri']->init(); + $grav['config']->init(); + $grav['plugins']->init(); + $grav['themes']->init(); + $grav['backups']->init(); + + // Initialize Plugins + $grav->fireEvent('onPluginsInitialized'); + + /** @var Scheduler $scheduler */ + $scheduler = $grav['scheduler']; + $grav->fireEvent('onSchedulerInitialized', new Event(['scheduler' => $scheduler])); + + $this->setHelp('foo'); + + $io = new SymfonyStyle($this->input, $this->output); + + if ($this->input->getOption('jobs')) { + // Show jobs list + + $jobs = $scheduler->getAllJobs(); + $job_states = $scheduler->getJobStates()->content(); + $rows = []; + + $table = new Table($this->output); + $table->setStyle('box'); + $headers = ['Job ID', 'Command', 'Run At', 'Status', 'Last Run', 'State']; + + $io->title('Scheduler Jobs Listing'); + + foreach ($jobs as $job) { + $job_status = ucfirst($job_states[$job->getId()]['state'] ?? 'ready'); + $last_run = $job_states[$job->getId()]['last-run'] ?? 0; + $status = $job_status === 'Failure' ? "{$job_status}" : "{$job_status}"; + $state = $job->getEnabled() ? 'Enabled' : 'Disabled'; + $row = [ + $job->getId(), + "{$job->getCommand()}", + "{$job->getAt()}", + $status, + '' . ($last_run === 0 ? 'Never' : date('Y-m-d H:i', $last_run)) . '', + $state, + + ]; + $rows[] = $row; + } + + if (!empty($rows)) { + $table->setHeaders($headers); + $table->setRows($rows); + $table->render(); + } else { + $io->text('no jobs found...'); + } + + $io->newLine(); + $io->note('For error details run "bin/grav scheduler -d"'); + $io->newLine(); + } elseif ($this->input->getOption('details')) { + $jobs = $scheduler->getAllJobs(); + $job_states = $scheduler->getJobStates()->content(); + + $io->title('Job Details'); + + $table = new Table($this->output); + $table->setStyle('box'); + $table->setHeaders(['Job ID', 'Last Run', 'Next Run', 'Errors']); + $rows = []; + + foreach ($jobs as $job) { + $job_state = $job_states[$job->getId()]; + $error = isset($job_state['error']) ? trim($job_state['error']) : false; + + /** @var CronExpression $expression */ + $expression = $job->getCronExpression(); + $next_run = $expression->getNextRunDate(); + + $row = []; + $row[] = $job->getId(); + if (!is_null($job_state['last-run'])) { + $row[] = '' . date('Y-m-d H:i', $job_state['last-run']) . ''; + } else { + $row[] = 'Never'; + } + $row[] = '' . $next_run->format('Y-m-d H:i') . ''; + + if ($error) { + $row[] = "{$error}"; + } else { + $row[] = 'None'; + } + $rows[] = $row; + } + + $table->setRows($rows); + $table->render(); + + } elseif ($this->input->getOption('install')) { + $io->title('Install Scheduler'); + + if ($scheduler->isCrontabSetup()) { + $io->success('All Ready! You have already set up Grav\'s Scheduler in your crontab'); + } else { + $io->error('You still need to set up Grav\'s Scheduler in your crontab'); + } + if (!Utils::isWindows()) { + $io->note('To install, run the following command from your terminal:'); + $io->newLine(); + $io->text(trim($scheduler->getCronCommand())); + } else { + $io->note('To install, create a scheduled task in Windows.'); + $io->text('Learn more at https://learn.getgrav.org/advanced/scheduler'); + } + } else { + // Run scheduler + $scheduler->run(); + + if ($this->input->getOption('verbose')) { + $io->title('Running Scheduled Jobs'); + $io->text($scheduler->getVerboseOutput()); + } + } + } +} diff --git a/system/src/Grav/Console/Cli/SecurityCommand.php b/system/src/Grav/Console/Cli/SecurityCommand.php index 3361d44..7f73086 100644 --- a/system/src/Grav/Console/Cli/SecurityCommand.php +++ b/system/src/Grav/Console/Cli/SecurityCommand.php @@ -1,8 +1,9 @@ setName("security") - ->setDescription("Capable of running various Security checks") + ->setName('security') + ->setDescription('Capable of running various Security checks') ->setHelp('The security runs various security checks on your Grav site'); $this->source = getcwd(); } - /** - * @return int|null|void - */ protected function serve() { - - /** @var Grav $grav */ $grav = Grav::instance(); + $grav->setup(); $grav['uri']->init(); $grav['config']->init(); $grav['debugger']->enabled(false); - $grav['streams']; $grav['plugins']->init(); $grav['themes']->init(); - $grav['twig']->init(); $grav['pages']->init(); - $this->progress = new ProgressBar($this->output, (count($grav['pages']->routes()) - 1)); + $this->progress = new ProgressBar($this->output, \count($grav['pages']->routes()) - 1); $this->progress->setFormat('Scanning %current% pages [%bar%] %percent:3s%% %elapsed:6s%'); $this->progress->setBarWidth(100); $io = new SymfonyStyle($this->input, $this->output); $io->title('Grav Security Check'); - $output = Security::detectXssFromPages($grav['pages'], [$this, 'outputProgress']); + $output = Security::detectXssFromPages($grav['pages'], false, [$this, 'outputProgress']); $io->newline(2); @@ -77,7 +70,7 @@ class SecurityCommand extends ConsoleCommand $io->writeln($counter++ .' - ' . $route . '' . implode(', ', $results_parts) . ''); } - $io->error('Security Scan complete: ' . count($output) . ' potential XSS issues found...'); + $io->error('Security Scan complete: ' . \count($output) . ' potential XSS issues found...'); } else { $io->success('Security Scan complete: No issues found...'); @@ -88,14 +81,14 @@ class SecurityCommand extends ConsoleCommand } /** - * @param $args + * @param array $args */ public function outputProgress($args) { switch ($args['type']) { case 'count': $steps = $args['steps']; - $freq = intval($steps > 100 ? round($steps / 100) : $steps); + $freq = (int)($steps > 100 ? round($steps / 100) : $steps); $this->progress->setMaxSteps($steps); $this->progress->setRedrawFrequency($freq); break; diff --git a/system/src/Grav/Console/Cli/YamlLinterCommand.php b/system/src/Grav/Console/Cli/YamlLinterCommand.php new file mode 100644 index 0000000..894f1c0 --- /dev/null +++ b/system/src/Grav/Console/Cli/YamlLinterCommand.php @@ -0,0 +1,82 @@ +setName('yamllinter') + ->addOption( + 'env', + 'e', + InputOption::VALUE_OPTIONAL, + 'The environment to trigger a specific configuration. For example: localhost, mysite.dev, www.mysite.com' + ) + ->setDescription('Checks various files for YAML errors') + ->setHelp("Checks various files for YAML errors"); + } + + protected function serve() + { + $grav = Grav::instance(); + $grav->setup(); + + $io = new SymfonyStyle($this->input, $this->output); + + $io->title('Yaml Linter'); + + $io->section('User Configuration'); + $errors = YamlLinter::lintConfig(); + + if (empty($errors)) { + $io->success('No YAML Linting issues with configuration'); + } else { + $this->displayErrors($errors, $io); + } + + + $io->section('Pages Frontmatter'); + $errors = YamlLinter::lintPages(); + + if (empty($errors)) { + $io->success('No YAML Linting issues with pages'); + } else { + $this->displayErrors($errors, $io); + } + + $io->section('Page Blueprints'); + $errors = YamlLinter::lintBlueprints(); + + if (empty($errors)) { + $io->success('No YAML Linting issues with blueprints'); + } else { + $this->displayErrors($errors, $io); + } + + } + + protected function displayErrors($errors, $io) + { + $io->error("YAML Linting issues found..."); + foreach ($errors as $path => $error) { + $io->writeln("$path - $error"); + } + } +} diff --git a/system/src/Grav/Console/ConsoleCommand.php b/system/src/Grav/Console/ConsoleCommand.php index a9c2217..2c63eaf 100644 --- a/system/src/Grav/Console/ConsoleCommand.php +++ b/system/src/Grav/Console/ConsoleCommand.php @@ -1,8 +1,9 @@ output->writeln(''); - $this->output->writeln("ERROR: Destination chosen to install does not appear to be a Grav instance:"); + $this->output->writeln('ERROR: Destination chosen to install does not appear to be a Grav instance:'); $this->output->writeln(" $path"); $this->output->writeln(''); exit; @@ -112,6 +112,11 @@ trait ConsoleTrait return $command->run($input, $this->output); } + public function invalidateCache() + { + Cache::invalidateCache(); + } + /** * Load the local config file * diff --git a/system/src/Grav/Console/Gpm/DirectInstallCommand.php b/system/src/Grav/Console/Gpm/DirectInstallCommand.php index 60c5fa8..bd27651 100644 --- a/system/src/Grav/Console/Gpm/DirectInstallCommand.php +++ b/system/src/Grav/Console/Gpm/DirectInstallCommand.php @@ -1,13 +1,15 @@ setName("direct-install") + ->setName('direct-install') ->setAliases(['directinstall']) ->addArgument( 'package-file', @@ -47,7 +51,7 @@ class DirectInstallCommand extends ConsoleCommand 'The destination where the package should be installed at. By default this would be where the grav instance has been launched from', GRAV_ROOT ) - ->setDescription("Installs Grav, plugin, or theme directly from a file or a URL") + ->setDescription('Installs Grav, plugin, or theme directly from a file or a URL') ->setHelp('The direct-install command installs Grav, plugin, or theme directly from a file or a URL'); } @@ -63,7 +67,7 @@ class DirectInstallCommand extends ConsoleCommand !Installer::isGravInstance($this->destination) || !Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK]) ) { - $this->output->writeln("ERROR: " . Installer::lastErrorMsg()); + $this->output->writeln('ERROR: ' . Installer::lastErrorMsg()); exit; } @@ -73,12 +77,12 @@ class DirectInstallCommand extends ConsoleCommand $package_file = $this->input->getArgument('package-file'); $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion('Are you sure you want to direct-install '.$package_file.' [y|N] ', false); + $question = new ConfirmationQuestion("Are you sure you want to direct-install {$package_file} [y|N] ", false); $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question); if (!$answer) { - $this->output->writeln("exiting..."); + $this->output->writeln('exiting...'); $this->output->writeln(''); exit; } @@ -86,32 +90,32 @@ class DirectInstallCommand extends ConsoleCommand $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); $tmp_zip = $tmp_dir . '/Grav-' . uniqid(); - $this->output->writeln(""); - $this->output->writeln("Preparing to install " . $package_file . ""); + $this->output->writeln(''); + $this->output->writeln("Preparing to install {$package_file}"); if (Response::isRemote($package_file)) { - $this->output->write(" |- Downloading package... 0%"); + $this->output->write(' |- Downloading package... 0%'); try { $zip = GPM::downloadPackage($package_file, $tmp_zip); } catch (\RuntimeException $e) { $this->output->writeln(''); - $this->output->writeln(" `- ERROR: " . $e->getMessage() . ""); + $this->output->writeln(" `- ERROR: {$e->getMessage()}"); $this->output->writeln(''); exit; } if ($zip) { $this->output->write("\x0D"); - $this->output->write(" |- Downloading package... 100%"); + $this->output->write(' |- Downloading package... 100%'); $this->output->writeln(''); } } else { - $this->output->write(" |- Copying package... 0%"); + $this->output->write(' |- Copying package... 0%'); $zip = GPM::copyPackage($package_file, $tmp_zip); if ($zip) { $this->output->write("\x0D"); - $this->output->write(" |- Copying package... 100%"); + $this->output->write(' |- Copying package... 100%'); $this->output->writeln(''); } } @@ -119,19 +123,19 @@ class DirectInstallCommand extends ConsoleCommand if (file_exists($zip)) { $tmp_source = $tmp_dir . '/Grav-' . uniqid(); - $this->output->write(" |- Extracting package... "); + $this->output->write(' |- Extracting package... '); $extracted = Installer::unZip($zip, $tmp_source); if (!$extracted) { $this->output->write("\x0D"); - $this->output->writeln(" |- Extracting package... failed"); + $this->output->writeln(' |- Extracting package... failed'); Folder::delete($tmp_source); Folder::delete($tmp_zip); exit; } $this->output->write("\x0D"); - $this->output->writeln(" |- Extracting package... ok"); + $this->output->writeln(' |- Extracting package... ok'); $type = GPM::getPackageType($extracted); @@ -147,26 +151,26 @@ class DirectInstallCommand extends ConsoleCommand $blueprint = GPM::getBlueprints($extracted); if ($blueprint) { if (isset($blueprint['dependencies'])) { - $depencencies = []; + $dependencies = []; foreach ($blueprint['dependencies'] as $dependency) { if (is_array($dependency)){ if (isset($dependency['name'])) { - $depencencies[] = $dependency['name']; + $dependencies[] = $dependency['name']; } if (isset($dependency['github'])) { - $depencencies[] = $dependency['github']; + $dependencies[] = $dependency['github']; } } else { - $depencencies[] = $dependency; + $dependencies[] = $dependency; } } - $this->output->writeln(" |- Dependencies found... [" . implode(',', $depencencies) . "]"); + $this->output->writeln(' |- Dependencies found... [' . implode(',', $dependencies) . ']'); $question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false); $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question); if (!$answer) { - $this->output->writeln("exiting..."); + $this->output->writeln('exiting...'); $this->output->writeln(''); Folder::delete($tmp_source); Folder::delete($tmp_zip); @@ -175,14 +179,14 @@ class DirectInstallCommand extends ConsoleCommand } } - if ($type == 'grav') { + if ($type === 'grav') { - $this->output->write(" |- Checking destination... "); + $this->output->write(' |- Checking destination... '); Installer::isValidDestination(GRAV_ROOT . '/system'); if (Installer::IS_LINK === Installer::lastErrorCode()) { $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... symbolic link"); - $this->output->writeln(" '- ERROR: symlinks found... " . GRAV_ROOT.""); + $this->output->writeln(' |- Checking destination... symbolic link'); + $this->output->writeln(" '- ERROR: symlinks found... " . GRAV_ROOT . ''); $this->output->writeln(''); Folder::delete($tmp_source); Folder::delete($tmp_zip); @@ -190,15 +194,16 @@ class DirectInstallCommand extends ConsoleCommand } $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... ok"); + $this->output->writeln(' |- Checking destination... ok'); - $this->output->write(" |- Installing package... "); - Installer::install($zip, GRAV_ROOT, ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true], $extracted); + $this->output->write(' |- Installing package... '); + + static::upgradeGrav($zip, $extracted); } else { $name = GPM::getPackageName($extracted); if (!$name) { - $this->output->writeln("ERROR: Name could not be determined. Please specify with --name|-n"); + $this->output->writeln('ERROR: Name could not be determined. Please specify with --name|-n'); $this->output->writeln(''); Folder::delete($tmp_source); Folder::delete($tmp_zip); @@ -208,31 +213,31 @@ class DirectInstallCommand extends ConsoleCommand $install_path = GPM::getInstallPath($type, $name); $is_update = file_exists($install_path); - $this->output->write(" |- Checking destination... "); + $this->output->write(' |- Checking destination... '); Installer::isValidDestination(GRAV_ROOT . DS . $install_path); if (Installer::lastErrorCode() == Installer::IS_LINK) { $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... symbolic link"); + $this->output->writeln(' |- Checking destination... symbolic link'); $this->output->writeln(" '- ERROR: symlink found... " . GRAV_ROOT . DS . $install_path . ''); $this->output->writeln(''); Folder::delete($tmp_source); Folder::delete($tmp_zip); exit; - } else { - $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... ok"); } - $this->output->write(" |- Installing package... "); + $this->output->write("\x0D"); + $this->output->writeln(' |- Checking destination... ok'); + + $this->output->write(' |- Installing package... '); Installer::install( $zip, $this->destination, $options = [ 'install_path' => $install_path, - 'theme' => (($type == 'theme')), + 'theme' => (($type === 'theme')), 'is_update' => $is_update ], $extracted @@ -244,10 +249,10 @@ class DirectInstallCommand extends ConsoleCommand $this->output->write("\x0D"); if(Installer::lastErrorCode()) { - $this->output->writeln(" '- " . Installer::lastErrorMsg() . ""); + $this->output->writeln(" '- " . Installer::lastErrorMsg() . ''); $this->output->writeln(''); } else { - $this->output->writeln(" |- Installing package... ok"); + $this->output->writeln(' |- Installing package... ok'); $this->output->writeln(" '- Success! "); $this->output->writeln(''); } @@ -262,6 +267,43 @@ class DirectInstallCommand extends ConsoleCommand $this->clearCache(); return true; + } + private function upgradeGrav($zip, $folder, $keepFolder = false) + { + static $ignores = [ + 'backup', + 'cache', + 'images', + 'logs', + 'tmp', + 'user', + '.htaccess', + 'robots.txt' + ]; + + if (!is_dir($folder)) { + Installer::setError('Invalid source folder'); + } + + try { + $script = $folder . '/system/install.php'; + /** Install $installer */ + if ((file_exists($script) && $install = include $script) && is_callable($install)) { + $install($zip); + } else { + Installer::install( + $zip, + GRAV_ROOT, + ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $ignores], + $folder, + $keepFolder + ); + + Cache::clearCache(); + } + } catch (\Exception $e) { + Installer::setError($e->getMessage()); + } } } diff --git a/system/src/Grav/Console/Gpm/IndexCommand.php b/system/src/Grav/Console/Gpm/IndexCommand.php index 8c45706..06d662b 100644 --- a/system/src/Grav/Console/Gpm/IndexCommand.php +++ b/system/src/Grav/Console/Gpm/IndexCommand.php @@ -1,14 +1,17 @@ setName("index") + ->setName('index') ->addOption( 'force', 'f', @@ -91,14 +84,11 @@ class IndexCommand extends ConsoleCommand InputOption::VALUE_NONE, 'Reverses the order of the output.' ) - ->setDescription("Lists the plugins and themes available for installation") + ->setDescription('Lists the plugins and themes available for installation') ->setHelp('The index command lists the plugins and themes available for installation') ; } - /** - * @return int|null|void - */ protected function serve() { $this->options = $this->input->getOptions(); @@ -123,7 +113,7 @@ class IndexCommand extends ConsoleCommand } foreach ($data as $type => $packages) { - $this->output->writeln("" . strtoupper($type) . " [ " . count($packages) . " ]"); + $this->output->writeln('' . strtoupper($type) . ' [ ' . \count($packages) . ' ]'); $packages = $this->sort($packages); if (!empty($packages)) { @@ -134,7 +124,7 @@ class IndexCommand extends ConsoleCommand foreach ($packages as $slug => $package) { $row = [ 'Count' => $index++ + 1, - 'Name' => "" . Utils::truncate($package->name, 20, false, ' ', '...') . " ", + 'Name' => '' . Utils::truncate($package->name, 20, false, ' ', '...') . ' ', 'Slug' => $slug, 'Version'=> $this->version($package), 'Installed' => $this->installed($package) @@ -149,55 +139,56 @@ class IndexCommand extends ConsoleCommand } $this->output->writeln('You can either get more informations about a package by typing:'); - $this->output->writeln(' ' . $this->argv . ' info '); + $this->output->writeln(" {$this->argv} info "); $this->output->writeln(''); $this->output->writeln('Or you can install a package by typing:'); - $this->output->writeln(' ' . $this->argv . ' install '); + $this->output->writeln(" {$this->argv} install "); $this->output->writeln(''); } /** - * @param $package + * @param Package $package * * @return string */ private function version($package) { $list = $this->gpm->{'getUpdatable' . ucfirst($package->package_type)}(); - $package = isset($list[$package->slug]) ? $list[$package->slug] : $package; - $type = ucfirst(preg_replace("/s$/", '', $package->package_type)); + $package = $list[$package->slug] ?? $package; + $type = ucfirst(preg_replace('/s$/', '', $package->package_type)); $updatable = $this->gpm->{'is' . $type . 'Updatable'}($package->slug); $installed = $this->gpm->{'is' . $type . 'Installed'}($package->slug); $local = $this->gpm->{'getInstalled' . $type}($package->slug); if (!$installed || !$updatable) { $version = $installed ? $local->version : $package->version; - return "v" . $version . ""; + return "v{$version}"; } if ($updatable) { - return "v" . $package->version . " -> v" . $package->available . ""; + return "v{$package->version} -> v{$package->available}"; } return ''; } /** - * @param $package + * @param Package $package * * @return string */ private function installed($package) { - $package = isset($list[$package->slug]) ? $list[$package->slug] : $package; - $type = ucfirst(preg_replace("/s$/", '', $package->package_type)); - $installed = $this->gpm->{'is' . $type . 'Installed'}($package->slug); + $package = $list[$package->slug] ?? $package; + $type = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $method = 'is' . $type . 'Installed'; + $installed = $this->gpm->{$method}($package->slug); return !$installed ? 'not installed' : 'installed'; } /** - * @param $data + * @param array $data * * @return mixed */ @@ -218,26 +209,28 @@ class IndexCommand extends ConsoleCommand $this->options['desc'] ]; - if (count(array_filter($filter))) { + if (\count(array_filter($filter))) { foreach ($data as $type => $packages) { foreach ($packages as $slug => $package) { $filter = true; // Filtering by string if ($this->options['filter']) { - $filter = preg_grep('/(' . (implode('|', $this->options['filter'])) . ')/i', [$slug, $package->name]); + $filter = preg_grep('/(' . implode('|', $this->options['filter']) . ')/i', [$slug, $package->name]); } // Filtering updatables only - if ($this->options['installed-only'] && $filter) { - $method = ucfirst(preg_replace("/s$/", '', $package->package_type)); - $filter = $this->gpm->{'is' . $method . 'Installed'}($package->slug); + if ($filter && $this->options['installed-only']) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $function = 'is' . $method . 'Installed'; + $filter = $this->gpm->{$function}($package->slug); } // Filtering updatables only - if ($this->options['updates-only'] && $filter) { - $method = ucfirst(preg_replace("/s$/", '', $package->package_type)); - $filter = $this->gpm->{'is' . $method . 'Updatable'}($package->slug); + if ($filter && $this->options['updates-only']) { + $method = ucfirst(preg_replace('/s$/', '', $package->package_type)); + $function = 'is' . $method . 'Updatable'; + $filter = $this->gpm->{$function}($package->slug); } if (!$filter) { @@ -251,7 +244,7 @@ class IndexCommand extends ConsoleCommand } /** - * @param $packages + * @param Packages $packages */ public function sort($packages) { diff --git a/system/src/Grav/Console/Gpm/InfoCommand.php b/system/src/Grav/Console/Gpm/InfoCommand.php index 8c1d50e..2fe1a2e 100644 --- a/system/src/Grav/Console/Gpm/InfoCommand.php +++ b/system/src/Grav/Console/Gpm/InfoCommand.php @@ -1,8 +1,9 @@ setName("info") + ->setName('info') ->addOption( 'force', 'f', @@ -51,7 +50,7 @@ class InfoCommand extends ConsoleCommand InputArgument::REQUIRED, 'The package of which more informations are desired. Use the "index" command for a list of packages' ) - ->setDescription("Shows more informations about a package") + ->setDescription('Shows more informations about a package') ->setHelp('The info shows more informations about a package'); } @@ -69,19 +68,19 @@ class InfoCommand extends ConsoleCommand $foundPackage = $this->gpm->findPackage($this->input->getArgument('package')); if (!$foundPackage) { - $this->output->writeln("The package '" . $this->input->getArgument('package') . "' was not found in the Grav repository."); + $this->output->writeln("The package '{$this->input->getArgument('package')}' was not found in the Grav repository."); $this->output->writeln(''); - $this->output->writeln("You can list all the available packages by typing:"); - $this->output->writeln(" " . $this->argv . " index"); + $this->output->writeln('You can list all the available packages by typing:'); + $this->output->writeln(" {$this->argv} index"); $this->output->writeln(''); exit; } - $this->output->writeln("Found package '" . $this->input->getArgument('package') . "' under the '" . ucfirst($foundPackage->package_type) . "' section"); + $this->output->writeln("Found package '{$this->input->getArgument('package')}' under the '" . ucfirst($foundPackage->package_type) . "' section"); $this->output->writeln(''); - $this->output->writeln("" . $foundPackage->name . " [" . $foundPackage->slug . "]"); - $this->output->writeln(str_repeat('-', strlen($foundPackage->name) + strlen($foundPackage->slug) + 3)); - $this->output->writeln("" . strip_tags($foundPackage->description_plain) . ""); + $this->output->writeln("{$foundPackage->name} [{$foundPackage->slug}]"); + $this->output->writeln(str_repeat('-', \strlen($foundPackage->name) + \strlen($foundPackage->slug) + 3)); + $this->output->writeln('' . strip_tags($foundPackage->description_plain) . ''); $this->output->writeln(''); $packageURL = ''; @@ -89,8 +88,8 @@ class InfoCommand extends ConsoleCommand $packageURL = '<' . $foundPackage->author['url'] . '>'; } - $this->output->writeln("" . str_pad("Author", - 12) . ": " . $foundPackage->author['name'] . ' <' . $foundPackage->author['email'] . '> ' . $packageURL); + $this->output->writeln('' . str_pad('Author', + 12) . ': ' . $foundPackage->author['name'] . ' <' . $foundPackage->author['email'] . '> ' . $packageURL); foreach ([ 'version', @@ -105,21 +104,21 @@ class InfoCommand extends ConsoleCommand 'zipball_url', 'license' ] as $info) { - if (isset($foundPackage->$info)) { + if (isset($foundPackage->{$info})) { $name = ucfirst($info); - $data = $foundPackage->$info; + $data = $foundPackage->{$info}; - if ($info == 'zipball_url') { - $name = "Download"; + if ($info === 'zipball_url') { + $name = 'Download'; } - if ($info == 'date') { - $name = "Last Update"; - $data = date('D, j M Y, H:i:s, P ', strtotime('2014-09-16T00:07:16Z')); + if ($info === 'date') { + $name = 'Last Update'; + $data = date('D, j M Y, H:i:s, P ', strtotime($data)); } $name = str_pad($name, 12); - $this->output->writeln("" . $name . ": " . $data); + $this->output->writeln("{$name}: {$data}"); } } @@ -131,48 +130,48 @@ class InfoCommand extends ConsoleCommand if ($installed && $updatable) { $local = $this->gpm->{'getInstalled'. $type}($foundPackage->slug); $this->output->writeln(''); - $this->output->writeln("Currently installed version: " . $local->version . ""); + $this->output->writeln("Currently installed version: {$local->version}"); $this->output->writeln(''); } // display changelog information $questionHelper = $this->getHelper('question'); - $question = new ConfirmationQuestion("Would you like to read the changelog? [y|N] ", + $question = new ConfirmationQuestion('Would you like to read the changelog? [y|N] ', false); $answer = $this->all_yes ? true : $questionHelper->ask($this->input, $this->output, $question); if ($answer) { $changelog = $foundPackage->changelog; - $this->output->writeln(""); + $this->output->writeln(''); foreach ($changelog as $version => $log) { $title = $version . ' [' . $log['date'] . ']'; $content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', function ($match) { - return "\n" . ucfirst($match[1]) . ":"; + return "\n" . ucfirst($match[1]) . ':'; }, $log['content']); - $this->output->writeln(''.$title.''); - $this->output->writeln(str_repeat('-', strlen($title))); + $this->output->writeln("{$title}"); + $this->output->writeln(str_repeat('-', \strlen($title))); $this->output->writeln($content); - $this->output->writeln(""); + $this->output->writeln(''); - $question = new ConfirmationQuestion("Press [ENTER] to continue or [q] to quit ", true); + $question = new ConfirmationQuestion('Press [ENTER] to continue or [q] to quit ', true); $answer = $this->all_yes ? false : $questionHelper->ask($this->input, $this->output, $question); if (!$answer) { break; } - $this->output->writeln(""); + $this->output->writeln(''); } } $this->output->writeln(''); if ($installed && $updatable) { - $this->output->writeln("You can update this package by typing:"); - $this->output->writeln(" " . $this->argv . " update " . $foundPackage->slug . ""); + $this->output->writeln('You can update this package by typing:'); + $this->output->writeln(" {$this->argv} update {$foundPackage->slug}"); } else { $this->output->writeln("You can install this package by typing:"); - $this->output->writeln(" " . $this->argv . " install " . $foundPackage->slug . ""); + $this->output->writeln(" {$this->argv} install {$foundPackage->slug}"); } $this->output->writeln(''); diff --git a/system/src/Grav/Console/Gpm/InstallCommand.php b/system/src/Grav/Console/Gpm/InstallCommand.php index 0311428..8d544d4 100644 --- a/system/src/Grav/Console/Gpm/InstallCommand.php +++ b/system/src/Grav/Console/Gpm/InstallCommand.php @@ -1,8 +1,9 @@ setName("install") + ->setName('install') ->addOption( 'force', 'f', @@ -82,16 +81,16 @@ class InstallCommand extends ConsoleCommand InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Package(s) to install. Use "bin/gpm index" to list packages. Use "bin/gpm direct-install" to install a specific version' ) - ->setDescription("Performs the installation of plugins and themes") + ->setDescription('Performs the installation of plugins and themes') ->setHelp('The install command allows to install plugins and themes'); } /** * Allows to set the GPM object, used for testing the class * - * @param $gpm + * @param GPM $gpm */ - public function setGpm($gpm) + public function setGpm(GPM $gpm) { $this->gpm = $gpm; } @@ -117,28 +116,26 @@ class InstallCommand extends ConsoleCommand !Installer::isGravInstance($this->destination) || !Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK]) ) { - $this->output->writeln("ERROR: " . Installer::lastErrorMsg()); + $this->output->writeln('ERROR: ' . Installer::lastErrorMsg()); exit; } $this->output->writeln(''); if (!$this->data['total']) { - $this->output->writeln("Nothing to install."); + $this->output->writeln('Nothing to install.'); $this->output->writeln(''); exit; } - if (count($this->data['not_found'])) { - $this->output->writeln("These packages were not found on Grav: " . implode(', ', - array_keys($this->data['not_found'])) . ""); + if (\count($this->data['not_found'])) { + $this->output->writeln('These packages were not found on Grav: ' . implode(', ', + array_keys($this->data['not_found'])) . ''); } - unset($this->data['not_found']); - unset($this->data['total']); + unset($this->data['not_found'], $this->data['total']); - - if (isset($this->local_config)) { + if (null !== $this->local_config) { // Symlinks available, ask if Grav should use them $this->use_symlinks = false; $helper = $this->getHelper('question'); @@ -149,8 +146,6 @@ class InstallCommand extends ConsoleCommand if ($answer) { $this->use_symlinks = true; } - - } $this->output->writeln(''); @@ -160,22 +155,22 @@ class InstallCommand extends ConsoleCommand } catch (\Exception $e) { //Error out if there are incompatible packages requirements and tell which ones, and what to do //Error out if there is any error in parsing the dependencies and their versions, and tell which one is broken - $this->output->writeln("" . $e->getMessage() . ""); + $this->output->writeln("{$e->getMessage()}"); return false; } if ($dependencies) { try { - $this->installDependencies($dependencies, 'install', "The following dependencies need to be installed..."); - $this->installDependencies($dependencies, 'update', "The following dependencies need to be updated..."); + $this->installDependencies($dependencies, 'install', 'The following dependencies need to be installed...'); + $this->installDependencies($dependencies, 'update', 'The following dependencies need to be updated...'); $this->installDependencies($dependencies, 'ignore', "The following dependencies can be updated as there is a newer version, but it's not mandatory...", false); } catch (\Exception $e) { - $this->output->writeln("Installation aborted"); + $this->output->writeln('Installation aborted'); return false; } - $this->output->writeln("Dependencies are OK"); - $this->output->writeln(""); + $this->output->writeln('Dependencies are OK'); + $this->output->writeln(''); } @@ -183,7 +178,7 @@ class InstallCommand extends ConsoleCommand foreach ($this->data as $data) { foreach ($data as $package_name => $package) { if (array_key_exists($package_name, $dependencies)) { - $this->output->writeln("Package " . $package_name . " already installed as dependency"); + $this->output->writeln("Package {$package_name} already installed as dependency"); } else { $is_valid_destination = Installer::isValidDestination($this->destination . DS . $package->install_path); if ($is_valid_destination || Installer::lastErrorCode() == Installer::NOT_FOUND) { @@ -195,24 +190,24 @@ class InstallCommand extends ConsoleCommand $this->askConfirmationIfMajorVersionUpdated($package); $this->gpm->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package->slug, $package->available, array_keys($data)); } catch (\Exception $e) { - $this->output->writeln("" . $e->getMessage() . ""); + $this->output->writeln("{$e->getMessage()}"); return false; } $helper = $this->getHelper('question'); - $question = new ConfirmationQuestion("The package $package_name is already installed, overwrite? [y|N] ", false); + $question = new ConfirmationQuestion("The package {$package_name} is already installed, overwrite? [y|N] ", false); $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question); if ($answer) { $is_update = true; $this->processPackage($package, $is_update); } else { - $this->output->writeln("Package " . $package_name . " not overwritten"); + $this->output->writeln("Package {$package_name} not overwritten"); } } else { if (Installer::lastErrorCode() == Installer::IS_LINK) { - $this->output->writeln("Cannot overwrite existing symlink for $package_name"); - $this->output->writeln(""); + $this->output->writeln("Cannot overwrite existing symlink for {$package_name}"); + $this->output->writeln(''); } } } @@ -220,7 +215,7 @@ class InstallCommand extends ConsoleCommand } } - if (count($this->demo_processing) > 0) { + if (\count($this->demo_processing) > 0) { foreach ($this->demo_processing as $package) { $this->installDemoContent($package); } @@ -235,27 +230,27 @@ class InstallCommand extends ConsoleCommand /** * If the package is updated from an older major release, show warning and ask confirmation * - * @param $package + * @param Package $package */ public function askConfirmationIfMajorVersionUpdated($package) { $helper = $this->getHelper('question'); $package_name = $package->name; - $new_version = $package->available ? $package->available : $this->gpm->getLatestVersionOfPackage($package->slug); + $new_version = $package->available ?: $this->gpm->getLatestVersionOfPackage($package->slug); $old_version = $package->version; $major_version_changed = explode('.', $new_version)[0] !== explode('.', $old_version)[0]; if ($major_version_changed) { if ($this->all_yes) { - $this->output->writeln("The package $package_name will be updated to a new major version $new_version, from $old_version"); + $this->output->writeln("The package {$package_name} will be updated to a new major version {$new_version}, from {$old_version}"); return; } - $question = new ConfirmationQuestion("The package $package_name will be updated to a new major version $new_version, from $old_version. Be sure to read what changed with the new major release. Continue? [y|N] ", false); + $question = new ConfirmationQuestion("The package {$package_name} will be updated to a new major version {$new_version}, from {$old_version}. Be sure to read what changed with the new major release. Continue? [y|N] ", false); if (!$helper->ask($this->input, $this->output, $question)) { - $this->output->writeln("Package " . $package_name . " not updated"); + $this->output->writeln("Package {$package_name} not updated"); exit; } } @@ -276,42 +271,42 @@ class InstallCommand extends ConsoleCommand public function installDependencies($dependencies, $type, $message, $required = true) { $packages = array_filter($dependencies, function ($action) use ($type) { return $action === $type; }); - if (count($packages) > 0) { + if (\count($packages) > 0) { $this->output->writeln($message); foreach ($packages as $dependencyName => $dependencyVersion) { - $this->output->writeln(" |- Package " . $dependencyName . ""); + $this->output->writeln(" |- Package {$dependencyName}"); } - $this->output->writeln(""); + $this->output->writeln(''); $helper = $this->getHelper('question'); - if ($type == 'install') { + if ($type === 'install') { $questionAction = 'Install'; } else { $questionAction = 'Update'; } - if (count($packages) == 1) { + if (\count($packages) === 1) { $questionArticle = 'this'; } else { $questionArticle = 'these'; } - if (count($packages) == 1) { + if (\count($packages) === 1) { $questionNoun = 'package'; } else { $questionNoun = 'packages'; } - $question = new ConfirmationQuestion("$questionAction $questionArticle $questionNoun? [Y|n] ", true); + $question = new ConfirmationQuestion("${questionAction} {$questionArticle} {$questionNoun}? [Y|n] ", true); $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question); if ($answer) { foreach ($packages as $dependencyName => $dependencyVersion) { $package = $this->gpm->findPackage($dependencyName); - $this->processPackage($package, ($type == 'update') ? true : false); + $this->processPackage($package, $type === 'update'); } $this->output->writeln(''); } else { @@ -323,20 +318,20 @@ class InstallCommand extends ConsoleCommand } /** - * @param $package - * @param bool $is_update True if the package is an update + * @param Package $package + * @param bool $is_update True if the package is an update */ private function processPackage($package, $is_update = false) { if (!$package) { - $this->output->writeln("Package not found on the GPM! "); + $this->output->writeln('Package not found on the GPM!'); $this->output->writeln(''); return; } $symlink = false; if ($this->use_symlinks) { - if ($this->getSymlinkSource($package) || !isset($package->version)) { + if (!isset($package->version) || $this->getSymlinkSource($package)) { $symlink = true; } } @@ -349,7 +344,7 @@ class InstallCommand extends ConsoleCommand /** * Add package to the queue to process the demo content, if demo content exists * - * @param $package + * @param Package $package */ private function processDemo($package) { @@ -362,7 +357,7 @@ class InstallCommand extends ConsoleCommand /** * Prompt to install the demo content of a package * - * @param $package + * @param Package $package */ private function installDemoContent($package) { @@ -373,11 +368,11 @@ class InstallCommand extends ConsoleCommand $pages_dir = $dest_dir . DS . 'pages'; // Demo content exists, prompt to install it. - $this->output->writeln("Attention: " . $package->name . " contains demo content"); + $this->output->writeln("Attention: {$package->name} contains demo content"); $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false); - $answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question); + $answer = $helper->ask($this->input, $this->output, $question); if (!$answer) { $this->output->writeln(" '- Skipped! "); @@ -402,15 +397,15 @@ class InstallCommand extends ConsoleCommand // backup current pages folder if (file_exists($dest_dir)) { if (rename($pages_dir, $dest_dir . DS . $pages_backup)) { - $this->output->writeln(" |- Backing up pages... ok"); + $this->output->writeln(' |- Backing up pages... ok'); } else { - $this->output->writeln(" |- Backing up pages... failed"); + $this->output->writeln(' |- Backing up pages... failed'); } } } // Confirmation received, copy over the data - $this->output->writeln(" |- Installing demo content... ok "); + $this->output->writeln(' |- Installing demo content... ok '); Folder::rcopy($demo_dir, $dest_dir); $this->output->writeln(" '- Success! "); $this->output->writeln(''); @@ -418,7 +413,7 @@ class InstallCommand extends ConsoleCommand } /** - * @param $package + * @param Package $package * * @return array|bool */ @@ -436,7 +431,7 @@ class InstallCommand extends ConsoleCommand } /** - * @param $package + * @param Package $package * * @return bool|string */ @@ -465,7 +460,7 @@ class InstallCommand extends ConsoleCommand } /** - * @param $package + * @param Package $package */ private function processSymlink($package) { @@ -475,13 +470,13 @@ class InstallCommand extends ConsoleCommand $to = $this->destination . DS . $package->install_path; $from = $this->getSymlinkSource($package); - $this->output->writeln("Preparing to Symlink " . $package->name . ""); - $this->output->write(" |- Checking source... "); + $this->output->writeln("Preparing to Symlink {$package->name}"); + $this->output->write(' |- Checking source... '); if (file_exists($from)) { - $this->output->writeln("ok"); + $this->output->writeln('ok'); - $this->output->write(" |- Checking destination... "); + $this->output->write(' |- Checking destination... '); $checks = $this->checkDestination($package); if (!$checks) { @@ -495,7 +490,7 @@ class InstallCommand extends ConsoleCommand symlink($from, $to); // extra white spaces to clear out the buffer properly - $this->output->writeln(" |- Symlinking package... ok "); + $this->output->writeln(' |- Symlinking package... ok '); $this->output->writeln(" '- Success! "); $this->output->writeln(''); } @@ -504,13 +499,13 @@ class InstallCommand extends ConsoleCommand return; } - $this->output->writeln("not found!"); + $this->output->writeln('not found!'); $this->output->writeln(" '- Installation failed or aborted."); } /** - * @param $package - * @param bool $is_update + * @param Package $package + * @param bool $is_update * * @return bool */ @@ -519,9 +514,9 @@ class InstallCommand extends ConsoleCommand $version = isset($package->available) ? $package->available : $package->version; $license = Licenses::get($package->slug); - $this->output->writeln("Preparing to install " . $package->name . " [v" . $version . "]"); + $this->output->writeln("Preparing to install {$package->name} [v{$version}]"); - $this->output->write(" |- Downloading package... 0%"); + $this->output->write(' |- Downloading package... 0%'); $this->file = $this->downloadPackage($package, $license); if (!$this->file) { @@ -531,14 +526,14 @@ class InstallCommand extends ConsoleCommand return false; } - $this->output->write(" |- Checking destination... "); + $this->output->write(' |- Checking destination... '); $checks = $this->checkDestination($package); if (!$checks) { $this->output->writeln(" '- Installation failed or aborted."); $this->output->writeln(''); } else { - $this->output->write(" |- Installing package... "); + $this->output->write(' |- Installing package... '); $installation = $this->installPackage($package, $is_update); if (!$installation) { $this->output->writeln(" '- Installation failed or aborted."); @@ -566,10 +561,10 @@ class InstallCommand extends ConsoleCommand $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); $this->tmp = $tmp_dir . '/Grav-' . uniqid(); $filename = $package->slug . basename($package->zipball_url); - $filename = preg_replace('/[\\\\\/:"*?&<>|]+/mi', '-', $filename); + $filename = preg_replace('/[\\\\\/:"*?&<>|]+/m', '-', $filename); $query = ''; - if ($package->premium) { + if (!empty($package->premium)) { $query = \json_encode(array_merge( $package->premium, [ @@ -588,16 +583,16 @@ class InstallCommand extends ConsoleCommand $error = str_replace("\n", "\n | '- ", $e->getMessage()); $this->output->write("\x0D"); // extra white spaces to clear out the buffer properly - $this->output->writeln(" |- Downloading package... error "); + $this->output->writeln(' |- Downloading package... error '); $this->output->writeln(" | '- " . $error); return false; } - Folder::mkdir($this->tmp); + Folder::create($this->tmp); $this->output->write("\x0D"); - $this->output->write(" |- Downloading package... 100%"); + $this->output->write(' |- Downloading package... 100%'); $this->output->writeln(''); file_put_contents($this->tmp . DS . $filename, $output); @@ -606,7 +601,7 @@ class InstallCommand extends ConsoleCommand } /** - * @param $package + * @param Package $package * * @return bool */ @@ -618,7 +613,7 @@ class InstallCommand extends ConsoleCommand if (Installer::lastErrorCode() == Installer::IS_LINK) { $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... symbolic link"); + $this->output->writeln(' |- Checking destination... symbolic link'); if ($this->all_yes) { $this->output->writeln(" | '- Skipped automatically."); @@ -634,13 +629,13 @@ class InstallCommand extends ConsoleCommand $this->output->writeln(" | '- You decided to not delete the symlink automatically."); return false; - } else { - unlink($this->destination . DS . $package->install_path); } + + unlink($this->destination . DS . $package->install_path); } $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... ok"); + $this->output->writeln(' |- Checking destination... ok'); return true; } @@ -657,14 +652,14 @@ class InstallCommand extends ConsoleCommand { $type = $package->package_type; - Installer::install($this->file, $this->destination, ['install_path' => $package->install_path, 'theme' => (($type == 'themes')), 'is_update' => $is_update]); + Installer::install($this->file, $this->destination, ['install_path' => $package->install_path, 'theme' => $type === 'themes', 'is_update' => $is_update]); $error_code = Installer::lastErrorCode(); Folder::delete($this->tmp); if ($error_code) { $this->output->write("\x0D"); // extra white spaces to clear out the buffer properly - $this->output->writeln(" |- Installing package... error "); + $this->output->writeln(' |- Installing package... error '); $this->output->writeln(" | '- " . Installer::lastErrorMsg()); return false; @@ -674,23 +669,23 @@ class InstallCommand extends ConsoleCommand if ($message) { $this->output->write("\x0D"); // extra white spaces to clear out the buffer properly - $this->output->writeln(" |- " . $message); + $this->output->writeln(" |- {$message}"); } $this->output->write("\x0D"); // extra white spaces to clear out the buffer properly - $this->output->writeln(" |- Installing package... ok "); + $this->output->writeln(' |- Installing package... ok '); return true; } /** - * @param $progress + * @param array $progress */ public function progress($progress) { $this->output->write("\x0D"); - $this->output->write(" |- Downloading package... " . str_pad($progress['percent'], 5, " ", + $this->output->write(' |- Downloading package... ' . str_pad($progress['percent'], 5, ' ', STR_PAD_LEFT) . '%'); } } diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php index d406a95..1562f4b 100644 --- a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -1,15 +1,18 @@ setName("self-upgrade") + ->setName('self-upgrade') ->setAliases(['selfupgrade', 'selfupdate']) ->addOption( 'force', @@ -78,13 +69,10 @@ class SelfupgradeCommand extends ConsoleCommand InputOption::VALUE_NONE, 'Option to overwrite packages if they already exist' ) - ->setDescription("Detects and performs an update of Grav itself when available") + ->setDescription('Detects and performs an update of Grav itself when available') ->setHelp('The update command updates Grav itself when a new version is available'); } - /** - * @return int|null|void - */ protected function serve() { $this->upgrader = new Upgrader($this->input->getOption('force')); @@ -100,26 +88,26 @@ class SelfupgradeCommand extends ConsoleCommand $release = strftime('%c', strtotime($this->upgrader->getReleaseDate())); if (!$this->upgrader->meetsRequirements()) { - $this->output->writeln("ATTENTION:"); - $this->output->writeln(" Grav has increased the minimum PHP requirement."); - $this->output->writeln(" You are currently running PHP " . phpversion() . ", but PHP " . $this->upgrader->minPHPVersion() . " is required."); - $this->output->writeln(" Additional information: http://getgrav.org/blog/changing-php-requirements"); - $this->output->writeln(""); - $this->output->writeln("Selfupgrade aborted."); - $this->output->writeln(""); + $this->output->writeln('ATTENTION:'); + $this->output->writeln(' Grav has increased the minimum PHP requirement.'); + $this->output->writeln(' You are currently running PHP ' . phpversion() . ', but PHP ' . $this->upgrader->minPHPVersion() . ' is required.'); + $this->output->writeln(' Additional information: http://getgrav.org/blog/changing-php-requirements'); + $this->output->writeln(''); + $this->output->writeln('Selfupgrade aborted.'); + $this->output->writeln(''); exit; } if (!$this->overwrite && !$this->upgrader->isUpgradable()) { - $this->output->writeln("You are already running the latest version of Grav (v" . $local . ") released on " . $release); + $this->output->writeln("You are already running the latest version of Grav (v{$local}) released on {$release}"); exit; } Installer::isValidDestination(GRAV_ROOT . '/system'); if (Installer::IS_LINK === Installer::lastErrorCode()) { - $this->output->writeln("ATTENTION: Grav is symlinked, cannot upgrade, aborting..."); + $this->output->writeln('ATTENTION: Grav is symlinked, cannot upgrade, aborting...'); $this->output->writeln(''); - $this->output->writeln("You are currently running a symbolically linked Grav v" . $local . ". Latest available is v". $remote . "."); + $this->output->writeln("You are currently running a symbolically linked Grav v{$local}. Latest available is v{$remote}."); exit; } @@ -129,51 +117,51 @@ class SelfupgradeCommand extends ConsoleCommand $questionHelper = $this->getHelper('question'); - $this->output->writeln("Grav v$remote is now available [release date: $release]."); - $this->output->writeln("You are currently using v" . GRAV_VERSION . "."); + $this->output->writeln("Grav v{$remote} is now available [release date: {$release}]."); + $this->output->writeln('You are currently using v' . GRAV_VERSION . '.'); if (!$this->all_yes) { - $question = new ConfirmationQuestion("Would you like to read the changelog before proceeding? [y|N] ", + $question = new ConfirmationQuestion('Would you like to read the changelog before proceeding? [y|N] ', false); $answer = $questionHelper->ask($this->input, $this->output, $question); if ($answer) { $changelog = $this->upgrader->getChangelog(GRAV_VERSION); - $this->output->writeln(""); + $this->output->writeln(''); foreach ($changelog as $version => $log) { $title = $version . ' [' . $log['date'] . ']'; $content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', function ($match) { - return "\n" . ucfirst($match[1]) . ":"; + return "\n" . ucfirst($match[1]) . ':'; }, $log['content']); $this->output->writeln($title); - $this->output->writeln(str_repeat('-', strlen($title))); + $this->output->writeln(str_repeat('-', \strlen($title))); $this->output->writeln($content); - $this->output->writeln(""); + $this->output->writeln(''); } - $question = new ConfirmationQuestion("Press [ENTER] to continue.", true); + $question = new ConfirmationQuestion('Press [ENTER] to continue.', true); $questionHelper->ask($this->input, $this->output, $question); } - $question = new ConfirmationQuestion("Would you like to upgrade now? [y|N] ", false); + $question = new ConfirmationQuestion('Would you like to upgrade now? [y|N] ', false); $answer = $questionHelper->ask($this->input, $this->output, $question); if (!$answer) { - $this->output->writeln("Aborting..."); + $this->output->writeln('Aborting...'); exit; } } - $this->output->writeln(""); - $this->output->writeln("Preparing to upgrade to v$remote.."); + $this->output->writeln(''); + $this->output->writeln("Preparing to upgrade to v{$remote}.."); - $this->output->write(" |- Downloading upgrade [" . $this->formatBytes($update['size']) . "]... 0%"); + $this->output->write(" |- Downloading upgrade [{$this->formatBytes($update['size'])}]... 0%"); $this->file = $this->download($update); - $this->output->write(" |- Installing upgrade... "); + $this->output->write(' |- Installing upgrade... '); $installation = $this->upgrade(); if (!$installation) { @@ -189,20 +177,20 @@ class SelfupgradeCommand extends ConsoleCommand } /** - * @param $package + * @param Package $package * * @return string */ private function download($package) { $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); - $this->tmp = $tmp_dir . '/Grav-' . uniqid(); + $this->tmp = $tmp_dir . '/Grav-' . uniqid('', false); $output = Response::get($package['download'], [], [$this, 'progress']); - Folder::mkdir($this->tmp); + Folder::create($this->tmp); $this->output->write("\x0D"); - $this->output->write(" |- Downloading upgrade [" . $this->formatBytes($package['size']) . "]... 100%"); + $this->output->write(" |- Downloading upgrade [{$this->formatBytes($package['size'])}]... 100%"); $this->output->writeln(''); file_put_contents($this->tmp . DS . $package['name'], $output); @@ -215,15 +203,24 @@ class SelfupgradeCommand extends ConsoleCommand */ private function upgrade() { - Installer::install($this->file, GRAV_ROOT, - ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true]); + if ($this->file) { + $folder = Installer::unZip($this->file, $this->tmp . '/zip'); + } else { + $folder = false; + } + + static::upgradeGrav($this->file, $folder); + $errorCode = Installer::lastErrorCode(); - Folder::delete($this->tmp); + + if ($this->tmp) { + Folder::delete($this->tmp); + } if ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)) { $this->output->write("\x0D"); // extra white spaces to clear out the buffer properly - $this->output->writeln(" |- Installing upgrade... error "); + $this->output->writeln(' |- Installing upgrade... error '); $this->output->writeln(" | '- " . Installer::lastErrorMsg()); return false; @@ -231,23 +228,23 @@ class SelfupgradeCommand extends ConsoleCommand $this->output->write("\x0D"); // extra white spaces to clear out the buffer properly - $this->output->writeln(" |- Installing upgrade... ok "); + $this->output->writeln(' |- Installing upgrade... ok '); return true; } /** - * @param $progress + * @param array $progress */ public function progress($progress) { $this->output->write("\x0D"); - $this->output->write(" |- Downloading upgrade [" . $this->formatBytes($progress["filesize"]) . "]... " . str_pad($progress['percent'], - 5, " ", STR_PAD_LEFT) . '%'); + $this->output->write(" |- Downloading upgrade [{$this->formatBytes($progress['filesize']) }]... " . str_pad($progress['percent'], + 5, ' ', STR_PAD_LEFT) . '%'); } /** - * @param $size + * @param int|float $size * @param int $precision * * @return string @@ -257,6 +254,44 @@ class SelfupgradeCommand extends ConsoleCommand $base = log($size) / log(1024); $suffixes = array('', 'k', 'M', 'G', 'T'); - return round(pow(1024, $base - floor($base)), $precision) . $suffixes[(int)floor($base)]; + return round(1024 ** ($base - floor($base)), $precision) . $suffixes[(int)floor($base)]; + } + + private function upgradeGrav($zip, $folder, $keepFolder = false) + { + static $ignores = [ + 'backup', + 'cache', + 'images', + 'logs', + 'tmp', + 'user', + '.htaccess', + 'robots.txt' + ]; + + if (!is_dir($folder)) { + Installer::setError('Invalid source folder'); + } + + try { + $script = $folder . '/system/install.php'; + /** Install $installer */ + if ((file_exists($script) && $install = include $script) && is_callable($install)) { + $install($zip); + } else { + Installer::install( + $zip, + GRAV_ROOT, + ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $ignores], + $folder, + $keepFolder + ); + + Cache::clearCache(); + } + } catch (\Exception $e) { + Installer::setError($e->getMessage()); + } } } diff --git a/system/src/Grav/Console/Gpm/UninstallCommand.php b/system/src/Grav/Console/Gpm/UninstallCommand.php index 34b04ee..99fc81a 100644 --- a/system/src/Grav/Console/Gpm/UninstallCommand.php +++ b/system/src/Grav/Console/Gpm/UninstallCommand.php @@ -1,8 +1,9 @@ setName("uninstall") + ->setName('uninstall') ->addOption( 'all-yes', 'y', @@ -61,13 +56,10 @@ class UninstallCommand extends ConsoleCommand InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The package(s) that are desired to be removed. Use the "index" command for a list of packages' ) - ->setDescription("Performs the uninstallation of plugins and themes") + ->setDescription('Performs the uninstallation of plugins and themes') ->setHelp('The uninstall command allows to uninstall plugins and themes'); } - /** - * @return int|null|void - */ protected function serve() { $this->gpm = new GPM(); @@ -77,37 +69,38 @@ class UninstallCommand extends ConsoleCommand $packages = array_map('strtolower', $this->input->getArgument('package')); $this->data = ['total' => 0, 'not_found' => []]; + $total = 0; foreach ($packages as $package) { $plugin = $this->gpm->getInstalledPlugin($package); $theme = $this->gpm->getInstalledTheme($package); if ($plugin || $theme) { $this->data[strtolower($package)] = $plugin ?: $theme; - $this->data['total']++; + $total++; } else { $this->data['not_found'][] = $package; } } + $this->data['total'] = $total; $this->output->writeln(''); if (!$this->data['total']) { - $this->output->writeln("Nothing to uninstall."); + $this->output->writeln('Nothing to uninstall.'); $this->output->writeln(''); exit; } if (count($this->data['not_found'])) { - $this->output->writeln("These packages were not found installed: " . implode(', ', - $this->data['not_found']) . ""); + $this->output->writeln('These packages were not found installed: ' . implode(', ', + $this->data['not_found']) . ''); } - unset($this->data['not_found']); - unset($this->data['total']); + unset($this->data['not_found'], $this->data['total']); foreach ($this->data as $slug => $package) { - $this->output->writeln("Preparing to uninstall " . $package->name . " [v" . $package->version . "]"); + $this->output->writeln("Preparing to uninstall {$package->name} [v{$package->version}]"); - $this->output->write(" |- Checking destination... "); + $this->output->write(' |- Checking destination... '); $checks = $this->checkDestination($slug, $package); if (!$checks) { @@ -131,8 +124,8 @@ class UninstallCommand extends ConsoleCommand /** - * @param $slug - * @param $package + * @param string $slug + * @param Package $package * * @return bool */ @@ -147,12 +140,12 @@ class UninstallCommand extends ConsoleCommand if (count($dependent_packages) > ($is_dependency ? 1 : 0)) { $this->output->writeln(''); $this->output->writeln(''); - $this->output->writeln("Uninstallation failed."); + $this->output->writeln('Uninstallation failed.'); $this->output->writeln(''); - if (count($dependent_packages) > ($is_dependency ? 2 : 1)) { - $this->output->writeln("The installed packages " . implode(', ', $dependent_packages) . " depends on this package. Please remove those first."); + if (\count($dependent_packages) > ($is_dependency ? 2 : 1)) { + $this->output->writeln('The installed packages ' . implode(', ', $dependent_packages) . ' depends on this package. Please remove those first.'); } else { - $this->output->writeln("The installed package " . implode(', ', $dependent_packages) . " depends on this package. Please remove it first."); + $this->output->writeln('The installed package ' . implode(', ', $dependent_packages) . ' depends on this package. Please remove it first.'); } $this->output->writeln(''); @@ -165,12 +158,12 @@ class UninstallCommand extends ConsoleCommand if ($is_dependency) { foreach ($dependencies as $key => $dependency) { - if (in_array($dependency['name'], $this->dependencies)) { + if (\in_array($dependency['name'], $this->dependencies, true)) { unset($dependencies[$key]); } } } else { - if (count($dependencies) > 0) { + if (\count($dependencies) > 0) { $this->output->writeln(' `- Dependencies found...'); $this->output->writeln(''); } @@ -182,7 +175,7 @@ class UninstallCommand extends ConsoleCommand $this->dependencies[] = $dependency['name']; - if (is_array($dependency)) { + if (\is_array($dependency)) { $dependency = $dependency['name']; } if ($dependency === 'grav' || $dependency === 'php') { @@ -194,9 +187,9 @@ class UninstallCommand extends ConsoleCommand $dependency_exists = $this->packageExists($dependency, $dependencyPackage); if ($dependency_exists == Installer::EXISTS) { - $this->output->writeln("A dependency on " . $dependencyPackage->name . " [v" . $dependencyPackage->version . "] was found"); + $this->output->writeln("A dependency on {$dependencyPackage->name} [v{$dependencyPackage->version}] was found"); - $question = new ConfirmationQuestion(" |- Uninstall " . $dependencyPackage->name . "? [y|N] ", false); + $question = new ConfirmationQuestion(" |- Uninstall {$dependencyPackage->name}? [y|N] ", false); $answer = $this->all_yes ? true : $questionHelper->ask($this->input, $this->output, $question); if ($answer) { @@ -210,7 +203,7 @@ class UninstallCommand extends ConsoleCommand } $this->output->writeln(''); } else { - $this->output->writeln(" '- You decided not to uninstall " . $dependencyPackage->name . "."); + $this->output->writeln(" '- You decided not to uninstall {$dependencyPackage->name}."); $this->output->writeln(''); } } @@ -225,21 +218,21 @@ class UninstallCommand extends ConsoleCommand $errorCode = Installer::lastErrorCode(); if ($errorCode && $errorCode !== Installer::IS_LINK && $errorCode !== Installer::EXISTS) { - $this->output->writeln(" |- Uninstalling " . $package->name . " package... error "); - $this->output->writeln(" | '- " . Installer::lastErrorMsg().""); + $this->output->writeln(" |- Uninstalling {$package->name} package... error "); + $this->output->writeln(" | '- " . Installer::lastErrorMsg() . ''); return false; } $message = Installer::getMessage(); if ($message) { - $this->output->writeln(" |- " . $message); + $this->output->writeln(" |- {$message}"); } if (!$is_dependency && $this->dependencies) { - $this->output->writeln("Finishing up uninstalling " . $package->name . ""); + $this->output->writeln("Finishing up uninstalling {$package->name}"); } - $this->output->writeln(" |- Uninstalling " . $package->name . " package... ok "); + $this->output->writeln(" |- Uninstalling {$package->name} package... ok "); @@ -247,8 +240,8 @@ class UninstallCommand extends ConsoleCommand } /** - * @param $slug - * @param $package + * @param string $slug + * @param Package $package * * @return bool */ @@ -261,7 +254,7 @@ class UninstallCommand extends ConsoleCommand if ($exists == Installer::IS_LINK) { $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... symbolic link"); + $this->output->writeln(' |- Checking destination... symbolic link'); if ($this->all_yes) { $this->output->writeln(" | '- Skipped automatically."); @@ -281,7 +274,7 @@ class UninstallCommand extends ConsoleCommand } $this->output->write("\x0D"); - $this->output->writeln(" |- Checking destination... ok"); + $this->output->writeln(' |- Checking destination... ok'); return true; } @@ -289,8 +282,8 @@ class UninstallCommand extends ConsoleCommand /** * Check if package exists * - * @param $slug - * @param $package + * @param string $slug + * @param Package $package * @return int */ private function packageExists($slug, $package) diff --git a/system/src/Grav/Console/Gpm/UpdateCommand.php b/system/src/Grav/Console/Gpm/UpdateCommand.php index 06913b1..d8ea735 100644 --- a/system/src/Grav/Console/Gpm/UpdateCommand.php +++ b/system/src/Grav/Console/Gpm/UpdateCommand.php @@ -1,8 +1,9 @@ setName("update") + ->setName('update') ->addOption( 'force', 'f', @@ -106,27 +93,24 @@ class UpdateCommand extends ConsoleCommand InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'The package or packages that is desired to update. By default all available updates will be applied.' ) - ->setDescription("Detects and performs an update of plugins and themes when available") + ->setDescription('Detects and performs an update of plugins and themes when available') ->setHelp('The update command updates plugins and themes when a new version is available'); } - /** - * @return int|null|void - */ protected function serve() { $this->upgrader = new Upgrader($this->input->getOption('force')); $local = $this->upgrader->getLocalVersion(); $remote = $this->upgrader->getRemoteVersion(); if ($local !== $remote) { - $this->output->writeln("WARNING: A new version of Grav is available. You should update Grav before updating plugins and themes. If you continue without updating Grav, some plugins or themes may stop working."); - $this->output->writeln(""); + $this->output->writeln('WARNING: A new version of Grav is available. You should update Grav before updating plugins and themes. If you continue without updating Grav, some plugins or themes may stop working.'); + $this->output->writeln(''); $questionHelper = $this->getHelper('question'); - $question = new ConfirmationQuestion("Continue with the update process? [Y|n] ", true); + $question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true); $answer = $questionHelper->ask($this->input, $this->output, $question); if (!$answer) { - $this->output->writeln("Update aborted. Exiting..."); + $this->output->writeln('Update aborted. Exiting...'); exit; } } @@ -141,7 +125,7 @@ class UpdateCommand extends ConsoleCommand $this->destination = realpath($this->input->getOption('destination')); if (!Installer::isGravInstance($this->destination)) { - $this->output->writeln("ERROR: " . Installer::lastErrorMsg()); + $this->output->writeln('ERROR: ' . Installer::lastErrorMsg()); exit; } if ($this->input->getOption('plugins') === false && $this->input->getOption('themes') === false) { @@ -153,27 +137,26 @@ class UpdateCommand extends ConsoleCommand if ($this->overwrite) { $this->data = $this->gpm->getInstallable($list_type); - $description = " can be overwritten"; + $description = ' can be overwritten'; } else { $this->data = $this->gpm->getUpdatable($list_type); - $description = " need updating"; + $description = ' need updating'; } $only_packages = array_map('strtolower', $this->input->getArgument('package')); if (!$this->overwrite && !$this->data['total']) { - $this->output->writeln("Nothing to update."); + $this->output->writeln('Nothing to update.'); exit; } - $this->output->write("Found " . $this->gpm->countInstalled() . " packages installed of which " . $this->data['total'] . "" . $description); + $this->output->write("Found {$this->gpm->countInstalled()} packages installed of which {$this->data['total']}{$description}"); $limit_to = $this->userInputPackages($only_packages); $this->output->writeln(''); - unset($this->data['total']); - unset($limit_to['total']); + unset($this->data['total'], $limit_to['total']); // updates review @@ -182,7 +165,7 @@ class UpdateCommand extends ConsoleCommand $index = 0; foreach ($this->data as $packages) { foreach ($packages as $slug => $package) { - if (count($only_packages) && !array_key_exists($slug, $limit_to)) { + if (!array_key_exists($slug, $limit_to) && \count($only_packages)) { continue; } @@ -192,11 +175,11 @@ class UpdateCommand extends ConsoleCommand $this->output->writeln( // index - str_pad($index++ + 1, 2, '0', STR_PAD_LEFT) . ". " . + str_pad($index++ + 1, 2, '0', STR_PAD_LEFT) . '. ' . // name - "" . str_pad($package->name, 15) . " " . + '' . str_pad($package->name, 15) . ' ' . // version - "[v" . $package->version . " -> v" . $package->available . "]" + "[v{$package->version} -> v{$package->available}]" ); $slugs[] = $slug; } @@ -204,13 +187,13 @@ class UpdateCommand extends ConsoleCommand if (!$this->all_yes) { // prompt to continue - $this->output->writeln(""); + $this->output->writeln(''); $questionHelper = $this->getHelper('question'); - $question = new ConfirmationQuestion("Continue with the update process? [Y|n] ", true); + $question = new ConfirmationQuestion('Continue with the update process? [Y|n] ', true); $answer = $questionHelper->ask($this->input, $this->output, $question); if (!$answer) { - $this->output->writeln("Update aborted. Exiting..."); + $this->output->writeln('Update aborted. Exiting...'); exit; } } @@ -228,13 +211,13 @@ class UpdateCommand extends ConsoleCommand $command_exec = $install_command->run($args, $this->output); if ($command_exec != 0) { - $this->output->writeln("Error: An error occurred while trying to install the packages"); + $this->output->writeln('Error: An error occurred while trying to install the packages'); exit; } } /** - * @param $only_packages + * @param array $only_packages * * @return array */ @@ -243,14 +226,14 @@ class UpdateCommand extends ConsoleCommand $found = ['total' => 0]; $ignore = []; - if (!count($only_packages)) { + if (!\count($only_packages)) { $this->output->writeln(''); } else { foreach ($only_packages as $only_package) { $find = $this->gpm->findPackage($only_package); if (!$find || (!$this->overwrite && !$this->gpm->isUpdatable($find->slug))) { - $name = isset($find->slug) ? $find->slug : $only_package; + $name = $find->slug ?? $only_package; $ignore[$name] = $name; } else { $found[$find->slug] = $find; @@ -264,18 +247,18 @@ class UpdateCommand extends ConsoleCommand $list = array_keys($list); if ($found['total'] !== $this->data['total']) { - $this->output->write(", only " . $found['total'] . " will be updated"); + $this->output->write(", only {$found['total']} will be updated"); } $this->output->writeln(''); - $this->output->writeln("Limiting updates for only " . implode(', ', - $list) . ""); + $this->output->writeln('Limiting updates for only ' . implode(', ', + $list) . ''); } - if (count($ignore)) { + if (\count($ignore)) { $this->output->writeln(''); - $this->output->writeln("Packages not found or not requiring updates: " . implode(', ', - $ignore) . ""); + $this->output->writeln('Packages not found or not requiring updates: ' . implode(', ', + $ignore) . ''); } } diff --git a/system/src/Grav/Console/Gpm/VersionCommand.php b/system/src/Grav/Console/Gpm/VersionCommand.php index eb7872e..becc327 100644 --- a/system/src/Grav/Console/Gpm/VersionCommand.php +++ b/system/src/Grav/Console/Gpm/VersionCommand.php @@ -1,8 +1,9 @@ setName("version") + ->setName('version') ->addOption( 'force', 'f', @@ -40,7 +36,7 @@ class VersionCommand extends ConsoleCommand InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'The package or packages that is desired to know the version of. By default and if not specified this would be grav' ) - ->setDescription("Shows the version of an installed package. If available also shows pending updates.") + ->setDescription('Shows the version of an installed package. If available also shows pending updates.') ->setHelp('The version command displays the current version of a package installed and, if available, the available version of pending updates'); } @@ -64,13 +60,13 @@ class VersionCommand extends ConsoleCommand $version = null; $updatable = false; - if ($package == 'grav') { + if ($package === 'grav') { $name = 'Grav'; $version = GRAV_VERSION; $upgrader = new Upgrader(); if ($upgrader->isUpgradable()) { - $updatable = ' [upgradable: v' . $upgrader->getRemoteVersion() . ']'; + $updatable = " [upgradable: v{$upgrader->getRemoteVersion()}]"; } } else { @@ -99,17 +95,17 @@ class VersionCommand extends ConsoleCommand $name = $installed->name; if ($this->gpm->isUpdatable($package)) { - $updatable = ' [updatable: v' . $installed->available . ']'; + $updatable = " [updatable: v{$installed->available}]"; } } } $updatable = $updatable ?: ''; - if ($installed || $package == 'grav') { - $this->output->writeln('You are running ' . $name . ' v' . $version . '' . $updatable); + if ($installed || $package === 'grav') { + $this->output->writeln("You are running {$name} v{$version}{$updatable}"); } else { - $this->output->writeln('Package ' . $package . ' not found'); + $this->output->writeln("Package {$package} not found"); } } } diff --git a/system/src/Grav/Console/TerminalObjects/Table.php b/system/src/Grav/Console/TerminalObjects/Table.php index c3078a9..4f9df63 100644 --- a/system/src/Grav/Console/TerminalObjects/Table.php +++ b/system/src/Grav/Console/TerminalObjects/Table.php @@ -1,8 +1,9 @@ caches = array_values($caches); - $this->count = count($caches); + $this->count = \count($caches); } /** diff --git a/system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php b/system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php index 3a69c1f..45c9d07 100644 --- a/system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php +++ b/system/src/Grav/Framework/Cache/Adapter/DoctrineCache.php @@ -1,8 +1,9 @@ getNamespace(); - $namespace && $doctrineCache->setNamespace($namespace); + if ($namespace) { + $doctrineCache->setNamespace($namespace); + } $this->driver = $doctrineCache; } @@ -96,20 +99,10 @@ class DoctrineCache extends AbstractCache /** * @inheritdoc - * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException */ public function doDeleteMultiple($keys) { - // TODO: Remove when Doctrine Cache has been updated to support the feature. - if (!method_exists($this->driver, 'deleteMultiple')) { - $success = true; - foreach ($keys as $key) { - $success = $this->delete($key) && $success; - } - - return $success; - } - return $this->driver->deleteMultiple($keys); } diff --git a/system/src/Grav/Framework/Cache/Adapter/FileCache.php b/system/src/Grav/Framework/Cache/Adapter/FileCache.php index 389b626..a5a60fa 100644 --- a/system/src/Grav/Framework/Cache/Adapter/FileCache.php +++ b/system/src/Grav/Framework/Cache/Adapter/FileCache.php @@ -1,8 +1,9 @@ initFileCache($namespace, $folder ?? ''); } /** @@ -62,7 +65,7 @@ class FileCache extends AbstractCache /** * @inheritdoc - * @throws \Psr\SimpleCache\CacheException + * @throws \Psr\SimpleCache\CacheException|InvalidArgumentException */ public function doSet($key, $value, $ttl) { @@ -126,8 +129,8 @@ class FileCache extends AbstractCache $hash = str_replace('/', '-', base64_encode(hash('sha256', static::class . $key, true))); $dir = $this->directory . $hash[0] . DIRECTORY_SEPARATOR . $hash[1] . DIRECTORY_SEPARATOR; - if ($mkdir && !file_exists($dir)) { - @mkdir($dir, 0777, true); + if ($mkdir) { + $this->mkdir($dir); } return $dir . substr($hash, 2, 20); @@ -136,9 +139,9 @@ class FileCache extends AbstractCache /** * @param string $namespace * @param string $directory - * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException */ - private function init($namespace, $directory) + protected function initFileCache($namespace, $directory) { if (!isset($directory[0])) { $directory = sys_get_temp_dir() . '/grav-cache'; @@ -153,9 +156,7 @@ class FileCache extends AbstractCache $directory .= DIRECTORY_SEPARATOR . $namespace; } - if (!file_exists($directory)) { - @mkdir($directory, 0777, true); - } + $this->mkdir($directory); $directory .= DIRECTORY_SEPARATOR; // On Windows the whole path is limited to 258 chars @@ -192,6 +193,28 @@ class FileCache extends AbstractCache } } + /** + * @param string $dir + * @throws \RuntimeException + */ + private function mkdir($dir) + { + // Silence error for open_basedir; should fail in mkdir instead. + if (@is_dir($dir)) { + return; + } + + $success = @mkdir($dir, 0777, true); + + if (!$success) { + // Take yet another look, make sure that the folder doesn't exist. + clearstatcache(true, $dir); + if (!@is_dir($dir)) { + throw new \RuntimeException(sprintf('Unable to create directory: %s', $dir)); + } + } + } + /** * @internal * @throws \ErrorException diff --git a/system/src/Grav/Framework/Cache/Adapter/MemoryCache.php b/system/src/Grav/Framework/Cache/Adapter/MemoryCache.php index 84911fb..2d0fe40 100644 --- a/system/src/Grav/Framework/Cache/Adapter/MemoryCache.php +++ b/system/src/Grav/Framework/Cache/Adapter/MemoryCache.php @@ -1,8 +1,9 @@ getNamespace()][$key]) ? $_SESSION[$this->getNamespace()][$key] : null; + $stored = $_SESSION[$this->getNamespace()][$key] ?? null; if (isset($stored[self::LIFETIME]) && $stored[self::LIFETIME] < time()) { unset($_SESSION[$this->getNamespace()][$key]); diff --git a/system/src/Grav/Framework/Cache/CacheInterface.php b/system/src/Grav/Framework/Cache/CacheInterface.php index 54af40c..5aefe35 100644 --- a/system/src/Grav/Framework/Cache/CacheInterface.php +++ b/system/src/Grav/Framework/Cache/CacheInterface.php @@ -1,8 +1,9 @@ 64) { + if (\strlen($key) > 64) { throw new InvalidArgumentException( - sprintf('Cache key length must be less than 65 characters, key had %s characters', strlen($key)) + sprintf('Cache key length must be less than 65 characters, key had %s characters', \strlen($key)) ); } if (strpbrk($key, '{}()/\@:') !== false) { @@ -308,7 +309,7 @@ trait CacheTrait /** * @param array $keys - * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException */ protected function validateKeys($keys) { @@ -322,9 +323,9 @@ trait CacheTrait } /** - * @param null|int|\DateInterval $ttl + * @param null|int|\DateInterval|mixed $ttl * @return int|null - * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \Psr\SimpleCache\InvalidArgumentException|InvalidArgumentException */ protected function convertTtl($ttl) { @@ -332,18 +333,18 @@ trait CacheTrait return $this->getDefaultLifetime(); } - if (is_int($ttl)) { + if (\is_int($ttl)) { return $ttl; } if ($ttl instanceof \DateInterval) { - $ttl = (int) \DateTime::createFromFormat('U', 0)->add($ttl)->format('U'); + $ttl = (int) \DateTime::createFromFormat('U', '0')->add($ttl)->format('U'); } throw new InvalidArgumentException( sprintf( 'Expiration date must be an integer, a DateInterval or null, "%s" given', - is_object($ttl) ? get_class($ttl) : gettype($ttl) + \is_object($ttl) ? \get_class($ttl) : \gettype($ttl) ) ); } diff --git a/system/src/Grav/Framework/Cache/Exception/CacheException.php b/system/src/Grav/Framework/Cache/Exception/CacheException.php index 71caff7..55da2f5 100644 --- a/system/src/Grav/Framework/Cache/Exception/CacheException.php +++ b/system/src/Grav/Framework/Cache/Exception/CacheException.php @@ -1,8 +1,9 @@ getOrderings()) { $next = null; foreach (array_reverse($orderings) as $field => $ordering) { - $next = ClosureExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next); + $next = ClosureExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next); } uasort($filtered, $next); @@ -106,7 +107,7 @@ class AbstractFileCollection extends AbstractLazyCollection implements FileColle $length = $criteria->getMaxResults(); if ($offset || $length) { - $filtered = array_slice($filtered, (int)$offset, $length); + $filtered = \array_slice($filtered, (int)$offset, $length); } return new ArrayCollection($filtered); diff --git a/system/src/Grav/Framework/Collection/AbstractIndexCollection.php b/system/src/Grav/Framework/Collection/AbstractIndexCollection.php new file mode 100644 index 0000000..0e4fae2 --- /dev/null +++ b/system/src/Grav/Framework/Collection/AbstractIndexCollection.php @@ -0,0 +1,510 @@ +entries = $entries; + } + + /** + * {@inheritDoc} + */ + public function toArray() + { + return $this->loadElements($this->entries); + } + + /** + * {@inheritDoc} + */ + public function first() + { + $value = reset($this->entries); + $key = key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function last() + { + $value = end($this->entries); + $key = key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function key() + { + return key($this->entries); + } + + /** + * {@inheritDoc} + */ + public function next() + { + $value = next($this->entries); + $key = key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function current() + { + $value = current($this->entries); + $key = key($this->entries); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function remove($key) + { + if (!array_key_exists($key, $this->entries)) { + return null; + } + + $value = $this->entries[$key]; + unset($this->entries[$key]); + + return $this->loadElement($key, $value); + } + + /** + * {@inheritDoc} + */ + public function removeElement($element) + { + $key = $this->isAllowedElement($element) ? $element->getKey() : null; + + if (!$key || !isset($this->entries[$key])) { + return false; + } + + unset($this->entries[$key]); + + return true; + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + public function offsetExists($offset) + { + return $this->containsKey($offset); + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->add($value); + } + + $this->set($offset, $value); + } + + /** + * Required by interface ArrayAccess. + * + * {@inheritDoc} + */ + public function offsetUnset($offset) + { + return $this->remove($offset); + } + + /** + * {@inheritDoc} + */ + public function containsKey($key) + { + return isset($this->entries[$key]) || array_key_exists($key, $this->entries); + } + + /** + * {@inheritDoc} + */ + public function contains($element) + { + $key = $this->isAllowedElement($element) ? $element->getKey() : null; + + return $key && isset($this->entries[$key]); + } + + /** + * {@inheritDoc} + */ + public function exists(Closure $p) + { + return $this->loadCollection($this->entries)->exists($p); + } + + /** + * {@inheritDoc} + */ + public function indexOf($element) + { + $key = $this->isAllowedElement($element) ? $element->getKey() : null; + + return $key && isset($this->entries[$key]) ? $key : null; + } + + /** + * {@inheritDoc} + */ + public function get($key) + { + if (!isset($this->entries[$key])) { + return null; + } + + return $this->loadElement($key, $this->entries[$key]); + } + + /** + * {@inheritDoc} + */ + public function getKeys() + { + return array_keys($this->entries); + } + + /** + * {@inheritDoc} + */ + public function getValues() + { + return array_values($this->loadElements($this->entries)); + } + + /** + * {@inheritDoc} + */ + public function count() + { + return \count($this->entries); + } + + /** + * {@inheritDoc} + */ + public function set($key, $value) + { + if (!$this->isAllowedElement($value)) { + throw new \InvalidArgumentException('Invalid argument $value'); + } + + if ($key !== $value->getKey()) { + $value->setKey($key); + } + + $this->entries[$key] = $this->getElementMeta($value); + } + + /** + * {@inheritDoc} + */ + public function add($element) + { + if (!$this->isAllowedElement($element)) { + throw new \InvalidArgumentException('Invalid argument $element'); + } + + $this->entries[$element->getKey()] = $this->getElementMeta($element); + + return true; + } + + /** + * {@inheritDoc} + */ + public function isEmpty() + { + return empty($this->entries); + } + + /** + * Required by interface IteratorAggregate. + * + * {@inheritDoc} + */ + public function getIterator() + { + return new ArrayIterator($this->loadElements()); + } + + /** + * {@inheritDoc} + */ + public function map(Closure $func) + { + return $this->loadCollection($this->entries)->map($func); + } + + /** + * {@inheritDoc} + */ + public function filter(Closure $p) + { + return $this->loadCollection($this->entries)->filter($p); + } + + /** + * {@inheritDoc} + */ + public function forAll(Closure $p) + { + return $this->loadCollection($this->entries)->forAll($p); + } + + /** + * {@inheritDoc} + */ + public function partition(Closure $p) + { + return $this->loadCollection($this->entries)->partition($p); + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return __CLASS__ . '@' . spl_object_hash($this); + } + + /** + * {@inheritDoc} + */ + public function clear() + { + $this->entries = []; + } + + /** + * {@inheritDoc} + */ + public function slice($offset, $length = null) + { + return $this->loadElements(\array_slice($this->entries, $offset, $length, true)); + } + + /** + * @param int $start + * @param int|null $limit + * @return static + */ + public function limit($start, $limit = null) + { + return $this->createFrom(\array_slice($this->entries, $start, $limit, true)); + } + + /** + * Reverse the order of the items. + * + * @return static + */ + public function reverse() + { + return $this->createFrom(array_reverse($this->entries)); + } + + /** + * Shuffle items. + * + * @return static + */ + public function shuffle() + { + $keys = $this->getKeys(); + shuffle($keys); + + return $this->createFrom(array_replace(array_flip($keys), $this->entries)); + } + + /** + * Select items from collection. + * + * Collection is returned in the order of $keys given to the function. + * + * @param array $keys + * @return static + */ + public function select(array $keys) + { + $list = []; + foreach ($keys as $key) { + if (isset($this->entries[$key])) { + $list[$key] = $this->entries[$key]; + } + } + + return $this->createFrom($list); + } + + /** + * Un-select items from collection. + * + * @param array $keys + * @return static + */ + public function unselect(array $keys) + { + return $this->select(array_diff($this->getKeys(), $keys)); + } + + /** + * Split collection into chunks. + * + * @param int $size Size of each chunk. + * @return array + */ + public function chunk($size) + { + return $this->loadCollection($this->entries)->chunk($size); + } + + /** + * @return string + */ + public function serialize() + { + return serialize(['entries' => $this->entries]); + } + + /** + * @param string $serialized + */ + public function unserialize($serialized) + { + $data = unserialize($serialized, ['allowed_classes' => false]); + + $this->entries = $data['entries']; + } + + /** + * Implements JsonSerializable interface. + * + * @return array + */ + public function jsonSerialize() + { + return $this->loadCollection()->jsonSerialize(); + } + + /** + * Creates a new instance from the specified elements. + * + * This method is provided for derived classes to specify how a new + * instance should be created when constructor semantics have changed. + * + * @param array $entries Elements. + * + * @return static + */ + protected function createFrom(array $entries) + { + return new static($entries); + } + + /** + * @return array + */ + protected function getEntries() : array + { + return $this->entries; + } + + /** + * @param array $entries + */ + protected function setEntries(array $entries) : void + { + $this->entries = $entries; + } + + /** + * @param string $key + * @param mixed $value + * @return mixed|null + */ + abstract protected function loadElement($key, $value); + + /** + * @param array|null $entries + * @return array + */ + abstract protected function loadElements(array $entries = null) : array; + + /** + * @param array|null $entries + * @return CollectionInterface + */ + abstract protected function loadCollection(array $entries = null) : CollectionInterface; + + /** + * @param mixed $value + * @return bool + */ + abstract protected function isAllowedElement($value) : bool; + + /** + * @param mixed $element + * @return mixed + */ + abstract protected function getElementMeta($element); +} diff --git a/system/src/Grav/Framework/Collection/AbstractLazyCollection.php b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php index af2f696..0653caa 100644 --- a/system/src/Grav/Framework/Collection/AbstractLazyCollection.php +++ b/system/src/Grav/Framework/Collection/AbstractLazyCollection.php @@ -1,8 +1,9 @@ collection->chunk($size); } + /** + * {@inheritDoc} + */ + public function select(array $keys) + { + $this->initialize(); + return $this->collection->select($keys); + } + + /** + * {@inheritDoc} + */ + public function unselect(array $keys) + { + $this->initialize(); + return $this->collection->unselect($keys); + } + /** * {@inheritDoc} */ diff --git a/system/src/Grav/Framework/Collection/ArrayCollection.php b/system/src/Grav/Framework/Collection/ArrayCollection.php index 4c922f4..f79ab96 100644 --- a/system/src/Grav/Framework/Collection/ArrayCollection.php +++ b/system/src/Grav/Framework/Collection/ArrayCollection.php @@ -1,8 +1,9 @@ containsKey($key)) { + $list[$key] = $this->get($key); + } + } + + return $this->createFrom($list); + } + + /** + * Un-select items from collection. + * + * @param array $keys + * @return static + */ + public function unselect(array $keys) + { + return $this->select(array_diff($this->getKeys(), $keys)); + } + + /** + * Implements JsonSerializable interface. * * @return array */ diff --git a/system/src/Grav/Framework/Collection/CollectionInterface.php b/system/src/Grav/Framework/Collection/CollectionInterface.php index 5f5e1fc..9239d4a 100644 --- a/system/src/Grav/Framework/Collection/CollectionInterface.php +++ b/system/src/Grav/Framework/Collection/CollectionInterface.php @@ -1,8 +1,9 @@ get_class($this), + '_type' => \get_class($this), '_version' => $this->version, - 'id' => $this->id + 'id' => $this->id, + 'cached' => $this->cached ]; if ($this->checksum) { @@ -163,8 +166,9 @@ class ContentBlock implements ContentBlockInterface { $this->checkVersion($serialized); - $this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId(); - $this->checksum = isset($serialized['checksum']) ? $serialized['checksum'] : null; + $this->id = $serialized['id'] ?? $this->generateId(); + $this->checksum = $serialized['checksum'] ?? null; + $this->cached = $serialized['cached'] ?? null; if (isset($serialized['content'])) { $this->setContent($serialized['content']); @@ -176,6 +180,34 @@ class ContentBlock implements ContentBlockInterface } } + /** + * @return bool + */ + public function isCached() + { + if (!$this->cached) { + return false; + } + + foreach ($this->blocks as $block) { + if (!$block->isCached()) { + return false; + } + } + + return true; + } + + /** + * @return $this + */ + public function disableCache() + { + $this->cached = false; + + return $this; + } + /** * @param string $checksum * @return $this diff --git a/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php index fff8f2e..330901e 100644 --- a/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php +++ b/system/src/Grav/Framework/ContentBlock/ContentBlockInterface.php @@ -1,8 +1,9 @@ (string) $element]; } if (empty($element['href'])) { @@ -175,7 +176,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface */ public function addInlineStyle($element, $priority = 0, $location = 'head') { - if (!is_array($element)) { + if (!\is_array($element)) { $element = ['content' => (string) $element]; } if (empty($element['content'])) { @@ -206,7 +207,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface */ public function addScript($element, $priority = 0, $location = 'head') { - if (!is_array($element)) { + if (!\is_array($element)) { $element = ['src' => (string) $element]; } if (empty($element['src'])) { @@ -243,7 +244,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface */ public function addInlineScript($element, $priority = 0, $location = 'head') { - if (!is_array($element)) { + if (!\is_array($element)) { $element = ['content' => (string) $element]; } if (empty($element['content'])) { @@ -274,7 +275,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface */ public function addHtml($html, $priority = 0, $location = 'bottom') { - if (empty($html) || !is_string($html)) { + if (empty($html) || !\is_string($html)) { return false; } if (!isset($this->html[$location])) { @@ -302,7 +303,7 @@ class HtmlBlock extends ContentBlock implements HtmlBlockInterface ]; foreach ($this->blocks as $block) { - if ($block instanceof HtmlBlock) { + if ($block instanceof self) { $blockAssets = $block->getAssetsFast(); $assets['frameworks'] += $blockAssets['frameworks']; diff --git a/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php b/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php index 204e199..ee486d2 100644 --- a/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php +++ b/system/src/Grav/Framework/ContentBlock/HtmlBlockInterface.php @@ -1,8 +1,9 @@ offsetGet($id); + } + + public function has($id): bool + { + return $this->offsetExists($id); + } +} \ No newline at end of file diff --git a/system/src/Grav/Framework/File/AbstractFile.php b/system/src/Grav/Framework/File/AbstractFile.php new file mode 100644 index 0000000..0411701 --- /dev/null +++ b/system/src/Grav/Framework/File/AbstractFile.php @@ -0,0 +1,425 @@ +filesystem = $filesystem ?? Filesystem::getInstance(); + $this->setFilepath($filepath); + } + + /** + * Unlock file when the object gets destroyed. + */ + public function __destruct() + { + if ($this->isLocked()) { + $this->unlock(); + } + } + + public function __clone() + { + $this->handle = null; + $this->locked = false; + } + + /** + * @return string + */ + public function serialize(): string + { + return serialize($this->doSerialize()); + } + + /** + * @param string $serialized + */ + public function unserialize($serialized): void + { + $this->doUnserialize(unserialize($serialized, ['allowed_classes' => false])); + } + + /** + * {@inheritdoc} + * @see FileInterface::getFilePath() + */ + public function getFilePath(): string + { + return $this->filepath; + } + + /** + * {@inheritdoc} + * @see FileInterface::getPath() + */ + public function getPath(): string + { + if (null === $this->path) { + $this->setPathInfo(); + } + + return $this->path; + } + + /** + * {@inheritdoc} + * @see FileInterface::getFilename() + */ + public function getFilename(): string + { + if (null === $this->filename) { + $this->setPathInfo(); + } + + return $this->filename; + } + + /** + * {@inheritdoc} + * @see FileInterface::getBasename() + */ + public function getBasename(): string + { + if (null === $this->basename) { + $this->setPathInfo(); + } + + return $this->basename; + } + + /** + * {@inheritdoc} + * @see FileInterface::getExtension() + */ + public function getExtension(bool $withDot = false): string + { + if (null === $this->extension) { + $this->setPathInfo(); + } + + return ($withDot ? '.' : '') . $this->extension; + } + + /** + * {@inheritdoc} + * @see FileInterface::exists() + */ + public function exists(): bool + { + return is_file($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::getCreationTime() + */ + public function getCreationTime(): int + { + return is_file($this->filepath) ? filectime($this->filepath) : time(); + } + + /** + * {@inheritdoc} + * @see FileInterface::getModificationTime() + */ + public function getModificationTime(): int + { + return is_file($this->filepath) ? filemtime($this->filepath) : time(); + } + + /** + * {@inheritdoc} + * @see FileInterface::lock() + */ + public function lock(bool $block = true): bool + { + if (!$this->handle) { + if (!$this->mkdir($this->getPath())) { + throw new \RuntimeException('Creating directory failed for ' . $this->filepath); + } + $this->handle = @fopen($this->filepath, 'cb+'); + if (!$this->handle) { + $error = error_get_last(); + + throw new \RuntimeException("Opening file for writing failed on error {$error['message']}"); + } + } + + $lock = $block ? LOCK_EX : LOCK_EX | LOCK_NB; + + // Some filesystems do not support file locks, only fail if another process holds the lock. + $this->locked = flock($this->handle, $lock, $wouldblock) || !$wouldblock; + + return $this->locked; + } + + /** + * {@inheritdoc} + * @see FileInterface::unlock() + */ + public function unlock(): bool + { + if (!$this->handle) { + return false; + } + + if ($this->locked) { + flock($this->handle, LOCK_UN); + $this->locked = false; + } + + fclose($this->handle); + $this->handle = null; + + return true; + } + + /** + * {@inheritdoc} + * @see FileInterface::isLocked() + */ + public function isLocked(): bool + { + return $this->locked; + } + + /** + * {@inheritdoc} + * @see FileInterface::isReadable() + */ + public function isReadable(): bool + { + return is_readable($this->filepath) && is_file($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::isWritable() + */ + public function isWritable(): bool + { + if (!file_exists($this->filepath)) { + return $this->isWritablePath($this->getPath()); + } + + return is_writable($this->filepath) && is_file($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::load() + */ + public function load() + { + return file_get_contents($this->filepath); + } + + /** + * {@inheritdoc} + * @see FileInterface::save() + */ + public function save($data): void + { + $filepath = $this->filepath; + $dir = $this->getPath(); + + if (!$this->mkdir($dir)) { + throw new \RuntimeException('Creating directory failed for ' . $filepath); + } + + try { + if ($this->handle) { + $tmp = true; + // As we are using non-truncating locking, make sure that the file is empty before writing. + if (@ftruncate($this->handle, 0) === false || @fwrite($this->handle, $data) === false) { + // Writing file failed, throw an error. + $tmp = false; + } + } else { + // Create file with a temporary name and rename it to make the save action atomic. + $tmp = $this->tempname($filepath); + if (@file_put_contents($tmp, $data) === false) { + $tmp = false; + } elseif (@rename($tmp, $filepath) === false) { + @unlink($tmp); + $tmp = false; + } + } + } catch (\Exception $e) { + $tmp = false; + } + + if ($tmp === false) { + throw new \RuntimeException('Failed to save file ' . $filepath); + } + + // Touch the directory as well, thus marking it modified. + @touch($dir); + } + + /** + * {@inheritdoc} + * @see FileInterface::rename() + */ + public function rename(string $path): bool + { + if ($this->exists() && !@rename($this->filepath, $path)) { + return false; + } + + $this->setFilepath($path); + + return true; + } + + /** + * {@inheritdoc} + * @see FileInterface::delete() + */ + public function delete(): bool + { + return @unlink($this->filepath); + } + + /** + * @param string $dir + * @return bool + * @throws \RuntimeException + * @internal + */ + protected function mkdir(string $dir): bool + { + // Silence error for open_basedir; should fail in mkdir instead. + if (@is_dir($dir)) { + return true; + } + + $success = @mkdir($dir, 0777, true); + + if (!$success) { + // Take yet another look, make sure that the folder doesn't exist. + clearstatcache(true, $dir); + if (!@is_dir($dir)) { + return false; + } + } + + return true; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return [ + 'filepath' => $this->filepath + ]; + } + + /** + * @param array $serialized + */ + protected function doUnserialize(array $serialized): void + { + $this->setFilepath($serialized['filepath']); + } + + /** + * @param string $filepath + */ + protected function setFilepath(string $filepath): void + { + $this->filepath = $filepath; + $this->filename = null; + $this->basename = null; + $this->path = null; + $this->extension = null; + } + + protected function setPathInfo(): void + { + $pathInfo = $this->filesystem->pathinfo($this->filepath); + + $this->filename = $pathInfo['filename'] ?? null; + $this->basename = $pathInfo['basename'] ?? null; + $this->path = $pathInfo['dirname'] ?? null; + $this->extension = $pathInfo['extension'] ?? null; + } + + /** + * @param string $dir + * @return bool + * @internal + */ + protected function isWritablePath(string $dir): bool + { + if ($dir === '') { + return false; + } + + if (!file_exists($dir)) { + // Recursively look up in the directory tree. + return $this->isWritablePath($this->filesystem->parent($dir)); + } + + return is_dir($dir) && is_writable($dir); + } + + /** + * @param string $filename + * @param int $length + * @return string + */ + protected function tempname(string $filename, int $length = 5) + { + do { + $test = $filename . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length); + } while (file_exists($test)); + + return $test; + } +} diff --git a/system/src/Grav/Framework/File/CsvFile.php b/system/src/Grav/Framework/File/CsvFile.php new file mode 100644 index 0000000..53af766 --- /dev/null +++ b/system/src/Grav/Framework/File/CsvFile.php @@ -0,0 +1,31 @@ +formatter = $formatter; + } + + /** + * {@inheritdoc} + * @see FileInterface::load() + */ + public function load() + { + $raw = parent::load(); + + try { + return $raw !== false ? $this->formatter->decode($raw) : false; + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf("Failed to load file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + * @see FileInterface::save() + */ + public function save($data): void + { + if (\is_string($data)) { + // Make sure that the string is valid data. + try { + $this->formatter->decode($data); + } catch (RuntimeException $e) { + throw new RuntimeException(sprintf("Failed to save file '%s': %s", $this->getFilePath(), $e->getMessage()), $e->getCode(), $e); + } + $encoded = $data; + } else { + $encoded = $this->formatter->encode($data); + } + + parent::save($encoded); + } +} diff --git a/system/src/Grav/Framework/File/File.php b/system/src/Grav/Framework/File/File.php new file mode 100644 index 0000000..c1a2173 --- /dev/null +++ b/system/src/Grav/Framework/File/File.php @@ -0,0 +1,37 @@ +config = $config; + } + + /** + * @return string + */ + public function serialize(): string + { + return serialize($this->doSerialize()); + } + + /** + * @param string $serialized + */ + public function unserialize($serialized): void + { + $this->doUnserialize(unserialize($serialized, ['allowed_classes' => false])); + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::getDefaultFileExtension() + */ + public function getDefaultFileExtension(): string + { + $extensions = $this->getSupportedFileExtensions(); + + // Call fails on bad configuration. + return reset($extensions); + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::getSupportedFileExtensions() + */ + public function getSupportedFileExtensions(): array + { + $extensions = $this->getConfig('file_extension'); + + // Call fails on bad configuration. + return \is_string($extensions) ? [$extensions] : $extensions; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::encode() + */ + abstract public function encode($data): string; + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + abstract public function decode($data); + + /** + * Get either full configuration or a single option. + * + * @param string|null $name Configuration option (optional) + * @return mixed + */ + protected function getConfig(string $name = null) + { + if (null !== $name) { + return $this->config[$name] ?? null; + } + + return $this->config; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return ['config' => $this->config]; + } + + /** + * Note: if overridden, make sure you call parent::doUnserialize() + * + * @param array $serialized + */ + protected function doUnserialize(array $serialized): void + { + $this->config = $serialized['config']; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/CsvFormatter.php b/system/src/Grav/Framework/File/Formatter/CsvFormatter.php new file mode 100644 index 0000000..8945df0 --- /dev/null +++ b/system/src/Grav/Framework/File/Formatter/CsvFormatter.php @@ -0,0 +1,109 @@ + ['.csv', '.tsv'], + 'delimiter' => ',' + ]; + + parent::__construct($config); + } + + /** + * Returns delimiter used to both encode and decode CSV. + * + * @return string + */ + public function getDelimiter(): string + { + // Call fails on bad configuration. + return $this->getConfig('delimiter'); + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::encode() + */ + public function encode($data, $delimiter = null): string + { + if (count($data) === 0) { + return ''; + } + $delimiter = $delimiter ?? $this->getDelimiter(); + $header = array_keys(reset($data)); + + // Encode the field names + $string = $this->encodeLine($header, $delimiter); + + // Encode the data + foreach ($data as $row) { + $string .= $this->encodeLine($row, $delimiter); + } + + return $string; + } + + /** + * {@inheritdoc} + * @see FileFormatterInterface::decode() + */ + public function decode($data, $delimiter = null): array + { + $delimiter = $delimiter ?? $this->getDelimiter(); + $lines = preg_split('/\r\n|\r|\n/', $data); + + if ($lines === false) { + throw new \RuntimeException('Decoding CSV failed'); + } + + // Get the field names + $header = str_getcsv(array_shift($lines), $delimiter); + + // Get the data + $list = []; + foreach ($lines as $line) { + $list[] = array_combine($header, str_getcsv($line, $delimiter)); + } + + return $list; + } + + protected function encodeLine(array $line, $delimiter = null): string + { + foreach ($line as $key => &$value) { + $value = $this->escape((string)$value); + } + unset($value); + + return implode($delimiter, $line). "\n"; + } + + protected function escape(string $value) + { + if (preg_match('/[,"\r\n]/u', $value)) { + $value = '"' . preg_replace('/"/', '""', $value) . '"'; + } + + return $value; + } +} diff --git a/system/src/Grav/Framework/File/Formatter/FormatterInterface.php b/system/src/Grav/Framework/File/Formatter/FormatterInterface.php index 273bf78..a8d5bc5 100644 --- a/system/src/Grav/Framework/File/Formatter/FormatterInterface.php +++ b/system/src/Grav/Framework/File/Formatter/FormatterInterface.php @@ -1,44 +1,10 @@ config = $config + [ - 'file_extension' => '.ini' - ]; - } + $config += [ + 'file_extension' => '.ini' + ]; - /** - * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. - */ - public function getFileExtension() - { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED); - - return $this->getDefaultFileExtension(); + parent::__construct($config); } /** * {@inheritdoc} + * @see FileFormatterInterface::encode() */ - public function getDefaultFileExtension() - { - $extensions = $this->getSupportedFileExtensions(); - - return (string) reset($extensions); - } - - /** - * {@inheritdoc} - */ - public function getSupportedFileExtensions() - { - return (array) $this->config['file_extension']; - } - - /** - * {@inheritdoc} - */ - public function encode($data) + public function encode($data): string { $string = ''; foreach ($data as $key => $value) { @@ -71,8 +48,9 @@ class IniFormatter implements FormatterInterface /** * {@inheritdoc} + * @see FileFormatterInterface::decode() */ - public function decode($data) + public function decode($data): array { $decoded = @parse_ini_string($data); diff --git a/system/src/Grav/Framework/File/Formatter/JsonFormatter.php b/system/src/Grav/Framework/File/Formatter/JsonFormatter.php index 4a2d2fa..1ae16ae 100644 --- a/system/src/Grav/Framework/File/Formatter/JsonFormatter.php +++ b/system/src/Grav/Framework/File/Formatter/JsonFormatter.php @@ -1,64 +1,83 @@ config = $config + [ + $config += [ 'file_extension' => '.json', 'encode_options' => 0, - 'decode_assoc' => true + 'decode_assoc' => true, + 'decode_depth' => 512, + 'decode_options' => 0 ]; + + parent::__construct($config); } /** - * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + * Returns options used in encode() function. + * + * @return int */ - public function getFileExtension() + public function getEncodeOptions(): int { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED); + return $this->getConfig('encode_options'); + } - return $this->getDefaultFileExtension(); + /** + * Returns options used in decode() function. + * + * @return int + */ + public function getDecodeOptions(): int + { + return $this->getConfig('decode_options'); + } + + /** + * Returns recursion depth used in decode() function. + * + * @return int + */ + public function getDecodeDepth(): int + { + return $this->getConfig('decode_depth'); + } + + /** + * Returns true if JSON objects will be converted into associative arrays. + * + * @return bool + */ + public function getDecodeAssoc(): bool + { + return $this->getConfig('decode_assoc'); } /** * {@inheritdoc} + * @see FileFormatterInterface::encode() */ - public function getDefaultFileExtension() + public function encode($data): string { - $extensions = $this->getSupportedFileExtensions(); + $encoded = @json_encode($data, $this->getEncodeOptions()); - return (string) reset($extensions); - } - - /** - * {@inheritdoc} - */ - public function getSupportedFileExtensions() - { - return (array) $this->config['file_extension']; - } - - /** - * {@inheritdoc} - */ - public function encode($data) - { - $encoded = @json_encode($data, $this->config['encode_options']); - - if ($encoded === false) { - throw new \RuntimeException('Encoding JSON failed'); + if ($encoded === false && json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Encoding JSON failed: ' . json_last_error_msg()); } return $encoded; @@ -66,13 +85,14 @@ class JsonFormatter implements FormatterInterface /** * {@inheritdoc} + * @see FileFormatterInterface::decode() */ public function decode($data) { - $decoded = @json_decode($data, $this->config['decode_assoc']); + $decoded = @json_decode($data, $this->getDecodeAssoc(), $this->getDecodeDepth(), $this->getDecodeOptions()); - if ($decoded === false) { - throw new \RuntimeException('Decoding JSON failed'); + if (null === $decoded && json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException('Decoding JSON failed: ' . json_last_error_msg()); } return $decoded; diff --git a/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php b/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php index 415f7ce..a2f84e8 100644 --- a/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php +++ b/system/src/Grav/Framework/File/Formatter/MarkdownFormatter.php @@ -1,23 +1,26 @@ config = $config + [ + $config += [ 'file_extension' => '.md', 'header' => 'header', 'body' => 'markdown', @@ -25,44 +28,59 @@ class MarkdownFormatter implements FormatterInterface 'yaml' => ['inline' => 20] ]; - $this->headerFormatter = $headerFormatter ?: new YamlFormatter($this->config['yaml']); + parent::__construct($config); + + $this->headerFormatter = $headerFormatter ?: new YamlFormatter($config['yaml']); } /** - * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + * Returns header field used in both encode() and decode(). + * + * @return string */ - public function getFileExtension() + public function getHeaderField(): string { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED); + return $this->getConfig('header'); + } - return $this->getDefaultFileExtension(); + /** + * Returns body field used in both encode() and decode(). + * + * @return string + */ + public function getBodyField(): string + { + return $this->getConfig('body'); + } + + /** + * Returns raw field used in both encode() and decode(). + * + * @return string + */ + public function getRawField(): string + { + return $this->getConfig('raw'); + } + + /** + * Returns header formatter object used in both encode() and decode(). + * + * @return FileFormatterInterface + */ + public function getHeaderFormatter(): FileFormatterInterface + { + return $this->headerFormatter; } /** * {@inheritdoc} + * @see FileFormatterInterface::encode() */ - public function getDefaultFileExtension() + public function encode($data): string { - $extensions = $this->getSupportedFileExtensions(); - - return (string) reset($extensions); - } - - /** - * {@inheritdoc} - */ - public function getSupportedFileExtensions() - { - return (array) $this->config['file_extension']; - } - - /** - * {@inheritdoc} - */ - public function encode($data) - { - $headerVar = $this->config['header']; - $bodyVar = $this->config['body']; + $headerVar = $this->getHeaderField(); + $bodyVar = $this->getBodyField(); $header = isset($data[$headerVar]) ? (array) $data[$headerVar] : []; $body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : ''; @@ -70,7 +88,7 @@ class MarkdownFormatter implements FormatterInterface // Create Markdown file with YAML header. $encoded = ''; if ($header) { - $encoded = "---\n" . trim($this->headerFormatter->encode($data['header'])) . "\n---\n\n"; + $encoded = "---\n" . trim($this->getHeaderFormatter()->encode($data['header'])) . "\n---\n\n"; } $encoded .= $body; @@ -82,13 +100,15 @@ class MarkdownFormatter implements FormatterInterface /** * {@inheritdoc} + * @see FileFormatterInterface::decode() */ - public function decode($data) + public function decode($data): array { - $headerVar = $this->config['header']; - $bodyVar = $this->config['body']; - $rawVar = $this->config['raw']; + $headerVar = $this->getHeaderField(); + $bodyVar = $this->getBodyField(); + $rawVar = $this->getRawField(); + // Define empty content $content = [ $headerVar => [], $bodyVar => '' @@ -109,7 +129,7 @@ class MarkdownFormatter implements FormatterInterface if ($rawVar) { $content[$rawVar] = $frontmatter; } - $content[$headerVar] = $this->headerFormatter->decode($frontmatter); + $content[$headerVar] = $this->getHeaderFormatter()->decode($frontmatter); $content[$bodyVar] = $matches[2]; } diff --git a/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php b/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php index 8830218..73ed812 100644 --- a/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php +++ b/system/src/Grav/Framework/File/Formatter/SerializeFormatter.php @@ -1,73 +1,64 @@ config = $config + [ - 'file_extension' => '.ser' - ]; + $config += [ + 'file_extension' => '.ser', + 'decode_options' => ['allowed_classes' => [\stdClass::class]] + ]; + + parent::__construct($config); } /** - * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + * Returns options used in decode(). + * + * By default only allow stdClass class. + * + * @return array|bool */ - public function getFileExtension() + public function getOptions() { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED); - - return $this->getDefaultFileExtension(); + return $this->getConfig('decode_options'); } /** * {@inheritdoc} + * @see FileFormatterInterface::encode() */ - public function getDefaultFileExtension() - { - $extensions = $this->getSupportedFileExtensions(); - - return (string) reset($extensions); - } - - /** - * {@inheritdoc} - */ - public function getSupportedFileExtensions() - { - return (array) $this->config['file_extension']; - } - - /** - * {@inheritdoc} - */ - public function encode($data) + public function encode($data): string { return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r'])); } /** * {@inheritdoc} + * @see FileFormatterInterface::decode() */ public function decode($data) { - $decoded = @unserialize($data); + $decoded = @unserialize($data, $this->getOptions()); - if ($decoded === false) { + if ($decoded === false && $data !== serialize(false)) { throw new \RuntimeException('Decoding serialized data failed'); } @@ -82,11 +73,11 @@ class SerializeFormatter implements FormatterInterface * @param array $replace * @return mixed */ - protected function preserveLines($data, $search, $replace) + protected function preserveLines($data, array $search, array $replace) { - if (is_string($data)) { + if (\is_string($data)) { $data = str_replace($search, $replace, $data); - } elseif (is_array($data)) { + } elseif (\is_array($data)) { foreach ($data as &$value) { $value = $this->preserveLines($value, $search, $replace); } diff --git a/system/src/Grav/Framework/File/Formatter/YamlFormatter.php b/system/src/Grav/Framework/File/Formatter/YamlFormatter.php index d59f9e7..189626d 100644 --- a/system/src/Grav/Framework/File/Formatter/YamlFormatter.php +++ b/system/src/Grav/Framework/File/Formatter/YamlFormatter.php @@ -1,72 +1,80 @@ config = $config + [ + $config += [ 'file_extension' => '.yaml', 'inline' => 5, 'indent' => 2, 'native' => true, 'compat' => true ]; + + parent::__construct($config); } /** - * @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead. + * @return int */ - public function getFileExtension() + public function getInlineOption(): int { - user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED); + return $this->getConfig('inline'); + } - return $this->getDefaultFileExtension(); + /** + * @return int + */ + public function getIndentOption(): int + { + return $this->getConfig('indent'); + } + + /** + * @return bool + */ + public function useNativeDecoder(): bool + { + return $this->getConfig('native'); + } + + /** + * @return bool + */ + public function useCompatibleDecoder(): bool + { + return $this->getConfig('compat'); } /** * {@inheritdoc} + * @see FileFormatterInterface::encode() */ - public function getDefaultFileExtension() - { - $extensions = $this->getSupportedFileExtensions(); - - return (string) reset($extensions); - } - - /** - * {@inheritdoc} - */ - public function getSupportedFileExtensions() - { - return (array) $this->config['file_extension']; - } - - /** - * {@inheritdoc} - */ - public function encode($data, $inline = null, $indent = null) + public function encode($data, $inline = null, $indent = null): string { try { - return (string) YamlParser::dump( + return YamlParser::dump( $data, - $inline ? (int) $inline : $this->config['inline'], - $indent ? (int) $indent : $this->config['indent'], + $inline ? (int) $inline : $this->getInlineOption(), + $indent ? (int) $indent : $this->getIndentOption(), YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE ); } catch (DumpException $e) { @@ -76,14 +84,15 @@ class YamlFormatter implements FormatterInterface /** * {@inheritdoc} + * @see FileFormatterInterface::decode() */ - public function decode($data) + public function decode($data): array { // Try native PECL YAML PHP extension first if available. - if ($this->config['native'] && function_exists('yaml_parse')) { + if (\function_exists('yaml_parse') && $this->useNativeDecoder()) { // Safely decode YAML. $saved = @ini_get('yaml.decode_php'); - @ini_set('yaml.decode_php', 0); + @ini_set('yaml.decode_php', '0'); $decoded = @yaml_parse($data); @ini_set('yaml.decode_php', $saved); @@ -95,7 +104,7 @@ class YamlFormatter implements FormatterInterface try { return (array) YamlParser::parse($data); } catch (ParseException $e) { - if ($this->config['compat']) { + if ($this->useCompatibleDecoder()) { return (array) FallbackYamlParser::parse($data); } diff --git a/system/src/Grav/Framework/File/IniFile.php b/system/src/Grav/Framework/File/IniFile.php new file mode 100644 index 0000000..cc861f9 --- /dev/null +++ b/system/src/Grav/Framework/File/IniFile.php @@ -0,0 +1,31 @@ +setNormalization() + * + * @return Filesystem + */ + public static function getInstance(bool $normalize = null): Filesystem + { + if ($normalize === true) { + $instance = &static::$safe; + } elseif ($normalize === false) { + $instance = &static::$unsafe; + } else { + $instance = &static::$default; + } + + if (null === $instance) { + $instance = new static($normalize); + } + + return $instance; + } + + /** + * Always use Filesystem::getInstance() instead. + * + * @param bool|null $normalize + */ + protected function __construct(bool $normalize = null) + { + $this->normalize = $normalize; + } + + /** + * Set path normalization. + * + * Default option enables normalization for the streams only, but you can force the normalization to be either + * on or off for every path. Disabling path normalization speeds up the calls, but may cause issues if paths were + * not normalized. + * + * @param bool|null $normalize + * + * @return Filesystem + */ + public function setNormalization(bool $normalize = null): self + { + return static::getInstance($normalize); + } + + /** + * Force all paths to be normalized. + * + * @return static + */ + public function unsafe(): self + { + return static::getInstance(true); + } + + /** + * Force all paths not to be normalized (speeds up the calls if given paths are known to be normalized). + * + * @return static + */ + public function safe(): self + { + return static::getInstance(false); + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::parent() + */ + public function parent(string $path, int $levels = 1): string + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + if ($this->normalize !== false) { + $path = $this->normalizePathPart($path); + } + + if ($path === '' || $path === '.') { + return ''; + } + + [$scheme, $parent] = $this->dirnameInternal($scheme, $path, $levels); + + return $parent !== $path ? $this->toString($scheme, $parent) : ''; + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::normalize() + */ + public function normalize(string $path): string + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + $path = $this->normalizePathPart($path); + + return $this->toString($scheme, $path); + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::dirname() + */ + public function dirname(string $path, int $levels = 1): string + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + if ($this->normalize || ($scheme && null === $this->normalize)) { + $path = $this->normalizePathPart($path); + } + + [$scheme, $path] = $this->dirnameInternal($scheme, $path, $levels); + + return $this->toString($scheme, $path); + } + + /** + * {@inheritdoc} + * @see FilesystemInterface::pathinfo() + */ + public function pathinfo(string $path, int $options = null) + { + [$scheme, $path] = $this->getSchemeAndHierarchy($path); + + if ($this->normalize || ($scheme && null === $this->normalize)) { + $path = $this->normalizePathPart($path); + } + + return $this->pathinfoInternal($scheme, $path, $options); + } + + /** + * @param string|null $scheme + * @param string $path + * @param int $levels + * + * @return array + */ + protected function dirnameInternal(?string $scheme, string $path, int $levels = 1): array + { + $path = \dirname($path, $levels); + + if (null !== $scheme && $path === '.') { + return [$scheme, '']; + } + + return [$scheme, $path]; + } + + /** + * @param string|null $scheme + * @param string $path + * @param int|null $options + * + * @return array + */ + protected function pathinfoInternal(?string $scheme, string $path, int $options = null) + { + $info = $options ? \pathinfo($path, $options) : \pathinfo($path); + + if (null !== $scheme) { + $info['scheme'] = $scheme; + $dirname = isset($info['dirname']) && $info['dirname'] !== '.' ? $info['dirname'] : null; + + if (null !== $dirname) { + $info['dirname'] = $scheme . '://' . $dirname; + } else { + $info = ['dirname' => $scheme . '://'] + $info; + } + } + + return $info; + } + + /** + * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> array(file, tmp)). + * + * @param string $filename + * + * @return array + */ + protected function getSchemeAndHierarchy(string $filename): array + { + $components = explode('://', $filename, 2); + + return 2 === \count($components) ? $components : [null, $components[0]]; + } + + /** + * @param string|null $scheme + * @param string $path + * + * @return string + */ + protected function toString(?string $scheme, string $path): string + { + if ($scheme) { + return $scheme . '://' . $path; + } + + return $path; + } + + /** + * @param string $path + * + * @return string + * @throws \RuntimeException + */ + protected function normalizePathPart(string $path): string + { + // Quick check for empty path. + if ($path === '' || $path === '.') { + return ''; + } + + // Quick check for root. + if ($path === '/') { + return '/'; + } + + // If the last character is not '/' or any of '\', './', '//' and '..' are not found, path is clean and we're done. + if ($path[-1] !== '/' && !preg_match('`(\\\\|\./|//|\.\.)`', $path)) { + return $path; + } + + // Convert backslashes + $path = strtr($path, ['\\' => '/']); + + $parts = explode('/', $path); + + // Keep absolute paths. + $root = ''; + if ($parts[0] === '') { + $root = '/'; + array_shift($parts); + } + + $list = []; + foreach ($parts as $i => $part) { + // Remove empty parts: // and /./ + if ($part === '' || $part === '.') { + continue; + } + + // Resolve /../ by removing path part. + if ($part === '..') { + $test = array_shift($list); + if ($test === null) { + // Oops, user tried to access something outside of our root folder. + throw new \RuntimeException("Bad path {$path}"); + } + } + + $list[] = $part; + } + + // Build path back together. + return $root . implode('/', $list); + } +} diff --git a/system/src/Grav/Framework/Filesystem/Interfaces/FilesystemInterface.php b/system/src/Grav/Framework/Filesystem/Interfaces/FilesystemInterface.php new file mode 100644 index 0000000..7082ebf --- /dev/null +++ b/system/src/Grav/Framework/Filesystem/Interfaces/FilesystemInterface.php @@ -0,0 +1,75 @@ += 1). + * + * @return string Returns parent path. + * @throws \RuntimeException + * @api + */ + public function parent(string $path, int $levels = 1): string; + + /** + * Normalize path by cleaning up `\`, `/./`, `//` and `/../`. + * + * @param string $path A filename or path, does not need to exist as a file. + * + * @return string Returns normalized path. + * @throws \RuntimeException + * @api + */ + public function normalize(string $path): string; + + /** + * Stream-safe `\dirname()` replacement. + * + * @see http://php.net/manual/en/function.dirname.php + * + * @param string $path A filename or path, does not need to exist as a file. + * @param int $levels The number of parent directories to go up (>= 1). + * + * @return string Returns path to the directory. + * @throws \RuntimeException + * @api + */ + public function dirname(string $path, int $levels = 1): string; + + /** + * Stream-safe `\pathinfo()` replacement. + * + * @see http://php.net/manual/en/function.pathinfo.php + * + * @param string $path A filename or path, does not need to exist as a file. + * @param int $options A PATHINFO_* constant. + * + * @return array|string + * @api + */ + public function pathinfo(string $path, int $options = null); +} diff --git a/system/src/Grav/Framework/Flex/Flex.php b/system/src/Grav/Framework/Flex/Flex.php new file mode 100644 index 0000000..e87e903 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Flex.php @@ -0,0 +1,311 @@ + blueprint file, ...] + * @param array $config + */ + public function __construct(array $types, array $config) + { + $this->config = $config; + $this->types = []; + + foreach ($types as $type => $blueprint) { + $this->addDirectoryType($type, $blueprint); + } + } + + /** + * @param string $type + * @param string $blueprint + * @param array $config + * @return $this + */ + public function addDirectoryType(string $type, string $blueprint, array $config = []) + { + $config = array_merge_recursive(['enabled' => true], $this->config['object'] ?? [], $config); + + $this->types[$type] = new FlexDirectory($type, $blueprint, $config); + + return $this; + } + + /** + * @param FlexDirectory $directory + * @return $this + */ + public function addDirectory(FlexDirectory $directory) + { + $this->types[$directory->getFlexType()] = $directory; + + return $this; + } + + /** + * @param string $type + * @return bool + */ + public function hasDirectory(string $type): bool + { + return isset($this->types[$type]); + } + + /** + * @param array|string[] $types + * @param bool $keepMissing + * @return array|FlexDirectory[] + */ + public function getDirectories(array $types = null, bool $keepMissing = false): array + { + if ($types === null) { + return $this->types; + } + + // Return the directories in the given order. + $directories = []; + foreach ($types as $type) { + $directories[$type] = $this->types[$type] ?? null; + } + + return $keepMissing ? $directories : array_filter($directories); + } + + /** + * @param string $type + * @return FlexDirectory|null + */ + public function getDirectory(string $type): ?FlexDirectory + { + return $this->types[$type] ?? null; + } + + /** + * @param string $type + * @param array|null $keys + * @param string|null $keyField + * @return FlexCollectionInterface|null + */ + public function getCollection(string $type, array $keys = null, string $keyField = null): ?FlexCollectionInterface + { + $directory = $type ? $this->getDirectory($type) : null; + + return $directory ? $directory->getCollection($keys, $keyField) : null; + } + + /** + * @param array $keys + * @param array $options In addition to the options in getObjects(), following options can be passed: + * collection_class: Class to be used to create the collection. Defaults to ObjectCollection. + * @return FlexCollectionInterface + * @throws \RuntimeException + */ + public function getMixedCollection(array $keys, array $options = []): FlexCollectionInterface + { + $collectionClass = $options['collection_class'] ?? ObjectCollection::class; + if (!class_exists($collectionClass)) { + throw new \RuntimeException(sprintf('Cannot create collection: Class %s does not exist', $collectionClass)); + } + + $objects = $this->getObjects($keys, $options); + + return new $collectionClass($objects); + } + + /** + * @param array $keys + * @param array $options Following optional options can be passed: + * types: List of allowed types. + * type: Allowed type if types isn't defined, otherwise acts as default_type. + * default_type: Set default type for objects given without type (only used if key_field isn't set). + * keep_missing: Set to true if you want to return missing objects as null. + * key_field: Key field which is used to match the objects. + * @return array + */ + public function getObjects(array $keys, array $options = []): array + { + $type = $options['type'] ?? null; + $defaultType = $options['default_type'] ?? $type ?? null; + $keyField = $options['key_field'] ?? 'flex_key'; + + // Prepare empty result lists for all requested Flex types. + $types = $options['types'] ?? (array)$type ?: null; + if ($types) { + $types = array_fill_keys($types, []); + } + $strict = isset($types); + + $guessed = []; + if ($keyField === 'flex_key') { + // We need to split Flex key lookups into individual directories. + $undefined = []; + $keyFieldFind = 'storage_key'; + + foreach ($keys as $flexKey) { + if (!$flexKey) { + continue; + } + + $flexKey = (string)$flexKey; + // Normalize key and type using fallback to default type if it was set. + [$key, $type, $guess] = $this->resolveKeyAndType($flexKey, $defaultType); + + if ($type === '' && $types) { + // Add keys which are not associated to any Flex type. They will be included to every Flex type. + foreach ($types as $type => &$array) { + $array[] = $key; + $guessed[$key][] = "{$type}.obj:{$key}"; + } + unset($array); + } elseif (!$strict || isset($types[$type])) { + // Collect keys by their Flex type. If allowed types are defined, only include values from those types. + $types[$type][] = $key; + if ($guess) { + $guessed[$key][] = "{$type}.obj:{$key}"; + } + } + } + } else { + // We are using a specific key field, make every key undefined. + $undefined = $keys; + $keyFieldFind = $keyField; + } + + if (!$types) { + return []; + } + + $list = [[]]; + foreach ($types as $type => $typeKeys) { + // Also remember to look up keys from undefined Flex types. + $lookupKeys = $undefined ? array_merge($typeKeys, $undefined) : $typeKeys; + + $collection = $this->getCollection($type, $lookupKeys, $keyFieldFind); + if ($collection && $keyFieldFind !== $keyField) { + $collection = $collection->withKeyField($keyField); + } + + $list[] = $collection ? $collection->toArray() : []; + } + + // Merge objects from individual types back together. + $list = array_merge(...$list); + + // Use the original key ordering. + if (!$guessed) { + $list = array_replace(array_fill_keys($keys, null), $list); + } else { + // We have mixed keys, we need to map flex keys back to storage keys. + $results = []; + foreach ($keys as $key) { + $flexKey = $guessed[$key] ?? $key; + if (\is_array($flexKey)) { + $result = null; + foreach ($flexKey as $tryKey) { + if ($result = $list[$tryKey] ?? null) { + // Use the first matching object (conflicting objects will be ignored for now). + break; + } + } + } else { + $result = $list[$flexKey] ?? null; + } + + $results[$key] = $result; + } + + $list = $results; + } + + // Remove missing objects if not asked to keep them. + if (empty($option['keep_missing'])) { + $list = array_filter($list); + } + + return $list; + } + + /** + * @param string $key + * @param string|null $type + * @param string|null $keyField + * @return FlexObjectInterface|null + */ + public function getObject(string $key, string $type = null, string $keyField = null): ?FlexObjectInterface + { + if (null === $type && null === $keyField) { + // Special handling for quick Flex key lookups. + $keyField = 'storage_key'; + [$type, $key] = $this->resolveKeyAndType($key, $type); + } else { + $type = $this->resolveType($type); + } + + if ($type === '' || $key === '') { + return null; + } + + $directory = $this->getDirectory($type); + + return $directory ? $directory->getObject($key, $keyField) : null; + } + + /** + * @return int + */ + public function count(): int + { + return \count($this->types); + } + + protected function resolveKeyAndType(string $flexKey, string $type = null): array + { + $guess = false; + if (strpos($flexKey, ':') !== false) { + [$type, $key] = explode(':', $flexKey, 2); + + $type = $this->resolveType($type); + } else { + $key = $flexKey; + $type = (string)$type; + $guess = true; + } + + return [$key, $type, $guess]; + } + + protected function resolveType(string $type = null): string + { + if (null !== $type && strpos($type, '.') !== false) { + return preg_replace('|\.obj$|', '', $type); + } + + return $type ?? ''; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexCollection.php b/system/src/Grav/Framework/Flex/FlexCollection.php new file mode 100644 index 0000000..5e5431b --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexCollection.php @@ -0,0 +1,525 @@ + true, + 'getType' => true, + 'getFlexDirectory' => true, + 'getCacheKey' => true, + 'getCacheChecksum' => true, + 'getTimestamp' => true, + 'hasProperty' => true, + 'getProperty' => true, + 'hasNestedProperty' => true, + 'getNestedProperty' => true, + 'orderBy' => true, + + 'render' => false, + 'isAuthorized' => 'session', + 'search' => true, + 'sort' => true, + ]; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::createFromArray() + */ + public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null) + { + $instance = new static($entries, $directory); + $instance->setKeyField($keyField); + + return $instance; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::__construct() + */ + public function __construct(array $entries = [], FlexDirectory $directory = null) + { + parent::__construct($entries); + + if ($directory) { + $this->setFlexDirectory($directory)->setKey($directory->getFlexType()); + } + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::search() + */ + public function search(string $search, $properties = null, array $options = null) + { + $matching = $this->call('search', [$search, $properties, $options]); + $matching = array_filter($matching); + + if ($matching) { + uksort($matching, function ($a, $b) { + return -($a <=> $b); + }); + } + + return $this->select(array_keys($matching)); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::sort() + */ + public function sort(array $order) + { + $criteria = Criteria::create()->orderBy($order); + + /** @var FlexCollectionInterface $matching */ + $matching = $this->matching($criteria); + + return $matching; + } + + /** + * @param array $filters + * @return FlexCollectionInterface|Collection + */ + public function filterBy(array $filters) + { + $expr = Criteria::expr(); + $criteria = Criteria::create(); + + foreach ($filters as $key => $value) { + $criteria->andWhere($expr->eq($key, $value)); + } + + return $this->matching($criteria); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexType() + */ + public function getFlexType(): string + { + return $this->_flexDirectory->getFlexType(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getFlexDirectory(): FlexDirectory + { + return $this->_flexDirectory; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getTimestamp() + */ + public function getTimestamp(): int + { + $timestamps = $this->getTimestamps(); + + return $timestamps ? max($timestamps) : time(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getCacheKey(): string + { + return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->call('getKey'))); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getCacheChecksum(): string + { + return sha1(json_encode($this->getTimestamps())); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getTimestamps(): array + { + /** @var int[] $timestamps */ + $timestamps = $this->call('getTimestamp'); + + return $timestamps; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getStorageKeys(): array + { + /** @var string[] $keys */ + $keys = $this->call('getStorageKey'); + + return $keys; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getFlexKeys(): array + { + /** @var string[] $keys */ + $keys = $this->call('getFlexKey'); + + return $keys; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::withKeyField() + */ + public function withKeyField(string $keyField = null) + { + $keyField = $keyField ?: 'key'; + if ($keyField === $this->getKeyField()) { + return $this; + } + + $entries = []; + foreach ($this as $key => $object) { + // TODO: remove hardcoded logic + if ($keyField === 'storage_key') { + $entries[$object->getStorageKey()] = $object; + } elseif ($keyField === 'flex_key') { + $entries[$object->getFlexKey()] = $object; + } elseif ($keyField === 'key') { + $entries[$object->getKey()] = $object; + } + } + + return $this->createFrom($entries, $keyField); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getIndex() + */ + public function getIndex() + { + return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField()); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + if (null === $layout) { + $layout = 'default'; + } + $type = $this->getFlexType(); + + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')'); + + $cache = $key = null; + foreach ($context as $value) { + if (!\is_scalar($value)) { + $key = false; + } + } + + if ($key !== false) { + $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context)); + $cache = $this->getCache('render'); + } + + try { + $data = $cache ? $cache->get($key) : null; + + $block = $data ? HtmlBlock::fromArray($data) : null; + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + $block = null; + } catch (\InvalidArgumentException $e) { + $debugger->addException($e); + $block = null; + } + + $checksum = $this->getCacheChecksum(); + if ($block && $checksum !== $block->getChecksum()) { + $block = null; + } + + if (!$block) { + $block = HtmlBlock::create($key); + $block->setChecksum($checksum); + if ($key === false) { + $block->disableCache(); + } + + $grav->fireEvent('onFlexCollectionRender', new Event([ + 'collection' => $this, + 'layout' => &$layout, + 'context' => &$context + ])); + + $output = $this->getTemplate($layout)->render( + ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'collection' => $this, 'layout' => $layout] + $context + ); + + if ($debugger->enabled()) { + $output = "\n\n{$output}\n\n"; + } + + $block->setContent($output); + + try { + $cache && $block->isCached() && $cache->set($key, $block->toArray()); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + } + } + + $debugger->stopTimer('flex-collection-' . $debugKey); + + return $block; + } + + /** + * @param bool $prefix + * @return string + * @deprecated 1.6 Use `->getFlexType()` instead. + */ + public function getType($prefix = false) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + $type = $prefix ? $this->getTypePrefix() : ''; + + return $type . $this->getFlexType(); + } + + /** + * @param FlexDirectory $type + * @return $this + */ + public function setFlexDirectory(FlexDirectory $type) + { + $this->_flexDirectory = $type; + + return $this; + } + + /** + * @return array + */ + public function getMetaData(string $key) : array + { + $object = $this->get($key); + + return $object instanceof FlexObjectInterface ? $object->getMetaData() : []; + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + return $this->_flexDirectory->getCache($namespace); + } + + /** + * @return string + */ + public function getKeyField(): string + { + return $this->_keyField ?? 'storage_key'; + } + + /** + * @param string $action + * @param string|null $scope + * @param UserInterface|null $user + * @return static + */ + public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) + { + $list = $this->call('isAuthorized', [$action, $scope, $user]); + $list = \array_filter($list); + + return $this->select(array_keys($list)); + } + + /** + * @param string $value + * @param string $field + * @return FlexObject|null + */ + public function find($value, $field = 'id') + { + if ($value) foreach ($this as $element) { + if (mb_strtolower($element->getProperty($field)) === mb_strtolower($value)) { + return $element; + } + } + + return null; + } + + /** + * @return array + */ + public function jsonSerialize() + { + $elements = []; + + /** + * @var string $key + * @var array|FlexObject $object + */ + foreach ($this->getElements() as $key => $object) { + $elements[$key] = \is_array($object) ? $object : $object->jsonSerialize(); + } + + return $elements; + } + + public function __debugInfo() + { + return [ + 'type:private' => $this->getFlexType(), + 'key:private' => $this->getKey(), + 'objects_key:private' => $this->getKeyField(), + 'objects:private' => $this->getElements() + ]; + } + + /** + * Creates a new instance from the specified elements. + * + * This method is provided for derived classes to specify how a new + * instance should be created when constructor semantics have changed. + * + * @param array $elements Elements. + * @param string|null $keyField + * + * @return static + * @throws \InvalidArgumentException + */ + protected function createFrom(array $elements, $keyField = null) + { + $collection = new static($elements, $this->_flexDirectory); + $collection->setKeyField($keyField ?: $this->_keyField); + + return $collection; + } + + /** + * @return string + */ + protected function getTypePrefix(): string + { + return 'c.'; + } + + /** + * @param string $layout + * @return TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + try { + return $twig->twig()->resolveTemplate( + [ + "flex-objects/layouts/{$this->getFlexType()}/collection/{$layout}.html.twig", + "flex-objects/layouts/_default/collection/{$layout}.html.twig" + ] + ); + } catch (LoaderError $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + return $twig->twig()->resolveTemplate(['flex-objects/layouts/404.html.twig']); + } + } + + /** + * @param string $type + * @return FlexDirectory + */ + protected function getRelatedDirectory($type): ?FlexDirectory + { + /** @var Flex $flex */ + $flex = Grav::instance()['flex_objects']; + + return $flex->getDirectory($type); + } + + protected function setKeyField($keyField = null): void + { + $this->_keyField = $keyField ?? 'storage_key'; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexDirectory.php b/system/src/Grav/Framework/Flex/FlexDirectory.php new file mode 100644 index 0000000..c75a901 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexDirectory.php @@ -0,0 +1,669 @@ +type = $type; + $this->blueprints = []; + $this->blueprint_file = $blueprint_file; + $this->defaults = $defaults; + $this->enabled = !empty($defaults['enabled']); + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return string + * @deprecated 1.6 Use ->getFlexType() method instead. + */ + public function getType(): string + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + return $this->type; + } + + /** + * @return string + */ + public function getFlexType(): string + { + return $this->type; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->getBlueprintInternal()->get('title', ucfirst($this->getFlexType())); + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->getBlueprintInternal()->get('description', ''); + } + + /** + * @param string|null $name + * @param mixed $default + * @return mixed + */ + public function getConfig(string $name = null, $default = null) + { + if (null === $this->config) { + $this->config = new Config(array_merge_recursive($this->getBlueprintInternal()->get('config', []), $this->defaults)); + } + + return null === $name ? $this->config : $this->config->get($name, $default); + } + + /** + * @param string $type + * @param string $context + * @return Blueprint + */ + public function getBlueprint(string $type = '', string $context = '') + { + $blueprint = $this->getBlueprintInternal($type, $context); + + if (empty($this->blueprints_init[$type])) { + $this->blueprints_init[$type] = true; + + $blueprint->setScope('object'); + $blueprint->init(); + if (empty($blueprint->fields())) { + throw new RuntimeException(sprintf('Flex: Blueprint for %s is missing', $this->type)); + } + } + + return $blueprint; + } + + /** + * @param string $view + * @return string + */ + public function getBlueprintFile(string $view = ''): string + { + $file = $this->blueprint_file; + if ($view !== '') { + $file = preg_replace('/\.yaml/', "/{$view}.yaml", $file); + } + + return $file; + } + + /** + * Get collection. In the site this will be filtered by the default filters (published etc). + * + * Use $directory->getIndex() if you want unfiltered collection. + * + * @param array|null $keys Array of keys. + * @param string|null $keyField Field to be used as the key. + * @return FlexCollectionInterface + */ + public function getCollection(array $keys = null, string $keyField = null): FlexCollectionInterface + { + // Get all selected entries. + $index = $this->getIndex($keys, $keyField); + + if (!Utils::isAdminPlugin()) { + // If not in admin, filter the list by using default filters. + $filters = (array)$this->getConfig('site.filter', []); + + foreach ($filters as $filter) { + $index = $index->{$filter}(); + } + } + + return $index; + } + + /** + * Get the full collection of all stored objects. + * + * Use $directory->getCollection() if you want a filtered collection. + * + * @param array|null $keys Array of keys. + * @param string|null $keyField Field to be used as the key. + * @return FlexIndexInterface + */ + public function getIndex(array $keys = null, string $keyField = null): FlexIndexInterface + { + $index = clone $this->loadIndex(); + $index = $index->withKeyField($keyField); + + if (null !== $keys) { + $index = $index->select($keys); + } + + return $index->getIndex(); + } + + /** + * Returns an object if it exists. + * + * Note: It is not safe to use the object without checking if the user can access it. + * + * @param string $key + * @param string|null $keyField Field to be used as the key. + * @return FlexObjectInterface|null + */ + public function getObject($key, string $keyField = null): ?FlexObjectInterface + { + return $this->getIndex(null, $keyField)->get($key); + } + + /** + * @param array $data + * @param string|null $key + * @return FlexObjectInterface + */ + public function update(array $data, string $key = null): FlexObjectInterface + { + $object = null !== $key ? $this->getIndex()->get($key): null; + + $storage = $this->getStorage(); + + if (null === $object) { + $object = $this->createObject($data, $key, true); + $key = $object->getStorageKey(); + + if ($key) { + $rows = $storage->replaceRows([$key => $object->prepareStorage()]); + } else { + $rows = $storage->createRows([$object->prepareStorage()]); + } + } else { + $oldKey = $object->getStorageKey(); + $object->update($data); + $newKey = $object->getStorageKey(); + + if ($oldKey !== $newKey) { + $object->triggerEvent('move'); + $storage->renameRow($oldKey, $newKey); + // TODO: media support. + } + + $object->save(); + } + + try { + $this->clearCache(); + } catch (InvalidArgumentException $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + // Caching failed, but we can ignore that for now. + } + + return $object; + } + + /** + * @param string $key + * @return FlexObjectInterface|null + */ + public function remove(string $key): ?FlexObjectInterface + { + $object = $this->getIndex()->get($key); + if (!$object) { + return null; + } + + $object->delete(); + + return $object; + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + $namespace = $namespace ?: 'index'; + $cache = $this->cache[$namespace] ?? null; + + if (null === $cache) { + try { + $grav = Grav::instance(); + + /** @var Cache $gravCache */ + $gravCache = $grav['cache']; + $config = $this->getConfig('cache.' . $namespace); + if (empty($config['enabled'])) { + $cache = new MemoryCache('flex-objects-' . $this->getFlexType()); + } else { + $timeout = $config['timeout'] ?? 60; + + $key = $gravCache->getKey(); + if (Utils::isAdminPlugin()) { + $key = substr($key, 0, -1); + } + $cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout); + } + } catch (\Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + $cache = new MemoryCache('flex-objects-' . $this->getFlexType()); + } + + // Disable cache key validation. + $cache->setValidation(false); + $this->cache[$namespace] = $cache; + } + + return $cache; + } + + /** + * @return $this + */ + public function clearCache() + { + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage(sprintf('Flex: Clearing all %s cache', $this->type), 'debug'); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $locator->clearCache(); + + $this->getCache('index')->clear(); + $this->getCache('object')->clear(); + $this->getCache('render')->clear(); + + $this->index = null; + + return $this; + } + + /** + * @param string|null $key + * @return string + */ + public function getStorageFolder(string $key = null): string + { + return $this->getStorage()->getStoragePath($key); + } + + /** + * @param string|null $key + * @return string + */ + public function getMediaFolder(string $key = null): string + { + return $this->getStorage()->getMediaPath($key); + } + + /** + * @return FlexStorageInterface + */ + public function getStorage(): FlexStorageInterface + { + if (null === $this->storage) { + $this->storage = $this->createStorage(); + } + + return $this->storage; + } + + /** + * @param array $data + * @param string $key + * @param bool $validate + * @return FlexObjectInterface + */ + public function createObject(array $data, string $key = '', bool $validate = false): FlexObjectInterface + { + /** @var string|FlexObjectInterface $className */ + $className = $this->objectClassName ?: $this->getObjectClass(); + + return new $className($data, $key, $this, $validate); + } + + /** + * @param array $entries + * @param string $keyField + * @return FlexCollectionInterface + */ + public function createCollection(array $entries, string $keyField = null): FlexCollectionInterface + { + /** @var string|FlexCollectionInterface $className */ + $className = $this->collectionClassName ?: $this->getCollectionClass(); + + return $className::createFromArray($entries, $this, $keyField); + } + + /** + * @param array $entries + * @param string $keyField + * @return FlexIndexInterface + */ + public function createIndex(array $entries, string $keyField = null): FlexIndexInterface + { + /** @var string|FlexIndexInterface $className */ + $className = $this->indexClassName ?: $this->getIndexClass(); + + return $className::createFromArray($entries, $this, $keyField); + } + + /** + * @return string + */ + public function getObjectClass(): string + { + if (!$this->objectClassName) { + $this->objectClassName = $this->getConfig('data.object', 'Grav\\Framework\\Flex\\FlexObject'); + } + + return $this->objectClassName; + + } + + /** + * @return string + */ + public function getCollectionClass(): string + { + if (!$this->collectionClassName) { + $this->collectionClassName = $this->getConfig('data.collection', 'Grav\\Framework\\Flex\\FlexCollection'); + } + + return $this->collectionClassName; + } + + + /** + * @return string + */ + public function getIndexClass(): string + { + if (!$this->indexClassName) { + $this->indexClassName = $this->getConfig('data.index', 'Grav\\Framework\\Flex\\FlexIndex'); + } + + return $this->indexClassName; + } + + /** + * @param array $entries + * @param string $keyField + * @return FlexCollectionInterface + */ + public function loadCollection(array $entries, string $keyField = null): FlexCollectionInterface + { + return $this->createCollection($this->loadObjects($entries), $keyField); + } + + /** + * @param array $entries + * @return FlexObjectInterface[] + * @internal + */ + public function loadObjects(array $entries): array + { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->startTimer('flex-objects', sprintf('Flex: Initializing %d %s', \count($entries), $this->type)); + + $storage = $this->getStorage(); + $cache = $this->getCache('object'); + + // Get storage keys for the objects. + $keys = []; + $rows = []; + foreach ($entries as $key => $value) { + $k = $value['storage_key']; + $keys[$k] = $key; + $rows[$k] = null; + } + + // Fetch rows from the cache. + try { + $rows = $cache->getMultiple(array_keys($rows)); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + } + + // Read missing rows from the storage. + $updated = []; + $rows = $storage->readRows($rows, $updated); + + // Store updated rows to the cache. + if ($updated) { + try { + if (!$cache instanceof MemoryCache) { + $debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug'); + } + $cache->setMultiple($updated); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + + // TODO: log about the issue. + } + } + + // Create objects from the rows. + $list = []; + foreach ($rows as $storageKey => $row) { + if ($row === null) { + $debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug'); + continue; + } + + if (isset($row['__error'])) { + $message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__error']); + $debugger->addException(new \RuntimeException($message)); + $debugger->addMessage($message, 'error'); + continue; + } + + $usedKey = $keys[$storageKey]; + $row += [ + 'storage_key' => $storageKey, + 'storage_timestamp' => $entries[$usedKey]['storage_timestamp'], + ]; + + $key = $entries[$usedKey]['key'] ?? $usedKey; + $object = $this->createObject($row, $key, false); + $list[$usedKey] = $object; + } + + $debugger->stopTimer('flex-objects'); + + return $list; + } + + /** + * @param string $type_view + * @param string $context + * @return Blueprint + */ + protected function getBlueprintInternal(string $type_view = '', string $context = '') + { + if (!isset($this->blueprints[$type_view])) { + if (!file_exists($this->blueprint_file)) { + throw new RuntimeException(sprintf('Flex: Blueprint file for %s is missing', $this->type)); + } + + $parts = explode('.', rtrim($type_view, '.'), 2); + $type = array_shift($parts); + $view = array_shift($parts) ?: ''; + + $blueprint = new Blueprint($this->getBlueprintFile($view)); + if ($context) { + $blueprint->setContext($context); + } + + $blueprint->load($type ?: null); + if ($blueprint->get('type') === 'flex-objects' && isset(Grav::instance()['admin'])) { + $blueprintBase = (new Blueprint('plugin://flex-objects/blueprints/flex-objects.yaml'))->load(); + $blueprint->extend($blueprintBase, true); + } + + $this->blueprints[$type_view] = $blueprint; + } + + return $this->blueprints[$type_view]; + } + + /** + * @return FlexStorageInterface + */ + protected function createStorage(): FlexStorageInterface + { + $this->collection = $this->createCollection([]); + + $storage = $this->getConfig('data.storage'); + + if (!\is_array($storage)) { + $storage = ['options' => ['folder' => $storage]]; + } + + $className = $storage['class'] ?? SimpleStorage::class; + $options = $storage['options'] ?? []; + + return new $className($options); + } + + /** + * @return FlexIndexInterface + */ + protected function loadIndex(): FlexIndexInterface + { + static $i = 0; + + $index = $this->index; + + if (null === $index) { + $i++; $j = $i; + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->startTimer('flex-keys-' . $this->type . $j, "Flex: Loading {$this->type} index"); + + $storage = $this->getStorage(); + $cache = $this->getCache('index'); + + try { + $keys = $cache->get('__keys'); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + $keys = null; + } + + if (null === $keys) { + /** @var string|FlexIndexInterface $className */ + $className = $this->getIndexClass(); + $keys = $className::loadEntriesFromStorage($storage); + if (!$cache instanceof MemoryCache) { + $debugger->addMessage(sprintf('Flex: Caching %s index of %d objects', $this->type, \count($keys)), + 'debug'); + } + try { + $cache->set('__keys', $keys); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + // TODO: log about the issue. + } + } + + // We need to do this in two steps as orderBy() calls loadIndex() again and we do not want infinite loop. + $this->index = $this->createIndex($keys); + /** @var FlexCollectionInterface $collection */ + $collection = $this->index->orderBy($this->getConfig('data.ordering', [])); + $this->index = $index = $collection->getIndex(); + + $debugger->stopTimer('flex-keys-' . $this->type . $j); + } + + return $index; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexForm.php b/system/src/Grav/Framework/Flex/FlexForm.php new file mode 100644 index 0000000..6ded345 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexForm.php @@ -0,0 +1,344 @@ +name = $name; + $this->form = $form; + + $uniqueId = $object->exists() ? $object->getStorageKey() : "{$object->getFlexType()}:new"; + $this->setObject($object); + $this->setId($this->getName()); + $this->setUniqueId(md5($uniqueId)); + $this->messages = []; + $this->submitted = false; + + $flash = $this->getFlash(); + if ($flash->exists()) { + $data = $flash->getData(); + $includeOriginal = (bool)($this->getBlueprint()->form()['images']['original'] ?? null); + + $this->data = $data ? new Data($data, $this->getBlueprint()) : null; + $this->files = $flash->getFilesByFields($includeOriginal); + } else { + $this->data = null; + $this->files = []; + } + } + + /** + * @return string + */ + public function getName(): string + { + $object = $this->getObject(); + $name = $this->name ?: 'object'; + + return "flex-{$object->getFlexType()}-{$name}"; + } + + /** + * @return Data|FlexObjectInterface|object + */ + public function getData() + { + return $this->data ?? $this->getObject(); + } + + /** + * Get a value from the form. + * + * Note: Used in form fields. + * + * @param string $name + * @return mixed + */ + public function getValue(string $name) + { + // Attempt to get value from the form data. + $value = $this->data ? $this->data[$name] : null; + + // Return the form data or fall back to the object property. + return $value ?? $this->getObject()->getFormValue($name); + } + + public function getDefaultValue(string $name) + { + return $this->object->getDefaultValue($name); + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->object->getDefaultValues(); + } + /** + * @return string + */ + public function getFlexType(): string + { + return $this->object->getFlexType(); + } + + /** + * @return FlexObjectInterface + */ + public function getObject(): FlexObjectInterface + { + return $this->object; + } + + public function updateObject(): FlexObjectInterface + { + $data = $this->data instanceof Data ? $this->data->toArray() : []; + $files = $this->files; + + return $this->getObject()->update($data, $files); + } + + /** + * @return Blueprint + */ + public function getBlueprint(): Blueprint + { + if (null === $this->blueprint) { + try { + $blueprint = $this->getObject()->getBlueprint(Utils::isAdminPlugin() ? '' : $this->name); + if ($this->form) { + // We have field overrides available. + $blueprint->extend(['form' => $this->form], true); + $blueprint->init(); + } + } catch (\RuntimeException $e) { + if (!isset($this->form['fields'])) { + throw $e; + } + + // Blueprint is not defined, but we have custom form fields available. + $blueprint = new Blueprint(null, ['form' => $this->form]); + $blueprint->load(); + $blueprint->setScope('object'); + $blueprint->init(); + } + + $this->blueprint = $blueprint; + } + + return $this->blueprint; + } + + /** + * @return Route|null + */ + public function getFileUploadAjaxRoute(): ?Route + { + $object = $this->getObject(); + if (!method_exists($object, 'route')) { + return null; + } + + return $object->route('/edit.json/task:media.upload'); + } + + /** + * @param string $field + * @param string $filename + * @return Route|null + */ + public function getFileDeleteAjaxRoute($field, $filename): ?Route + { + $object = $this->getObject(); + if (!method_exists($object, 'route')) { + return null; + } + + return $object->route('/edit.json/task:media.delete'); + } + + public function getMediaTaskRoute(array $params = [], $extension = null): string + { + $grav = Grav::instance(); + /** @var Flex $flex */ + $flex = $grav['flex_objects']; + + if (method_exists($flex, 'adminRoute')) { + return $flex->adminRoute($this->getObject(), $params, $extension ?? 'json'); + } + + return ''; + } + + /** + * Implements \Serializable::unserialize(). + * + * @param string $data + */ + public function unserialize($data): void + { + $data = unserialize($data, ['allowed_classes' => [FlexObject::class]]); + + $this->doUnserialize($data); + } + + public function __get($name) + { + $method = "get{$name}"; + if (method_exists($this, $method)) { + return $this->{$method}(); + } + + $form = $this->getBlueprint()->form(); + + return $form[$name] ?? null; + } + + public function __set($name, $value) + { + $method = "set{$name}"; + if (method_exists($this, $method)) { + $this->{$method}($value); + } + } + + public function __isset($name) + { + $method = "get{$name}"; + if (method_exists($this, $method)) { + return true; + } + + $form = $this->getBlueprint()->form(); + + return isset($form[$name]); + } + + public function __unset($name) + { + } + + /** + * Note: this method clones the object. + * + * @param FlexObjectInterface $object + * @return $this + */ + protected function setObject(FlexObjectInterface $object): self + { + $this->object = clone $object; + + return $this; + } + + /** + * @param string $layout + * @return Template|TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + return $twig->twig()->resolveTemplate( + [ + "flex-objects/layouts/{$this->getFlexType()}/form/{$layout}.html.twig", + "flex-objects/layouts/_default/form/{$layout}.html.twig", + "forms/{$layout}/form.html.twig", + 'forms/default/form.html.twig' + ] + ); + } + + /** + * @param array $data + * @param array $files + * @throws \Exception + */ + protected function doSubmit(array $data, array $files) + { + /** @var FlexObject $object */ + $object = clone $this->getObject(); + + $object->update($data, $files); + $object->save(); + + $this->setObject($object); + $this->reset(); + } + + protected function doSerialize(): array + { + return $this->doTraitSerialize() + [ + 'object' => $this->object, + ]; + } + + protected function doUnserialize(array $data): void + { + $this->doTraitUnserialize($data); + + $this->object = $data['object']; + } + + /** + * Filter validated data. + * + * @param \ArrayAccess $data + */ + protected function filterData(\ArrayAccess $data): void + { + if ($data instanceof Data) { + $data->filter(true, true); + } + } +} diff --git a/system/src/Grav/Framework/Flex/FlexIndex.php b/system/src/Grav/Framework/Flex/FlexIndex.php new file mode 100644 index 0000000..8b96b4a --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexIndex.php @@ -0,0 +1,704 @@ +getStorage()), $directory); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::createFromArray() + */ + public static function createFromArray(array $entries, FlexDirectory $directory, string $keyField = null) + { + $instance = new static($entries, $directory); + $instance->setKeyField($keyField); + + return $instance; + } + + /** + * @param FlexStorageInterface $storage + * @return array + */ + public static function loadEntriesFromStorage(FlexStorageInterface $storage): array + { + return $storage->getExistingKeys(); + } + + /** + * Initializes a new FlexIndex. + * + * @param array $entries + * @param FlexDirectory|null $directory + */ + public function __construct(array $entries = [], FlexDirectory $directory = null) + { + parent::__construct($entries); + + $this->_flexDirectory = $directory; + $this->setKeyField(null); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::search() + */ + public function search(string $search, $properties = null, array $options = null) + { + return $this->__call('search', [$search, $properties, $options]); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::sort() + */ + public function sort(array $orderings) + { + return $this->orderBy($orderings); + } + + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::filterBy() + */ + public function filterBy(array $filters) + { + return $this->__call('filterBy', [$filters]); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexType() + */ + public function getFlexType(): string + { + return $this->_flexDirectory->getFlexType(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexDirectory() + */ + public function getFlexDirectory(): FlexDirectory + { + return $this->_flexDirectory; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getTimestamp() + */ + public function getTimestamp(): int + { + $timestamps = $this->getTimestamps(); + + return $timestamps ? max($timestamps) : time(); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getCacheKey() + */ + public function getCacheKey(): string + { + return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->_keyField); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getCacheChecksum() + */ + public function getCacheChecksum(): string + { + return sha1($this->getCacheKey() . json_encode($this->getTimestamps())); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getTimestamps() + */ + public function getTimestamps(): array + { + return $this->getIndexMap('storage_timestamp'); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getStorageKeys() + */ + public function getStorageKeys(): array + { + return $this->getIndexMap('storage_key'); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getFlexKeys() + */ + public function getFlexKeys(): array + { + // Get storage keys for the objects. + $keys = []; + $type = $this->_flexDirectory->getFlexType() . '.obj:'; + + foreach ($this->getEntries() as $key => $value) { + $keys[$key] = $value['flex_key'] ?? $type . $value['storage_key']; + } + + return $keys; + } + + /** + * {@inheritdoc} + * @see FlexIndexInterface::withKeyField() + */ + public function withKeyField(string $keyField = null) + { + $keyField = $keyField ?: 'key'; + if ($keyField === $this->getKeyField()) { + return $this; + } + + $type = $keyField === 'flex_key' ? $this->_flexDirectory->getFlexType() . '.obj:' : ''; + $entries = []; + foreach ($this->getEntries() as $key => $value) { + if (!isset($value['key'])) { + $value['key'] = $key; + } + + if (isset($value[$keyField])) { + $entries[$value[$keyField]] = $value; + } elseif ($keyField === 'flex_key') { + $entries[$type . $value['storage_key']] = $value; + } + } + + return $this->createFrom($entries, $keyField); + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::getIndex() + */ + public function getIndex() + { + return $this; + } + + /** + * {@inheritdoc} + * @see FlexCollectionInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + return $this->__call('render', [$layout, $context]); + } + + /** + * @param bool $prefix + * @return string + * @deprecated 1.6 Use `->getFlexType()` instead. + */ + public function getType($prefix = false) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + $type = $prefix ? $this->getTypePrefix() : ''; + + return $type . $this->getFlexType(); + } + + /** + * {@inheritdoc} + * @see FlexIndexInterface::getFlexKeys() + */ + public function getIndexMap(string $indexKey = null) + { + if (null === $indexKey) { + return $this->getEntries(); + } + + // Get storage keys for the objects. + $index = []; + foreach ($this->getEntries() as $key => $value) { + $index[$key] = $value[$indexKey] ?? null; + } + + return $index; + } + + /** + * @return array + */ + public function getMetaData(string $key): array + { + return $this->getEntries()[$key] ?? []; + } + + /** + * @return string + */ + public function getKeyField() : string + { + return $this->_keyField ?? 'storage_key'; + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + return $this->_flexDirectory->getCache($namespace); + } + + /** + * @param array $orderings + * @return FlexIndex|FlexCollection + */ + public function orderBy(array $orderings) + { + if (!$orderings || !$this->count()) { + return $this; + } + + // Check if ordering needs to load the objects. + if (array_diff_key($orderings, $this->getIndexKeys())) { + return $this->__call('orderBy', [$orderings]); + } + + // Ordering can be done by using index only. + $previous = null; + foreach (array_reverse($orderings) as $field => $ordering) { + $field = (string)$field; + if ($this->getKeyField() === $field) { + $keys = $this->getKeys(); + $search = array_combine($keys, $keys) ?: []; + } elseif ($field === 'flex_key') { + $search = $this->getFlexKeys(); + } else { + $search = $this->getIndexMap($field); + } + + // Update current search to match the previous ordering. + if (null !== $previous) { + $search = array_replace($previous, $search); + } + + // Order by current field. + if ($ordering === 'DESC') { + arsort($search, SORT_NATURAL); + } else { + asort($search, SORT_NATURAL); + } + + $previous = $search; + } + + return $this->createFrom(array_replace($previous, $this->getEntries())); + } + + /** + * {@inheritDoc} + */ + public function call($method, array $arguments = []) + { + return $this->__call('call', [$method, $arguments]); + } + + public function __call($name, $arguments) + { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + + /** @var FlexCollection $className */ + $className = $this->_flexDirectory->getCollectionClass(); + $cachedMethods = $className::getCachedMethods(); + + $flexType = $this->getFlexType(); + + if (!empty($cachedMethods[$name])) { + $type = $cachedMethods[$name]; + if ($type === 'session') { + /** @var Session $session */ + $session = Grav::instance()['session']; + $cacheKey = $session->getId() . $session->user->username; + } else { + $cacheKey = ''; + } + $key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey()); + + $cache = $this->getCache('object'); + + try { + $result = $cache->get($key); + + // Make sure the keys aren't changed if the returned type is the same index type. + if ($result instanceof self && $flexType === $result->getFlexType()) { + $result = $result->withKeyField($this->getKeyField()); + } + } catch (InvalidArgumentException $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + } + + if (!isset($result)) { + $collection = $this->loadCollection(); + $result = $collection->{$name}(...$arguments); + + try { + // If flex collection is returned, convert it back to flex index. + if ($result instanceof FlexCollection) { + $cached = $result->getFlexDirectory()->getIndex($result->getKeys(), $this->getKeyField()); + } else { + $cached = $result; + } + + $cache->set($key, $cached); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + + // TODO: log error. + } + } + } else { + $collection = $this->loadCollection(); + $result = $collection->{$name}(...$arguments); + if (!isset($cachedMethods[$name])) { + $class = \get_class($collection); + $debugger->addMessage("Call '{$class}:{$name}()' isn't cached", 'debug'); + } + } + + return $result; + } + + /** + * @return string + */ + public function serialize() + { + return serialize(['type' => $this->getFlexType(), 'entries' => $this->getEntries()]); + } + + /** + * @param string $serialized + */ + public function unserialize($serialized) + { + $data = unserialize($serialized, ['allowed_classes' => false]); + + $this->_flexDirectory = Grav::instance()['flex_objects']->getDirectory($data['type']); + $this->setEntries($data['entries']); + } + + /** + * @param array $entries + * @param string $keyField + * @return static + */ + protected function createFrom(array $entries, string $keyField = null) + { + $index = new static($entries, $this->_flexDirectory); + $index->setKeyField($keyField ?? $this->_keyField); + + return $index; + } + + /** + * @param string|null $keyField + */ + protected function setKeyField(string $keyField = null) + { + $this->_keyField = $keyField ?? 'storage_key'; + } + + protected function getIndexKeys() + { + if (null === $this->_indexKeys) { + $entries = $this->getEntries(); + $first = reset($entries); + if ($first) { + $keys = array_keys($first); + $keys = array_combine($keys, $keys) ?: []; + } else { + $keys = []; + } + + $this->setIndexKeys($keys); + } + + return $this->_indexKeys; + } + + /** + * @param array $indexKeys + */ + protected function setIndexKeys(array $indexKeys) + { + // Add defaults. + $indexKeys += [ + 'key' => 'key', + 'storage_key' => 'storage_key', + 'storage_timestamp' => 'storage_timestamp', + 'flex_key' => 'flex_key' + ]; + + + $this->_indexKeys = $indexKeys; + } + + /** + * @return string + */ + protected function getTypePrefix() + { + return 'i.'; + } + + /** + * @param string $key + * @param mixed $value + * @return ObjectInterface|null + */ + protected function loadElement($key, $value): ?ObjectInterface + { + $objects = $this->_flexDirectory->loadObjects([$key => $value]); + + return $objects ? reset($objects): null; + } + + /** + * @param array|null $entries + * @return ObjectInterface[] + */ + protected function loadElements(array $entries = null): array + { + return $this->_flexDirectory->loadObjects($entries ?? $this->getEntries()); + } + + /** + * @param array|null $entries + * @return ObjectCollectionInterface + */ + protected function loadCollection(array $entries = null): CollectionInterface + { + return $this->_flexDirectory->loadCollection($entries ?? $this->getEntries(), $this->_keyField); + } + + /** + * @param mixed $value + * @return bool + */ + protected function isAllowedElement($value): bool + { + return $value instanceof FlexObject; + } + + /** + * @param FlexObjectInterface $object + * @return mixed + */ + protected function getElementMeta($object) + { + return $object->getTimestamp(); + } + + /** + * @param FlexStorageInterface $storage + * @param array $index Saved index + * @param array $entries Updated index + * @return array Compiled list of entries + */ + protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries): array + { + // Calculate removed objects. + $removed = array_diff_key($index, $entries); + + // First get rid of all removed objects. + if ($removed) { + $index = array_diff_key($index, $removed); + } + + if ($entries) { + // Calculate difference between saved index and current data. + foreach ($index as $key => $entry) { + $storage_key = $entry['storage_key'] ?? null; + if (isset($entries[$storage_key]) && $entries[$storage_key]['storage_timestamp'] === $entry['storage_timestamp']) { + // Entry is up to date, no update needed. + unset($entries[$storage_key]); + } + } + + if (empty($entries) && empty($removed)) { + // No objects were added, updated or removed. + return $index; + } + } elseif (!$removed) { + // There are no objects and nothing was removed. + return []; + } + + // Index should be updated, lock the index file for saving. + $indexFile = static::getIndexFile($storage); + $indexFile->lock(); + + // Read all the data rows into an array. + $keys = array_fill_keys(array_keys($entries), null); + $rows = $storage->readRows($keys); + + $keyField = $storage->getKeyField(); + + // Go through all the updated objects and refresh their index data. + $updated = $added = []; + foreach ($rows as $key => $row) { + if (null !== $row) { + $entry = ['key' => $key] + $entries[$key]; + if ($keyField !== 'storage_key' && isset($row[$keyField])) { + $entry['key'] = $row[$keyField]; + } + static::updateIndexData($entry, $row); + if (isset($row['__error'])) { + $entry['__error'] = true; + static::onException(new \RuntimeException(sprintf('Object failed to load: %s (%s)', $key, $row['__error']))); + } + if (isset($index[$key])) { + // Update object in the index. + $updated[$key] = $entry; + } else { + // Add object into the index. + $added[$key] = $entry; + } + + // Either way, update the entry. + $index[$key] = $entry; + } elseif (isset($index[$key])) { + // Remove object from the index. + $removed[$key] = $index[$key]; + unset($index[$key]); + } + } + + // Sort the index before saving it. + ksort($index, SORT_NATURAL); + + static::onChanges($index, $added, $updated, $removed); + + $indexFile->save(['count' => \count($index), 'index' => $index]); + $indexFile->unlock(); + + return $index; + } + + protected static function updateIndexData(array &$entry, array $data) + { + } + + protected static function loadEntriesFromIndex(FlexStorageInterface $storage) + { + $indexFile = static::getIndexFile($storage); + + $data = []; + try { + $data = (array)$indexFile->content(); + } catch (\Exception $e) { + $e = new \RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e); + + static::onException($e); + } + + return $data['index'] ?? []; + } + + protected static function getIndexFile(FlexStorageInterface $storage) + { + // Load saved index file. + $grav = Grav::instance(); + $locator = $grav['locator']; + $filename = $locator->findResource($storage->getStoragePath() . '/index.yaml', true, true); + + return CompiledYamlFile::instance($filename); + } + + protected static function onException(\Exception $e) + { + $grav = Grav::instance(); + + /** @var Logger $logger */ + $logger = $grav['log']; + $logger->addAlert($e->getMessage()); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addException($e); + $debugger->addMessage($e, 'error'); + } + + protected static function onChanges(array $entries, array $added, array $updated, array $removed) + { + $message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', \count($entries), \count($added), \count($updated), \count($removed)); + + $grav = Grav::instance(); + + /** @var Logger $logger */ + $logger = $grav['log']; + $logger->addDebug($message); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->addMessage($message, 'debug'); + } + + public function __debugInfo() + { + return [ + 'type:private' => $this->getFlexType(), + 'key:private' => $this->getKey(), + 'entries_key:private' => $this->getKeyField(), + 'entries:private' => $this->getEntries() + ]; + } +} diff --git a/system/src/Grav/Framework/Flex/FlexObject.php b/system/src/Grav/Framework/Flex/FlexObject.php new file mode 100644 index 0000000..7fbdbc0 --- /dev/null +++ b/system/src/Grav/Framework/Flex/FlexObject.php @@ -0,0 +1,877 @@ + true, + 'getType' => true, + 'getFlexType' => true, + 'getFlexDirectory' => true, + 'getCacheKey' => true, + 'getCacheChecksum' => true, + 'getTimestamp' => true, + 'value' => true, + 'exists' => true, + 'hasProperty' => true, + 'getProperty' => true, + + // FlexAclTrait + 'isAuthorized' => 'session', + ]; + } + + public static function createFromStorage(array $elements, array $storage, FlexDirectory $directory, bool $validate = false) + { + $instance = new static($elements, $storage['key'], $directory, $validate); + $instance->setStorage($storage); + + return $instance; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::__construct() + */ + public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false) + { + $this->_flexDirectory = $directory; + + if ($validate) { + $blueprint = $this->getFlexDirectory()->getBlueprint(); + + $blueprint->validate($elements); + + $elements = $blueprint->filter($elements); + } + + $this->filterElements($elements); + + $this->objectConstruct($elements, $key); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFlexType() + */ + public function getFlexType(): string + { + return $this->_flexDirectory->getFlexType(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFlexDirectory() + */ + public function getFlexDirectory(): FlexDirectory + { + return $this->_flexDirectory; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getTimestamp() + */ + public function getTimestamp(): int + { + return $this->_storage['storage_timestamp'] ?? 0; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getCacheKey() + */ + public function getCacheKey(): string + { + return $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getStorageKey(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getCacheChecksum() + */ + public function getCacheChecksum(): string + { + return (string)$this->getTimestamp(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::search() + */ + public function search(string $search, $properties = null, array $options = null): float + { + $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); + $properties = $properties ?? $this->getFlexDirectory()->getConfig('data.search.fields', []); + if (!$properties) { + foreach ($this->getFlexDirectory()->getConfig('admin.list.fields', []) as $property => $value) { + if (!empty($value['link'])) { + $properties[] = $property; + } + } + } + + $weight = 0; + foreach ((array)$properties as $property) { + $weight += $this->searchNestedProperty($property, $search, $options); + } + + return $weight > 0 ? min($weight, 1) : 0; + } + + /** + * {@inheritdoc} + * @see ObjectInterface::getFlexKey() + */ + public function getKey() + { + return $this->_key ?: $this->getFlexType() . '@@' . spl_object_hash($this); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFlexKey() + */ + public function getFlexKey(): string + { + return $this->_storage['flex_key'] ?? $this->_flexDirectory->getFlexType() . '.obj:' . $this->getStorageKey(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getStorageKey() + */ + public function getStorageKey(): string + { + return $this->_storage['storage_key'] ?? $this->getTypePrefix() . $this->getFlexType() . '@@' . spl_object_hash($this); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getMetaData() + */ + public function getMetaData(): array + { + return $this->getStorage(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::exists() + */ + public function exists(): bool + { + $key = $this->getStorageKey(); + + return $key && $this->getFlexDirectory()->getStorage()->hasKey($key); + } + + /** + * @param string $property + * @param string $search + * @param array|null $options + * @return float + */ + public function searchProperty(string $property, string $search, array $options = null): float + { + $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); + $value = $this->getProperty($property); + + return $this->searchValue($property, $value, $search, $options); + } + + /** + * @param string $property + * @param string $search + * @param array|null $options + * @return float + */ + public function searchNestedProperty(string $property, string $search, array $options = null): float + { + $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); + $value = $this->getNestedProperty($property); + + return $this->searchValue($property, $value, $search, $options); + } + + /** + * @param string $name + * @param mixed $value + * @param string $search + * @param array|null $options + * @return float + */ + protected function searchValue(string $name, $value, string $search, array $options = null): float + { + $search = trim($search); + + if ($search === '') { + return 0; + } + + if (!\is_string($value) || $value === '') { + return 0; + } + + $tested = false; + if (($tested |= !empty($options['starts_with'])) && Utils::startsWith($value, $search, $options['case_sensitive'] ?? false)) { + return (float)$options['starts_with']; + } + if (($tested |= !empty($options['ends_with'])) && Utils::endsWith($value, $search, $options['case_sensitive'] ?? false)) { + return (float)$options['ends_with']; + } + if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $options['case_sensitive'] ?? false)) { + return (float)($options['contains'] ?? 1); + } + + return 0; + } + + /** + * Get any changes based on data sent to update + * + * @return array + */ + public function getChanges(): array + { + return $this->_changes ?? []; + } + + /** + * @return string + */ + protected function getTypePrefix(): string + { + return 'o.'; + } + + /** + * @param bool $prefix + * @return string + * @deprecated 1.6 Use `->getFlexType()` instead. + */ + public function getType($prefix = false) + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getFlexType() method instead', E_USER_DEPRECATED); + + $type = $prefix ? $this->getTypePrefix() : ''; + + return $type . $this->getFlexType(); + } + + /** + * Alias of getBlueprint() + * + * @return Blueprint + * @deprecated 1.6 Admin compatibility + */ + public function blueprints() + { + return $this->getBlueprint(); + } + + /** + * @param string|null $namespace + * @return CacheInterface + */ + public function getCache(string $namespace = null) + { + return $this->_flexDirectory->getCache($namespace); + } + + /** + * @param string|null $key + * @return $this + */ + public function setStorageKey($key = null) + { + $this->_storage['storage_key'] = $key; + + return $this; + } + + /** + * @param int $timestamp + * @return $this + */ + public function setTimestamp($timestamp = null) + { + $this->_storage['storage_timestamp'] = $timestamp ?? time(); + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + if (null === $layout) { + $layout = 'default'; + } + + $type = $this->getFlexType(); + + $grav = Grav::instance(); + + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; + $debugger->startTimer('flex-object-' . ($debugKey = uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')'); + + $cache = $key = null; + foreach ($context as $value) { + if (!\is_scalar($value)) { + $key = false; + } + } + + if ($key !== false) { + $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context)); + $cache = $this->getCache('render'); + } + + try { + $data = $cache && $key ? $cache->get($key) : null; + + $block = $data ? HtmlBlock::fromArray($data) : null; + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + + $block = null; + } catch (\InvalidArgumentException $e) { + $debugger->addException($e); + + $block = null; + } + + $checksum = $this->getCacheChecksum(); + if ($block && $checksum !== $block->getChecksum()) { + $block = null; + } + + if (!$block) { + $block = HtmlBlock::create($key ?: null); + $block->setChecksum($checksum); + if ($key === false) { + $block->disableCache(); + } + + $grav->fireEvent('onFlexObjectRender', new Event([ + 'object' => $this, + 'layout' => &$layout, + 'context' => &$context + ])); + + $output = $this->getTemplate($layout)->render( + ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'object' => $this, 'layout' => $layout] + $context + ); + + if ($debugger->enabled()) { + $name = $this->getKey() . ' (' . $type . ')'; + $output = "\n\n{$output}\n\n"; + } + + $block->setContent($output); + + try { + $cache && $key && $block->isCached() && $cache->set($key, $block->toArray()); + } catch (InvalidArgumentException $e) { + $debugger->addException($e); + } + } + + $debugger->stopTimer('flex-object-' . $debugKey); + + return $block; + } + + /** + * @return array + */ + public function jsonSerialize() + { + return $this->getElements(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::prepareStorage() + */ + public function prepareStorage(): array + { + return $this->getElements(); + } + + /** + * @param string $name + * @return $this + */ + public function triggerEvent($name) + { + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::update() + */ + public function update(array $data, array $files = []) + { + if ($data) { + $blueprint = $this->getBlueprint(); + + // Process updated data through the object filters. + $this->filterElements($data); + + // Get currently stored data. + $elements = $this->getElements(); + + // Merge existing object to the test data to be validated. + $test = $blueprint->mergeData($elements, $data); + + // Validate and filter elements and throw an error if any issues were found. + $blueprint->validate($test + ['storage_key' => $this->getStorageKey(), 'timestamp' => $this->getTimestamp()]); + $data = $blueprint->filter($data, false, true); + + // Finally update the object. + foreach ($blueprint->flattenData($data) as $key => $value) { + if ($value === null) { + $this->unsetNestedProperty($key); + } else { + $this->setNestedProperty($key, $value); + } + } + + // Store the changes + $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements); + } + + if ($files && method_exists($this, 'setUpdatedMedia')) { + $this->setUpdatedMedia($files); + } + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::create() + */ + public function create(string $key = null) + { + if ($key) { + $this->setStorageKey($key); + } + + if ($this->exists()) { + throw new \RuntimeException('Cannot create new object (Already exists)'); + } + + return $this->save(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::save() + */ + public function save() + { + $this->triggerEvent('onBeforeSave'); + + $result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]); + + $value = reset($result); + $storageKey = (string)key($result); + if ($value && $storageKey) { + $this->setStorageKey($storageKey); + if (!$this->hasKey()) { + $this->setKey($storageKey); + } + } + + // FIXME: For some reason locator caching isn't cleared for the file, investigate! + $locator = Grav::instance()['locator']; + $locator->clearCache(); + + // Make sure that the object exists before continuing (just in case). + if (!$this->exists()) { + throw new \RuntimeException('Saving failed: Object does not exist!'); + } + + if (method_exists($this, 'saveUpdatedMedia')) { + $this->saveUpdatedMedia(); + } + + try { + $this->getFlexDirectory()->clearCache(); + if (method_exists($this, 'clearMediaCache')) { + $this->clearMediaCache(); + } + } catch (\Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + // Caching failed, but we can ignore that for now. + } + + $this->triggerEvent('onAfterSave'); + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::delete() + */ + public function delete() + { + $this->triggerEvent('onBeforeDelete'); + + $this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]); + + try { + $this->getFlexDirectory()->clearCache(); + if (method_exists($this, 'clearMediaCache')) { + $this->clearMediaCache(); + } + } catch (\Exception $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + // Caching failed, but we can ignore that for now. + } + + $this->triggerEvent('onAfterDelete'); + + return $this; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getBlueprint() + */ + public function getBlueprint(string $name = '') + { + return $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getForm() + */ + public function getForm(string $name = '', array $form = null) + { + if (!isset($this->_forms[$name])) { + $this->_forms[$name] = $this->createFormObject($name, $form); + } + + return $this->_forms[$name]; + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getDefaultValue() + */ + public function getDefaultValue(string $name, string $separator = null) + { + $separator = $separator ?: '.'; + $path = explode($separator, $name) ?: []; + $offset = array_shift($path) ?? ''; + + $current = $this->getDefaultValues(); + + if (!isset($current[$offset])) { + return null; + } + + $current = $current[$offset]; + + while ($path) { + $offset = array_shift($path); + + if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) { + $current = $current[$offset]; + } elseif (\is_object($current) && isset($current->{$offset})) { + $current = $current->{$offset}; + } else { + return null; + } + }; + + return $current; + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->getBlueprint()->getDefaults(); + } + + /** + * {@inheritdoc} + * @see FlexObjectInterface::getFormValue() + */ + public function getFormValue(string $name, $default = null, string $separator = null) + { + if ($name === 'storage_key') { + return $this->getStorageKey(); + } + if ($name === 'storage_timestamp') { + return $this->getTimestamp(); + } + + return $this->getNestedProperty($name, $default, $separator); + } + + /** + * @param string $name + * @param mixed|null $default + * @param string|null $separator + * @return mixed + * + * @deprecated 1.6 Use ->getFormValue() method instead. + */ + public function value($name, $default = null, $separator = null) + { + return $this->getFormValue($name, $default, $separator); + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return $this->getFlexKey(); + } + + public function __debugInfo() + { + return [ + 'type:private' => $this->getFlexType(), + 'key:private' => $this->getKey(), + 'elements:private' => $this->getElements(), + 'storage:private' => $this->getStorage() + ]; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + return [ + 'type' => $this->getFlexType(), + 'key' => $this->getKey(), + 'elements' => $this->getElements(), + 'storage' => $this->getStorage() + ]; + } + + /** + * @param array $serialized + */ + protected function doUnserialize(array $serialized): void + { + $type = $serialized['type'] ?? 'unknown'; + + if (!isset($serialized['key'], $serialized['type'], $serialized['elements'])) { + throw new \InvalidArgumentException("Cannot unserialize '{$type}': Bad data"); + } + + $grav = Grav::instance(); + /** @var Flex|null $flex */ + $flex = $grav['flex_objects'] ?? null; + $directory = $flex ? $flex->getDirectory($type) : null; + if (!$directory) { + throw new \InvalidArgumentException("Cannot unserialize '{$type}': Not found"); + } + $this->setFlexDirectory($directory); + $this->setStorage($serialized['storage']); + $this->setKey($serialized['key']); + $this->setElements($serialized['elements']); + } + + /** + * @param FlexDirectory $directory + */ + public function setFlexDirectory(FlexDirectory $directory): void + { + $this->_flexDirectory = $directory; + } + /** + * @param array $storage + */ + protected function setStorage(array $storage) : void + { + $this->_storage = $storage; + } + + /** + * @return array + */ + protected function getStorage() : array + { + return $this->_storage ?? []; + } + + /** + * @param string $type + * @param string $property + * @return FlexCollectionInterface + */ + protected function getCollectionByProperty($type, $property) + { + $directory = $this->getRelatedDirectory($type); + $collection = $directory->getCollection(); + $list = $this->getNestedProperty($property) ?: []; + + /** @var FlexCollection $collection */ + $collection = $collection->filter(function ($object) use ($list) { return \in_array($object->id, $list, true); }); + + return $collection; + } + + /** + * @param string $type + * @return FlexDirectory + * @throws \RuntimeException + */ + protected function getRelatedDirectory($type): FlexDirectory + { + /** @var Flex $flex */ + $flex = Grav::instance()['flex_objects']; + $directory = $flex->getDirectory($type); + if (!$directory) { + throw new \RuntimeException(ucfirst($type). ' directory does not exist!'); + } + + return $directory; + } + + /** + * @param string $layout + * @return Template|TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + try { + return $twig->twig()->resolveTemplate( + [ + "flex-objects/layouts/{$this->getFlexType()}/object/{$layout}.html.twig", + "flex-objects/layouts/_default/object/{$layout}.html.twig" + ] + ); + } catch (LoaderError $e) { + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; + $debugger->addException($e); + + return $twig->twig()->resolveTemplate(['flex-objects/layouts/404.html.twig']); + } + } + + /** + * Filter data coming to constructor or $this->update() request. + * + * NOTE: The incoming data can be an arbitrary array so do not assume anything from its content. + * + * @param array $elements + */ + protected function filterElements(array &$elements): void + { + if (!empty($elements['storage_key'])) { + $this->_storage['storage_key'] = trim($elements['storage_key']); + } + if (!empty($elements['storage_timestamp'])) { + $this->_storage['storage_timestamp'] = (int)$elements['storage_timestamp']; + } + + unset ($elements['storage_key'], $elements['storage_timestamp'], $elements['_post_entries_save']); + } + + /** + * This methods allows you to override form objects in child classes. + * + * @param string $name Form name + * @param array|null $form Form fields + * @return FlexFormInterface + */ + protected function createFormObject(string $name, array $form = null) + { + return new FlexForm($name, $this, $form); + } +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php new file mode 100644 index 0000000..cfcb8a9 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexAuthorizeInterface.php @@ -0,0 +1,32 @@ + 'ASC'|'DESC', ...]. + * + * @return FlexCollectionInterface Returns a sorted version from the collection. + */ + public function sort(array $orderings); + + /** + * Filter collection by filter array with keys and values. + * + * @param array $filters + * @return FlexCollectionInterface + */ + public function filterBy(array $filters); + + /** + * Get timestamps from all the objects in the collection. + * + * This method can be used for example in caching. + * + * @return int[] Returns [key => timestamp, ...] pairs. + */ + public function getTimestamps(): array; + + /** + * Get storage keys from all the objects in the collection. + * + * @see FlexDirectory::getObject() If you want to get Flex Object from the Flex Directory. + * + * @return string[] Returns [key => storage_key, ...] pairs. + */ + public function getStorageKeys(): array; + + /** + * Get Flex keys from all the objects in the collection. + * + * @see Flex::getObjects() If you want to get list of Flex Objects from any Flex Directory. + * + * @return string[] Returns[key => flex_key, ...] pairs. + */ + public function getFlexKeys(): array; + + /** + * Return new collection with a different key. + * + * @param string|null $keyField Switch key field of the collection. + * + * @return FlexCollectionInterface Returns a new Flex Collection with new key field. + * @api + */ + public function withKeyField(string $keyField = null); + + /** + * Get Flex Index from the Flex Collection. + * + * @return FlexIndexInterface Returns a Flex Index from the current collection. + */ + public function getIndex(); +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php new file mode 100644 index 0000000..482aef2 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexCommonInterface.php @@ -0,0 +1,64 @@ + [storage_key => xxx, storage_timestamp => 123456, ...]] + */ + public static function loadEntriesFromStorage(FlexStorageInterface $storage): array; + + /** + * Return new collection with a different key. + * + * @param string|null $keyField Switch key field of the collection. + * + * @return FlexIndexInterface Returns a new Flex Collection with new key field. + * @api + */ + public function withKeyField(string $keyField = null); + + /** + * @param string $indexKey + * @return array + */ + public function getIndexMap(string $indexKey = null); +} diff --git a/system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php b/system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php new file mode 100644 index 0000000..a10ee84 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Interfaces/FlexObjectInterface.php @@ -0,0 +1,208 @@ + [storage_key => key, storage_timestamp => timestamp], ...]`. + */ + public function getExistingKeys(): array; + + /** + * Check if the key exists in the storage. + * + * @param string $key Storage key of an object. + * + * @return bool Returns `true` if the key exists in the storage, `false` otherwise. + */ + public function hasKey(string $key): bool; + + /** + * Check if the key exists in the storage. + * + * @param string[] $keys Storage key of an object. + * + * @return bool[] Returns keys with `true` if the key exists in the storage, `false` otherwise. + */ + public function hasKeys(array $keys): array; + + /** + * Create new rows into the storage. + * + * New keys will be assigned when the objects are created. + * + * @param array $rows List of rows as `[row, ...]`. + * + * @return array Returns created rows as `[key => row, ...] pairs. + */ + public function createRows(array $rows): array; + + /** + * Read rows from the storage. + * + * If you pass object or array as value, that value will be used to save I/O. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * @param array $fetched Optional reference to store only fetched items. + * + * @return array Returns rows. Note that non-existing rows will have `null` as their value. + */ + public function readRows(array $rows, array &$fetched = null): array; + + /** + * Update existing rows in the storage. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * + * @return array Returns updated rows. Note that non-existing rows will not be saved and have `null` as their value. + */ + public function updateRows(array $rows): array; + + /** + * Delete rows from the storage. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * + * @return array Returns deleted rows. Note that non-existing rows have `null` as their value. + */ + public function deleteRows(array $rows): array; + + /** + * Replace rows regardless if they exist or not. + * + * All rows should have a specified key for replace to work properly. + * + * @param array $rows Array of `[key => row, ...]` pairs. + * + * @return array Returns both created and updated rows. + */ + public function replaceRows(array $rows): array; + + /** + * @param string $src + * @param string $dst + * + * @return bool + */ + public function renameRow(string $src, string $dst): bool; + + /** + * Get filesystem path for the collection or object storage. + * + * @param string|null $key Optional storage key. + * + * @return string Path in the filesystem. Can be URI. + */ + public function getStoragePath(string $key = null): string; + + /** + * Get filesystem path for the collection or object media. + * + * @param string|null $key Optional storage key. + * + * @return string Path in the filesystem. Can be URI. + */ + public function getMediaPath(string $key = null): string; +} diff --git a/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php new file mode 100644 index 0000000..cc460cc --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/AbstractFilesystemStorage.php @@ -0,0 +1,154 @@ +hasKey((string)$key); + } + + return $list; + } + + /** + * @return string + */ + public function getKeyField(): string + { + return $this->keyField; + } + + protected function initDataFormatter($formatter): void + { + // Initialize formatter. + if (!\is_array($formatter)) { + $formatter = ['class' => $formatter]; + } + $formatterClassName = $formatter['class'] ?? JsonFormatter::class; + $formatterOptions = $formatter['options'] ?? []; + + $this->dataFormatter = new $formatterClassName($formatterOptions); + } + + /** + * @param string $filename + * @return null|string + */ + protected function detectDataFormatter(string $filename): ?string + { + if (preg_match('|(\.[a-z0-9]*)$|ui', $filename, $matches)) { + switch ($matches[1]) { + case '.json': + return JsonFormatter::class; + case '.yaml': + return YamlFormatter::class; + case '.md': + return MarkdownFormatter::class; + } + } + + return null; + } + + /** + * @param string $filename + * @return File + */ + protected function getFile(string $filename) + { + $filename = $this->resolvePath($filename); + + switch ($this->dataFormatter->getDefaultFileExtension()) { + case '.json': + $file = CompiledJsonFile::instance($filename); + break; + case '.yaml': + $file = CompiledYamlFile::instance($filename); + break; + case '.md': + $file = CompiledMarkdownFile::instance($filename); + break; + default: + throw new RuntimeException('Unknown extension type ' . $this->dataFormatter->getDefaultFileExtension()); + } + + return $file; + } + + /** + * @param string $path + * @return string + */ + protected function resolvePath(string $path): string + { + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + if (!$locator->isStream($path)) { + return $path; + } + + return (string)($locator->findResource($path) ?: $locator->findResource($path, true, true)); + } + + /** + * Generates a random, unique key for the row. + * + * @return string + */ + protected function generateKey(): string + { + return substr(hash('sha256', random_bytes(32)), 0, 32); + } + + /** + * Checks if a key is valid. + * + * @param string $key + * @return bool + */ + protected function validateKey(string $key): bool + { + return (bool) preg_match('/^[^\\/\\?\\*:;{}\\\\\\n]+$/u', $key); + } +} diff --git a/system/src/Grav/Framework/Flex/Storage/FileStorage.php b/system/src/Grav/Framework/Flex/Storage/FileStorage.php new file mode 100644 index 0000000..e263e6c --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/FileStorage.php @@ -0,0 +1,82 @@ +dataPattern = '{FOLDER}/{KEY}'; + + if (!isset($options['formatter']) && isset($options['pattern'])) { + $options['formatter'] = $this->detectDataFormatter($options['pattern']); + } + + parent::__construct($options); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getMediaPath() + */ + public function getMediaPath(string $key = null): string + { + return $key ? \dirname($this->getStoragePath($key)) . '/' . $key : $this->getStoragePath(); + } + + /** + * {@inheritdoc} + */ + protected function getKeyFromPath(string $path): string + { + return basename($path, $this->dataFormatter->getDefaultFileExtension()); + } + + /** + * {@inheritdoc} + */ + protected function buildIndex(): array + { + if (!file_exists($this->getStoragePath())) { + return []; + } + + $flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS; + $iterator = new \FilesystemIterator($this->getStoragePath(), $flags); + $list = []; + /** @var \SplFileInfo $info */ + foreach ($iterator as $filename => $info) { + if (!$info->isFile() || !($key = $this->getKeyFromPath($filename)) || strpos($info->getFilename(), '.') === 0) { + continue; + } + + $list[$key] = [ + 'storage_key' => $key, + 'storage_timestamp' => $info->getMTime() + ]; + } + + ksort($list, SORT_NATURAL); + + return $list; + } +} diff --git a/system/src/Grav/Framework/Flex/Storage/FolderStorage.php b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php new file mode 100644 index 0000000..76397c2 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/FolderStorage.php @@ -0,0 +1,470 @@ +initDataFormatter($options['formatter'] ?? []); + $this->initOptions($options); + + // Make sure that the data folder exists. + $folder = $this->resolvePath($this->dataFolder); + if (!file_exists($folder)) { + try { + Folder::create($folder); + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage())); + } + } + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getExistingKeys() + */ + public function getExistingKeys(): array + { + return $this->buildIndex(); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::hasKey() + */ + public function hasKey(string $key): bool + { + return $key && strpos($key, '@@') === false && file_exists($this->getPathFromKey($key)); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::createRows() + */ + public function createRows(array $rows): array + { + $list = []; + foreach ($rows as $key => $row) { + // Create new file and save it. + $key = $this->getNewKey(); + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + $list[$key] = $this->saveFile($file, $row); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::readRows() + */ + public function readRows(array $rows, array &$fetched = null): array + { + $list = []; + foreach ($rows as $key => $row) { + if (null === $row || (!\is_object($row) && !\is_array($row))) { + // Only load rows which haven't been loaded before. + $key = (string)$key; + if (!$this->hasKey($key)) { + $list[$key] = null; + } else { + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + $list[$key] = $this->loadFile($file); + } + if (null !== $fetched) { + $fetched[$key] = $list[$key]; + } + } else { + // Keep the row if it has been loaded. + $list[$key] = $row; + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::updateRows() + */ + public function updateRows(array $rows): array + { + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + if (!$this->hasKey($key)) { + $list[$key] = null; + } else { + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + $list[$key] = $this->saveFile($file, $row); + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::deleteRows() + */ + public function deleteRows(array $rows): array + { + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + if (!$this->hasKey($key)) { + $list[$key] = null; + } else { + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + $list[$key] = $this->deleteFile($file); + + $storage = $this->getStoragePath($key); + $media = $this->getMediaPath($key); + + $this->deleteFolder($storage, true); + $media && $this->deleteFolder($media, true); + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::replaceRows() + */ + public function replaceRows(array $rows): array + { + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + if (strpos($key, '@@')) { + $key = $this->getNewKey(); + } + $path = $this->getPathFromKey($key); + $file = $this->getFile($path); + $list[$key] = $this->saveFile($file, $row); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::renameRow() + */ + public function renameRow(string $src, string $dst): bool + { + if ($this->hasKey($dst)) { + throw new \RuntimeException("Cannot rename object: key '{$dst}' is already taken"); + } + + if (!$this->hasKey($src)) { + return false; + } + + return $this->moveFolder($this->getMediaPath($src), $this->getMediaPath($dst)); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getStoragePath() + */ + public function getStoragePath(string $key = null): string + { + if (null === $key) { + $path = $this->dataFolder; + } else { + $path = sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2)); + } + + return $path; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getMediaPath() + */ + public function getMediaPath(string $key = null): string + { + return null !== $key ? \dirname($this->getStoragePath($key)) : $this->getStoragePath(); + } + + /** + * Get filesystem path from the key. + * + * @param string $key + * @return string + */ + public function getPathFromKey(string $key): string + { + return sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2)); + } + + /** + * @param File $file + * @return array|null + */ + protected function loadFile(File $file): ?array + { + if (!$file->exists()) { + return null; + } + + try { + $content = (array)$file->content(); + if (isset($content[0])) { + throw new \RuntimeException('Broken object file.'); + } + } catch (\RuntimeException $e) { + $content = ['__error' => $e->getMessage()]; + } + + return $content; + } + + /** + * @param File $file + * @param array $data + * @return array + */ + protected function saveFile(File $file, array $data): array + { + try { + $file->save($data); + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($file->filename())) { + $locator->clearCache($file->filename()); + } + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Flex saveFile(%s): %s', $file->filename(), $e->getMessage())); + } + + return $data; + } + + /** + * @param File $file + * @return array|string + */ + protected function deleteFile(File $file) + { + try { + $data = $file->content(); + $file->delete(); + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($file->filename())) { + $locator->clearCache($file->filename()); + } + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Flex deleteFile(%s): %s', $file->filename(), $e->getMessage())); + } + + return $data; + } + + /** + * @param string $src + * @param string $dst + * @return bool + */ + protected function moveFolder(string $src, string $dst): bool + { + try { + Folder::move($this->resolvePath($src), $this->resolvePath($dst)); + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($src) || $locator->isStream($dst)) { + $locator->clearCache(); + } + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Flex moveFolder(%s, %s): %s', $src, $dst, $e->getMessage())); + } + + return true; + } + + /** + * @param string $path + * @param bool $include_target + * @return bool + */ + protected function deleteFolder(string $path, bool $include_target = false): bool + { + try { + $success = Folder::delete($this->resolvePath($path), $include_target); + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + if ($locator->isStream($path)) { + $locator->clearCache(); + } + + return $success; + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Flex deleteFolder(%s): %s', $path, $e->getMessage())); + } + } + + /** + * Get key from the filesystem path. + * + * @param string $path + * @return string + */ + protected function getKeyFromPath(string $path): string + { + return basename($path); + } + + /** + * Returns list of all stored keys in [key => timestamp] pairs. + * + * @return array + */ + protected function buildIndex(): array + { + $path = $this->getStoragePath(); + if (!file_exists($path)) { + return []; + } + + if ($this->prefixed) { + $list = $this->buildPrefixedIndexFromFilesystem($path); + } else { + $list = $this->buildIndexFromFilesystem($path); + } + + ksort($list, SORT_NATURAL); + + return $list; + } + + protected function buildIndexFromFilesystem($path) + { + $flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS; + + $iterator = new \FilesystemIterator($path, $flags); + $list = []; + /** @var \SplFileInfo $info */ + foreach ($iterator as $filename => $info) { + if (!$info->isDir()) { + continue; + } + + $key = $this->getKeyFromPath($filename); + $filename = $this->getPathFromKey($key); + $modified = is_file($filename) ? filemtime($filename) : null; + if (null === $modified) { + continue; + } + + $list[$key] = [ + 'storage_key' => $key, + 'storage_timestamp' => $modified + ]; + } + + return $list; + } + + protected function buildPrefixedIndexFromFilesystem($path) + { + $flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS; + + $iterator = new \FilesystemIterator($path, $flags); + $list = []; + /** @var \SplFileInfo $info */ + foreach ($iterator as $filename => $info) { + if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) { + continue; + } + + $list[] = $this->buildIndexFromFilesystem($filename); + } + + if (!$list) { + return []; + } + + return \count($list) > 1 ? array_merge(...$list) : $list[0]; + } + + /** + * @return string + */ + protected function getNewKey(): string + { + // Make sure that the file doesn't exist. + do { + $key = $this->generateKey(); + } while (file_exists($this->getPathFromKey($key))); + + return $key; + } + + /** + * @param array $options + */ + protected function initOptions(array $options): void + { + $extension = $this->dataFormatter->getDefaultFileExtension(); + + /** @var string $pattern */ + $pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern; + + $this->dataFolder = $options['folder']; + $this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/')); + $this->indexed = (bool)($options['indexed'] ?? false); + $this->keyField = $options['key'] ?? 'storage_key'; + + $pattern = preg_replace(['/{FOLDER}/', '/{KEY}/', '/{KEY:2}/'], ['%1$s', '%2$s', '%3$s'], $pattern); + $this->dataPattern = \dirname($pattern) . '/' . basename($pattern, $extension) . $extension; + } +} diff --git a/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php new file mode 100644 index 0000000..088b952 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Storage/SimpleStorage.php @@ -0,0 +1,323 @@ +detectDataFormatter($options['folder']); + $this->initDataFormatter($formatter); + + $extension = $this->dataFormatter->getDefaultFileExtension(); + $pattern = basename($options['folder']); + + $this->dataPattern = basename($pattern, $extension) . $extension; + $this->dataFolder = \dirname($options['folder']); + + // Make sure that the data folder exists. + if (!file_exists($this->dataFolder)) { + try { + Folder::create($this->dataFolder); + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage())); + } + } + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getExistingKeys() + */ + public function getExistingKeys(): array + { + return $this->buildIndex(); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::hasKey() + */ + public function hasKey(string $key): bool + { + if (null === $this->data) { + $this->buildIndex(); + } + + return $key && strpos($key, '@@') === false && isset($this->data[$key]); + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::createRows() + */ + public function createRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + $key = $this->getNewKey(); + $this->data[$key] = $list[$key] = $row; + } + + if ($list) { + $this->save(); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::readRows() + */ + public function readRows(array $rows, array &$fetched = null): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + if (null === $row || (!\is_object($row) && !\is_array($row))) { + // Only load rows which haven't been loaded before. + $key = (string)$key; + $list[$key] = $this->hasKey($key) ? $this->data[$key] : null; + if (null !== $fetched) { + $fetched[$key] = $list[$key]; + } + } else { + // Keep the row if it has been loaded. + $list[$key] = $row; + } + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::updateRows() + */ + public function updateRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + if ($this->hasKey($key)) { + $this->data[$key] = $list[$key] = $row; + } + } + + if ($list) { + $this->save(); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::deleteRows() + */ + public function deleteRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + $key = (string)$key; + if ($this->hasKey($key)) { + unset($this->data[$key]); + $list[$key] = $row; + } + } + + if ($list) { + $this->save(); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::replaceRows() + */ + public function replaceRows(array $rows): array + { + if (null === $this->data) { + $this->buildIndex(); + } + + $list = []; + foreach ($rows as $key => $row) { + if (strpos($key, '@@')) { + $key = $this->getNewKey(); + } + $this->data[$key] = $list[$key] = $row; + } + + if ($list) { + $this->save(); + } + + return $list; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::renameRow() + */ + public function renameRow(string $src, string $dst): bool + { + if (null === $this->data) { + $this->buildIndex(); + } + + if ($this->hasKey($dst)) { + throw new \RuntimeException("Cannot rename object: key '{$dst}' is already taken"); + } + + if (!$this->hasKey($src)) { + return false; + } + + // Change single key in the array without changing the order or value. + $keys = array_keys($this->data); + $keys[array_search($src, $keys, true)] = $dst; + + $data = array_combine($keys, $this->data); + if (false === $data) { + throw new \LogicException('Bad data'); + } + + $this->data = $data; + + return true; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getStoragePath() + */ + public function getStoragePath(string $key = null): string + { + return $this->dataFolder . '/' . $this->dataPattern; + } + + /** + * {@inheritdoc} + * @see FlexStorageInterface::getMediaPath() + */ + public function getMediaPath(string $key = null): string + { + return sprintf('%s/%s/%s', $this->dataFolder, basename($this->dataPattern, $this->dataFormatter->getDefaultFileExtension()), $key); + } + + protected function save() : void + { + if (null === $this->data) { + $this->buildIndex(); + } + + try { + $file = $this->getFile($this->getStoragePath()); + $file->save($this->data); + $file->free(); + } catch (\RuntimeException $e) { + throw new \RuntimeException(sprintf('Flex save(): %s', $e->getMessage())); + } + } + + /** + * Get key from the filesystem path. + * + * @param string $path + * @return string + */ + protected function getKeyFromPath(string $path): string + { + return basename($path); + } + + /** + * Returns list of all stored keys in [key => timestamp] pairs. + * + * @return array + */ + protected function buildIndex(): array + { + $file = $this->getFile($this->getStoragePath()); + $modified = $file->modified(); + + $this->data = (array) $file->content(); + + $list = []; + foreach ($this->data as $key => $info) { + $list[$key] = [ + 'storage_key' => $key, + 'storage_timestamp' => $modified + ]; + } + + return $list; + } + + /** + * @return string + */ + protected function getNewKey(): string + { + if (null === $this->data) { + $this->buildIndex(); + } + + // Make sure that the key doesn't exist. + do { + $key = $this->generateKey(); + } while (isset($this->data[$key])); + + return $key; + } +} diff --git a/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php new file mode 100644 index 0000000..e1b63d9 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Traits/FlexAuthorizeTrait.php @@ -0,0 +1,60 @@ +isAuthorizedAction($user, $action, $scope) || $this->isAuthorizedSuperAdmin($user)); + } + + protected function isAuthorizedSuperAdmin(UserInterface $user): bool + { + return $user->authorize('admin.super'); + } + + protected function isAuthorizedAction(UserInterface $user, string $action, string $scope = null) : bool + { + $scope = $scope ?? isset(Grav::instance()['admin']) ? 'admin' : 'site'; + + if ($action === 'save' && $this instanceof FlexObjectInterface) { + $action = $this->exists() ? 'update' : 'create'; + } + + $directory = $this instanceof FlexDirectory ? $this : $this->getFlexDirectory(); + $config = $directory->getConfig(); + $allowed = $config->get("{$scope}.actions.{$action}") ?? $config->get("actions.{$action}") ?? true; + + return $allowed && $user->authorize(sprintf($this->_authorize, $scope, $action)); + } + + protected function setAuthorizeRule(string $authorize) : void + { + $this->_authorize = $authorize; + } +} diff --git a/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php new file mode 100644 index 0000000..8b22706 --- /dev/null +++ b/system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php @@ -0,0 +1,346 @@ + $this->getUpdatedMedia() + ]; + } + + /** + * @return string + */ + public function getStorageFolder() + { + return $this->exists() ? $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()) : ''; + } + + /** + * @return string + */ + public function getMediaFolder() + { + return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : ''; + } + + /** + * @return MediaCollectionInterface + */ + public function getMedia() + { + if ($this->media === null) { + /** @var AbstractMedia $media */ + $media = $this->getExistingMedia(); + + // Include uploaded media to the object media. + /** @var FormFlashFile $upload */ + foreach ($this->getUpdatedMedia() as $filename => $upload) { + // Just make sure we do not include removed or moved media. + if ($upload && $upload->getError() === \UPLOAD_ERR_OK && !$upload->isMoved()) { + $media->add($filename, MediumFactory::fromUploadedFile($upload)); + } + } + + $media->setTimestamps(); + } + + return $this->media; + } + + public function checkUploadedMediaFile(UploadedFileInterface $uploadedFile) + { + $grav = Grav::instance(); + $language = $grav['language']; + + switch ($uploadedFile->getError()) { + case UPLOAD_ERR_OK: + break; + case UPLOAD_ERR_NO_FILE: + if ($uploadedFile instanceof FormFlashFile) { + break; + } + throw new RuntimeException($language->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 400); + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400); + case UPLOAD_ERR_NO_TMP_DIR: + throw new RuntimeException($language->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 400); + default: + throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400); + } + + $filename = $uploadedFile->getClientFilename(); + + if (!Utils::checkFilename($filename)) { + throw new RuntimeException(sprintf($language->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filename, 'Bad filename'), 400); + } + + $grav_limit = Utils::getUploadLimit(); + if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) { + throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400); + } + + $this->checkMediaFilename($filename); + } + + public function checkMediaFilename(string $filename) + { + // Check the file extension. + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + $grav = Grav::instance(); + + /** @var Config $config */ + $config = $grav['config']; + + // If not a supported type, return + if (!$extension || !$config->get("media.types.{$extension}")) { + $language = $grav['language']; + throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400); + } + } + + public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null): void + { + $this->checkUploadedMediaFile($uploadedFile); + + if ($filename) { + $this->checkMediaFilename(basename($filename)); + } else { + $filename = $uploadedFile->getClientFilename(); + } + + $media = $this->getMedia(); + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + $path = $media->getPath(); + if (!$path) { + $language = $grav['language']; + + throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400); + } + + if ($locator->isStream($path)) { + $path = $locator->findResource($path, true, true); + $locator->clearCache($path); + } + + try { + // Upload it + $filepath = sprintf('%s/%s', $path, $filename); + Folder::create(\dirname($filepath)); + if ($uploadedFile instanceof FormFlashFile) { + $metadata = $uploadedFile->getMetaData(); + if ($metadata) { + $file = YamlFile::instance($filepath . '.meta.yaml'); + $file->save(['upload' => $metadata]); + } + if ($uploadedFile->getError() === \UPLOAD_ERR_OK) { + $uploadedFile->moveTo($filepath); + } elseif (!file_exists($filepath) && $pos = strpos($filename, '/')) { + $origpath = sprintf('%s/%s', $path, substr($filename, $pos)); + if (file_exists($origpath)) { + copy($origpath, $filepath); + } + } + } else { + $uploadedFile->moveTo($filepath); + } + } catch (\Exception $e) { + $language = $grav['language']; + + throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400); + } + + $this->clearMediaCache(); + } + + public function deleteMediaFile(string $filename): void + { + $grav = Grav::instance(); + $language = $grav['language']; + + $basename = basename($filename); + $dirname = dirname($filename); + $dirname = $dirname === '.' ? '' : '/' . $dirname; + + if (!Utils::checkFilename($basename)) { + throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': Bad filename: ' . $filename, 400); + } + + $media = $this->getMedia(); + $path = $media->getPath(); + if (!$path) { + return; + } + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + $targetPath = $path . '/' . $dirname; + $targetFile = $path . '/' . $filename; + if ($locator->isStream($targetFile)) { + $targetPath = $locator->findResource($targetPath, true, true); + $targetFile = $locator->findResource($targetFile, true, true); + $locator->clearCache($targetPath); + $locator->clearCache($targetFile); + } + + $fileParts = pathinfo($basename); + + if (!file_exists($targetPath)) { + return; + } + + if (file_exists($targetFile)) { + $result = unlink($targetFile); + if (!$result) { + throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500); + } + } + + // Remove Extra Files + foreach (scandir($targetPath, SCANDIR_SORT_NONE) as $file) { + $preg_name = preg_quote($fileParts['filename'], '`'); + $preg_ext =preg_quote($fileParts['extension'], '`'); + $preg_filename = preg_quote($basename, '`'); + + if (preg_match("`({$preg_name}@\d+x\.{$preg_ext}(?:\.meta\.yaml)?$|{$preg_filename}\.meta\.yaml)$`", $file)) { + $testPath = $targetPath . '/' . $file; + if ($locator->isStream($testPath)) { + $testPath = $locator->findResource($testPath, true, true); + $locator->clearCache($testPath); + } + + if (is_file($testPath)) { + $result = unlink($testPath); + if (!$result) { + throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500); + } + } + } + } + + $this->clearMediaCache(); + } + + /** + * @param array $files + */ + protected function setUpdatedMedia(array $files): void + { + $list = []; + foreach ($files as $field => $group) { + if ($field === '' || \strpos($field, '/', true)) { + continue; + } + foreach ($group as $filename => $file) { + $list[$filename] = $file; + } + } + + $this->_uploads = $list; + } + + /** + * @return array + */ + protected function getUpdatedMedia(): array + { + return $this->_uploads ?? []; + } + + protected function saveUpdatedMedia(): void + { + /** + * @var string $filename + * @var UploadedFileInterface $file + */ + foreach ($this->getUpdatedMedia() as $filename => $file) { + if ($file) { + $this->uploadMediaFile($file, $filename); + } else { + $this->deleteMediaFile($filename); + } + } + + $this->setUpdatedMedia([]); + } + + /** + * @param string $uri + * @return Medium|null + */ + protected function createMedium($uri) + { + $grav = Grav::instance(); + + /** @var UniformResourceLocator $locator */ + $locator = $grav['locator']; + + $file = $uri && $locator->isStream($uri) ? $locator->findResource($uri) : $uri; + + return $file && file_exists($file) ? MediumFactory::fromFile($file) : null; + } + + /** + * @return Cache + */ + protected function getMediaCache() + { + return $this->getCache('object'); + } + + protected function offsetLoad_media() + { + return $this->getMedia(); + } + + protected function offsetSerialize_media() + { + return null; + } + + abstract public function getFlexDirectory(): FlexDirectory; + + abstract public function getStorageKey(); +} diff --git a/system/src/Grav/Framework/Form/FormFlash.php b/system/src/Grav/Framework/Form/FormFlash.php new file mode 100644 index 0000000..e2ba826 --- /dev/null +++ b/system/src/Grav/Framework/Form/FormFlash.php @@ -0,0 +1,519 @@ + $args[0], + 'unique_id' => $args[1] ?? null, + 'form_name' => $args[2] ?? null, + ]; + } + + $this->sessionId = $config['session_id'] ?? 'no-session'; + $this->uniqueId = $config['unique_id'] ?? ''; + + $folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : ''); + + /** @var UniformResourceLocator $locator */ + $locator = Grav::instance()['locator']; + + $this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder; + $file = $this->getTmpIndex(); + $this->exists = $file->exists(); + + if ($this->exists) { + try { + $data = (array)$file->content(); + } catch (\Exception $e) { + $data = []; + } + $this->formName = $content['form'] ?? $config['form_name'] ?? ''; + $this->url = $data['url'] ?? ''; + $this->user = $data['user'] ?? null; + $this->updatedTimestamp = $data['timestamps']['updated'] ?? time(); + $this->createdTimestamp = $data['timestamps']['created'] ?? $this->updatedTimestamp; + $this->data = $data['data'] ?? null; + $this->files = $data['files'] ?? []; + } else { + $this->formName = $config['form_name'] ?? ''; + $this->url = ''; + $this->createdTimestamp = $this->updatedTimestamp = time(); + $this->files = []; + } + } + + /** + * @inheritDoc + */ + public function getSessionId(): string + { + return $this->sessionId; + } + + /** + * @inheritDoc + */ + public function getUniqueId(): string + { + return $this->uniqueId; + } + + /** + * @deprecated 1.6.11 Use '->getUniqueId()' method instead. + */ + public function getUniqieId(): string + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6.11, use ->getUniqueId() method instead', E_USER_DEPRECATED); + + return $this->getUniqueId(); + } + + /** + * @inheritDoc + */ + public function getFormName(): string + { + return $this->formName; + } + + + /** + * @inheritDoc + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * @inheritDoc + */ + public function getUsername(): string + { + return $this->user['username'] ?? ''; + } + + /** + * @inheritDoc + */ + public function getUserEmail(): string + { + return $this->user['email'] ?? ''; + } + + /** + * @inheritDoc + */ + public function getCreatedTimestamp(): int + { + return $this->createdTimestamp; + } + + /** + * @inheritDoc + */ + public function getUpdatedTimestamp(): int + { + return $this->updatedTimestamp; + } + + + /** + * @inheritDoc + */ + public function getData(): ?array + { + return $this->data; + } + + /** + * @inheritDoc + */ + public function setData(?array $data): void + { + $this->data = $data; + } + + /** + * @inheritDoc + */ + public function exists(): bool + { + return $this->exists; + } + + /** + * @inheritDoc + */ + public function save(): self + { + if (!($this->folder && $this->uniqueId)) { + return $this; + } + + if ($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; + } elseif ($this->exists) { + // Delete empty form flash if it exists (it carries no information). + return $this->delete(); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function delete(): self + { + if ($this->folder && $this->uniqueId) { + $this->removeTmpDir(); + $this->files = []; + $this->exists = false; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getFilesByField(string $field): array + { + if (!isset($this->uploadObjects[$field])) { + $objects = []; + foreach ($this->files[$field] ?? [] as $name => $upload) { + $objects[$name] = $upload ? new FormFlashFile($field, $upload, $this) : null; + } + $this->uploadedFiles[$field] = $objects; + } + + return $this->uploadedFiles[$field]; + } + + /** + * @inheritDoc + */ + public function getFilesByFields($includeOriginal = false): array + { + $list = []; + foreach ($this->files as $field => $values) { + if (!$includeOriginal && strpos($field, '/')) { + continue; + } + $list[$field] = $this->getFilesByField($field); + } + + return $list; + } + + /** + * @inheritDoc + */ + public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string + { + $tmp_dir = $this->getTmpDir(); + $tmp_name = Utils::generateRandomString(12); + $name = $upload->getClientFilename(); + + // Prepare upload data for later save + $data = [ + 'name' => $name, + 'type' => $upload->getClientMediaType(), + 'size' => $upload->getSize(), + 'tmp_name' => $tmp_name + ]; + + Folder::create($tmp_dir); + $upload->moveTo("{$tmp_dir}/{$tmp_name}"); + + $this->addFileInternal($field, $name, $data, $crop); + + return $name; + } + + + /** + * @inheritDoc + */ + public function addFile(string $filename, string $field, array $crop = null): bool + { + if (!file_exists($filename)) { + throw new \RuntimeException("File not found: {$filename}"); + } + + // Prepare upload data for later save + $data = [ + 'name' => basename($filename), + 'type' => Utils::getMimeByLocalFile($filename), + 'size' => filesize($filename), + ]; + + $this->addFileInternal($field, $data['name'], $data, $crop); + + return true; + } + + /** + * @inheritDoc + */ + public function removeFile(string $name, string $field = null): bool + { + if (!$name) { + return false; + } + + $field = $field ?: 'undefined'; + + $upload = $this->files[$field][$name] ?? null; + if (null !== $upload) { + $this->removeTmpFile($upload['tmp_name'] ?? ''); + } + $upload = $this->files[$field . '/original'][$name] ?? null; + if (null !== $upload) { + $this->removeTmpFile($upload['tmp_name'] ?? ''); + } + + // Mark file as deleted. + $this->files[$field][$name] = null; + $this->files[$field . '/original'][$name] = null; + + unset( + $this->uploadedFiles[$field][$name], + $this->uploadedFiles[$field . '/original'][$name] + ); + + return true; + } + + /** + * @inheritDoc + */ + public function clearFiles() + { + foreach ($this->files as $field => $files) { + foreach ($files as $name => $upload) { + $this->removeTmpFile($upload['tmp_name'] ?? ''); + } + } + + $this->files = []; + } + + /** + * @inheritDoc + */ + public function jsonSerialize(): array + { + return [ + 'form' => $this->formName, + 'unique_id' => $this->uniqueId, + 'url' => $this->url, + 'user' => $this->user, + 'timestamps' => [ + 'created' => $this->createdTimestamp, + 'updated' => time(), + ], + 'data' => $this->data, + 'files' => $this->files + ]; + } + + /** + * @param string $url + * @return $this + */ + public function setUrl(string $url): self + { + $this->url = $url; + + return $this; + } + + /** + * @param UserInterface|null $user + * @return $this + */ + public function setUser(UserInterface $user = null) + { + if ($user && $user->username) { + $this->user = [ + 'username' => $user->username, + 'email' => $user->email ?? '' + ]; + } else { + $this->user = null; + } + + return $this; + } + + /** + * @param string|null $username + * @return $this + */ + public function setUserName(string $username = null): self + { + $this->user['username'] = $username; + + return $this; + } + + /** + * @param string|null $email + * @return $this + */ + public function setUserEmail(string $email = null): self + { + $this->user['email'] = $email; + + return $this; + } + + /** + * @return string + */ + public function getTmpDir(): string + { + return $this->folder && $this->uniqueId ? "{$this->folder}/{$this->uniqueId}" : ''; + } + + /** + * @return YamlFile + */ + protected function getTmpIndex(): YamlFile + { + // Do not use CompiledYamlFile as the file can change multiple times per second. + return YamlFile::instance($this->getTmpDir() . '/index.yaml'); + } + + /** + * @param string $name + */ + protected function removeTmpFile(string $name): void + { + $tmpDir = $this->getTmpDir(); + $filename = $tmpDir ? $tmpDir . '/' . $name : ''; + if ($name && $filename && is_file($filename)) { + unlink($filename); + } + } + + protected function removeTmpDir(): void + { + $tmpDir = $this->getTmpDir(); + if ($tmpDir && file_exists($tmpDir)) { + Folder::delete($tmpDir); + } + } + + /** + * @param string $field + * @param string $name + * @param array $data + * @param array|null $crop + */ + protected function addFileInternal(?string $field, string $name, array $data, array $crop = null): void + { + if (!($this->folder && $this->uniqueId)) { + throw new \RuntimeException('Cannot upload files: form flash folder not defined'); + } + + $field = $field ?: 'undefined'; + if (!isset($this->files[$field])) { + $this->files[$field] = []; + } + + $oldUpload = $this->files[$field][$name] ?? null; + + if ($crop) { + // Deal with crop upload + if ($oldUpload) { + $originalUpload = $this->files[$field . '/original'][$name] ?? null; + if ($originalUpload) { + // If there is original file already present, remove the modified file + $this->files[$field . '/original'][$name]['crop'] = $crop; + $this->removeTmpFile($oldUpload['tmp_name'] ?? ''); + } else { + // Otherwise make the previous file as original + $oldUpload['crop'] = $crop; + $this->files[$field . '/original'][$name] = $oldUpload; + } + } else { + $this->files[$field . '/original'][$name] = [ + 'name' => $name, + 'type' => $data['type'], + 'crop' => $crop + ]; + } + } else { + // Deal with replacing upload + $originalUpload = $this->files[$field . '/original'][$name] ?? null; + $this->files[$field . '/original'][$name] = null; + + $this->removeTmpFile($oldUpload['tmp_name'] ?? ''); + $this->removeTmpFile($originalUpload['tmp_name'] ?? ''); + } + + // Prepare data to be saved later + $this->files[$field][$name] = $data; + } +} diff --git a/system/src/Grav/Framework/Form/FormFlashFile.php b/system/src/Grav/Framework/Form/FormFlashFile.php new file mode 100644 index 0000000..9183689 --- /dev/null +++ b/system/src/Grav/Framework/Form/FormFlashFile.php @@ -0,0 +1,159 @@ +field = $field; + $this->upload = $upload; + $this->flash = $flash; + + $tmpFile = $this->getTmpFile(); + if (!$tmpFile && $this->isOk()) { + $this->upload['error'] = \UPLOAD_ERR_NO_FILE; + } + + if (!isset($this->upload['size'])) { + $this->upload['size'] = $tmpFile && $this->isOk() ? filesize($tmpFile) : 0; + } + } + + /** + * @return StreamInterface + */ + public function getStream() + { + $this->validateActive(); + + $resource = \fopen($this->getTmpFile(), 'rb'); + + return Stream::create($resource); + } + + public function moveTo($targetPath) + { + $this->validateActive(); + + if (!\is_string($targetPath) || empty($targetPath)) { + throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); + } + + $this->moved = \copy($this->getTmpFile(), $targetPath); + + if (false === $this->moved) { + throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath)); + } + + $this->flash->removeFile($this->getClientFilename(), $this->field); + } + + public function getSize() + { + return $this->upload['size']; + } + + public function getError() + { + return $this->upload['error'] ?? \UPLOAD_ERR_OK; + } + + public function getClientFilename() + { + return $this->upload['name'] ?? 'unknown'; + } + + public function getClientMediaType() + { + return $this->upload['type'] ?? 'application/octet-stream'; + } + + public function isMoved() : bool + { + return $this->moved; + } + + public function getMetaData() : array + { + if (isset($this->upload['crop'])) { + return ['crop' => $this->upload['crop']]; + } + + return []; + } + + public function getDestination() + { + return $this->upload['path'] ?? ''; + } + + public function jsonSerialize() + { + return $this->upload; + } + + public function getTmpFile() : ?string + { + $tmpName = $this->upload['tmp_name'] ?? null; + + if (!$tmpName) { + return null; + } + + $tmpFile = $this->flash->getTmpDir() . '/' . $tmpName; + + return file_exists($tmpFile) ? $tmpFile : null; + } + + public function __debugInfo() + { + return [ + 'field:private' => $this->field, + 'moved:private' => $this->moved, + 'upload:private' => $this->upload, + ]; + } + + /** + * @throws \RuntimeException if is moved or not ok + */ + private function validateActive(): void + { + if (!$this->isOk()) { + throw new \RuntimeException('Cannot retrieve stream due to upload error'); + } + + if ($this->moved) { + throw new \RuntimeException('Cannot retrieve stream after it has already been moved'); + } + + if (!$this->getTmpFile()) { + throw new \RuntimeException('Cannot retrieve stream as the file is missing'); + } + } + + /** + * @return bool return true if there is no upload error + */ + private function isOk(): bool + { + return \UPLOAD_ERR_OK === $this->getError(); + } +} diff --git a/system/src/Grav/Framework/Form/Interfaces/FormFactoryInterface.php b/system/src/Grav/Framework/Form/Interfaces/FormFactoryInterface.php new file mode 100644 index 0000000..fb57db1 --- /dev/null +++ b/system/src/Grav/Framework/Form/Interfaces/FormFactoryInterface.php @@ -0,0 +1,38 @@ +id; + } + + public function setId(string $id): void + { + $this->id = $id; + } + + public function getUniqueId(): string + { + return $this->uniqueid; + } + + public function setUniqueId(string $uniqueId): void + { + $this->uniqueid = $uniqueId; + } + + public function getName(): string + { + return $this->name; + } + + public function getFormName(): string + { + return $this->name; + } + + public function getNonceName(): string + { + return 'form-nonce'; + } + + public function getNonceAction(): string + { + return 'form'; + } + + public function getNonce(): string + { + return Utils::getNonce($this->getNonceAction()); + } + + public function getAction(): string + { + return ''; + } + + public function getTask(): string + { + return $this->getBlueprint()->get('form/task') ?? ''; + } + + public function getData(string $name = null) + { + return null !== $name ? $this->data[$name] : $this->data; + } + + /** + * @return array|UploadedFileInterface[] + */ + public function getFiles(): array + { + return $this->files ?? []; + } + + public function getValue(string $name) + { + return $this->data[$name] ?? null; + } + + public function getDefaultValue(string $name) + { + $path = explode('.', $name) ?: []; + $offset = array_shift($path) ?? ''; + + $current = $this->getDefaultValues(); + + if (!isset($current[$offset])) { + return null; + } + + $current = $current[$offset]; + + while ($path) { + $offset = array_shift($path); + + if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) { + $current = $current[$offset]; + } elseif (\is_object($current) && isset($current->{$offset})) { + $current = $current->{$offset}; + } else { + return null; + } + }; + + return $current; + } + + /** + * @return array + */ + public function getDefaultValues(): array + { + return $this->getBlueprint()->getDefaults(); + } + + /** + * @param ServerRequestInterface $request + * @return FormInterface|$this + */ + public function handleRequest(ServerRequestInterface $request): FormInterface + { + // Set current form to be active. + $grav = Grav::instance(); + $forms = $grav['forms'] ?? null; + if ($forms) { + $forms->setActiveForm($this); + + /** @var Twig $twig */ + $twig = $grav['twig']; + $twig->twig_vars['form'] = $this; + + } + + try { + [$data, $files] = $this->parseRequest($request); + + $this->submit($data, $files); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + } + + return $this; + } + + /** + * @param ServerRequestInterface $request + * @return FormInterface|$this + */ + public function setRequest(ServerRequestInterface $request): FormInterface + { + [$data, $files] = $this->parseRequest($request); + + $this->data = new Data($data, $this->getBlueprint()); + $this->files = $files; + + return $this; + } + + public function isValid(): bool + { + return $this->status === 'success'; + } + + public function getError(): ?string + { + return !$this->isValid() ? $this->message : null; + } + + public function getErrors(): array + { + return !$this->isValid() ? $this->messages : []; + } + + public function isSubmitted(): bool + { + return $this->submitted; + } + + public function validate(): bool + { + if (!$this->isValid()) { + return false; + } + + try { + $this->validateData($this->data); + $this->validateUploads($this->getFiles()); + } catch (ValidationException $e) { + $this->setErrors($e->getMessages()); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + } + + $this->filterData($this->data); + + return $this->isValid(); + } + + /** + * @param array $data + * @param UploadedFileInterface[] $files + * @return FormInterface|$this + */ + public function submit(array $data, array $files = null): FormInterface + { + try { + if ($this->isSubmitted()) { + throw new \RuntimeException('Form has already been submitted'); + } + + $this->data = new Data($data, $this->getBlueprint()); + $this->files = $files ?? []; + + if (!$this->validate()) { + return $this; + } + + $this->doSubmit($this->data->toArray(), $this->files); + + $this->submitted = true; + } catch (\Exception $e) { + $this->setError($e->getMessage()); + } + + return $this; + } + + public function reset(): void + { + // Make sure that the flash object gets deleted. + $this->getFlash()->delete(); + + $this->data = null; + $this->files = []; + $this->status = 'success'; + $this->message = null; + $this->messages = []; + $this->submitted = false; + $this->flash = null; + } + + public function getFields(): array + { + return $this->getBlueprint()->fields(); + } + + public function getButtons(): array + { + return $this->getBlueprint()->get('form/buttons') ?? []; + } + + public function getTasks(): array + { + return $this->getBlueprint()->get('form/tasks') ?? []; + } + + abstract public function getBlueprint(): Blueprint; + + /** + * Implements \Serializable::serialize(). + * + * @return string + */ + public function serialize(): string + { + return serialize($this->doSerialize()); + } + + /** + * Implements \Serializable::unserialize(). + * + * @param string $serialized + */ + public function unserialize($serialized): void + { + $data = unserialize($serialized, ['allowed_classes' => false]); + + $this->doUnserialize($data); + } + + /** + * Get form flash object. + * + * @return FormFlash + */ + public function getFlash(): FormFlash + { + if (null === $this->flash) { + $grav = Grav::instance(); + $config = [ + 'session_id' => $this->getSessionId(), + 'unique_id' => $this->getUniqueId(), + 'form_name' => $this->getName(), + 'folder' => $this->getFlashFolder() + ]; + + + $this->flash = new FormFlash($config); + $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null); + } + + return $this->flash; + } + + /** + * Get all available form flash objects for this form. + * + * @return FormFlash[] + */ + public function getAllFlashes(): array + { + $folder = $this->getFlashFolder(); + if (!$folder || !is_dir($folder)) { + return []; + } + + $name = $this->getName(); + + $list = []; + /** @var \SplFileInfo $file */ + foreach (new \FilesystemIterator($folder) as $file) { + $uniqueId = $file->getFilename(); + $config = [ + 'session_id' => $this->getSessionId(), + 'unique_id' => $uniqueId, + 'form_name' => $name, + 'folder' => $this->getFlashFolder() + ]; + $flash = new FormFlash($config); + if ($flash->exists() && $flash->getFormName() === $name) { + $list[] = $flash; + } + } + + return $list; + + } + + /** + * {@inheritdoc} + * @see FormInterface::render() + */ + public function render(string $layout = null, array $context = []) + { + if (null === $layout) { + $layout = 'default'; + } + + $grav = Grav::instance(); + + $block = HtmlBlock::create(); + $block->disableCache(); + + $output = $this->getTemplate($layout)->render( + ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'form' => $this, 'layout' => $layout] + $context + ); + + $block->setContent($output); + + return $block; + } + + protected function getSessionId(): string + { + /** @var Grav $grav */ + $grav = Grav::instance(); + + /** @var SessionInterface $session */ + $session = $grav['session'] ?? null; + + return $session ? ($session->getId() ?? '') : ''; + } + + protected function unsetFlash(): void + { + $this->flash = null; + } + + protected function getFlashFolder(): ?string + { + $grav = Grav::instance(); + + /** @var UserInterface $user */ + $user = $grav['user'] ?? null; + $userExists = $user && $user->exists(); + $username = $userExists ? $user->username : null; + $mediaFolder = $userExists ? $user->getMediaFolder() : null; + $session = $grav['session'] ?? null; + $sessionId = $session ? $session->getId() : null; + + // Fill template token keys/value pairs. + $dataMap = [ + '[FORM_NAME]' => $this->getName(), + '[SESSIONID]' => $sessionId ?? '!!', + '[USERNAME]' => $username ?? '!!', + '[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!', + '[ACCOUNT]' => $mediaFolder ?? '!!' + ]; + + $flashFolder = $this->getBlueprint()->get('form/flash_folder', 'tmp://forms/[SESSIONID]'); + + $path = str_replace(array_keys($dataMap), array_values($dataMap), $flashFolder); + + // Make sure we only return valid paths. + return strpos($path, '!!') === false ? rtrim($path, '/') : null; + } + + /** + * Set a single error. + * + * @param string $error + */ + protected function setError(string $error): void + { + $this->status = 'error'; + $this->message = $error; + } + + /** + * Set all errors. + * + * @param array $errors + */ + protected function setErrors(array $errors): void + { + $this->status = 'error'; + $this->messages = $errors; + } + + /** + * @param string $layout + * @return TemplateWrapper + * @throws LoaderError + * @throws SyntaxError + */ + protected function getTemplate($layout) + { + $grav = Grav::instance(); + + /** @var Twig $twig */ + $twig = $grav['twig']; + + return $twig->twig()->resolveTemplate( + [ + "forms/{$layout}/form.html.twig", + 'forms/default/form.html.twig' + ] + ); + } + + /** + * Parse PSR-7 ServerRequest into data and files. + * + * @param ServerRequestInterface $request + * @return array + */ + protected function parseRequest(ServerRequestInterface $request): array + { + $method = $request->getMethod(); + if (!\in_array($method, ['PUT', 'POST', 'PATCH'])) { + throw new \RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method)); + } + + $body = $request->getParsedBody(); + $data = isset($body['data']) ? $this->decodeData($body['data']) : null; + + $flash = $this->getFlash(); + /* + if (null !== $data) { + $flash->setData($data); + $flash->save(); + } + */ + + $blueprint = $this->getBlueprint(); + $includeOriginal = (bool)($blueprint->form()['images']['original'] ?? null); + $files = $flash->getFilesByFields($includeOriginal); + + $data = $blueprint->processForm($data ?? [], $body['toggleable_data'] ?? []); + + return [ + $data, + $files ?? [] + ]; + } + + /** + * Form submit logic goes here. + * + * @param array $data + * @param array $files + * @return mixed + */ + abstract protected function doSubmit(array $data, array $files); + + /** + * Validate data and throw validation exceptions if validation fails. + * + * @param \ArrayAccess $data + * @throws ValidationException + * @throws \Exception + */ + protected function validateData(\ArrayAccess $data): void + { + if ($data instanceof Data) { + $data->validate(); + } + } + + /** + * Filter validated data. + * + * @param \ArrayAccess $data + */ + protected function filterData(\ArrayAccess $data): void + { + if ($data instanceof Data) { + $data->filter(); + } + } + + /** + * Validate all uploaded files. + * + * @param array $files + */ + protected function validateUploads(array $files): void + { + foreach ($files as $file) { + if (null === $file) { + continue; + } + if ($file instanceof UploadedFileInterface) { + $this->validateUpload($file); + } else { + $this->validateUploads($file); + } + } + } + + /** + * Validate uploaded file. + * + * @param UploadedFileInterface $file + */ + protected function validateUpload(UploadedFileInterface $file): void + { + // Handle bad filenames. + $filename = $file->getClientFilename(); + + if (!Utils::checkFilename($filename)) { + $grav = Grav::instance(); + throw new \RuntimeException( + sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename') + ); + } + } + + /** + * Decode POST data + * + * @param array $data + * @return array + */ + protected function decodeData($data): array + { + if (!\is_array($data)) { + return []; + } + + // Decode JSON encoded fields and merge them to data. + if (isset($data['_json'])) { + $data = array_replace_recursive($data, $this->jsonDecode($data['_json'])); + unset($data['_json']); + } + + return $data; + } + + /** + * Recursively JSON decode POST data. + * + * @param array $data + * @return array + */ + protected function jsonDecode(array $data): array + { + foreach ($data as $key => &$value) { + if (\is_array($value)) { + $value = $this->jsonDecode($value); + } elseif (trim($value) === '') { + unset($data[$key]); + } else { + $value = json_decode($value, true); + if ($value === null && json_last_error() !== JSON_ERROR_NONE) { + unset($data[$key]); + $this->setError("Badly encoded JSON data (for {$key}) was sent to the form"); + } + } + } + + return $data; + } + + /** + * @return array + */ + protected function doSerialize(): array + { + $data = $this->data instanceof Data ? $this->data->toArray() : null; + + return [ + 'name' => $this->name, + 'id' => $this->id, + 'uniqueid' => $this->uniqueid, + 'submitted' => $this->submitted, + 'status' => $this->status, + 'message' => $this->message, + 'messages' => $this->messages, + 'data' => $data, + 'files' => $this->files, + ]; + } + + /** + * @param array $data + */ + protected function doUnserialize(array $data): void + { + $this->name = $data['name']; + $this->id = $data['id']; + $this->uniqueid = $data['uniqueid']; + $this->submitted = $data['submitted'] ?? false; + $this->status = $data['status'] ?? 'success'; + $this->message = $data['message'] ?? null; + $this->messages = $data['messages'] ?? []; + $this->data = isset($data['data']) ? new Data($data['data'], $this->getBlueprint()) : null; + $this->files = $data['files'] ?? []; + } +} diff --git a/system/src/Grav/Framework/Interfaces/RenderInterface.php b/system/src/Grav/Framework/Interfaces/RenderInterface.php new file mode 100644 index 0000000..ca6d9bc --- /dev/null +++ b/system/src/Grav/Framework/Interfaces/RenderInterface.php @@ -0,0 +1,38 @@ +render('custom', ['variable' => 'value']); + * @example {% render object layout 'custom' with { variable: 'value' } %} + * + * @param string|null $layout Layout to be used. + * @param array $context Extra context given to the renderer. + * + * @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output. + * @api + */ + public function render(string $layout = null, array $context = []); +} diff --git a/system/src/Grav/Framework/Media/Interfaces/MediaCollectionInterface.php b/system/src/Grav/Framework/Media/Interfaces/MediaCollectionInterface.php new file mode 100644 index 0000000..e834bc8 --- /dev/null +++ b/system/src/Grav/Framework/Media/Interfaces/MediaCollectionInterface.php @@ -0,0 +1,17 @@ +hasProperty($offset)) { return $default; @@ -57,9 +58,9 @@ trait NestedPropertyTrait $offset = array_shift($path); - if ((is_array($current) || is_a($current, 'ArrayAccess')) && isset($current[$offset])) { + if ((\is_array($current) || is_a($current, 'ArrayAccess')) && isset($current[$offset])) { $current = $current[$offset]; - } elseif (is_object($current) && isset($current->{$offset})) { + } elseif (\is_object($current) && isset($current->{$offset})) { $current = $current->{$offset}; } else { return $default; @@ -80,8 +81,8 @@ trait NestedPropertyTrait public function setNestedProperty($property, $value, $separator = null) { $separator = $separator ?: '.'; - $path = explode($separator, $property); - $offset = array_shift($path); + $path = explode($separator, $property) ?: []; + $offset = array_shift($path) ?? ''; if (!$path) { $this->setProperty($offset, $value); @@ -97,12 +98,12 @@ trait NestedPropertyTrait // Handle arrays and scalars. if ($current === null) { $current = [$offset => []]; - } elseif (is_array($current)) { + } elseif (\is_array($current)) { if (!isset($current[$offset])) { $current[$offset] = []; } } else { - throw new \RuntimeException('Cannot set nested property on non-array value'); + throw new \RuntimeException("Cannot set nested property {$property} on non-array value"); } $current = &$current[$offset]; @@ -122,8 +123,8 @@ trait NestedPropertyTrait public function unsetNestedProperty($property, $separator = null) { $separator = $separator ?: '.'; - $path = explode($separator, $property); - $offset = array_shift($path); + $path = explode($separator, $property) ?: []; + $offset = array_shift($path) ?? ''; if (!$path) { $this->unsetProperty($offset); @@ -141,12 +142,12 @@ trait NestedPropertyTrait if ($current === null) { return $this; } - if (is_array($current)) { + if (\is_array($current)) { if (!isset($current[$offset])) { return $this; } } else { - throw new \RuntimeException('Cannot set nested property on non-array value'); + throw new \RuntimeException("Cannot unset nested property {$property} on non-array value"); } $current = &$current[$offset]; @@ -159,7 +160,7 @@ trait NestedPropertyTrait /** * @param string $property Object property to be updated. - * @param string $default Default value. + * @param mixed $default Default value. * @param string $separator Separator, defaults to '.' * @return $this * @throws \RuntimeException diff --git a/system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php b/system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php index 1524bd6..029dbba 100644 --- a/system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php +++ b/system/src/Grav/Framework/Object/Access/OverloadedPropertyTrait.php @@ -1,8 +1,9 @@ getIterator() as $key => $value) { - $list[$key] = is_object($value) ? clone $value : $value; + $list[$key] = \is_object($value) ? clone $value : $value; } return $this->createFrom($list); @@ -45,7 +46,7 @@ trait ObjectCollectionTrait /** * @param string $property Object property to be matched. - * @return array Key/Value pairs of the properties. + * @return bool[] Key/Value pairs of the properties. */ public function doHasProperty($property) { @@ -62,7 +63,7 @@ trait ObjectCollectionTrait /** * @param string $property Object property to be fetched. * @param mixed $default Default value if not set. - * @return array Key/Value pairs of the properties. + * @return mixed[] Key/Value pairs of the properties. */ public function doGetProperty($property, $default = null) { @@ -78,7 +79,7 @@ trait ObjectCollectionTrait /** * @param string $property Object property to be updated. - * @param string $value New value. + * @param mixed $value New value. * @return $this */ public function doSetProperty($property, $value) @@ -107,7 +108,7 @@ trait ObjectCollectionTrait /** * @param string $property Object property to be updated. - * @param string $default Default value. + * @param mixed $default Default value. * @return $this */ public function doDefProperty($property, $default) @@ -123,15 +124,19 @@ trait ObjectCollectionTrait /** * @param string $method Method name. * @param array $arguments List of arguments passed to the function. - * @return array Return values. + * @return mixed[] Return values. */ public function call($method, array $arguments = []) { $list = []; + /** + * @var string|int $id + * @var ObjectInterface $element + */ foreach ($this->getIterator() as $id => $element) { - $list[$id] = method_exists($element, $method) - ? call_user_func_array([$element, $method], $arguments) : null; + $callable = method_exists($element, $method) ? [$element, $method] : null; + $list[$id] = $callable ? \call_user_func_array($callable, $arguments) : null; } return $list; diff --git a/system/src/Grav/Framework/Object/Base/ObjectTrait.php b/system/src/Grav/Framework/Object/Base/ObjectTrait.php index c736381..baeeb2b 100644 --- a/system/src/Grav/Framework/Object/Base/ObjectTrait.php +++ b/system/src/Grav/Framework/Object/Base/ObjectTrait.php @@ -1,8 +1,9 @@ _key ?: $this->getType() . '@' . spl_object_hash($this); + return $this->_key ?: $this->getType() . '@@' . spl_object_hash($this); + } + + /** + * @return bool + */ + public function hasKey() + { + return !empty($this->_key); } /** * @param string $property Object property name. - * @return bool True if property has been defined (can be null). + * @return bool|bool[] True if property has been defined (can be null). */ public function hasProperty($property) { @@ -67,7 +76,7 @@ trait ObjectTrait /** * @param string $property Object property to be fetched. * @param mixed $default Default value if property has not been set. - * @return mixed Property value. + * @return mixed|mixed[] Property value. */ public function getProperty($property, $default = null) { @@ -76,7 +85,7 @@ trait ObjectTrait /** * @param string $property Object property to be updated. - * @param string $value New value. + * @param mixed $value New value. * @return $this */ public function setProperty($property, $value) @@ -139,7 +148,7 @@ trait ObjectTrait */ protected function doSerialize() { - return $this->jsonSerialize(); + return ['key' => $this->getKey(), 'type' => $this->getType(), 'elements' => $this->getElements()]; } /** @@ -162,7 +171,7 @@ trait ObjectTrait */ public function jsonSerialize() { - return ['key' => $this->getKey(), 'type' => $this->getType(), 'elements' => $this->getElements()]; + return $this->doSerialize(); } /** diff --git a/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php b/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php index 5ad3897..0eefb78 100644 --- a/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php +++ b/system/src/Grav/Framework/Object/Collection/ObjectExpressionVisitor.php @@ -1,8 +1,9 @@ setElements($elements)); - $this->setKey($key); + $this->setKey($key ?? ''); + } + + /** + * @param array $ordering + * @return Collection|static + */ + public function orderBy(array $ordering) + { + $criteria = Criteria::create()->orderBy($ordering); + + return $this->matching($criteria); + } + + /** + * @param int $start + * @param int|null $limit + * @return static + */ + public function limit($start, $limit = null) + { + return $this->createFrom($this->slice($start, $limit)); } /** @@ -54,18 +77,24 @@ class ObjectCollection extends ArrayCollection implements ObjectCollectionInterf if ($orderings = $criteria->getOrderings()) { $next = null; + /** + * @var string $field + * @var string $ordering + */ foreach (array_reverse($orderings) as $field => $ordering) { - $next = ObjectExpressionVisitor::sortByField($field, $ordering == Criteria::DESC ? -1 : 1, $next); + $next = ObjectExpressionVisitor::sortByField($field, $ordering === Criteria::DESC ? -1 : 1, $next); } - uasort($filtered, $next); + if ($next) { + uasort($filtered, $next); + } } $offset = $criteria->getFirstResult(); $length = $criteria->getMaxResults(); if ($offset || $length) { - $filtered = array_slice($filtered, (int)$offset, $length); + $filtered = \array_slice($filtered, (int)$offset, $length); } return $this->createFrom($filtered); diff --git a/system/src/Grav/Framework/Object/ObjectIndex.php b/system/src/Grav/Framework/Object/ObjectIndex.php new file mode 100644 index 0000000..4ba9b75 --- /dev/null +++ b/system/src/Grav/Framework/Object/ObjectIndex.php @@ -0,0 +1,251 @@ +getTypePrefix() : ''; + + if (static::$type) { + return $type . static::$type; + } + + $class = \get_class($this); + return $type . strtolower(substr($class, strrpos($class, '\\') + 1)); + } + + /** + * @return string + */ + public function getKey() + { + return $this->_key ?: $this->getType() . '@@' . spl_object_hash($this); + } + + /** + * @param string $key + * @return $this + */ + public function setKey($key) + { + $this->_key = $key; + + return $this; + } + + /** + * @param string $property Object property name. + * @return array True if property has been defined (can be null). + */ + public function hasProperty($property) + { + return $this->__call('hasProperty', [$property]); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @return array Property values. + */ + public function getProperty($property, $default = null) + { + return $this->__call('getProperty', [$property, $default]); + } + + /** + * @param string $property Object property to be updated. + * @param string $value New value. + * @return ObjectCollectionInterface + */ + public function setProperty($property, $value) + { + return $this->__call('setProperty', [$property, $value]); + } + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @return ObjectCollectionInterface + */ + public function defProperty($property, $default) + { + return $this->__call('defProperty', [$property, $default]); + } + + /** + * @param string $property Object property to be unset. + * @return ObjectCollectionInterface + */ + public function unsetProperty($property) + { + return $this->__call('unsetProperty', [$property]); + } + + /** + * @param string $property Object property name. + * @param string $separator Separator, defaults to '.' + * @return bool True if property has been defined (can be null). + */ + public function hasNestedProperty($property, $separator = null) + { + return $this->__call('hasNestedProperty', [$property, $separator]); + } + + /** + * @param string $property Object property to be fetched. + * @param mixed $default Default value if property has not been set. + * @param string $separator Separator, defaults to '.' + * @return mixed Property value. + */ + public function getNestedProperty($property, $default = null, $separator = null) + { + return $this->__call('getNestedProperty', [$property, $default, $separator]); + } + + /** + * @param string $property Object property to be updated. + * @param string $value New value. + * @param string $separator Separator, defaults to '.' + * @return ObjectCollectionInterface + */ + public function setNestedProperty($property, $value, $separator = null) + { + return $this->__call('setNestedProperty', [$property, $value, $separator]); + } + + /** + * @param string $property Object property to be defined. + * @param mixed $default Default value. + * @param string $separator Separator, defaults to '.' + * @return ObjectCollectionInterface + */ + public function defNestedProperty($property, $default, $separator = null) + { + return $this->__call('defNestedProperty', [$property, $default, $separator]); + } + + /** + * @param string $property Object property to be unset. + * @return ObjectCollectionInterface + */ + public function unsetNestedProperty($property, $separator = null) + { + return $this->__call('unsetNestedProperty', [$property, $separator]); + } + + /** + * Create a copy from this collection by cloning all objects in the collection. + * + * @return static + */ + public function copy() + { + $list = []; + foreach ($this->getIterator() as $key => $value) { + $list[$key] = \is_object($value) ? clone $value : $value; + } + + return $this->createFrom($list); + } + + /** + * @return array + */ + public function getObjectKeys() + { + return $this->getKeys(); + } + + /** + * @param array $ordering + * @return ObjectCollectionInterface + */ + public function orderBy(array $ordering) + { + return $this->__call('orderBy', [$ordering]); + } + + /** + * {@inheritDoc} + */ + public function call($method, array $arguments = []) + { + return $this->__call('call', [$method, $arguments]); + } + + /** + * Group items in the collection by a field and return them as associated array. + * + * @param string $property + * @return array + */ + public function group($property) + { + return $this->__call('group', [$property]); + } + + /** + * Group items in the collection by a field and return them as associated array of collections. + * + * @param string $property + * @return ObjectCollectionInterface[] + */ + public function collectionGroup($property) + { + return $this->__call('collectionGroup', [$property]); + } + + /** + * {@inheritDoc} + */ + public function matching(Criteria $criteria) + { + /** @var ObjectCollectionInterface $collection */ + $collection = $this->loadCollection($this->getEntries()); + + return $collection->matching($criteria); + } + + abstract public function __call($name, $arguments); + + /** + * @return string + */ + protected function getTypePrefix() + { + return ''; + } +} diff --git a/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php b/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php index 400b163..7092e15 100644 --- a/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php +++ b/system/src/Grav/Framework/Object/Property/ArrayPropertyTrait.php @@ -1,8 +1,9 @@ setElements($elements); - $this->setKey($key); + $this->setKey($key ?? ''); } /** @@ -94,7 +95,7 @@ trait ArrayPropertyTrait */ protected function getElements() { - return $this->_elements; + return array_filter($this->_elements, function ($val) { return $val !== null; }); } /** diff --git a/system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php b/system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php index 28b9432..1066b99 100644 --- a/system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php +++ b/system/src/Grav/Framework/Object/Property/LazyPropertyTrait.php @@ -1,8 +1,9 @@ initObjectProperties(); $this->setElements($elements); - $this->setKey($key); + $this->setKey($key ?? ''); } /** @@ -110,7 +111,7 @@ trait ObjectPropertyTrait if ($doCreate === true) { $this->_definedProperties[$property] = true; $this->{$property} = null; - } elseif (is_callable($doCreate)) { + } elseif (\is_callable($doCreate)) { $this->_definedProperties[$property] = true; $this->{$property} = $this->offsetLoad($property, $doCreate()); } else { @@ -152,7 +153,7 @@ trait ObjectPropertyTrait protected function initObjectProperties() { $this->_definedProperties = []; - foreach (get_object_vars($this) as $property => $value) { + foreach (\get_object_vars($this) as $property => $value) { if ($property[0] !== '_') { $this->_definedProperties[$property] = ($value !== null); } @@ -182,7 +183,10 @@ trait ObjectPropertyTrait $elements = []; foreach ($properties as $offset => $value) { - $elements[$offset] = $this->offsetSerialize($offset, $value); + $serialized = $this->offsetSerialize($offset, $value); + if ($serialized !== null) { + $elements[$offset] = $this->offsetSerialize($offset, $value); + } } return $elements; diff --git a/system/src/Grav/Framework/Object/PropertyObject.php b/system/src/Grav/Framework/Object/PropertyObject.php index 99f7e7f..f740fc6 100644 --- a/system/src/Grav/Framework/Object/PropertyObject.php +++ b/system/src/Grav/Framework/Object/PropertyObject.php @@ -1,8 +1,9 @@ 'page', + 'limit' => 10, + 'display' => 5, + 'opening' => 0, + 'ending' => 0, + 'url' => null + ]; + + /** @var array */ + private $items; + + public function isEnabled(): bool + { + return $this->count() > 1; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getRoute(): ?Route + { + return $this->route; + } + + public function getTotalPages(): int + { + return $this->pages; + } + + public function getPageNumber(): int + { + return $this->page; + } + + public function getPrevNumber(int $count = 1): ?int + { + $page = $this->page - $count; + + return $page >= 1 ? $page : null; + } + + public function getNextNumber(int $count = 1): ?int + { + $page = $this->page + $count; + + return $page <= $this->pages ? $page : null; + } + + public function getPage(int $page, string $label = null): ?PaginationPage + { + if ($page < 1 || $page > $this->pages) { + return null; + } + + $start = ($page - 1) * $this->limit; + if ($this->getOptions()['type'] === 'page') { + $name = 'page'; + $offset = $page; + } else { + $name = 'start'; + $offset = $start; + } + + return new PaginationPage( + [ + 'label' => $label ?? (string)$page, + 'number' => $page, + 'offset_start' => $start, + 'offset_end' => min($start + $this->limit, $this->total) - 1, + 'enabled' => $page !== $this->page || $this->viewAll, + 'active' => $page === $this->page, + 'route' => $this->route->withGravParam($name, $offset) + ] + ); + } + + public function getFirstPage(string $label = null, int $count = 0): ?PaginationPage + { + return $this->getPage(1 + $count, $label ?? $this->getOptions()['label_first'] ?? null); + } + + public function getPrevPage(string $label = null, int $count = 1): ?PaginationPage + { + return $this->getPage($this->page - $count, $label ?? $this->getOptions()['label_prev'] ?? null); + } + + public function getNextPage(string $label = null, int $count = 1): ?PaginationPage + { + return $this->getPage($this->page + $count, $label ?? $this->getOptions()['label_next'] ?? null); + } + + public function getLastPage(string $label = null, int $count = 0): ?PaginationPage + { + return $this->getPage($this->pages - $count, $label ?? $this->getOptions()['label_last'] ?? null); + } + + public function getStart(): int + { + return $this->start; + } + + public function getLimit(): int + { + return $this->limit; + } + + public function getTotal(): int + { + return $this->total; + } + + public function count(): int + { + $this->loadItems(); + + return \count($this->items); + } + + public function getIterator() + { + $this->loadItems(); + + return new \ArrayIterator($this->items); + } + + public function getPages(): array + { + $this->loadItems(); + + return $this->items; + } + + protected function loadItems() + { + $this->calculateRange(); + + // Make list like: 1 ... 4 5 6 ... 10 + $range = range($this->pagesStart, $this->pagesStop); + //$range[] = 1; + //$range[] = $this->pages; + natsort($range); + $range = array_unique($range); + + $this->items = []; + foreach ($range as $i) { + $this->items[$i] = $this->getPage($i); + } + } + + protected function setRoute(Route $route) + { + $this->route = $route; + + return $this; + } + + protected function setOptions(array $options = null) + { + $this->options = $options ? array_merge($this->defaultOptions, $options) : $this->defaultOptions; + + return $this; + } + + protected function setPage(int $page = null) + { + $this->page = (int)max($page, 1); + $this->start = null; + + return $this; + } + + /** + * @param int $start + * @return $this + */ + protected function setStart(int $start = null) + { + $this->start = (int)max($start, 0); + $this->page = null; + + return $this; + } + + /** + * @param int|null $limit + * @return $this + */ + protected function setLimit(int $limit = null) + { + $this->limit = (int)max($limit ?? $this->getOptions()['limit'], 0); + + // No limit, display all records in a single page. + $this->viewAll = !$limit; + + return $this; + } + + /** + * @param int $total + * @return $this + */ + protected function setTotal(int $total) + { + $this->total = (int)max($total, 0); + + return $this; + } + + protected function initialize(Route $route, int $total, int $pos = null, int $limit = null, array $options = null) + { + $this->setRoute($route); + $this->setOptions($options); + $this->setTotal($total); + if ($this->getOptions()['type'] === 'start') { + $this->setStart($pos); + } else { + $this->setPage($pos); + } + $this->setLimit($limit); + $this->calculateLimits(); + } + + protected function calculateLimits() + { + $limit = $this->limit; + $total = $this->total; + + if (!$limit || $limit > $total) { + // All records fit into a single page. + $this->start = 0; + $this->page = 1; + $this->pages = 1; + + return; + } + + if (null === $this->start) { + // If we are using page, convert it to start. + $this->start = (int)(($this->page - 1) * $limit); + } + + if ($this->start > $total - $limit) { + // If start is greater than total count (i.e. we are asked to display records that don't exist) + // then set start to display the last natural page of results. + $this->start = (int)max(0, (ceil($total / $limit) - 1) * $limit); + } + + // Set the total pages and current page values. + $this->page = (int)ceil(($this->start + 1) / $limit); + $this->pages = (int)ceil($total / $limit); + } + + protected function calculateRange() + { + $options = $this->getOptions(); + $displayed = $options['display']; + $opening = $options['opening']; + $ending = $options['ending']; + + // Set the pagination iteration loop values. + $this->pagesStart = $this->page - (int)($displayed / 2); + if ($this->pagesStart < 1 + $opening) { + $this->pagesStart = 1 + $opening; + } + if ($this->pagesStart + $displayed - $opening > $this->pages) { + $this->pagesStop = $this->pages; + if ($this->pages < $displayed) { + $this->pagesStart = 1 + $opening; + } else { + $this->pagesStart = $this->pages - $displayed + 1 + $opening; + } + } else { + $this->pagesStop = (int)max(1, $this->pagesStart + $displayed - 1 - $ending); + } + } +} diff --git a/system/src/Grav/Framework/Pagination/AbstractPaginationPage.php b/system/src/Grav/Framework/Pagination/AbstractPaginationPage.php new file mode 100644 index 0000000..04f536b --- /dev/null +++ b/system/src/Grav/Framework/Pagination/AbstractPaginationPage.php @@ -0,0 +1,53 @@ +options['active'] ?? false; + } + + public function isEnabled(): bool + { + return $this->options['enabled'] ?? false; + } + + public function getOptions(): array + { + return $this->options ?? []; + } + + public function getNumber(): ?int + { + return $this->options['number'] ?? null; + } + + public function getLabel(): string + { + return $this->options['label'] ?? (string)$this->getNumber(); + } + + public function getUrl(): ?string + { + return $this->options['route'] ? (string)$this->options['route']->getUri() : null; + } + + protected function setOptions(array $options): void + { + $this->options = $options; + } +} diff --git a/system/src/Grav/Framework/Pagination/Interfaces/PaginationInterface.php b/system/src/Grav/Framework/Pagination/Interfaces/PaginationInterface.php new file mode 100644 index 0000000..0a14525 --- /dev/null +++ b/system/src/Grav/Framework/Pagination/Interfaces/PaginationInterface.php @@ -0,0 +1,43 @@ +initialize($route, $total, $pos, $limit, $options); + } +} diff --git a/system/src/Grav/Framework/Pagination/PaginationPage.php b/system/src/Grav/Framework/Pagination/PaginationPage.php new file mode 100644 index 0000000..ca35a8d --- /dev/null +++ b/system/src/Grav/Framework/Pagination/PaginationPage.php @@ -0,0 +1,18 @@ +setOptions($options); + } +} diff --git a/system/src/Grav/Framework/Psr7/AbstractUri.php b/system/src/Grav/Framework/Psr7/AbstractUri.php index 5ddda9b..e0d46b6 100644 --- a/system/src/Grav/Framework/Psr7/AbstractUri.php +++ b/system/src/Grav/Framework/Psr7/AbstractUri.php @@ -1,8 +1,9 @@ user === $user && $this->password === $password) { return $this; diff --git a/system/src/Grav/Framework/Psr7/Request.php b/system/src/Grav/Framework/Psr7/Request.php new file mode 100644 index 0000000..24ea3b6 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Request.php @@ -0,0 +1,34 @@ +message = new \Nyholm\Psr7\Request($method, $uri, $headers, $body, $version); + } +} diff --git a/system/src/Grav/Framework/Psr7/Response.php b/system/src/Grav/Framework/Psr7/Response.php new file mode 100644 index 0000000..960621d --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Response.php @@ -0,0 +1,265 @@ +message = new \Nyholm\Psr7\Response($status, $headers, $body, $version, $reason); + } + + /** + * Json. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Json + * response to the client. + * + * @param mixed $data The data + * @param int $status The HTTP status code. + * @param int $options Json encoding options + * @param int $depth Json encoding max depth + * @return static + */ + public function withJson($data, int $status = null, int $options = 0, int $depth = 512): ResponseInterface + { + $json = (string) json_encode($data, $options, $depth); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException(json_last_error_msg(), json_last_error()); + } + + $response = $this->getResponse() + ->withHeader('Content-Type', 'application/json;charset=utf-8') + ->withBody(new Stream($json)); + + if ($status !== null) { + $response = $response->withStatus($status); + } + + $new = clone $this; + $new->message = $response; + + return $new; + } + + /** + * Redirect. + * + * Note: This method is not part of the PSR-7 standard. + * + * This method prepares the response object to return an HTTP Redirect + * response to the client. + * + * @param string $url The redirect destination. + * @param int|null $status The redirect HTTP status code. + * @return static + */ + public function withRedirect(string $url, $status = null): ResponseInterface + { + $response = $this->getResponse()->withHeader('Location', $url); + + if ($status === null) { + $status = 302; + } + + $new = clone $this; + $new->message = $response->withStatus($status); + + return $new; + } + + /** + * Is this response empty? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isEmpty(): bool + { + return \in_array($this->getResponse()->getStatusCode(), [204, 205, 304], true); + } + + + /** + * Is this response OK? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOk(): bool + { + return $this->getResponse()->getStatusCode() === 200; + } + + /** + * Is this response a redirect? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirect(): bool + { + return \in_array($this->getResponse()->getStatusCode(), [301, 302, 303, 307, 308], true); + } + + /** + * Is this response forbidden? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + * @api + */ + public function isForbidden(): bool + { + return $this->getResponse()->getStatusCode() === 403; + } + + /** + * Is this response not Found? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isNotFound(): bool + { + return $this->getResponse()->getStatusCode() === 404; + } + + /** + * Is this response informational? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isInformational(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 100 && $response->getStatusCode() < 200; + } + + /** + * Is this response successful? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isSuccessful(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 200 && $response->getStatusCode() < 300; + } + + /** + * Is this response a redirection? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isRedirection(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 300 && $response->getStatusCode() < 400; + } + + /** + * Is this response a client error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isClientError(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 400 && $response->getStatusCode() < 500; + } + + /** + * Is this response a server error? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isServerError(): bool + { + $response = $this->getResponse(); + + return $response->getStatusCode() >= 500 && $response->getStatusCode() < 600; + } + + /** + * Convert response to string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string + */ + public function __toString(): string + { + $response = $this->getResponse(); + $output = sprintf( + 'HTTP/%s %s %s%s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase(), + self::EOL + ); + + foreach ($response->getHeaders() as $name => $values) { + $output .= sprintf('%s: %s', $name, $response->getHeaderLine($name)) . self::EOL; + } + + $output .= self::EOL; + $output .= (string) $response->getBody(); + + return $output; + } +} diff --git a/system/src/Grav/Framework/Psr7/ServerRequest.php b/system/src/Grav/Framework/Psr7/ServerRequest.php new file mode 100644 index 0000000..bb6b8b8 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/ServerRequest.php @@ -0,0 +1,372 @@ +message = new \Nyholm\Psr7\ServerRequest($method, $uri, $headers, $body, $version, $serverParams); + } + + /** + * Get serverRequest content character set, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null + */ + public function getContentCharset(): ?string + { + $mediaTypeParams = $this->getMediaTypeParams(); + + if (isset($mediaTypeParams['charset'])) { + return $mediaTypeParams['charset']; + } + + return null; + } + + /** + * Get serverRequest content type. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null The serverRequest content type, if known + */ + public function getContentType(): ?string + { + $result = $this->getRequest()->getHeader('Content-Type'); + + return $result ? $result[0] : null; + } + + /** + * Get serverRequest content length, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return int|null + */ + public function getContentLength(): ?int + { + $result = $this->getRequest()->getHeader('Content-Length'); + + return $result ? (int) $result[0] : null; + } + + /** + * Fetch cookie value from cookies sent by the client to the server. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * + * @return mixed + */ + public function getCookieParam($key, $default = null) + { + $cookies = $this->getRequest()->getCookieParams(); + $result = $default; + + if (isset($cookies[$key])) { + $result = $cookies[$key]; + } + + return $result; + } + + /** + * Get serverRequest media type, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return string|null The serverRequest media type, minus content-type params + */ + public function getMediaType(): ?string + { + $contentType = $this->getContentType(); + + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + if ($contentTypeParts === false) { + return null; + } + return strtolower($contentTypeParts[0]); + } + + return null; + } + + /** + * Get serverRequest media type params, if known. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return mixed[] + */ + public function getMediaTypeParams(): array + { + $contentType = $this->getContentType(); + $contentTypeParams = []; + + if ($contentType) { + $contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType); + if ($contentTypeParts !== false) { + $contentTypePartsLength = count($contentTypeParts); + for ($i = 1; $i < $contentTypePartsLength; $i++) { + $paramParts = explode('=', $contentTypeParts[$i]); + $contentTypeParams[strtolower($paramParts[0])] = $paramParts[1]; + } + } + } + + return $contentTypeParams; + } + + /** + * Fetch serverRequest parameter value from body or query string (in that order). + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key The parameter key. + * @param string $default The default value. + * + * @return mixed The parameter value. + */ + public function getParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $getParams = $this->getQueryParams(); + $result = $default; + + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->$key; + } elseif (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Fetch associative array of body and query string parameters. + * + * Note: This method is not part of the PSR-7 standard. + * + * @return mixed[] + */ + public function getParams(): array + { + $params = $this->getQueryParams(); + $postParams = $this->getParsedBody(); + + if ($postParams) { + $params = array_merge($params, (array)$postParams); + } + + return $params; + } + + /** + * Fetch parameter value from serverRequest body. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getParsedBodyParam($key, $default = null) + { + $postParams = $this->getParsedBody(); + $result = $default; + + if (is_array($postParams) && isset($postParams[$key])) { + $result = $postParams[$key]; + } elseif (is_object($postParams) && property_exists($postParams, $key)) { + $result = $postParams->{$key}; + } + + return $result; + } + + /** + * Fetch parameter value from query string. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getQueryParam($key, $default = null) + { + $getParams = $this->getQueryParams(); + $result = $default; + + if (isset($getParams[$key])) { + $result = $getParams[$key]; + } + + return $result; + } + + /** + * Retrieve a server parameter. + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getServerParam($key, $default = null) + { + $serverParams = $this->getRequest()->getServerParams(); + + return $serverParams[$key] ?? $default; + } + + /** + * Does this serverRequest use a given method? + * + * Note: This method is not part of the PSR-7 standard. + * + * @param string $method HTTP method + * @return bool + */ + public function isMethod($method): bool + { + return $this->getRequest()->getMethod() === $method; + } + + /** + * Is this a DELETE serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isDelete(): bool + { + return $this->isMethod('DELETE'); + } + + /** + * Is this a GET serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isGet(): bool + { + return $this->isMethod('GET'); + } + + /** + * Is this a HEAD serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isHead(): bool + { + return $this->isMethod('HEAD'); + } + + /** + * Is this a OPTIONS serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isOptions(): bool + { + return $this->isMethod('OPTIONS'); + } + + /** + * Is this a PATCH serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPatch(): bool + { + return $this->isMethod('PATCH'); + } + + /** + * Is this a POST serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPost(): bool + { + return $this->isMethod('POST'); + } + + /** + * Is this a PUT serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isPut(): bool + { + return $this->isMethod('PUT'); + } + + /** + * Is this an XHR serverRequest? + * + * Note: This method is not part of the PSR-7 standard. + * + * @return bool + */ + public function isXhr(): bool + { + return $this->getRequest()->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; + } +} diff --git a/system/src/Grav/Framework/Psr7/Stream.php b/system/src/Grav/Framework/Psr7/Stream.php new file mode 100644 index 0000000..9431052 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Stream.php @@ -0,0 +1,25 @@ +stream = \Nyholm\Psr7\Stream::create($body); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/MessageDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/MessageDecoratorTrait.php new file mode 100644 index 0000000..28ed1f5 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/MessageDecoratorTrait.php @@ -0,0 +1,140 @@ + + */ +trait MessageDecoratorTrait +{ + /** @var MessageInterface */ + private $message; + + /** + * Returns the decorated message. + * + * Since the underlying Message is immutable as well + * exposing it is not an issue, because it's state cannot be altered + * + * @return MessageInterface + */ + public function getMessage(): MessageInterface + { + return $this->message; + } + + /** + * {@inheritdoc} + */ + public function getProtocolVersion(): string + { + return $this->message->getProtocolVersion(); + } + + /** + * {@inheritdoc} + */ + public function withProtocolVersion($version): self + { + $new = clone $this; + $new->message = $this->message->withProtocolVersion($version); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getHeaders(): array + { + return $this->message->getHeaders(); + } + + /** + * {@inheritdoc} + */ + public function hasHeader($header): bool + { + return $this->message->hasHeader($header); + } + + /** + * {@inheritdoc} + */ + public function getHeader($header): array + { + return $this->message->getHeader($header); + } + + /** + * {@inheritdoc} + */ + public function getHeaderLine($header): string + { + return $this->message->getHeaderLine($header); + } + + /** + * {@inheritdoc} + */ + public function getBody(): StreamInterface + { + return $this->message->getBody(); + } + + /** + * {@inheritdoc} + */ + public function withHeader($header, $value): self + { + $new = clone $this; + $new->message = $this->message->withHeader($header, $value); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withAddedHeader($header, $value): self + { + $new = clone $this; + $new->message = $this->message->withAddedHeader($header, $value); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withoutHeader($header): self + { + $new = clone $this; + $new->message = $this->message->withoutHeader($header); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function withBody(StreamInterface $body): self + { + $new = clone $this; + $new->message = $this->message->withBody($body); + + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/RequestDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/RequestDecoratorTrait.php new file mode 100644 index 0000000..ab1ffad --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/RequestDecoratorTrait.php @@ -0,0 +1,112 @@ + + */ +trait RequestDecoratorTrait +{ + use MessageDecoratorTrait { + getMessage as private; + } + + /** + * Returns the decorated request. + * + * Since the underlying Request is immutable as well + * exposing it is not an issue, because it's state cannot be altered + * + * @return RequestInterface + */ + public function getRequest(): RequestInterface + { + /** @var RequestInterface $message */ + $message = $this->getMessage(); + + return $message; + } + + /** + * Exchanges the underlying request with another. + * + * @param RequestInterface $request + * @return self + */ + public function withRequest(RequestInterface $request): self + { + $new = clone $this; + $new->message = $request; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getRequestTarget(): string + { + return $this->getRequest()->getRequestTarget(); + } + + /** + * {@inheritdoc} + */ + public function withRequestTarget($requestTarget): self + { + $new = clone $this; + $new->message = $this->getRequest()->withRequestTarget($requestTarget); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getMethod(): string + { + return $this->getRequest()->getMethod(); + } + + /** + * {@inheritdoc} + */ + public function withMethod($method): self + { + $new = clone $this; + $new->message = $this->getRequest()->withMethod($method); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getUri(): UriInterface + { + return $this->getRequest()->getUri(); + } + + /** + * {@inheritdoc} + */ + public function withUri(UriInterface $uri, $preserveHost = false): self + { + $new = clone $this; + $new->message = $this->getRequest()->withUri($uri, $preserveHost); + + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/ResponseDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/ResponseDecoratorTrait.php new file mode 100644 index 0000000..32a1bfb --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/ResponseDecoratorTrait.php @@ -0,0 +1,82 @@ + + */ +trait ResponseDecoratorTrait +{ + use MessageDecoratorTrait { + getMessage as private; + } + + /** + * Returns the decorated response. + * + * Since the underlying Response is immutable as well + * exposing it is not an issue, because it's state cannot be altered + * + * @return ResponseInterface + */ + public function getResponse(): ResponseInterface + { + /** @var ResponseInterface $message */ + $message = $this->getMessage(); + + return $message; + } + + /** + * Exchanges the underlying response with another. + * + * @param ResponseInterface $response + * + * @return self + */ + public function withResponse(ResponseInterface $response): self + { + $new = clone $this; + $new->message = $response; + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getStatusCode(): int + { + return $this->getResponse()->getStatusCode(); + } + + /** + * {@inheritdoc} + */ + public function withStatus($code, $reasonPhrase = ''): self + { + $new = clone $this; + $new->message = $this->getResponse()->withStatus($code, $reasonPhrase); + + return $new; + } + + /** + * {@inheritdoc} + */ + public function getReasonPhrase(): string + { + return $this->getResponse()->getReasonPhrase(); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrait.php new file mode 100644 index 0000000..f9297dd --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/ServerRequestDecoratorTrait.php @@ -0,0 +1,168 @@ +getMessage(); + + return $message; + } + + /** + * @inheritdoc + */ + public function getAttribute($name, $default = null) + { + return $this->getRequest()->getAttribute($name, $default); + } + + /** + * @inheritdoc + */ + public function getAttributes() + { + return $this->getRequest()->getAttributes(); + } + + + /** + * @inheritdoc + */ + public function getCookieParams() + { + return $this->getRequest()->getCookieParams(); + } + + /** + * @inheritdoc + */ + public function getParsedBody() + { + return $this->getRequest()->getParsedBody(); + } + + /** + * @inheritdoc + */ + public function getQueryParams() + { + return $this->getRequest()->getQueryParams(); + } + + /** + * @inheritdoc + */ + public function getServerParams() + { + return $this->getRequest()->getServerParams(); + } + + /** + * @inheritdoc + */ + public function getUploadedFiles() + { + return $this->getRequest()->getUploadedFiles(); + } + + /** + * @inheritdoc + */ + public function withAttribute($name, $value) + { + $new = clone $this; + $new->message = $this->getRequest()->withAttribute($name, $value); + + return $new; + } + + public function withAttributes(array $attributes) + { + $new = clone $this; + foreach ($attributes as $attribute => $value) { + $new->message = $new->withAttribute($attribute, $value); + } + + return $new; + } + + /** + * @inheritdoc + */ + public function withoutAttribute($name) + { + $new = clone $this; + $new->message = $this->getRequest()->withoutAttribute($name); + + return $new; + } + + /** + * @inheritdoc + */ + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->message = $this->getRequest()->withCookieParams($cookies); + + return $new; + } + + /** + * @inheritdoc + */ + public function withParsedBody($data) + { + $new = clone $this; + $new->message = $this->getRequest()->withParsedBody($data); + + return $new; + } + + /** + * @inheritdoc + */ + public function withQueryParams(array $query) + { + $new = clone $this; + $new->message = $this->getRequest()->withQueryParams($query); + + return $new; + } + + /** + * @inheritdoc + */ + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->message = $this->getRequest()->withUploadedFiles($uploadedFiles); + + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/StreamDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/StreamDecoratorTrait.php new file mode 100644 index 0000000..f61ea4b --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/StreamDecoratorTrait.php @@ -0,0 +1,145 @@ +stream->__toString(); + } + + public function __destruct() + { + $this->stream->close(); + } + + /** + * {@inheritdoc} + */ + public function close(): void + { + $this->stream->close(); + } + + /** + * {@inheritdoc} + */ + public function detach() + { + return $this->stream->detach(); + } + + /** + * {@inheritdoc} + */ + public function getSize(): ?int + { + return $this->stream->getSize(); + } + + /** + * {@inheritdoc} + */ + public function tell(): int + { + return $this->stream->tell(); + } + + /** + * {@inheritdoc} + */ + public function eof(): bool + { + return $this->stream->eof(); + } + + /** + * {@inheritdoc} + */ + public function isSeekable(): bool + { + return $this->stream->isSeekable(); + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = \SEEK_SET): void + { + $this->stream->seek($offset, $whence); + } + + /** + * {@inheritdoc} + */ + public function rewind(): void + { + $this->stream->rewind(); + } + + /** + * {@inheritdoc} + */ + public function isWritable(): bool + { + return $this->stream->isWritable(); + } + + /** + * {@inheritdoc} + */ + public function write($string): int + { + return $this->stream->write($string); + } + + /** + * {@inheritdoc} + */ + public function isReadable(): bool + { + return $this->stream->isReadable(); + } + + /** + * {@inheritdoc} + */ + public function read($length): string + { + return $this->stream->read($length); + } + + /** + * {@inheritdoc} + */ + public function getContents(): string + { + return $this->stream->getContents(); + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/UploadedFileDecoratorTrait.php b/system/src/Grav/Framework/Psr7/Traits/UploadedFileDecoratorTrait.php new file mode 100644 index 0000000..677c4d2 --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/UploadedFileDecoratorTrait.php @@ -0,0 +1,51 @@ +uploadedFile->getStream(); + } + + public function moveTo($targetPath): void + { + $this->uploadedFile->moveTo($targetPath); + } + + public function getSize(): ?int + { + return $this->uploadedFile->getSize(); + } + + public function getError(): int + { + return $this->uploadedFile->getError(); + } + + public function getClientFilename(): ?string + { + return $this->uploadedFile->getClientFilename(); + } + + public function getClientMediaType(): ?string + { + return $this->uploadedFile->getClientMediaType(); + } +} diff --git a/system/src/Grav/Framework/Psr7/Traits/UriDecorationTrait.php b/system/src/Grav/Framework/Psr7/Traits/UriDecorationTrait.php new file mode 100644 index 0000000..9e7946a --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Traits/UriDecorationTrait.php @@ -0,0 +1,128 @@ +uri->__toString(); + } + + public function getScheme(): string + { + return $this->uri->getScheme(); + } + + public function getAuthority(): string + { + return $this->uri->getAuthority(); + } + + public function getUserInfo(): string + { + return $this->uri->getUserInfo(); + } + + public function getHost(): string + { + return $this->uri->getHost(); + } + + public function getPort(): ?int + { + return $this->uri->getPort(); + } + + public function getPath(): string + { + return $this->uri->getPath(); + } + + public function getQuery(): string + { + return $this->uri->getQuery(); + } + + public function getFragment(): string + { + return $this->uri->getFragment(); + } + + public function withScheme($scheme): UriInterface + { + /** @var UriInterface|UriDecorationTrait $new */ + $new = clone $this; + $new->uri = $this->uri->withScheme($scheme); + + return $new; + } + + public function withUserInfo($user, $password = null): UriInterface + { + /** @var UriInterface|UriDecorationTrait $new */ + $new = clone $this; + $new->uri = $this->uri->withUserInfo($user, $password); + + return $new; + } + + public function withHost($host): UriInterface + { + /** @var UriInterface|UriDecorationTrait $new */ + $new = clone $this; + $new->uri = $this->uri->withHost($host); + + return $new; + } + + public function withPort($port): UriInterface + { + /** @var UriInterface|UriDecorationTrait $new */ + $new = clone $this; + $new->uri = $this->uri->withPort($port); + + return $new; + } + + public function withPath($path): UriInterface + { + /** @var UriInterface|UriDecorationTrait $new */ + $new = clone $this; + $new->uri = $this->uri->withPath($path); + + return $new; + } + + public function withQuery($query): UriInterface + { + /** @var UriInterface|UriDecorationTrait $new */ + $new = clone $this; + $new->uri = $this->uri->withQuery($query); + + return $new; + } + + public function withFragment($fragment): UriInterface + { + /** @var UriInterface|UriDecorationTrait $new */ + $new = clone $this; + $new->uri = $this->uri->withFragment($fragment); + + return $new; + } +} diff --git a/system/src/Grav/Framework/Psr7/UploadedFile.php b/system/src/Grav/Framework/Psr7/UploadedFile.php new file mode 100644 index 0000000..d6b062f --- /dev/null +++ b/system/src/Grav/Framework/Psr7/UploadedFile.php @@ -0,0 +1,33 @@ +uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType); + } +} diff --git a/system/src/Grav/Framework/Psr7/Uri.php b/system/src/Grav/Framework/Psr7/Uri.php new file mode 100644 index 0000000..28614ca --- /dev/null +++ b/system/src/Grav/Framework/Psr7/Uri.php @@ -0,0 +1,134 @@ +uri = new \Nyholm\Psr7\Uri($uri); + } + + /** + * @return array + */ + public function getQueryParams(): array + { + return UriFactory::parseQuery($this->getQuery()); + } + + /** + * @param array $params + * @return UriInterface + */ + public function withQueryParams(array $params): UriInterface + { + $query = UriFactory::buildQuery($params); + + return $this->withQuery($query); + } + + /** + * Whether the URI has the default port of the current scheme. + * + * `$uri->getPort()` may return the standard port. This method can be used for some non-http/https Uri. + * + * @return bool + */ + public function isDefaultPort(): bool + { + return $this->getPort() === null || GuzzleUri::isDefaultPort($this); + } + + /** + * Whether the URI is absolute, i.e. it has a scheme. + * + * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true + * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative + * to another URI, the base URI. Relative references can be divided into several forms: + * - network-path references, e.g. '//example.com/path' + * - absolute-path references, e.g. '/path' + * - relative-path references, e.g. 'subpath' + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4 + */ + public function isAbsolute(): bool + { + return GuzzleUri::isAbsolute($this); + } + + /** + * Whether the URI is a network-path reference. + * + * A relative reference that begins with two slash characters is termed an network-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isNetworkPathReference(): bool + { + return GuzzleUri::isNetworkPathReference($this); + } + + /** + * Whether the URI is a absolute-path reference. + * + * A relative reference that begins with a single slash character is termed an absolute-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isAbsolutePathReference(): bool + { + return GuzzleUri::isAbsolutePathReference($this); + } + + /** + * Whether the URI is a relative-path reference. + * + * A relative reference that does not begin with a slash character is termed a relative-path reference. + * + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.2 + */ + public function isRelativePathReference(): bool + { + return GuzzleUri::isRelativePathReference($this); + } + + /** + * Whether the URI is a same-document reference. + * + * A same-document reference refers to a URI that is, aside from its fragment + * component, identical to the base URI. When no base URI is given, only an empty + * URI reference (apart from its fragment) is considered a same-document reference. + * + * @param UriInterface|null $base An optional base URI to compare against + * @return bool + * @link https://tools.ietf.org/html/rfc3986#section-4.4 + */ + public function isSameDocumentReference(UriInterface $base = null): bool + { + return GuzzleUri::isSameDocumentReference($this, $base); + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Exception/InvalidArgumentException.php b/system/src/Grav/Framework/RequestHandler/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..4cacd71 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Exception/InvalidArgumentException.php @@ -0,0 +1,47 @@ +invalidMiddleware = $invalidMiddleware; + } + + /** + * Return the invalid middleware + * + * @return mixed|null + */ + public function getInvalidMiddleware() + { + return $this->invalidMiddleware; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Exception/NotFoundException.php b/system/src/Grav/Framework/RequestHandler/Exception/NotFoundException.php new file mode 100644 index 0000000..5e8d22a --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Exception/NotFoundException.php @@ -0,0 +1,39 @@ +getMethod()), ['PUT', 'PATCH', 'DELETE'])) { + parent::__construct($request, 'Method Not Allowed', 405, $previous); + } else { + parent::__construct($request, 'Not Found', 404, $previous); + } + } + + public function getRequest(): ServerRequestInterface + { + return $this->request; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Exception/NotHandledException.php b/system/src/Grav/Framework/RequestHandler/Exception/NotHandledException.php new file mode 100644 index 0000000..6696013 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Exception/NotHandledException.php @@ -0,0 +1,16 @@ + 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 419 => 'Page Expired', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 511 => 'Network Authentication Required', + ]; + + /** @var ServerRequestInterface */ + private $request; + + /** + * @param ServerRequestInterface $request + * @param string $message + * @param int $code + * @param \Throwable|null $previous + */ + public function __construct(ServerRequestInterface $request, string $message, int $code = 500, \Throwable $previous = null) + { + $this->request = $request; + + parent::__construct($message, $code, $previous); + } + + /** + * @return ServerRequestInterface + */ + public function getRequest() : ServerRequestInterface + { + return $this->request; + } + + public function getHttpCode() : int + { + $code = $this->getCode(); + + return isset(self::$phrases[$code]) ? $code : 500; + } + + public function getHttpReason() : ?string + { + return self::$phrases[$this->getCode()] ?? self::$phrases[500]; + } +} diff --git a/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php b/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php new file mode 100644 index 0000000..ec1de6d --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Middlewares/Exceptions.php @@ -0,0 +1,44 @@ +handle($request); + } catch (\Throwable $exception) { + $response = [ + 'error' => [ + 'type' => \get_class($exception), + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode("\n", $exception->getTraceAsString()), + ] + ]; + + /** @var string $json */ + $json = json_encode($response); + + return new Response($exception->getCode() ?: 500, ['Content-Type' => 'application/json'], $json); + } + } +} diff --git a/system/src/Grav/Framework/RequestHandler/RequestHandler.php b/system/src/Grav/Framework/RequestHandler/RequestHandler.php new file mode 100644 index 0000000..c4d5387 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/RequestHandler.php @@ -0,0 +1,66 @@ +middleware = $middleware; + $this->handler = $default; + $this->container = $container; + } + + /** + * Add callable initializing Middleware that will be executed as soon as possible. + * + * @param string $name + * @param callable $callable + * @return $this + */ + public function addCallable(string $name, callable $callable): self + { + $this->container[$name] = $callable; + array_unshift($this->middleware, $name); + + return $this; + } + + /** + * Add Middleware that will be executed as soon as possible. + * + * @param string $name + * @param MiddlewareInterface $middleware + * @return $this + */ + public function addMiddleware(string $name, MiddlewareInterface $middleware): self + { + $this->container[$name] = $middleware; + array_unshift($this->middleware, $name); + + return $this; + } +} \ No newline at end of file diff --git a/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php b/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php new file mode 100644 index 0000000..153d1a1 --- /dev/null +++ b/system/src/Grav/Framework/RequestHandler/Traits/RequestHandlerTrait.php @@ -0,0 +1,59 @@ + */ + protected $middleware; + + /** @var callable */ + private $handler; + + /** @var ContainerInterface|null */ + private $container; + + /** + * {@inheritdoc} + * @throws InvalidArgumentException + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $middleware = array_shift($this->middleware); + + // Use default callable if there is no middleware. + if ($middleware === null) { + return \call_user_func($this->handler, $request); + } + + if ($middleware instanceof MiddlewareInterface) { + return $middleware->process($request, clone $this); + } + + if (null === $this->container || !$this->container->has($middleware)) { + throw new InvalidArgumentException( + sprintf('The middleware is not a valid %s and is not passed in the Container', MiddlewareInterface::class), + $middleware + ); + } + + array_unshift($this->middleware, $this->container->get($middleware)); + + return $this->handle($request); + } +} \ No newline at end of file diff --git a/system/src/Grav/Framework/Route/Route.php b/system/src/Grav/Framework/Route/Route.php index 63f6f7e..58224fe 100644 --- a/system/src/Grav/Framework/Route/Route.php +++ b/system/src/Grav/Framework/Route/Route.php @@ -1,8 +1,9 @@ $this->getUriPath(), + 'path' => $this->getUriPath(true), 'query' => $this->getUriQuery(), 'grav' => [ 'root' => $this->root, 'language' => $this->language, 'route' => $this->route, + 'extension' => $this->extension, 'grav_params' => $this->gravParams, 'query_params' => $this->queryParams, ], @@ -91,6 +96,14 @@ class Route return '/' . $this->route; } + /** + * @return string + */ + public function getExtension() + { + return $this->extension; + } + /** * @param int $offset * @param int|null $length @@ -101,7 +114,7 @@ class Route $parts = explode('/', $this->route); if ($offset !== 0 || $length !== null) { - $parts = array_slice($parts, $offset, $length); + $parts = \array_slice($parts, $offset, $length); } return $parts; @@ -141,16 +154,11 @@ class Route * If the parameter exists in both, return Grav parameter. * * @param string $param - * @return string|null + * @return string|array|null */ public function getParam($param) { - $value = $this->getGravParam($param); - if ($value === null) { - $value = $this->getQueryParam($param); - } - - return $value; + return $this->getGravParam($param) ?? $this->getQueryParam($param); } /** @@ -159,16 +167,64 @@ class Route */ public function getGravParam($param) { - return isset($this->gravParams[$param]) ? $this->gravParams[$param] : null; + return $this->gravParams[$param] ?? null; } /** * @param string $param - * @return string|null + * @return string|array|null */ public function getQueryParam($param) { - return isset($this->queryParams[$param]) ? $this->queryParams[$param] : null; + return $this->queryParams[$param] ?? null; + } + + /** + * Allow the ability to set the route to something else + * + * @param string $route + * @return $this + */ + public function withRoute($route) + { + $this->route = $route; + + return $this; + } + + /** + * Allow the ability to set the root to something else + * + * @param string $root + * @return $this + */ + public function withRoot($root) + { + $this->root = $root; + + return $this; + } + + /** + * @param string $path + * @return Route + */ + public function withAddedPath($path) + { + $this->route .= '/' . ltrim($path, '/'); + + return $this; + } + + /** + * @param string $extension + * @return Route + */ + public function withExtension($extension) + { + $this->extension = $extension; + + return $this; } /** @@ -191,6 +247,25 @@ class Route return $this->withParam('queryParams', $param, $value); } + public function withoutParams() + { + return $this->withoutGravParams()->withoutQueryParams(); + } + + public function withoutGravParams() + { + $this->gravParams = []; + + return $this; + } + + public function withoutQueryParams() + { + $this->queryParams = []; + + return $this; + } + /** * @return \Grav\Framework\Uri\Uri */ @@ -200,11 +275,12 @@ class Route } /** + * @param bool $includeRoot * @return string */ - public function __toString() + public function toString(bool $includeRoot = false) { - $url = $this->getUriPath(); + $url = $this->getUriPath($includeRoot); if ($this->queryParams) { $url .= '?' . $this->getUriQuery(); @@ -213,6 +289,17 @@ class Route return $url; } + /** + * @return string + * @deprecated 1.6 Use ->toString(true) or ->getUri() instead. + */ + public function __toString() + { + user_error(__CLASS__ . '::' . __FUNCTION__ . '() will change in the future to return route, not relative url: use ->toString(true) or ->getUri() instead.', E_USER_DEPRECATED); + + return $this->toString(true); + } + /** * @param string $type * @param string $param @@ -221,35 +308,44 @@ class Route */ protected function withParam($type, $param, $value) { - $oldValue = isset($this->{$type}[$param]) ? $this->{$type}[$param] : null; + $values = $this->{$type} ?? []; + $oldValue = $values[$param] ?? null; if ($oldValue === $value) { return $this; } - $new = clone $this; + $new = $this->copy(); if ($value === null) { - unset($new->{$type}[$param]); + unset($values[$param]); } else { - $new->{$type}[$param] = $value; + $values[$param] = $value; } + $new->{$type} = $values; + return $new; } + protected function copy() + { + return clone $this; + } + /** + * @param bool $includeRoot * @return string */ - protected function getUriPath() + protected function getUriPath($includeRoot = false) { - $parts = [$this->root]; + $parts = $includeRoot ? [$this->root] : ['']; if ($this->language !== '') { $parts[] = $this->language; } if ($this->route !== '') { - $parts[] = $this->route; + $parts[] = $this->extension ? $this->route . '.' . $this->extension : $this->route; } if ($this->gravParams) { @@ -277,14 +373,15 @@ class Route $this->root = $gravParts['root']; $this->language = $gravParts['language']; $this->route = $gravParts['route']; - $this->gravParams = $gravParts['params']; - $this->queryParams = $parts['query_params']; + $this->extension = $gravParts['extension'] ?? ''; + $this->gravParams = $gravParts['params'] ?: []; + $this->queryParams = $parts['query_params'] ?: []; } else { $this->root = RouteFactory::getRoot(); $this->language = RouteFactory::getLanguage(); - $path = isset($parts['path']) ? $parts['path'] : '/'; + $path = $parts['path'] ?? '/'; if (isset($parts['params'])) { $this->route = trim(rawurldecode($path), '/'); $this->gravParams = $parts['params']; diff --git a/system/src/Grav/Framework/Route/RouteFactory.php b/system/src/Grav/Framework/Route/RouteFactory.php index 830d7c7..6b3414a 100644 --- a/system/src/Grav/Framework/Route/RouteFactory.php +++ b/system/src/Grav/Framework/Route/RouteFactory.php @@ -1,8 +1,9 @@ $val]; } } diff --git a/system/src/Grav/Framework/Session/Exceptions/SessionException.php b/system/src/Grav/Framework/Session/Exceptions/SessionException.php new file mode 100644 index 0000000..4caab82 --- /dev/null +++ b/system/src/Grav/Framework/Session/Exceptions/SessionException.php @@ -0,0 +1,14 @@ + 'nocache', 'use_trans_sid' => 0, 'use_cookies' => 1, 'lazy_write' => 1, 'use_strict_mode' => 1 - ); + ]; $this->setOptions($options); @@ -141,35 +141,30 @@ class Session implements SessionInterface 'cache_limiter' => true, 'cache_expire' => true, 'use_trans_sid' => true, - 'trans_sid_tags' => true, // PHP 7.1 - 'trans_sid_hosts' => true, // PHP 7.1 - 'sid_length' => true, // PHP 7.1 - 'sid_bits_per_character' => true, // PHP 7.1 + 'trans_sid_tags' => true, + 'trans_sid_hosts' => true, + 'sid_length' => true, + 'sid_bits_per_character' => true, 'upload_progress.enabled' => true, 'upload_progress.cleanup' => true, 'upload_progress.prefix' => true, 'upload_progress.name' => true, 'upload_progress.freq' => true, 'upload_progress.min-freq' => true, - 'lazy_write' => true, - 'url_rewriter.tags' => true, // Not used in PHP 7.1 - 'hash_function' => true, // Not used in PHP 7.1 - 'hash_bits_per_character' => true, // Not used in PHP 7.1 - 'entropy_file' => true, // Not used in PHP 7.1 - 'entropy_length' => true, // Not used in PHP 7.1 + 'lazy_write' => true ]; foreach ($options as $key => $value) { - if (is_array($value)) { + if (\is_array($value)) { // Allow nested options. foreach ($value as $key2 => $value2) { $ckey = "{$key}.{$key2}"; if (isset($value2, $allowedOptions[$ckey])) { - $this->ini_set("session.{$ckey}", $value2); + $this->setOption($ckey, $value2); } } } elseif (isset($value, $allowedOptions[$key])) { - $this->ini_set("session.{$key}", $value); + $this->setOption($key, $value); } } } @@ -179,6 +174,10 @@ class Session implements SessionInterface */ public function start($readonly = false) { + if (\PHP_SAPI === 'cli') { + return $this; + } + // Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836 if (isset($_COOKIE[session_name()]) && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[session_name()])) { unset($_COOKIE[session_name()]); @@ -190,10 +189,17 @@ class Session implements SessionInterface } $success = @session_start($options); + $user = $success ? $this->__get('user') : null; if (!$success) { $last = error_get_last(); $error = $last ? $last['message'] : 'Unknown error'; - throw new \RuntimeException('Failed to start session: ' . $error, 500); + + throw new SessionException('Failed to start session: ' . $error, 500); + } + + if ($user && !$user->isValid()) { + $this->clear(); + throw new SessionException('User Invalid', 500); } $params = session_get_cookie_params(); @@ -300,7 +306,7 @@ class Session implements SessionInterface */ public function __get($name) { - return isset($_SESSION[$name]) ? $_SESSION[$name] : null; + return $_SESSION[$name] ?? null; } /** @@ -333,16 +339,17 @@ class Session implements SessionInterface * @param string $key * @param mixed $value */ - protected function ini_set($key, $value) + protected function setOption($key, $value) { - if (!is_string($value)) { - if (is_bool($value)) { + if (!\is_string($value)) { + if (\is_bool($value)) { $value = $value ? '1' : '0'; + } else { + $value = (string)$value; } - $value = (string)$value; } $this->options[$key] = $value; - ini_set($key, $value); + ini_set("session.{$key}", $value); } } diff --git a/system/src/Grav/Framework/Session/SessionInterface.php b/system/src/Grav/Framework/Session/SessionInterface.php index 74c5f2a..75df0e9 100644 --- a/system/src/Grav/Framework/Session/SessionInterface.php +++ b/system/src/Grav/Framework/Session/SessionInterface.php @@ -1,8 +1,9 @@ getQueryParams(); - return isset($queryParams[$key]) ? $queryParams[$key] : null; + return $queryParams[$key] ?? null; } /** diff --git a/system/src/Grav/Framework/Uri/UriFactory.php b/system/src/Grav/Framework/Uri/UriFactory.php index c6bd428..e5cb1d3 100644 --- a/system/src/Grav/Framework/Uri/UriFactory.php +++ b/system/src/Grav/Framework/Uri/UriFactory.php @@ -1,8 +1,9 @@ = 1 && $port <= 65535))) { + if (null === $port || (\is_int($port) && ($port >= 0 && $port <= 65535))) { return $port; } - throw new \InvalidArgumentException('Uri port must be null or an integer between 1 and 65535'); + throw new \InvalidArgumentException('Uri port must be null or an integer between 0 and 65535'); } /** @@ -103,7 +104,7 @@ class UriPartsFilter */ public static function filterPath($path) { - if (!is_string($path)) { + if (!\is_string($path)) { throw new \InvalidArgumentException('Uri path must be a string'); } @@ -113,7 +114,7 @@ class UriPartsFilter return rawurlencode($match[0]); }, $path - ); + ) ?? ''; } /** @@ -125,7 +126,7 @@ class UriPartsFilter */ public static function filterQueryOrFragment($query) { - if (!is_string($query)) { + if (!\is_string($query)) { throw new \InvalidArgumentException('Uri query string and fragment must be a string'); } @@ -135,6 +136,6 @@ class UriPartsFilter return rawurlencode($match[0]); }, $query - ); + ) ?? ''; } } diff --git a/system/src/Grav/Installer/Install.php b/system/src/Grav/Installer/Install.php new file mode 100644 index 0000000..5345ce3 --- /dev/null +++ b/system/src/Grav/Installer/Install.php @@ -0,0 +1,287 @@ + [ + 'name' => 'PHP', + 'versions' => [ + '7.3' => '7.3.1', + '7.2' => '7.2.0', + '7.1' => '7.1.3', + '' => '7.2.14' + ] + ], + 'grav' => [ + 'name' => 'Grav', + 'versions' => [ + '1.5' => '1.5.0', + '' => '1.5.7' + ] + ], + 'plugins' => [ + 'admin' => [ + 'name' => 'Admin', + 'optional' => true, + 'versions' => [ + '1.8' => '1.8.0', + '' => '1.8.16' + ] + ], + 'email' => [ + 'name' => 'Email', + 'optional' => true, + 'versions' => [ + '2.7' => '2.7.0', + '' => '2.7.2' + ] + ], + 'form' => [ + 'name' => 'Form', + 'optional' => true, + 'versions' => [ + '2.16' => '2.16.0', + '' => '2.16.4' + ] + ], + 'login' => [ + 'name' => 'Login', + 'optional' => true, + 'versions' => [ + '2.8' => '2.8.0', + '' => '2.8.3' + ] + ], + ] + ]; + + private $ignores = [ + 'backup', + 'cache', + 'images', + 'logs', + 'tmp', + 'user', + '.htaccess', + 'robots.txt' + ]; + + private $classMap = [ + // 'Grav\\Installer\\Test' => __DIR__ . '/Test.php', + ]; + + private $zip; + private $location; + + private static $instance; + + public static function instance() + { + if (null === self::$instance) { + self::$instance = new static(); + } + + return self::$instance; + } + + private function __construct() + { + } + + public function setZip(string $zip) + { + $this->zip = $zip; + + return $this; + } + + public function __invoke(?string $zip) + { + $this->zip = $zip; + + $failedRequirements = $this->checkRequirements(); + if ($failedRequirements) { + $error = ['Following requirements have failed:']; + + foreach ($failedRequirements as $name => $req) { + $error[] = "{$req['title']} >= v{$req['minimum']} required, you have v{$req['installed']}"; + } + + throw new \RuntimeException(implode("
\n", $error)); + } + + $this->prepare(); + $this->install(); + $this->finalize(); + } + + /** + * NOTE: This method can only be called after $grav['plugins']->init(). + * + * @return array List of failed requirements. If the list is empty, installation can go on. + */ + public function checkRequirements(): array + { + $results = []; + + $this->checkVersion($results, 'php','php', $this->requires['php'], PHP_VERSION); + $this->checkVersion($results, 'grav', 'grav', $this->requires['grav'], GRAV_VERSION); + $this->checkPlugins($results, $this->requires['plugins']); + + return $results; + } + + /** + * @throws \RuntimeException + */ + public function prepare(): void + { + // Locate the new Grav update and the target site from the filesystem. + $location = dirname(realpath(__DIR__), 4); + $target = dirname(realpath(GRAV_ROOT . '/index.php')); + if ($location === $target) { + // We cannot copy files into themselves, abort! + throw new \RuntimeException('Grav has already been installed here!', 400); + } + + // Make sure that none of the Grav\Installer classes have been loaded, otherwise installation may fail! + foreach ($this->classMap as $class_name => $path) { + if (\class_exists($class_name, false)) { + throw new \RuntimeException(sprintf('Cannot update Grav, class %s has already been loaded!', $class_name), 500); + } + } + + $grav = Grav::instance(); + + /** @var ClassLoader $loader */ + $loader = $grav['loader']; + + // Override Grav\Installer classes by using this version of Grav. + $loader->addClassMap($this->classMap); + + $this->location = $location; + } + + /** + * @throws \RuntimeException + */ + public function install(): void + { + if (!$this->location) { + throw new \RuntimeException('Oops, installer was run without prepare()!', 500); + } + + try { + Installer::install( + $this->zip, + GRAV_ROOT, + ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $this->ignores], + $this->location, + !($this->zip && is_file($this->zip)) + ); + } catch (\Exception $e) { + Installer::setError($e->getMessage()); + } + + $errorCode = Installer::lastErrorCode(); + + $success = !(is_string($errorCode) || ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR))); + + if (!$success) { + throw new \RuntimeException(Installer::lastErrorMsg()); + } + } + + /** + * @throws \RuntimeException + */ + public function finalize(): void + { + Cache::clearCache(); + + clearstatcache(); + if (function_exists('opcache_reset')) { + @opcache_reset(); + } + } + + protected function checkVersion(array &$results, $type, $name, array $check, $version): void + { + if (null === $version && !empty($check['optional'])) { + return; + } + + $major = $minor = 0; + $versions = $check['versions'] ?? []; + foreach ($versions as $major => $minor) { + if (!$major || version_compare($version, $major, '<')) { + continue; + } + + if (version_compare($version, $minor, '>=')) { + return; + } + + break; + } + + if (!$major) { + $minor = reset($versions); + } + + $recommended = end($versions); + + if (version_compare($recommended, $minor, '<=')) { + $recommended = null; + } + + $results[$name] = [ + 'type' => $type, + 'name' => $name, + 'title' => $check['name'] ?? $name, + 'installed' => $version, + 'minimum' => $minor, + 'recommended' => $recommended + ]; + } + + protected function checkPlugins(array &$results, array $plugins): void + { + if (!\class_exists('Plugins')) { + return; + } + + foreach ($plugins as $name => $check) { + $plugin = Plugins::get($name); + if (!$plugin) { + $this->checkVersion($results, 'plugin', $name, $check, null); + continue; + } + + $blueprint = $plugin->blueprints(); + $version = (string)$blueprint->get('version'); + $check['name'] = ($blueprint->get('name') ?? $check['name'] ?? $name) . ' Plugin'; + $this->checkVersion($results, 'plugin', $name, $check, $version); + } + } +} \ No newline at end of file diff --git a/tests/phpstan/phpstan-bootstrap.php b/tests/phpstan/phpstan-bootstrap.php new file mode 100644 index 0000000..b145fa1 --- /dev/null +++ b/tests/phpstan/phpstan-bootstrap.php @@ -0,0 +1,7 @@ + $autoload)); +$grav->setup('tests'); +$grav['config']->init(); + +// Find all plugins in Grav installation and autoload their classes. + +/** @var UniformResourceLocator $locator */ +$locator = Grav::instance()['locator']; +$iterator = $locator->getIterator('plugins://'); +foreach($iterator as $directory) { + if (!$directory->isDir()) { + continue; + } + $autoloader = $directory->getPathname() . '/vendor/autoload.php'; + if (file_exists($autoloader)) { + require $autoloader; + } +} diff --git a/tests/phpstan/plugins.neon b/tests/phpstan/plugins.neon new file mode 100644 index 0000000..9b8d46c --- /dev/null +++ b/tests/phpstan/plugins.neon @@ -0,0 +1,20 @@ +includes: + #- '../../vendor/phpstan/phpstan-strict-rules/rules.neon' + - '../../vendor/phpstan/phpstan-deprecation-rules/rules.neon' +parameters: + fileExtensions: + - php + excludes_analyse: + - %currentWorkingDirectory%/user/plugins/*/vendor/* + - %currentWorkingDirectory%/user/plugins/*/tests/* + bootstrap: tests/phpstan/plugins-bootstrap.php + reportUnmatchedIgnoredErrors: true + universalObjectCratesClasses: + - Grav\Common\Config\Config + - Grav\Common\Config\Languages + - Grav\Common\Config\Setup + - Grav\Common\Data\Data + - Grav\Common\GPM\Common\Package + - Grav\Common\GPM\Local\Package + - Grav\Common\GPM\Remote\Package + - Grav\Common\Session diff --git a/tests/unit/Grav/Common/AssetsTest.php b/tests/unit/Grav/Common/AssetsTest.php index a54b88a..a5516a5 100644 --- a/tests/unit/Grav/Common/AssetsTest.php +++ b/tests/unit/Grav/Common/AssetsTest.php @@ -32,201 +32,298 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->add('test.css'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); $array = $this->assets->getCss(); - $this->assertSame([ - 'asset' => '/test.css', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => '', - 'group' => 'head', - 'modified' => false, - 'query' => '' - ], reset($array)); + + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"\/test.css", + "asset_type":"css", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet" + }, + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); $this->assets->add('test.js'); $js = $this->assets->js(); - $this->assertSame('' . PHP_EOL, $js); + $this->assertSame('' . PHP_EOL, $js); - $array = $this->assets->getCss(); - $this->assertSame([ - 'asset' => '/test.css', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => '', - 'group' => 'head', - 'modified' => false, - 'query' => '' - ], reset($array)); + $array = $this->assets->getJs(); + + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"js", + "elements":{ + "asset":"\/test.js", + "asset_type":"js", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":[ + + ], + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); //test addCss(). Test adding asset to a separate group $this->assets->reset(); $this->assets->addCSS('test.css'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); $array = $this->assets->getCss(); - $this->assertSame([ - 'asset' => '/test.css', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => '', - 'group' => 'head', - 'modified' => false, - 'query' => '' - ], reset($array)); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"\/test.css", + "asset_type":"css", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet" + }, + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); //test addCss(). Testing with remote URL $this->assets->reset(); $this->assets->addCSS('http://www.somesite.com/test.css'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); $array = $this->assets->getCss(); - $this->assertSame([ - 'asset' => 'http://www.somesite.com/test.css', - 'remote' => true, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => '', - 'group' => 'head', - 'modified' => false, - 'query' => '' - ], reset($array)); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"http:\/\/www.somesite.com\/test.css", + "asset_type":"css", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet" + }, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); //test addCss() adding asset to a separate group, and with an alternate rel attribute $this->assets->reset(); - $this->assets->addCSS('test.css', ['group' => 'alternate']); - $css = $this->assets->css('alternate', ['rel' => 'alternate']); - $this->assertSame('' . PHP_EOL, $css); + $this->assets->addCSS('test.css', ['group' => 'alternate', 'rel' => 'alternate']); + $css = $this->assets->css('alternate'); + $this->assertSame('' . PHP_EOL, $css); //test addJs() $this->assets->reset(); $this->assets->addJs('test.js'); $js = $this->assets->js(); - $this->assertSame('' . PHP_EOL, $js); + $this->assertSame('' . PHP_EOL, $js); $array = $this->assets->getJs(); - $this->assertSame([ - 'asset' => '/test.js', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => '', - 'group' => 'head', - 'modified' => false, - 'query' => '' - ], reset($array)); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"js", + "elements":{ + "asset":"\/test.js", + "asset_type":"js", + "order":0, + "group":"head", + "position":"pipeline", + "priority":10, + "attributes":[], + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); //Test CSS Groups $this->assets->reset(); - $this->assets->addCSS('test.css', null, true, 'footer'); + $this->assets->addCSS('test.css', ['group' => 'footer']); $css = $this->assets->css(); $this->assertEmpty($css); $css = $this->assets->css('footer'); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); $array = $this->assets->getCss(); - $this->assertSame([ - 'asset' => '/test.css', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => '', - 'group' => 'footer', - 'modified' => false, - 'query' => '' - ], reset($array)); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "css", + "elements": { + "asset": "/test.css", + "asset_type": "css", + "order": 0, + "group": "footer", + "position": "pipeline", + "priority": 10, + "attributes": { + "type": "text/css", + "rel": "stylesheet" + }, + "modified": false, + "query": "" + } + } + '; + $this->assertJsonStringEqualsJsonString($expected, $actual); //Test JS Groups $this->assets->reset(); - $this->assets->addJs('test.js', null, true, null, 'footer'); + $this->assets->addJs('test.js', ['group' => 'footer']); $js = $this->assets->js(); $this->assertEmpty($js); $js = $this->assets->js('footer'); - $this->assertSame('' . PHP_EOL, $js); + $this->assertSame('' . PHP_EOL, $js); $array = $this->assets->getJs(); - $this->assertSame([ - 'asset' => '/test.js', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => '', - 'group' => 'footer', - 'modified' => false, - 'query' => '' - ], reset($array)); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "footer", + "position": "pipeline", + "priority": 10, + "attributes": [], + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); //Test async / defer $this->assets->reset(); - $this->assets->addJs('test.js', null, true, 'async', null); + $this->assets->addJs('test.js', ['loading' => 'async']); $js = $this->assets->js(); $this->assertSame('' . PHP_EOL, $js); $array = $this->assets->getJs(); - $this->assertSame([ - 'asset' => '/test.js', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => 'async', - 'group' => 'head', - 'modified' => false, - 'query' => '' - ], reset($array)); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "head", + "position": "pipeline", + "priority": 10, + "attributes": { + "loading": "async" + }, + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); $this->assets->reset(); - $this->assets->addJs('test.js', null, true, 'defer', null); + $this->assets->addJs('test.js', ['loading' => 'defer']); $js = $this->assets->js(); $this->assertSame('' . PHP_EOL, $js); $array = $this->assets->getJs(); - $this->assertSame([ - 'asset' => '/test.js', - 'remote' => false, - 'priority' => 10, - 'order' => 0, - 'pipeline' => true, - 'loading' => 'defer', - 'group' => 'head', - 'modified' => false, - 'query' => '' - ], reset($array)); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "head", + "position": "pipeline", + "priority": 10, + "attributes": { + "loading": "defer" + }, + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); //Test inline $this->assets->reset(); - $this->assets->addJs('/system/assets/jquery/jquery-2.x.min.js', null, true, 'inline', null); - $js = $this->assets->js(); - $this->assertContains('jQuery Foundation', $js); + $this->assets->setJsPipeline(true); + $this->assets->addJs('/system/assets/jquery/jquery-3.x.min.js'); + $js = $this->assets->js('head', ['loading' => 'inline']); + $this->assertContains('"jquery",[],function()', $js); $this->assets->reset(); - $this->assets->addCss('/system/assets/debugger.css', null, true, null, 'inline'); - $css = $this->assets->css(); + $this->assets->setCssPipeline(true); + $this->assets->addCss('/system/assets/debugger.css'); + $css = $this->assets->css('head', ['loading' => 'inline']); $this->assertContains('div.phpdebugbar', $css); $this->assets->reset(); - $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto', null, true, null, 'inline'); - $css = $this->assets->css(); - $this->assertContains('font-family: \'Roboto\';', $css); + $this->assets->setCssPipeline(true); + $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto'); + $css = $this->assets->css('head', ['loading' => 'inline']); + $this->assertContains('font-family:\'Roboto\';', $css); //Test adding media queries $this->assets->reset(); $this->assets->add('test.css', ['media' => 'only screen and (min-width: 640px)']); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); + + } public function testAddingAssetPropertiesWithArray() @@ -275,6 +372,81 @@ class AssetsTest extends \Codeception\TestCase\Test '' . PHP_EOL, $js); } + public function testAddingLegacyFormat() + { + // regular CSS add + //test addCss(). Test adding asset to a separate group + $this->assets->reset(); + $this->assets->addCSS('test.css', 15, true, 'bottom', 'async'); + $css = $this->assets->css('bottom'); + $this->assertSame('' . PHP_EOL, $css); + + $array = $this->assets->getCss(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type":"css", + "elements":{ + "asset":"\/test.css", + "asset_type":"css", + "order":0, + "group":"bottom", + "position":"pipeline", + "priority":15, + "attributes":{ + "type":"text\/css", + "rel":"stylesheet", + "loading":"async" + }, + "modified":false, + "query":"" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + $this->assets->reset(); + $this->assets->addJs('test.js', 15, false, 'defer', 'bottom'); + $js = $this->assets->js('bottom'); + $this->assertSame('' . PHP_EOL, $js); + + $array = $this->assets->getJs(); + /** @var Assets\BaseAsset $item */ + $item = reset($array); + $actual = json_encode($item); + $expected = ' + { + "type": "js", + "elements": { + "asset": "/test.js", + "asset_type": "js", + "order": 0, + "group": "bottom", + "position": "after", + "priority": 15, + "attributes": { + "loading": "defer" + }, + "modified": false, + "query": "" + } + }'; + $this->assertJsonStringEqualsJsonString($expected, $actual); + + + $this->assets->reset(); + $this->assets->addInlineCss('body { color: black }', 15, 'bottom'); + $css = $this->assets->css('bottom'); + $this->assertSame('' . PHP_EOL, $css); + + $this->assets->reset(); + $this->assets->addInlineJs('alert("test")', 15, 'bottom', ['id' => 'foo']); + $js = $this->assets->js('bottom'); + $this->assertSame('' . PHP_EOL, $js); + + } + public function testAddingCSSAssetPropertiesWithArrayFromCollection() { $this->assets->registerCollection('test', ['/system/assets/whoops.css']); @@ -284,25 +456,25 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->addCss('test', ['priority' => 1]); $this->assets->addCss('test.css', ['priority' => 2]); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL . - '' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); //Test multiple groups $this->assets->reset(); $this->assets->addCss('test', ['priority' => 1, 'group' => 'footer']); $this->assets->addCss('test.css', ['priority' => 2]); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); $css = $this->assets->css('footer'); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); //Test adding array of assets //Test priority too $this->assets->reset(); $this->assets->addCss(['test', 'test.css'], ['loading' => 'async']); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL . - '' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); } public function testPriorityOfAssets() @@ -312,8 +484,8 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->add('test-after.css'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL . - '' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); //---------------- $this->assets->reset(); @@ -321,8 +493,8 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->add('test.css', 2); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL . - '' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL, $css); //---------------- $this->assets->reset(); @@ -331,9 +503,9 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->add('test-before.css', 3); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL . - '' . PHP_EOL . - '' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL . + '' . PHP_EOL . + '' . PHP_EOL, $css); } public function testPipeline() @@ -344,37 +516,53 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->add('test.css', null, true); $this->assets->setCssPipeline(true); $css = $this->assets->css(); - $this->assertSame('', $css); + $this->assertRegExp('##', $css); //Add a core Grav CSS file, which is found. Pipeline will now return a file $this->assets->add('/system/assets/debugger.css', null, true); $css = $this->assets->css(); - $this->assertContains('', $css); + $this->assertRegExp('##', $css); } public function testPipelineWithTimestamp() { $this->assets->reset(); $this->assets->setTimestamp('foo'); + $this->assets->setCssPipeline(true); //Add a core Grav CSS file, which is found. Pipeline will now return a file $this->assets->add('/system/assets/debugger.css', null, true); $css = $this->assets->css(); - $this->assertContains('', $css); - $this->assertContains($this->assets->getTimestamp(), $css); + $this->assertRegExp('##', $css); + } + + public function testInline() + { + $this->assets->reset(); + + //File not existing. Pipeline searches for that file without reaching it. Output is empty. + $this->assets->add('test.css', ['loading' => 'inline']); + $css = $this->assets->css(); + $this->assertSame("\n", $css); + + $this->assets->reset(); + //Add a core Grav CSS file, which is found. Pipeline will now return its content. + $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto', ['loading' => 'inline']); + $this->assets->addCss('/system/assets/debugger.css', ['loading' => 'inline']); + $css = $this->assets->css(); + $this->assertContains('font-family: \'Roboto\';', $css); + $this->assertContains('div.phpdebugbar-header', $css); } public function testInlinePipeline() { $this->assets->reset(); + $this->assets->setCssPipeline(true); //File not existing. Pipeline searches for that file without reaching it. Output is empty. - $this->assets->add('test.css', null, true); - $this->assets->setCssPipeline(true); + $this->assets->add('test.css'); $css = $this->assets->css('head', ['loading' => 'inline']); - $this->assertSame('', $css); + $this->assertSame("\n", $css); //Add a core Grav CSS file, which is found. Pipeline will now return its content. $this->assets->addCss('https://fonts.googleapis.com/css?family=Roboto', null, true); @@ -407,56 +595,56 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->setTimestamp('foo'); $this->assets->addCSS('test.css'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); // local CSS already with param $this->assets->reset(); $this->assets->setTimestamp('foo'); $this->assets->addCSS('test.css?bar'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); // external CSS already $this->assets->reset(); $this->assets->setTimestamp('foo'); $this->assets->addCSS('http://somesite.com/test.css'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); // external CSS already with param $this->assets->reset(); $this->assets->setTimestamp('foo'); $this->assets->addCSS('http://somesite.com/test.css?bar'); $css = $this->assets->css(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); // local JS nothing extra $this->assets->reset(); $this->assets->setTimestamp('foo'); $this->assets->addJs('test.js'); $css = $this->assets->js(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); // local JS already with param $this->assets->reset(); $this->assets->setTimestamp('foo'); $this->assets->addJs('test.js?bar'); $css = $this->assets->js(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); // external JS already $this->assets->reset(); $this->assets->setTimestamp('foo'); $this->assets->addJs('http://somesite.com/test.js'); $css = $this->assets->js(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); // external JS already with param $this->assets->reset(); $this->assets->setTimestamp('foo'); $this->assets->addJs('http://somesite.com/test.js?bar'); $css = $this->assets->js(); - $this->assertSame('' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); } public function testAddInlineCss() @@ -464,7 +652,7 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->reset(); $this->assets->addInlineCss('body { color: black }'); $css = $this->assets->css(); - $this->assertSame(PHP_EOL . '' . PHP_EOL, $css); + $this->assertSame('' . PHP_EOL, $css); } public function testAddInlineJs() @@ -472,7 +660,7 @@ class AssetsTest extends \Codeception\TestCase\Test $this->assets->reset(); $this->assets->addInlineJs('alert("test")'); $js = $this->assets->js(); - $this->assertSame(PHP_EOL . '' . PHP_EOL, $js); + $this->assertSame('' . PHP_EOL, $js); } public function testGetCollections() @@ -499,49 +687,41 @@ class AssetsTest extends \Codeception\TestCase\Test { $this->assets->addInlineJs('alert("test")'); $this->assets->reset(); - $this->assertCount(0, (array) $this->assets->js()); + $this->assertCount(0, (array) $this->assets->getJs()); $this->assets->addAsyncJs('jquery'); $this->assets->reset(); - - $this->assertCount(0, (array) $this->assets->js()); + $this->assertCount(0, (array) $this->assets->getJs()); $this->assets->addInlineCss('body { color: black }'); $this->assets->reset(); - - $this->assertCount(0, (array) $this->assets->css()); + $this->assertCount(0, (array) $this->assets->getCss()); $this->assets->add('/system/assets/debugger.css', null, true); $this->assets->reset(); - - $this->assertCount(0, (array) $this->assets->css()); + $this->assertCount(0, (array) $this->assets->getCss()); } public function testResetJs() { $this->assets->addInlineJs('alert("test")'); $this->assets->resetJs(); - $this->assertCount(0, (array) $this->assets->js()); + $this->assertCount(0, (array) $this->assets->getJs()); $this->assets->addAsyncJs('jquery'); $this->assets->resetJs(); - - $this->assertCount(0, (array) $this->assets->js()); + $this->assertCount(0, (array) $this->assets->getJs()); } public function testResetCss() { - $this->assertCount(0, (array) $this->assets->js()); - $this->assets->addInlineCss('body { color: black }'); $this->assets->resetCss(); - - $this->assertCount(0, (array) $this->assets->css()); + $this->assertCount(0, (array) $this->assets->getCss()); $this->assets->add('/system/assets/debugger.css', null, true); $this->assets->resetCss(); - - $this->assertCount(0, (array) $this->assets->css()); + $this->assertCount(0, (array) $this->assets->getCss()); } public function testAddDirCss() diff --git a/tests/unit/Grav/Common/Helpers/ExcerptsTest.php b/tests/unit/Grav/Common/Helpers/ExcerptsTest.php index 0cc48a4..b57b385 100644 --- a/tests/unit/Grav/Common/Helpers/ExcerptsTest.php +++ b/tests/unit/Grav/Common/Helpers/ExcerptsTest.php @@ -19,7 +19,7 @@ class ExcerptsTest extends \Codeception\TestCase\Test /** @var Grav $grav */ protected $grav; - /** @var Page $page */ + /** @var PageInterface $page */ protected $page; /** @var Pages $pages */ diff --git a/tests/unit/Grav/Common/InflectorTest.php b/tests/unit/Grav/Common/InflectorTest.php index 9caa3e8..9fad489 100644 --- a/tests/unit/Grav/Common/InflectorTest.php +++ b/tests/unit/Grav/Common/InflectorTest.php @@ -13,7 +13,7 @@ class InflectorTest extends \Codeception\TestCase\Test /** @var Grav $grav */ protected $grav; - /** @var Uri $uri */ + /** @var Inflector $uri */ protected $inflector; protected function _before() diff --git a/tests/unit/Grav/Common/Markdown/ParsedownTest.php b/tests/unit/Grav/Common/Markdown/ParsedownTest.php index e73e11f..58a1acf 100644 --- a/tests/unit/Grav/Common/Markdown/ParsedownTest.php +++ b/tests/unit/Grav/Common/Markdown/ParsedownTest.php @@ -2,6 +2,7 @@ use Codeception\Util\Fixtures; use Grav\Common\Grav; +use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Uri; use Grav\Common\Config\Config; use Grav\Common\Page\Pages; @@ -56,14 +57,19 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->pages->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/item2/item2-2'); - $this->parsedown = new Parsedown($page, $defaults); + + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); } protected function _after() @@ -179,14 +185,18 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->uri->initializeWithURL('http://testing.dev/')->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/'); - $this->parsedown = new Parsedown($page, $defaults); + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); $this->assertSame('

', $this->parsedown->text('![](home-sample-image.jpg)')); @@ -230,15 +240,18 @@ class ParsedownTest extends \Codeception\TestCase\Test $this->uri->initializeWithURL('http://testing.dev/')->init(); $defaults = [ - 'extra' => false, - 'auto_line_breaks' => false, - 'auto_url_links' => false, - 'escape_markup' => false, - 'special_chars' => ['>' => 'gt', '<' => 'lt'], + 'markdown' => [ + 'extra' => false, + 'auto_line_breaks' => false, + 'auto_url_links' => false, + 'escape_markup' => false, + 'special_chars' => ['>' => 'gt', '<' => 'lt'], + ], + 'images' => $this->config->get('system.images', []) ]; $page = $this->pages->dispatch('/'); - $this->parsedown = new Parsedown($page, $defaults); - + $excerpts = new Excerpts($page, $defaults); + $this->parsedown = new Parsedown($excerpts); $this->assertSame('

Down a Level

', $this->parsedown->text('[Down a Level](item1-3)')); diff --git a/tests/unit/Grav/Common/Page/PagesTest.php b/tests/unit/Grav/Common/Page/PagesTest.php index 3a70571..831ee3d 100644 --- a/tests/unit/Grav/Common/Page/PagesTest.php +++ b/tests/unit/Grav/Common/Page/PagesTest.php @@ -18,7 +18,7 @@ class PagesTest extends \Codeception\TestCase\Test /** @var Pages $pages */ protected $pages; - /** @var Page $root_page */ + /** @var PageInterface $root_page */ protected $root_page; protected function _before() diff --git a/tests/unit/Grav/Common/UriTest.php b/tests/unit/Grav/Common/UriTest.php index 32db8a1..9977d67 100644 --- a/tests/unit/Grav/Common/UriTest.php +++ b/tests/unit/Grav/Common/UriTest.php @@ -302,7 +302,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'https://api.getgrav.com:4040', 'extension' => null, 'addNonce' => 'https://username:password@api.getgrav.com:4040/v1/post/128/page:x/nonce:{{nonce}}?all=1', - '__toString' => 'https://username:password@api.getgrav.com:4040/v1/post/128/page:x?all=1' + 'toOriginalString' => 'https://username:password@api.getgrav.com:4040/v1/post/128/page:x?all=1' ], 'https://google.com:443/' => [ 'scheme' => 'https://', @@ -392,7 +392,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => null, 'addNonce' => 'http://localhost/this%20is%20the%20path/my%20page/nonce:{{nonce}}', - '__toString' => 'http://localhost/this%20is%20the%20path/my%20page' + 'toOriginalString' => 'http://localhost/this%20is%20the%20path/my%20page' ], 'http://localhost/pölöpölö/päläpälä' => [ 'scheme' => 'http://', @@ -415,7 +415,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => null, 'addNonce' => 'http://localhost/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4/nonce:{{nonce}}', - '__toString' => 'http://localhost/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4' + 'toOriginalString' => 'http://localhost/p%C3%B6l%C3%B6p%C3%B6l%C3%B6/p%C3%A4l%C3%A4p%C3%A4l%C3%A4' ], // Query params tests. 'http://localhost:8080/grav/it/ueper?test=x&test2=y' => [ @@ -573,7 +573,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => 'html', 'addNonce' => 'http://localhost/a-page.html/nonce:{{nonce}}', - '__toString' => 'http://localhost/a-page', // FIXME <- + 'toOriginalString' => 'http://localhost/a-page.html', ], 'http://localhost/a-page.json' => [ 'scheme' => 'http://', @@ -596,7 +596,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => 'json', 'addNonce' => 'http://localhost/a-page.json/nonce:{{nonce}}', - '__toString' => 'http://localhost/a-page', // FIX ME <- + 'toOriginalString' => 'http://localhost/a-page.json', ], 'http://localhost/admin/ajax.json/task:getnewsfeed' => [ 'scheme' => 'http://', @@ -619,7 +619,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => 'json', 'addNonce' => 'http://localhost/admin/ajax.json/task:getnewsfeed/nonce:{{nonce}}', - '__toString' => 'http://localhost/admin/ajax/task:getnewsfeed', + 'toOriginalString' => 'http://localhost/admin/ajax.json/task:getnewsfeed', ], 'http://localhost/grav/admin/media.json/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==' => [ 'scheme' => 'http://', @@ -642,7 +642,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => 'json', 'addNonce' => 'http://localhost/grav/admin/media.json/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==/nonce:{{nonce}}', - '__toString' => 'http://localhost/grav/admin/media/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==', // FIXME <- + 'toOriginalString' => 'http://localhost/grav/admin/media.json/route:L1VzZXJzL3JodWsvd29ya3NwYWNlL2dyYXYtZGVtby1zYW1wbGVyL3VzZXIvYXNzZXRzL3FRMXB4Vk1ERTNJZzh5Ni5qcGc=/task:removeFileFromBlueprint/proute:/blueprint:Y29uZmlnL2RldGFpbHM=/type:config/field:deep.nested.custom_file/path:dXNlci9hc3NldHMvcVExcHhWTURFM0lnOHk2LmpwZw==', ], 'http://localhost/a-page.foo' => [ 'scheme' => 'http://', @@ -665,7 +665,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => 'foo', 'addNonce' => 'http://localhost/a-page.foo/nonce:{{nonce}}', - '__toString' => 'http://localhost/a-page.foo' + 'toOriginalString' => 'http://localhost/a-page.foo' ], // Fragment tests. 'http://localhost:8080/a/b/c#my-fragment' => [ @@ -712,7 +712,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => '', 'extension' => null, //'addNonce' => '%22%3E%3Cscript%3Ealert%3C/localhost/script%3E:/nonce:{{nonce}}', // FIXME <- - '__toString' => '%22%3E%3Cscript%3Ealert%3C/localhost/script%3E:' // FIXME <- + 'toOriginalString' => '%22%3E%3Cscript%3Ealert%3C/localhost/script%3E:' // FIXME <- ], 'http://">' => [ 'scheme' => 'http://', @@ -735,7 +735,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://unknown', 'extension' => null, 'addNonce' => 'http://unknown/script%3E/nonce:{{nonce}}', - '__toString' => 'http://unknown/script%3E' + 'toOriginalString' => 'http://unknown/script%3E' ], 'http://localhost/">' => [ 'scheme' => 'http://', @@ -758,7 +758,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => null, 'addNonce' => 'http://localhost/%22%3E%3Cscript%3Ealert%3C/script%3E/nonce:{{nonce}}', - '__toString' => 'http://localhost/%22%3E%3Cscript%3Ealert%3C/script%3E' + 'toOriginalString' => 'http://localhost/%22%3E%3Cscript%3Ealert%3C/script%3E' ], 'http://localhost/something/p1:foo/p2:">' => [ 'scheme' => 'http://', @@ -781,7 +781,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => null, //'addNonce' => 'http://localhost/something/script%3E/p1:foo/p2:%22%3E%3Cscript%3Ealert%3C/nonce:{{nonce}}', // FIXME <- - '__toString' => 'http://localhost/something/script%3E/p1:foo/p2:%22%3E%3Cscript%3Ealert%3C' + 'toOriginalString' => 'http://localhost/something/script%3E/p1:foo/p2:%22%3E%3Cscript%3Ealert%3C' ], 'http://localhost/something?p=">' => [ 'scheme' => 'http://', @@ -804,7 +804,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => null, 'addNonce' => 'http://localhost/something/nonce:{{nonce}}?p=%22%3E%3Cscript%3Ealert%3C/script%3E', - '__toString' => 'http://localhost/something?p=%22%3E%3Cscript%3Ealert%3C/script%3E' + 'toOriginalString' => 'http://localhost/something?p=%22%3E%3Cscript%3Ealert%3C/script%3E' ], 'http://localhost/something#">' => [ 'scheme' => 'http://', @@ -827,7 +827,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'http://localhost', 'extension' => null, 'addNonce' => 'http://localhost/something/nonce:{{nonce}}#%22%3E%3Cscript%3Ealert%3C/script%3E', - '__toString' => 'http://localhost/something#%22%3E%3Cscript%3Ealert%3C/script%3E' + 'toOriginalString' => 'http://localhost/something#%22%3E%3Cscript%3Ealert%3C/script%3E' ], 'https://www.getgrav.org/something/"><' => [ 'scheme' => 'https://', @@ -850,7 +850,7 @@ class UriTest extends \Codeception\TestCase\Test 'rootUrl' => 'https://www.getgrav.org', 'extension' => null, 'addNonce' => 'https://www.getgrav.org/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C/nonce:{{nonce}}', - '__toString' => 'https://www.getgrav.org/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C' + 'toOriginalString' => 'https://www.getgrav.org/something/%22%3E%3Cscript%3Eeval%28atob%28%22aGlzdG9yeS5wdXNoU3RhdGUoJycsJycsJy8nKTskKCdoZWFkLGJvZHknKS5odG1sKCcnKS5sb2FkKCcvJyk7JC5wb3N0KCcvYWRtaW4nLGZ1bmN0aW9uKGRhdGEpeyQucG9zdCgkKGRhdGEpLmZpbmQoJ1tpZD1hZG1pbi11c2VyLWRldGFpbHNdIGEnKS5hdHRyKCdocmVmJykseydhZG1pbi1ub25jZSc6JChkYXRhKS5maW5kKCdbZGF0YS1jbGVhci1jYWNoZV0nKS5hdHRyKCdkYXRhLWNsZWFyLWNhY2hlJykuc3BsaXQoJzonKS5wb3AoKS50cmltKCksJ2RhdGFbcGFzc3dvcmRdJzonSW0zdjFsaDR4eDByJywndGFzayc6J3NhdmUnfSl9KQ==%22%29%29%3C/script%3E%3C' ], ]; @@ -868,7 +868,7 @@ class UriTest extends \Codeception\TestCase\Test protected function runTestSet(array $tests, $method, $params = []) { foreach ($tests as $url => $candidates) { - if (!array_key_exists($method, $candidates) && $method !== '__toString') { + if (!array_key_exists($method, $candidates) && $method !== 'toOriginalString') { continue; } if ($method === 'addNonce') { @@ -881,7 +881,7 @@ class UriTest extends \Codeception\TestCase\Test } $this->uri->initializeWithURL($url)->init(); - if ($method === '__toString' && !isset($candidates[$method])) { + if ($method === 'toOriginalString' && !isset($candidates[$method])) { $expected = $url; } else { $expected = $candidates[$method]; @@ -921,7 +921,7 @@ class UriTest extends \Codeception\TestCase\Test public function testToString() { - $this->runTestSet($this->tests, '__toString'); + $this->runTestSet($this->tests, 'toOriginalString'); } public function testScheme() @@ -1121,6 +1121,24 @@ class UriTest extends \Codeception\TestCase\Test ]; $this->assertSame('http://foo:bar@localhost:8080/test?x=2#xxx', Uri::buildUrl($parsed_url)); + + /** @var Uri $uri */ + $uri = Grav::instance()['uri']; + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.foo', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.foo', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html', '/subdir/path1')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom', Uri::buildUrl($uri->toArray(true))); + + $uri->initializeWithUrlAndRootPath('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom?fig=something', '/subdir')->init(); + $this->assertSame('https://testing.dev/subdir/path1/path2/file.html/foo:blah/bang:boom?fig=something', Uri::buildUrl($uri->toArray(true))); } public function testConvertUrl() diff --git a/tests/unit/Grav/Common/UtilsTest.php b/tests/unit/Grav/Common/UtilsTest.php index 5e3684c..e786242 100644 --- a/tests/unit/Grav/Common/UtilsTest.php +++ b/tests/unit/Grav/Common/UtilsTest.php @@ -2,6 +2,7 @@ use Codeception\Util\Fixtures; use Grav\Common\Grav; +use Grav\Common\Uri; use Grav\Common\Utils; /** @@ -12,10 +13,14 @@ class UtilsTest extends \Codeception\TestCase\Test /** @var Grav $grav */ protected $grav; + /** @var Uri $uri */ + protected $uri; + protected function _before() { $grav = Fixtures::get('grav'); $this->grav = $grav(); + $this->uri = $this->grav['uri']; } protected function _after() @@ -35,6 +40,13 @@ class UtilsTest extends \Codeception\TestCase\Test $this->assertFalse(Utils::startsWith('ENGLISH', 'en')); $this->assertFalse(Utils::startsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', 'e')); + + $this->assertTrue(Utils::startsWith('english', 'En', false)); + $this->assertTrue(Utils::startsWith('English', 'EN', false)); + $this->assertTrue(Utils::startsWith('ENGLISH', 'en', false)); + $this->assertTrue(Utils::startsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'e', false)); + } public function testEndsWith() @@ -50,6 +62,12 @@ class UtilsTest extends \Codeception\TestCase\Test $this->assertFalse(Utils::endsWith('ENGLISH', 'Sh')); $this->assertFalse(Utils::endsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', 'DEUSTCH')); + + $this->assertTrue(Utils::endsWith('english', 'SH', false)); + $this->assertTrue(Utils::endsWith('EngliSh', 'sH', false)); + $this->assertTrue(Utils::endsWith('ENGLISH', 'sh', false)); + $this->assertTrue(Utils::endsWith('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'english', false)); } public function testContains() @@ -65,6 +83,12 @@ class UtilsTest extends \Codeception\TestCase\Test $this->assertFalse(Utils::contains('ENGLISH', 'SCH')); $this->assertFalse(Utils::contains('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', 'DEUSTCH')); + + $this->assertTrue(Utils::contains('EngliSh', 'GLI', false)); + $this->assertTrue(Utils::contains('EngliSh', 'ENGLISH', false)); + $this->assertTrue(Utils::contains('ENGLISH', 'ish', false)); + $this->assertTrue(Utils::contains('ENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISHENGLISH', + 'english', false)); } public function testSubstrToString() @@ -72,6 +96,10 @@ class UtilsTest extends \Codeception\TestCase\Test $this->assertEquals('en', Utils::substrToString('english', 'glish')); $this->assertEquals('english', Utils::substrToString('english', 'test')); $this->assertNotEquals('en', Utils::substrToString('english', 'lish')); + + $this->assertEquals('en', Utils::substrToString('english', 'GLISH', false)); + $this->assertEquals('english', Utils::substrToString('english', 'TEST', false)); + $this->assertNotEquals('en', Utils::substrToString('english', 'LISH', false)); } public function testMergeObjects() @@ -123,6 +151,8 @@ class UtilsTest extends \Codeception\TestCase\Test public function testTruncateHtml() { + $this->assertEquals('T...', Utils::truncateHtml('This is a string to truncate', 1)); + $this->assertEquals('This is...', Utils::truncateHtml('This is a string to truncate', 7)); $this->assertEquals('

T...

', Utils::truncateHtml('

This is a string to truncate

', 1)); $this->assertEquals('

This...

', Utils::truncateHtml('

This is a string to truncate

', 4)); $this->assertEquals('

This is a...

', Utils::truncateHtml('

This is a string to truncate

', 10)); @@ -134,6 +164,8 @@ class UtilsTest extends \Codeception\TestCase\Test public function testSafeTruncateHtml() { + $this->assertEquals('This...', Utils::safeTruncateHtml('This is a string to truncate', 1)); + $this->assertEquals('This is a...', Utils::safeTruncateHtml('This is a string to truncate', 3)); $this->assertEquals('

This...

', Utils::safeTruncateHtml('

This is a string to truncate

', 1)); $this->assertEquals('

This is...

', Utils::safeTruncateHtml('

This is a string to truncate

', 2)); $this->assertEquals('

This is a string to...

', Utils::safeTruncateHtml('

This is a string to truncate

', 5)); @@ -192,7 +224,19 @@ class UtilsTest extends \Codeception\TestCase\Test $this->assertEquals('test', Utils::normalizePath('../test')); $this->assertEquals('/test', Utils::normalizePath('/../test')); $this->assertEquals('/test2', Utils::normalizePath('/test/../test2')); - $this->assertEquals('/test/test2', Utils::normalizePath('/test/./test2')); + $this->assertEquals('/test3', Utils::normalizePath('/test/../test2/../test3')); + + $this->assertEquals('//cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css', Utils::normalizePath('//cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css')); + $this->assertEquals('//use.fontawesome.com/releases/v5.8.1/css/all.css', Utils::normalizePath('//use.fontawesome.com/releases/v5.8.1/css/all.css')); + $this->assertEquals('//use.fontawesome.com/releases/v5.8.1/webfonts/fa-brands-400.eot', Utils::normalizePath('//use.fontawesome.com/releases/v5.8.1/css/../webfonts/fa-brands-400.eot')); + + $this->assertEquals('http://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css', Utils::normalizePath('http://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css')); + $this->assertEquals('http://use.fontawesome.com/releases/v5.8.1/css/all.css', Utils::normalizePath('http://use.fontawesome.com/releases/v5.8.1/css/all.css')); + $this->assertEquals('http://use.fontawesome.com/releases/v5.8.1/webfonts/fa-brands-400.eot', Utils::normalizePath('http://use.fontawesome.com/releases/v5.8.1/css/../webfonts/fa-brands-400.eot')); + + $this->assertEquals('https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css', Utils::normalizePath('https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css')); + $this->assertEquals('https://use.fontawesome.com/releases/v5.8.1/css/all.css', Utils::normalizePath('https://use.fontawesome.com/releases/v5.8.1/css/all.css')); + $this->assertEquals('https://use.fontawesome.com/releases/v5.8.1/webfonts/fa-brands-400.eot', Utils::normalizePath('https://use.fontawesome.com/releases/v5.8.1/css/../webfonts/fa-brands-400.eot')); } public function testIsFunctionDisabled() @@ -330,4 +374,153 @@ class UtilsTest extends \Codeception\TestCase\Test { $this->assertTrue(Utils::verifyNonce(Utils::getNonce('test-action'), 'test-action')); } + + public function testUrl() + { + $this->uri->initializeWithUrl('http://testing.dev/path1/path2')->init(); + + // Fail hard + $this->assertSame(false, Utils::url('', true)); + $this->assertSame(false, Utils::url('')); + $this->assertSame(false, Utils::url(new stdClass())); + $this->assertSame(false, Utils::url(['foo','bar','baz'])); + $this->assertSame(false, Utils::url('user://does/not/exist')); + + // Fail Gracefully + $this->assertSame('/', Utils::url('/', false, true)); + $this->assertSame('/', Utils::url('', false, true)); + $this->assertSame('/', Utils::url(new stdClass(), false, true)); + $this->assertSame('/', Utils::url(['foo','bar','baz'], false, true)); + $this->assertSame('/user/does/not/exist', Utils::url('user://does/not/exist', false, true)); + + // Simple paths + $this->assertSame('/', Utils::url('/')); + $this->assertSame('/path1', Utils::url('/path1')); + $this->assertSame('/path1/path2', Utils::url('/path1/path2')); + $this->assertSame('/random/path1/path2', Utils::url('/random/path1/path2')); + $this->assertSame('/foobar.jpg', Utils::url('/foobar.jpg')); + $this->assertSame('/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); + $this->assertSame('/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + $this->assertSame('/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg')); + + // Simple paths with domain + $this->assertSame('http://testing.dev/', Utils::url('/', true)); + $this->assertSame('http://testing.dev/path1', Utils::url('/path1', true)); + $this->assertSame('http://testing.dev/path1/path2', Utils::url('/path1/path2', true)); + $this->assertSame('http://testing.dev/random/path1/path2', Utils::url('/random/path1/path2', true)); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg', true)); + $this->assertSame('http://testing.dev/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg', true)); + + // Relative paths from Grav root. + $this->assertSame('/subdir', Utils::url('subdir')); + $this->assertSame('/subdir/path1', Utils::url('subdir/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('subdir/path1/path2')); + $this->assertSame('/path1', Utils::url('path1')); + $this->assertSame('/path1/path2', Utils::url('path1/path2')); + $this->assertSame('/foobar.jpg', Utils::url('foobar.jpg')); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('foobar.jpg', true)); + + // Relative paths from Grav root with domain. + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('foobar.jpg', true)); + $this->assertSame('http://testing.dev/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + + // All Non-existing streams should be treated as external URI / protocol. + $this->assertSame('http://domain.com/path', Utils::url('http://domain.com/path')); + $this->assertSame('ftp://domain.com/path', Utils::url('ftp://domain.com/path')); + $this->assertSame('sftp://domain.com/path', Utils::url('sftp://domain.com/path')); + $this->assertSame('ssh://domain.com', Utils::url('ssh://domain.com')); + $this->assertSame('pop://domain.com', Utils::url('pop://domain.com')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', true)); + // $this->assertSame('mailto:joe@domain.com', Utils::url('mailto:joe@domain.com', true)); // FIXME <- + } + + public function testUrlWithRoot() + { + $this->uri->initializeWithUrlAndRootPath('http://testing.dev/subdir/path1/path2', '/subdir')->init(); + + // Fail hard + $this->assertSame(false, Utils::url('', true)); + $this->assertSame(false, Utils::url('')); + $this->assertSame(false, Utils::url(new stdClass())); + $this->assertSame(false, Utils::url(['foo','bar','baz'])); + $this->assertSame(false, Utils::url('user://does/not/exist')); + + // Fail Gracefully + $this->assertSame('/subdir/', Utils::url('/', false, true)); + $this->assertSame('/subdir/', Utils::url('', false, true)); + $this->assertSame('/subdir/', Utils::url(new stdClass(), false, true)); + $this->assertSame('/subdir/', Utils::url(['foo','bar','baz'], false, true)); + $this->assertSame('/subdir/user/does/not/exist', Utils::url('user://does/not/exist', false, true)); + + // Simple paths + $this->assertSame('/subdir/', Utils::url('/')); + $this->assertSame('/subdir/path1', Utils::url('/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('/path1/path2')); + $this->assertSame('/subdir/random/path1/path2', Utils::url('/random/path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('/foobar.jpg')); + $this->assertSame('/subdir/path1/foobar.jpg', Utils::url('/path1/foobar.jpg')); + $this->assertSame('/subdir/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg')); + $this->assertSame('/subdir/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg')); + + // Simple paths with domain + $this->assertSame('http://testing.dev/subdir/', Utils::url('/', true)); + $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/path1', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2', Utils::url('/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/random/path1/path2', Utils::url('/random/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/path1/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2/foobar.jpg', Utils::url('/path1/path2/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/random/path1/path2/foobar.jpg', Utils::url('/random/path1/path2/foobar.jpg', true)); + + // Paths including the grav base. + $this->assertSame('/subdir/', Utils::url('/subdir')); + $this->assertSame('/subdir/path1', Utils::url('/subdir/path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('/subdir/path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg')); + $this->assertSame('/subdir/path1/foobar.jpg', Utils::url('/subdir/path1/foobar.jpg')); + + // Relative paths from Grav root with domain. + $this->assertSame('http://testing.dev/subdir/', Utils::url('/subdir', true)); + $this->assertSame('http://testing.dev/subdir/path1', Utils::url('/subdir/path1', true)); + $this->assertSame('http://testing.dev/subdir/path1/path2', Utils::url('/subdir/path1/path2', true)); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('/subdir/foobar.jpg', true)); + $this->assertSame('http://testing.dev/subdir/path1/foobar.jpg', Utils::url('/subdir/path1/foobar.jpg', true)); + + // Relative paths from Grav root. + $this->assertSame('/subdir/subdir', Utils::url('subdir')); + $this->assertSame('/subdir/subdir/path1', Utils::url('subdir/path1')); + $this->assertSame('/subdir/subdir/path1/path2', Utils::url('subdir/path1/path2')); + $this->assertSame('/subdir/path1', Utils::url('path1')); + $this->assertSame('/subdir/path1/path2', Utils::url('path1/path2')); + $this->assertSame('/subdir/foobar.jpg', Utils::url('foobar.jpg')); + $this->assertSame('http://testing.dev/subdir/foobar.jpg', Utils::url('foobar.jpg', true)); + + // All Non-existing streams should be treated as external URI / protocol. + $this->assertSame('http://domain.com/path', Utils::url('http://domain.com/path')); + $this->assertSame('ftp://domain.com/path', Utils::url('ftp://domain.com/path')); + $this->assertSame('sftp://domain.com/path', Utils::url('sftp://domain.com/path')); + $this->assertSame('ssh://domain.com', Utils::url('ssh://domain.com')); + $this->assertSame('pop://domain.com', Utils::url('pop://domain.com')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz')); + $this->assertSame('foo://bar/baz', Utils::url('foo://bar/baz', true)); + // $this->assertSame('mailto:joe@domain.com', Utils::url('mailto:joe@domain.com', true)); // FIXME <- + } + + public function testUrlWithStreams() + { + + } + + public function testUrlwithExternals() + { + $this->uri->initializeWithUrl('http://testing.dev/path1/path2')->init(); + $this->assertSame('http://foo.com', Utils::url('http://foo.com')); + $this->assertSame('https://foo.com', Utils::url('https://foo.com')); + $this->assertSame('//foo.com', Utils::url('//foo.com')); + $this->assertSame('//foo.com?param=x', Utils::url('//foo.com?param=x')); + } } diff --git a/tests/unit/Grav/Framework/File/Formatter/CsvFormatterTest.php b/tests/unit/Grav/Framework/File/Formatter/CsvFormatterTest.php new file mode 100644 index 0000000..70a8c9e --- /dev/null +++ b/tests/unit/Grav/Framework/File/Formatter/CsvFormatterTest.php @@ -0,0 +1,47 @@ + 1, 'col2' => 2, 'col3' => 3], + ['col1' => 'aaa', 'col2' => 'bbb', 'col3' => 'ccc'], + ]; + + $encoded = (new CsvFormatter())->encode($data); + + $lines = array_filter(explode(PHP_EOL, $encoded)); + + self::assertCount(3, $lines); + self::assertEquals('col1,col2,col3', $lines[0]); + + } + + /** + * TBD - If indexes are all numeric, what's the purpose + * of displaying header + */ + public function testEncodeWithIndexColumns() + { + $data = [ + [0 => 1, 1 => 2, 2 => 3], + ]; + + $encoded = (new CsvFormatter())->encode($data); + + $lines = array_filter(explode(PHP_EOL, $encoded)); + + self::assertCount(2, $lines); + self::assertEquals('0,1,2', $lines[0]); + } + + public function testEncodeEmptyData() + { + $encoded = (new CsvFormatter())->encode([]); + self::assertEquals('', $encoded); + } + +} diff --git a/tests/unit/Grav/Framework/Filesystem/FilesystemTest.php b/tests/unit/Grav/Framework/Filesystem/FilesystemTest.php new file mode 100644 index 0000000..132aeb9 --- /dev/null +++ b/tests/unit/Grav/Framework/Filesystem/FilesystemTest.php @@ -0,0 +1,322 @@ + [ + 'parent' => '', + 'normalize' => '', + 'dirname' => '', + 'pathinfo' => [ + 'basename' => '', + 'filename' => '', + ] + ], + '.' => [ + 'parent' => '', + 'normalize' => '', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => '.', + 'extension' => '', + 'filename' => '', + ] + ], + './' => [ + 'parent' => '', + 'normalize' => '', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => '.', + 'extension' => '', + 'filename' => '', + + ] + ], + '././.' => [ + 'parent' => '', + 'normalize' => '', + 'dirname' => './.', + 'pathinfo' => [ + 'dirname' => './.', + 'basename' => '.', + 'extension' => '', + 'filename' => '', + ] + ], + '.file' => [ + 'parent' => '.', + 'normalize' => '.file', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => '.file', + 'extension' => 'file', + 'filename' => '', + ] + ], + '/' => [ + 'parent' => '', + 'normalize' => '/', + 'dirname' => '/', + 'pathinfo' => [ + 'dirname' => '/', + 'basename' => '', + 'filename' => '', + ] + ], + '/absolute' => [ + 'parent' => '/', + 'normalize' => '/absolute', + 'dirname' => '/', + 'pathinfo' => [ + 'dirname' => '/', + 'basename' => 'absolute', + 'filename' => 'absolute', + ] + ], + '/absolute/' => [ + 'parent' => '/', + 'normalize' => '/absolute', + 'dirname' => '/', + 'pathinfo' => [ + 'dirname' => '/', + 'basename' => 'absolute', + 'filename' => 'absolute', + ] + ], + '/very/long/absolute/path' => [ + 'parent' => '/very/long/absolute', + 'normalize' => '/very/long/absolute/path', + 'dirname' => '/very/long/absolute', + 'pathinfo' => [ + 'dirname' => '/very/long/absolute', + 'basename' => 'path', + 'filename' => 'path', + ] + ], + 'relative' => [ + 'parent' => '.', + 'normalize' => 'relative', + 'dirname' => '.', + 'pathinfo' => [ + 'dirname' => '.', + 'basename' => 'relative', + 'filename' => 'relative', + ] + ], + 'very/long/relative/path' => [ + 'parent' => 'very/long/relative', + 'normalize' => 'very/long/relative/path', + 'dirname' => 'very/long/relative', + 'pathinfo' => [ + 'dirname' => 'very/long/relative', + 'basename' => 'path', + 'filename' => 'path', + ] + ], + 'path/to/file.jpg' => [ + 'parent' => 'path/to', + 'normalize' => 'path/to/file.jpg', + 'dirname' => 'path/to', + 'pathinfo' => [ + 'dirname' => 'path/to', + 'basename' => 'file.jpg', + 'extension' => 'jpg', + 'filename' => 'file', + ] + ], + 'user://' => [ + 'parent' => '', + 'normalize' => 'user://', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user://.' => [ + 'parent' => '', + 'normalize' => 'user://', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user://././.' => [ + 'parent' => '', + 'normalize' => 'user://', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user://./././file' => [ + 'parent' => 'user://', + 'normalize' => 'user://file', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => 'file', + 'filename' => 'file', + 'scheme' => 'user', + ] + ], + 'user://./././folder/file' => [ + 'parent' => 'user://folder', + 'normalize' => 'user://folder/file', + 'dirname' => 'user://folder', + 'pathinfo' => [ + 'dirname' => 'user://folder', + 'basename' => 'file', + 'filename' => 'file', + 'scheme' => 'user', + ] + ], + 'user://.file' => [ + 'parent' => 'user://', + 'normalize' => 'user://.file', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => '.file', + 'extension' => 'file', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user:///' => [ + 'parent' => '', + 'normalize' => 'user:///', + 'dirname' => 'user:///', + 'pathinfo' => [ + 'dirname' => 'user:///', + 'basename' => '', + 'filename' => '', + 'scheme' => 'user', + ] + ], + 'user:///absolute' => [ + 'parent' => 'user:///', + 'normalize' => 'user:///absolute', + 'dirname' => 'user:///', + 'pathinfo' => [ + 'dirname' => 'user:///', + 'basename' => 'absolute', + 'filename' => 'absolute', + 'scheme' => 'user', + ] + ], + 'user:///very/long/absolute/path' => [ + 'parent' => 'user:///very/long/absolute', + 'normalize' => 'user:///very/long/absolute/path', + 'dirname' => 'user:///very/long/absolute', + 'pathinfo' => [ + 'dirname' => 'user:///very/long/absolute', + 'basename' => 'path', + 'filename' => 'path', + 'scheme' => 'user', + ] + ], + 'user://relative' => [ + 'parent' => 'user://', + 'normalize' => 'user://relative', + 'dirname' => 'user://', + 'pathinfo' => [ + 'dirname' => 'user://', + 'basename' => 'relative', + 'filename' => 'relative', + 'scheme' => 'user', + ] + ], + 'user://very/long/relative/path' => [ + 'parent' => 'user://very/long/relative', + 'normalize' => 'user://very/long/relative/path', + 'dirname' => 'user://very/long/relative', + 'pathinfo' => [ + 'dirname' => 'user://very/long/relative', + 'basename' => 'path', + 'filename' => 'path', + 'scheme' => 'user', + ] + ], + 'user://path/to/file.jpg' => [ + 'parent' => 'user://path/to', + 'normalize' => 'user://path/to/file.jpg', + 'dirname' => 'user://path/to', + 'pathinfo' => [ + 'dirname' => 'user://path/to', + 'basename' => 'file.jpg', + 'extension' => 'jpg', + 'filename' => 'file', + 'scheme' => 'user', + ] + ], + ]; + + protected function _before() + { + $this->class = Filesystem::getInstance(); + } + + protected function _after() + { + unset($this->class); + } + + protected function runTestSet(array $tests, $method) + { + $class = $this->class; + foreach ($tests as $path => $candidates) { + if (!array_key_exists($method, $candidates)) { + continue; + } + + $expected = $candidates[$method]; + + $result = $class->{$method}($path); + + $this->assertSame($expected, $result, "Test {$method}('{$path}')"); + + if (function_exists($method) && !strpos($path, '://')) { + $cmp_result = $method($path); + + $this->assertSame($cmp_result, $result, "Compare to original {$method}('{$path}')"); + } + } + } + + public function testParent() + { + $this->runTestSet($this->tests, 'parent'); + } + + public function testNormalize() + { + $this->runTestSet($this->tests, 'normalize'); + } + + public function testDirname() + { + $this->runTestSet($this->tests, 'dirname'); + } + + public function testPathinfo() + { + $this->runTestSet($this->tests, 'pathinfo'); + } +} diff --git a/user/themes/figureslibres/js/script_k.js b/user/config/backups.yaml similarity index 100% rename from user/themes/figureslibres/js/script_k.js rename to user/config/backups.yaml diff --git a/user/config/plugins/admin.yaml b/user/config/plugins/admin.yaml deleted file mode 100644 index 01acf61..0000000 --- a/user/config/plugins/admin.yaml +++ /dev/null @@ -1,54 +0,0 @@ -enabled: true -route: /admin -cache_enabled: false -theme: grav -content_padding: true -twofa_enabled: true -sidebar: - activate: tab - hover_delay: 100 - size: auto -dashboard: - days_of_stats: 7 -widgets: - dashboard-maintenance: true - dashboard-statistics: true - dashboard-notifications: true - dashboard-feed: true - dashboard-pages: true -pages: - show_parents: both - show_modular: true -session: - timeout: 1800 -warnings: - delete_page: true -edit_mode: normal -frontend_preview_target: inline -show_github_msg: true -pages_list_display_field: title -google_fonts: false -admin_icons: line-awesome -enable_auto_updates_check: true -notifications: - feed: true - dashboard: true - plugins: true - themes: true -popularity: - enabled: true - ignore: - - '/test*' - - /modular - history: - daily: '30' - monthly: '12' - visitors: '20' -pagemedia: - resize_width: 0 - resize_height: 0 - res_min_width: 0 - res_min_height: 0 - res_max_width: 0 - res_max_height: 0 - resize_quality: 0.8 diff --git a/user/config/plugins/piwik.yaml b/user/config/plugins/piwik.yaml deleted file mode 100644 index 0d01882..0000000 --- a/user/config/plugins/piwik.yaml +++ /dev/null @@ -1,3 +0,0 @@ -enabled: true -siteId: '0' -sitePiWikURL: dev.figureslibres.net diff --git a/user/config/scheduler.yaml b/user/config/scheduler.yaml new file mode 100644 index 0000000..e69de29 diff --git a/user/config/site.yaml b/user/config/site.yaml index 6312d4b..4be78ed 100644 --- a/user/config/site.yaml +++ b/user/config/site.yaml @@ -1,17 +1,7 @@ -title: 'Figures Libres' -default_lang: fr +title: Grav author: - name: Figli - email: bonjour@figureslibres.io -taxonomies: - - category - - tag + name: Joe Bloggs + email: 'joe@example.com' metadata: - description: 'Grav is an easy to use, yet powerful, open source flat-file CMS' -summary: - enabled: true - format: short - size: 300 - delimiter: '===' -blog: - route: /blog + description: 'Grav is an easy to use, yet powerful, open source flat-file CMS' + diff --git a/user/config/system.yaml b/user/config/system.yaml index c745680..1b6c188 100644 --- a/user/config/system.yaml +++ b/user/config/system.yaml @@ -12,7 +12,9 @@ pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' intl_enabled: true languages: supported: { } + default_lang: null include_default_lang: true + pages_fallback_only: false translations: true translations_fallback: true session_store_active: false @@ -50,10 +52,10 @@ pages: '>': gt '<': lt types: - - txt - - xml - html - htm + - xml + - txt - json - rss - atom @@ -72,6 +74,7 @@ pages: - .git - .idea ignore_hidden: true + hide_empty_folders: false url_taxonomy_filters: true frontmatter: process_twig: false @@ -84,6 +87,9 @@ cache: method: file driver: auto prefix: g + purge_at: '0 4 * * *' + clear_at: '0 3 * * *' + clear_job_type: standard clear_images_by_default: true cli_compatibility: false lifetime: 604800 @@ -116,6 +122,10 @@ assets: errors: display: true log: true +log: + handler: file + syslog: + facility: local6 debugger: enabled: false shutdown: @@ -127,6 +137,7 @@ images: cache_perms: '0755' debug: false auto_fix_orientation: false + seofriendly: false media: enable_media_timestamp: false unsupported_inline_types: { } @@ -138,6 +149,7 @@ session: initialize: true timeout: 1800 name: grav-site + uniqueness: path secure: false httponly: true split: true @@ -148,6 +160,9 @@ gpm: method: auto verify_peer: true official_gpm_only: true +accounts: + type: data + storage: file strict_mode: yaml_compat: true twig_compat: true diff --git a/user/pages/01.home/default.md b/user/pages/01.home/default.md index a3019e2..ef0069d 100644 --- a/user/pages/01.home/default.md +++ b/user/pages/01.home/default.md @@ -4,3 +4,4 @@ body_classes: 'title-center title-h1h2' --- **Figures Libres** est un collectif né de la rencontre de deux démarches citoyennes : porter des messages d'utilité [publique](/projets/publique?id=publique), [sociale](/projets/sociale?id=sociale) et [culturelle](/projets/culturelle?id=culturelle) ; Œuvrer sur logiciels libres pour créer sans consommer et libérer l'information. Nous sommes graphistes, développeurs, designers, photographes, artistes, enseignant⋅e⋅s, militant⋅e⋅s… et croisons nos pratiques et compétences pour avancer plus loin à plusieurs. Convaincu⋅e⋅s que la création est un outil démocratique, nous partageons les convictions de nos commanditaires. Nous préférons maîtriser nos outils, plutôt que l'inverse, les adapter à nos envies et à vos besoins. Comme nous sommes joueur⋅euse⋅s et utopistes nous aimons les construire avec vous et les partager avec tous. Votre projet sera un site, une affiche, une campagne, une application, un livre, une exposition, une manifestation… tant mieux ! nous n'aimons pas les réponses toutes faites ; mais bien faites. + \ No newline at end of file diff --git a/user/pages/02.projets/culturelle/tous-des-sang-meles/projet.md b/user/pages/02.projets/culturelle/tous-des-sang-meles/projet.md index 417a1d0..b991bdc 100644 --- a/user/pages/02.projets/culturelle/tous-des-sang-meles/projet.md +++ b/user/pages/02.projets/culturelle/tous-des-sang-meles/projet.md @@ -9,8 +9,6 @@ taxonomy: - 'MAC VAL' --- -Catalogue d’exposition - […] Nous sommes tous des passants, des migrants, des métis, des hybrides, des étrangers, des constructions, des êtres en relation. Tous, des sang-mêlés. L’ouvrage met en lumière l’exposition à travers cinq carnets de tailles et de couleurs différentes, dont les titrages mélangent plusieurs polices de caractères. La diversité des formats et des couleurs exprime la notion de différence, de singularité et de multi culturalité, sujets transversaux de l’exposition. Cet objet modulable, multipliant les approches, génère plusieurs lectures différentes. Une expérience éditoriale menée par Maud Boyer pour le design, Bachir Soussi-Chiadmi et Kevin Tessier pour le développement du HTML2Print diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3084.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3084.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3084.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3084.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3102.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3102.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3102.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3102.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3109.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3109.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3109.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3109.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3115.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3115.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3115.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3115.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3122.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3122.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3122.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3122.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3124.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3124.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3124.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3124.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3126.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3126.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3126.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3126.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3128.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3128.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3128.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3128.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3133.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3133.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3133.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3133.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3151.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3151.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3151.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3151.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3159.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3159.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3159.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3159.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3166.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3166.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3166.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3166.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3170.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3170.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3170.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3170.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3178.jpg b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3178.jpg similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/OSC_3178.jpg rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/OSC_3178.jpg diff --git a/user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/projet.md b/user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/projet.md old mode 100755 new mode 100644 similarity index 100% rename from user/pages/02.projets/publique/programme-popsu-plate-forme-d'observation-des-projets-et-strategies-urbaines/projet.md rename to user/pages/02.projets/publique/programme-popsu-plate-forme-dobservation-des-projets-et-strategies-urbaines/projet.md diff --git a/user/themes/figureslibres/css-compiled/theme.css b/user/themes/figureslibres/css-compiled/theme.css index 36695c8..020b469 100644 --- a/user/themes/figureslibres/css-compiled/theme.css +++ b/user/themes/figureslibres/css-compiled/theme.css @@ -139,13 +139,23 @@ header a { font-size: 2.5rem; line-height: 3rem; } .home #text_figli a#culturelle { font-family: "Avara"; } -#item .content-title { display: -ms-flexbox; display: flex; margin: 0.5rem 0; } +.home #text_figli #item .e-content p { font-size: 1rem; line-height: 1.2rem; } -#item .content-title > * { margin-right: 0.5rem; font-size: 1.15rem; font-weight: bold; } +#item h2 { width: auto; line-height: 4rem !important; font-size: 4rem !important; font-weight: normal !important; } -#item .content-title h2 { margin: 0 0.5rem 0 0; } +#item span { font-size: 1rem; } -#item .content-title h2 a { font-weight: bold; } +#item.publique h2 { font-family: "playfairdisplay-regularitalic"; } + +#item.publique span { font-family: "playfairdisplay-regularitalic"; font-weight: normal; } + +#item.sociale h2 { font-family: "syne"; } + +#item.sociale span { font-family: "syne"; font-weight: normal; } + +#item.culturelle h2 { font-family: "Avara"; } + +#item.culturelle span { font-family: "Avara"; font-weight: normal; } a.publique, span.publique { font-family: "playfairdisplay-regularitalic"; } @@ -153,12 +163,38 @@ a.sociale, span.sociale { font-family: "syne"; } a.culturelle, span.culturelle { font-family: "Avara"; } -.sidebar-right ul, .sidebar-right a { font-size: 0.8rem; line-height: 1.25rem; } +.sidebar-right ul, .sidebar-right a { font-size: 0.7rem; line-height: 0.9rem; } .other_projets h3 { font-size: 2.5rem; margin: 0; } .card h2 { margin: 0; font-size: 1rem; } +#text_figli { z-index: 999; } + +#text_figli p > a { z-index: 999; } + +#text_figli p > a:before, #text_figli p > a:after { content: ""; position: absolute; bottom: -1px; width: 0px; height: 1px; margin: 1px 0 0; transition: all 0.2s ease-in-out; transition-duration: 0.3s; opacity: 0; background-color: black; } + +#text_figli p > a::before { left: 50%; } + +#text_figli p > a:after { right: 50%; } + +#text_figli p > a:hover { cursor: pointer; } + +#text_figli p > a:hover:before, #text_figli p > a:hover:after { width: 100%; opacity: 1; } + +#text_figli p > a:hover:before, #text_figli p > a:hover:after { width: 50%; } + +.circle_hover { position: absolute; width: 0px; height: 0px; background: lightblue; border-radius: 500px; transform: translate(-50%, -50%); z-index: 0; transition: height .2s, width .2s; } + +.circle_hover.open { width: 200px; height: 200px; transition: height .5s, width .5s; } + +.circle_hover.publique { background: lightyellow; } + +.circle_hover.sociale { background: lightgreen; } + +.circle_hover.culturelle { background: lightblue; } + header { position: fixed; top: 0; left: 0; margin: 0 0 0 0.5rem; padding: 0.5rem 0; width: calc( 80% - 0.5rem); border-bottom: 1px solid black; } * { box-sizing: border-box; } @@ -167,37 +203,55 @@ html, body, header { background: #f9f9f9fe; } body #start { width: calc(( 100% / 2 ) - (1rem / 2)); margin: 0.5rem auto auto 0.5rem; } -body #start #text_figli p a { position: relative; } - body #start #text_figli p a.marg { margin-right: 0.5rem; } -body #start #text_figli #item_list { width: 80vw; } +body #start #text_figli #item_list { width: 80vw; margin-top: 0.5rem; } -body #start #text_figli #item_list .card { width: calc((100% / 5) - ( 0.5rem * 2)); height: auto; display: inline-block; vertical-align: top; line-height: 0; margin-right: 0.5rem; position: relative; margin-top: 0.5rem; } +body #start #text_figli #item_list .card { width: calc((100% / 5) - ( 0.5rem * 2)); display: inline-block; vertical-align: top; position: relative; margin: 0 0.5rem 0.5rem 0; height: 100%; } -body #start #text_figli #item_list .card a { display: block; } +body #start #text_figli #item_list .card > .card-image.open a img { filter: grayscale(0); transition: filter 0.3s ease; } -body #start #text_figli #item_list .card .card-header { display: none; } +body #start #text_figli #item_list .card a { display: block; border-radius: 15px; overflow: auto; } + +body #start #text_figli #item_list .card a img { filter: grayscale(1); transition: filter 0.3s ease; } + +body #start #text_figli #item_list .card .card-header { width: 100%; padding: 0.5rem; -ms-flex-wrap: wrap; flex-wrap: wrap; display: none; } body #start #text_figli #item_list .card .card-header > * { font-size: 0.8rem; line-height: 1rem; margin-right: 0.5rem; } -body #start #text_figli #item_list .card:hover .card-header { width: 100%; background: rgba(255, 255, 255, 0.5); padding: 0.5rem; display: -ms-inline-flexbox; display: inline-flex; -ms-flex-wrap: wrap; flex-wrap: wrap; position: absolute; transform: translateY(-100%); } +body #start #text_figli #item_list .card:hover a { display: block; } + +body #start #text_figli #item_list .card:hover a img { filter: grayscale(0); transition: filter 0.3s ease; } + +body #start #text_figli #item_list .card:hover .card-header { display: -ms-inline-flexbox; display: inline-flex; position: absolute; transform: translateY(-100%); } body #start #text_figli #item_list .card:last-of-type { vertical-align: baseline !important; } -body #start #text_figli #item_list .card #item { width: calc( 307% - 0.5rem); } +body #start #text_figli #item_list .card #item { width: calc( 500% + ( 0.5rem * 5)); padding: 2rem; margin-top: 0.5rem; border-radius: 15px; } -body #start #text_figli #item_list .card #item .content-title { margin: 30px auto; } +body #start #text_figli #item_list .card #item .content-title { margin: 0 auto 0 auto; display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; -ms-flex-pack: justify; justify-content: space-between; -ms-flex-align: baseline; align-items: baseline; } body #start #text_figli #item_list .card #item .content-title a { display: block; } -body #start #text_figli #item_list .card #item .e-content { margin: 30px auto; } +body #start #text_figli #item_list .card #item .e-content { margin: 1rem auto 2rem auto; } -body #start #text_figli #item_list .card #item .e-content p { font-size: 1rem; line-height: 1rem; } +body #start #text_figli #item_list .card #item .e-content p { font-size: 1rem; line-height: 1.2rem; } + +body #start #text_figli #item_list .card #item.publique { background: lightyellow; } + +body #start #text_figli #item_list .card #item.sociale { background: lightgreen; } + +body #start #text_figli #item_list .card #item.culturelle { background: lightblue; } + +body #start #text_figli #item_list.publique .card:hover .card-header { background: lightyellow; } + +body #start #text_figli #item_list.sociale .card:hover .card-header { background: lightgreen; } + +body #start #text_figli #item_list.culturelle .card:hover .card-header { background: lightblue; } body:not(.home) #start { margin-top: calc( 2rem + 3rem); } -.sidebar-right { position: fixed; width: 20%; height: 100%; right: 0; top: 0; display: -ms-inline-flexbox; display: inline-flex; } +.sidebar-right { position: fixed; width: 15%; height: 100%; right: 0; top: 0; display: -ms-inline-flexbox; display: inline-flex; } .sidebar-right .list-projets { margin: auto; } @@ -209,6 +263,16 @@ body:not(.home) #start { margin-top: calc( 2rem + 3rem); } .sidebar-right .list-projets ul a { color: grey; } +.sidebar-right .list-projets ul a::before { content: " "; width: 5px; height: 5px; display: inline-block; border-radius: 5px; vertical-align: middle; margin-right: 5px; } + +.sidebar-right .list-projets ul a#culturelle::before { background: lightblue; } + +.sidebar-right .list-projets ul a#sociale::before { background: lightgreen; } + +.sidebar-right .list-projets ul a#publique::before { background: lightyellow; } + +.sidebar-right .list-projets ul a.active { color: black; } + #item_list { display: -ms-inline-flexbox; display: inline-flex; -ms-flex-wrap: wrap; flex-wrap: wrap; width: 100%; } #item_list .card { width: calc(( 100% / 2 ) - (1rem / 2)); } @@ -221,7 +285,7 @@ body:not(.home) #start { margin-top: calc( 2rem + 3rem); } #item .e-content { columns: 300px 2; margin-bottom: 0.5rem; } -#item .card-image img { margin-bottom: 0.5rem; } +#item .card-image img:not(:last-of-type) { margin-bottom: 0.5rem; } .other_projets { border-top: 1px solid black; display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; margin-top: 1rem; margin-bottom: 1rem; } @@ -235,14 +299,17 @@ body:not(.home) #start { margin-top: calc( 2rem + 3rem); } .other_projets .card .card-header > * { margin-right: 0.5rem; } -@media screen and (max-width: 414px) { header a { font-size: 1.5rem; line-height: 2rem; } +@media screen and (max-width: 414px) { body #start { width: 100%; } + body #start #text_figli #item_list .card { width: 100%; } + body #start #text_figli #item_list .card #item { width: calc(100% + (.5rem * 3)); } + header a { font-size: 1.5rem; line-height: 2rem; } .home p { font-size: 1.5rem !important; line-height: 2rem !important; } .other_projets h3 { font-size: 1.5rem; } header { width: calc( 100% - (0.5rem * 2)); } body #start { width: calc( 100% - (0.5rem * 2)); } body:not(.home) #start { margin-top: 3rem; } body .sidebar-right { display: none; } - #item_list .card { width: calc( 100%); } + #item_list .card { width: calc( 100%) !important; } #item_list .card:nth-child(odd) { margin-right: 0; } #item_list .card .card-header { margin-bottom: 0.5rem; } #item_list .card .card-header > * { margin-right: 0.5rem; } @@ -250,8 +317,10 @@ body:not(.home) #start { margin-top: calc( 2rem + 3rem); } .other_projets .card { width: 100%; } .other_projets .card:nth-child(-1n+3) { margin-right: 0; } } -@media screen and (max-width: 768px) and (orientation: portrait) { header a { font-size: 2.5rem; line-height: 3rem; } - .home p { font-size: 2.5rem !important; line-height: 3rem !important; } +@media screen and (max-width: 768px) and (orientation: portrait) { body #start { width: calc(( 80% )); } + body #start #text_figli #item_list .card { width: calc(( 100% / 3 ) - (1rem / 2)); } + body #start #text_figli #item_list .card #item { width: calc(300% + (.5rem * 3)); } + header a { font-size: 2.5rem; line-height: 3rem; } .other_projets h3 { font-size: 2.5rem; } body:not(.home) #start { margin-top: 5rem; } #item_list .card { width: calc( 100%); } @@ -281,4 +350,8 @@ body:not(.home) #start { margin-top: calc( 2rem + 3rem); } .other_projets h3 { font-size: 1.5rem; } body:not(.home) #start { margin-top: 3rem; } } -/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGhlbWUuY3NzIiwic291cmNlcyI6WyJ0aGVtZS5zY3NzIiwiY29uZmlndXJhdGlvbnMvX2ZvbnRzLnNjc3MiLCJjb25maWd1cmF0aW9ucy9fdmFyaWFibGUuc2NzcyIsImNvbmZpZ3VyYXRpb25zL19leHRlbmQuc2NzcyIsInRoZW1lL19yZXNldC5zY3NzIiwidGhlbWUvX3R5cG9ncmFwaHkuc2NzcyIsInRoZW1lL19oZWFkZXIuc2NzcyIsInRoZW1lL19zdGFydC5zY3NzIiwidGhlbWUvX3NpZGViYXIuc2NzcyIsInRoZW1lL19ibG9nLnNjc3MiLCJ0aGVtZS9faXRlbS5zY3NzIiwidGhlbWUvX21vYmlsZS5zY3NzIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIENvcmUgdmFyaWFibGVzIGFuZCBtaXhpbnNcbkBpbXBvcnQgJ2NvbmZpZ3VyYXRpb25zL19mb250cyc7XG5AaW1wb3J0ICdjb25maWd1cmF0aW9ucy9fdmFyaWFibGUnO1xuQGltcG9ydCAnY29uZmlndXJhdGlvbnMvX2V4dGVuZCc7XG5AaW1wb3J0ICd0aGVtZS9yZXNldCc7XG5AaW1wb3J0ICd0aGVtZS90eXBvZ3JhcGh5JztcblxuQGltcG9ydCAndGhlbWUvaGVhZGVyJztcbkBpbXBvcnQgJ3RoZW1lL3N0YXJ0JztcbkBpbXBvcnQgJ3RoZW1lL3NpZGViYXInO1xuQGltcG9ydCAndGhlbWUvYmxvZyc7XG5AaW1wb3J0ICd0aGVtZS9pdGVtJztcbkBpbXBvcnQgJ3RoZW1lL21vYmlsZSc7IFxuIiwiLy8gRm9udCBGYW1pbHlcbiRmb250LWZhbWlseS1kZWZhdWx0OiAgXHRcdFx0XCJSYWxld2F5XCIsIFwiSGVsdmV0aWNhXCIsIFwiVGFob21hXCIsIFwiR2VuZXZhXCIsIFwiQXJpYWxcIiwgc2Fucy1zZXJpZjtcbiRmb250LWZhbWlseS1oZWFkZXI6ICBcdFx0XHRcIk1vbnRzZXJyYXRcIiwgXCJIZWx2ZXRpY2FcIiwgXCJUYWhvbWFcIiwgXCJHZW5ldmFcIiwgXCJBcmlhbFwiLCBzYW5zLXNlcmlmO1xuJGZvbnQtZmFtaWx5LW1vbm86IFx0XHRcdFx0XCJJbmNvbnNvbGF0YVwiLCBtb25vc3BhY2U7XG4kZm9udC1mYW1pbHktc2VyaWY6XHRcdFx0XHRcIkdlb3JnaWFcIiwgXCJUaW1lc1wiLCBcIlRpbWVzIE5ldyBSb21hblwiLCBzZXJpZjtcbi8vIEBmb250LWZhY2VcblxuJGxhdG86IFwibGF0b1wiO1xuJGZhbWlseTogXCJsYXRvXCI7XG4kZmlsZS1yZWd1bGFyOiBcIkxhdG8tUmVndWxhclwiO1xuJGNhdGVnb3J5OiBcInNhbnMtc2VyaWZcIjtcblxuQG1peGluIGZvbnQtZmFjZSgkbGF0bywgJGZpbGUtcmVndWxhciwgJGZhbWlseSwgJGNhdGVnb3J5OlwiXCIpIHtcbiAgICAkZmlsZXBhdGg6IFwiLi4vZm9udHMvXCIgKyAkZmFtaWx5ICsgXCIvXCIgKyAkZmlsZS1yZWd1bGFyO1xuICAgIEBmb250LWZhY2Uge1xuICAgICAgICBmb250LWZhbWlseTogXCIjeyRsYXRvfVwiO1xuICAgICAgICBzcmM6IHVybCgkZmlsZXBhdGggKyBcIi5lb3RcIik7XG4gICAgICAgIHNyYzogdXJsKCRmaWxlcGF0aCArIFwiLmVvdD8jaWVmaXhcIikgZm9ybWF0KCdlbWJlZGRlZC1vcGVudHlwZScpLFxuICAgICAgICAgICAgICB1cmwoJGZpbGVwYXRoICsgXCIud29mZlwiKSBmb3JtYXQoJ3dvZmYnKSxcbiAgICAgICAgICAgICAgdXJsKCRmaWxlcGF0aCArIFwiLnR0ZlwiKSAgZm9ybWF0KCd0cnVldHlwZScpLFxuICAgICAgICAgICAgICB1cmwoJGZpbGVwYXRoICsgXCIuc3ZnI1wiICsgJGxhdG8gKyBcIlwiKSBmb3JtYXQoJ3N2ZycpO1xuICAgIH1cblxuICAgICUjeyRsYXRvfSB7XG4gICAgICAgIGZvbnQ6IHtcbiAgICAgICAgICAgIEBpZiAkY2F0ZWdvcnkgIT0gXCJcIiB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JGxhdG99XCIsICN7JGNhdGVnb3J5fTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIEBlbHNlIHtcbiAgICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skbGF0b31cIjtcbiAgICAgICAgICAgICAgICB3ZWlnaHQ6IG5vcm1hbDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbn1cblxuJHN5bmU6IFwic3luZVwiO1xuJGZhbWlseS1ib2xkOiBcInN5bmVcIjtcbiRmaWxlLWJvbGQ6IFwiU3luZS1Cb2xkXCI7XG5cbkBtaXhpbiBmb250LWZhY2UoJHN5bmUsICRmaWxlLWJvbGQsICRmYW1pbHktYm9sZCwgJGNhdGVnb3J5OlwiXCIpIHtcbiAgICAkZmlsZXBhdGg6IFwiLi4vZm9udHMvXCIgKyAkZmFtaWx5LWJvbGQgKyBcIi9cIiArICRmaWxlLWJvbGQ7XG4gICAgQGZvbnQtZmFjZSB7XG4gICAgICAgIGZvbnQtZmFtaWx5OiBcIiN7JHN5bmV9XCI7XG4gICAgICAgIHNyYzogdXJsKCRmaWxlcGF0aCArIFwiLmVvdFwiKTtcbiAgICAgICAgc3JjOiB1cmwoJGZpbGVwYXRoICsgXCIuZW90PyNpZWZpeFwiKSBmb3JtYXQoJ2VtYmVkZGVkLW9wZW50eXBlJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi53b2ZmXCIpIGZvcm1hdCgnd29mZicpLFxuICAgIH1cblxuICAgICUjeyRzeW5lfSB7XG4gICAgICAgIGZvbnQ6IHtcbiAgICAgICAgICAgIEBpZiAkY2F0ZWdvcnkgIT0gXCJcIiB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JHN5bmV9XCIsICN7JGNhdGVnb3J5fTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIEBlbHNlIHtcbiAgICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skc3luZX1cIjtcbiAgICAgICAgICAgICAgICB3ZWlnaHQ6IG5vcm1hbDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbn1cblxuJGF2YXJhOiBcIkF2YXJhXCI7XG4kZmFtaWx5LWJvbGQtaXRhbGljOiBcImF2YXJhXCI7XG4kZmlsZS1ib2xkLWl0YWxpYzogXCJBdmFyYS1Cb2xkX0l0YWxpY193ZWJcIjtcblxuQG1peGluIGZvbnQtZmFjZSgkYXZhcmEsICRmaWxlLWJvbGQtaXRhbGljLCAkZmFtaWx5LWJvbGQtaXRhbGljLCAkY2F0ZWdvcnk6XCJcIikge1xuICAgICRmaWxlcGF0aDogXCIuLi9mb250cy9cIiArICRmYW1pbHktYm9sZC1pdGFsaWMgKyBcIi9cIiArICRmaWxlLWJvbGQtaXRhbGljO1xuICAgIEBmb250LWZhY2Uge1xuICAgICAgICBmb250LWZhbWlseTogXCIjeyRhdmFyYX1cIjtcbiAgICAgICAgc3JjOiB1cmwoJGZpbGVwYXRoICsgXCIuZW90XCIpO1xuICAgICAgICBzcmM6IHVybCgkZmlsZXBhdGggKyBcIi5lb3Q/I2llZml4XCIpIGZvcm1hdCgnZW1iZWRkZWQtb3BlbnR5cGUnKSxcbiAgICAgICAgICAgICAgdXJsKCRmaWxlcGF0aCArIFwiLndvZmZcIikgZm9ybWF0KCd3b2ZmJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi50dGZcIikgIGZvcm1hdCgndHJ1ZXR5cGUnKSxcbiAgICAgICAgICAgICAgdXJsKCRmaWxlcGF0aCArIFwiLnN2ZyNcIiArICRhdmFyYSArIFwiXCIpIGZvcm1hdCgnc3ZnJyk7XG4gICAgfVxuXG4gICAgJSN7JGF2YXJhfSB7XG4gICAgICAgIGZvbnQ6IHtcbiAgICAgICAgICAgIEBpZiAkY2F0ZWdvcnkgIT0gXCJcIiB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JGF2YXJhfVwiLCAjeyRjYXRlZ29yeX07XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBAZWxzZSB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JGF2YXJhfVwiO1xuICAgICAgICAgICAgICAgIHdlaWdodDogbm9ybWFsO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfVxufVxuXG4kcGxheWZhaXI6IFwicGxheWZhaXJkaXNwbGF5LXJlZ3VsYXJpdGFsaWNcIjtcbiRmYW1pbHktaXRhbGljOiBcInBsYXlmYWlyXCI7XG4kZmlsZS1pdGFsaWM6IFwicGxheWZhaXJkaXNwbGF5LXJlZ3VsYXJpdGFsaWNcIjtcblxuQG1peGluIGZvbnQtZmFjZSgkcGxheWZhaXIsICRmaWxlLWl0YWxpYywgJGZhbWlseS1pdGFsaWMsICRjYXRlZ29yeTpcIlwiKSB7XG4gICAgJGZpbGVwYXRoOiBcIi4uL2ZvbnRzL1wiICsgJGZhbWlseS1pdGFsaWMgKyBcIi9cIiArICRmaWxlLWl0YWxpYztcbiAgICBAZm9udC1mYWNlIHtcbiAgICAgICAgZm9udC1mYW1pbHk6IFwiI3skcGxheWZhaXJ9XCI7XG4gICAgICAgIHNyYzogdXJsKCRmaWxlcGF0aCArIFwiLmVvdFwiKTtcbiAgICAgICAgc3JjOiB1cmwoJGZpbGVwYXRoICsgXCIuZW90PyNpZWZpeFwiKSBmb3JtYXQoJ2VtYmVkZGVkLW9wZW50eXBlJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi53b2ZmXCIpIGZvcm1hdCgnd29mZicpLFxuICAgICAgICAgICAgICB1cmwoJGZpbGVwYXRoICsgXCIudHRmXCIpICBmb3JtYXQoJ3RydWV0eXBlJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi5zdmcjXCIgKyAkcGxheWZhaXIgKyBcIlwiKSBmb3JtYXQoJ3N2ZycpO1xuICAgIH1cblxuICAgICUjeyRwbGF5ZmFpcn0ge1xuICAgICAgICBmb250OiB7XG4gICAgICAgICAgICBAaWYgJGNhdGVnb3J5ICE9IFwiXCIge1xuICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skcGxheWZhaXJ9XCIsICN7JGNhdGVnb3J5fTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIEBlbHNlIHtcbiAgICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skcGxheWZhaXJ9XCI7XG4gICAgICAgICAgICAgICAgd2VpZ2h0OiBub3JtYWw7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG59XG4iLCJAaW5jbHVkZSBmb250LWZhY2UoJGxhdG8sICRmaWxlLXJlZ3VsYXIsICRmYW1pbHksICRjYXRlZ29yeSk7Ly8xXG5AaW5jbHVkZSBmb250LWZhY2UoJHN5bmUsICRmaWxlLWJvbGQsICRmYW1pbHktYm9sZCwgJGNhdGVnb3J5KTsvLzJcbkBpbmNsdWRlIGZvbnQtZmFjZSgkYXZhcmEsICRmaWxlLWJvbGQtaXRhbGljLCAkZmFtaWx5LWJvbGQtaXRhbGljLCAkY2F0ZWdvcnkpOy8vMlxuQGluY2x1ZGUgZm9udC1mYWNlKCRwbGF5ZmFpciwgJGZpbGUtaXRhbGljLCAkZmFtaWx5LWl0YWxpYywgJGNhdGVnb3J5KTsvLzJcblxuLy8gZm9udC1zaXplXG4kdHh0LWZpZ2xpOiAyLjVyZW07XG5cbi8vIGxpbmUtaGVpZ2h0XG4kRmxob21lOiAzcmVtO1xuIFxuJG1hcmc6IDAuNXJlbTtcbiRtYXJnSTogMXJlbTtcbiRXaTogODAlO1xuXG4kV2k1MDogY2FsYygoIDEwMCUgLyAyICkgLSAoI3skbWFyZ0l9IC8gMikpO1xuJFdpMzM6IGNhbGMoKCAxMDAlIC8gMyApIC0gKCN7JG1hcmdJfSAvIDMpKTtcblxuLy8gbW9iaWxlXG4kdHh0LWZpZ2xpLW06IDEuNXJlbTtcblxuLy8gbGluZS1oZWlnaHRcbiRGbGhvbWUtbTogMnJlbTtcbiRXaS1tOjEwMCU7XG5cbi8vIHRhYmxldHRlXG4kdHh0LWZpZ2xpLXQ6IDIuNXJlbTtcblxuLy8gbGluZS1oZWlnaHRcbiRGbGhvbWUtdDogM3JlbTtcblxuLy8gOnJvb3Qge1xuLy8gICAtLWNvbG9yOiByZWQ7XG4vLyB9XG4vL1xuLy8gOnJvb3QgLmNhdGVnb3J5e1xuLy8gICAtLWNvbG9yOmJsdWU7XG4vLyB9XG4vL1xuLy8gLmNhcmR7XG4vLyAgIGNvbG9yOiB2YXIoLS1jb2xvcik7XG4vLyB9XG4vL1xuLy8gOnJvb3QgLmNhdGVnb3J5e1xuLy8gICBjb2xvcjogdmFyKC0tY29sb3IpO1xuLy8gfVxuIiwiIiwiLyohIG5vcm1hbGl6ZS5jc3MgdjguMC4xIHwgTUlUIExpY2Vuc2UgfCBnaXRodWIuY29tL25lY29sYXMvbm9ybWFsaXplLmNzcyAqL1xuXG4vKiBEb2N1bWVudFxuICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi9cblxuLyoqXG4gKiAxLiBDb3JyZWN0IHRoZSBsaW5lIGhlaWdodCBpbiBhbGwgYnJvd3NlcnMuXG4gKiAyLiBQcmV2ZW50IGFkanVzdG1lbnRzIG9mIGZvbnQgc2l6ZSBhZnRlciBvcmllbnRhdGlvbiBjaGFuZ2VzIGluIGlPUy5cbiAqL1xuXG5odG1sIHtcbiAgbWFyZ2luOiAwO1xuICBwYWRkaW5nOiAwO1xuICBsaW5lLWhlaWdodDogMS4xNTsgLyogMSAqL1xuICAtd2Via2l0LXRleHQtc2l6ZS1hZGp1c3Q6IDEwMCU7IC8qIDIgKi9cbiAgZm9udC1zaXplOiAxNnB4O1xufVxuXG4vKiBTZWN0aW9uc1xuICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi9cblxuLyoqXG4gKiBSZW1vdmUgdGhlIG1hcmdpbiBpbiBhbGwgYnJvd3NlcnMuXG4gKi9cblxuYm9keSB7XG4gIG1hcmdpbjogMDtcbiAgcGFkZGluZzogMDtcbn1cblxuLyoqXG4gKiBSZW5kZXIgdGhlIGBtYWluYCBlbGVtZW50IGNvbnNpc3RlbnRseSBpbiBJRS5cbiAqL1xuXG5tYWluIHtcbiAgZGlzcGxheTogYmxvY2s7XG59XG5cbi8qKlxuICogQ29ycmVjdCB0aGUgZm9udCBzaXplIGFuZCBtYXJnaW4gb24gYGgxYCBlbGVtZW50cyB3aXRoaW4gYHNlY3Rpb25gIGFuZFxuICogYGFydGljbGVgIGNvbnRleHRzIGluIENocm9tZSwgRmlyZWZveCwgYW5kIFNhZmFyaS5cbiAqL1xuXG5oMSB7XG4gIGZvbnQtc2l6ZTogYXV0bztcbiAgbWFyZ2luOjA7XG59XG5cbi8qIEdyb3VwaW5nIGNvbnRlbnRcbiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovXG5cbi8qKlxuICogMS4gQWRkIHRoZSBjb3JyZWN0IGJveCBzaXppbmcgaW4gRmlyZWZveC5cbiAqIDIuIFNob3cgdGhlIG92ZXJmbG93IGluIEVkZ2UgYW5kIElFLlxuICovXG5cbmhyIHtcbiAgLXdlYmtpdC1ib3gtc2l6aW5nOiBjb250ZW50LWJveDtcbiAgICAgICAgICBib3gtc2l6aW5nOiBjb250ZW50LWJveDsgLyogMSAqL1xuICBoZWlnaHQ6IDA7IC8qIDEgKi9cbiAgb3ZlcmZsb3c6IHZpc2libGU7IC8qIDIgKi9cbn1cblxuLyoqXG4gKiAxLiBDb3JyZWN0IHRoZSBpbmhlcml0YW5jZSBhbmQgc2NhbGluZyBvZiBmb250IHNpemUgaW4gYWxsIGJyb3dzZXJzLlxuICogMi4gQ29ycmVjdCB0aGUgb2RkIGBlbWAgZm9udCBzaXppbmcgaW4gYWxsIGJyb3dzZXJzLlxuICovXG5cbnByZSB7XG4gIGZvbnQtZmFtaWx5OiBtb25vc3BhY2UsIG1vbm9zcGFjZTsgLyogMSAqL1xuICBmb250LXNpemU6IDFlbTsgLyogMiAqL1xufVxuXG4vKiBUZXh0LWxldmVsIHNlbWFudGljc1xuICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi9cblxuLyoqXG4gKiBSZW1vdmUgdGhlIGdyYXkgYmFja2dyb3VuZCBvbiBhY3RpdmUgbGlua3MgaW4gSUUgMTAuXG4gKi9cbnB7XG4gbWFyZ2luOiAwO1xufVxuXG51bHtcbiAgbGlzdC1zdHlsZTogbm9uZTtcbn1cbnVsLGxpe1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG59XG5cbmEge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiB0cmFuc3BhcmVudDtcbiAgY29sb3I6IGJsYWNrO1xuICB0ZXh0LWRlY29yYXRpb246IG5vbmU7XG59XG5cbi8qKlxuICogMS4gUmVtb3ZlIHRoZSBib3R0b20gYm9yZGVyIGluIENocm9tZSA1Ny1cbiAqIDIuIEFkZCB0aGUgY29ycmVjdCB0ZXh0IGRlY29yYXRpb24gaW4gQ2hyb21lLCBFZGdlLCBJRSwgT3BlcmEsIGFuZCBTYWZhcmkuXG4gKi9cblxuYWJiclt0aXRsZV0ge1xuICBib3JkZXItYm90dG9tOiBub25lOyAvKiAxICovXG4gIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOyAvKiAyICovXG4gIC13ZWJraXQtdGV4dC1kZWNvcmF0aW9uOiB1bmRlcmxpbmUgZG90dGVkO1xuICAgICAgICAgIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lIGRvdHRlZDsgLyogMiAqL1xufVxuXG4vKipcbiAqIEFkZCB0aGUgY29ycmVjdCBmb250IHdlaWdodCBpbiBDaHJvbWUsIEVkZ2UsIGFuZCBTYWZhcmkuXG4gKi9cblxuYixcbnN0cm9uZyB7XG4gIGZvbnQtd2VpZ2h0OiBib2xkZXI7XG59XG5cbi8qKlxuICogMS4gQ29ycmVjdCB0aGUgaW5oZXJpdGFuY2UgYW5kIHNjYWxpbmcgb2YgZm9udCBzaXplIGluIGFsbCBicm93c2Vycy5cbiAqIDIuIENvcnJlY3QgdGhlIG9kZCBgZW1gIGZvbnQgc2l6aW5nIGluIGFsbCBicm93c2Vycy5cbiAqL1xuXG5jb2RlLFxua2JkLFxuc2FtcCB7XG4gIGZvbnQtZmFtaWx5OiBtb25vc3BhY2UsIG1vbm9zcGFjZTsgLyogMSAqL1xuICBmb250LXNpemU6IDFlbTsgLyogMiAqL1xufVxuXG4vKipcbiAqIEFkZCB0aGUgY29ycmVjdCBmb250IHNpemUgaW4gYWxsIGJyb3dzZXJzLlxuICovXG5cbnNtYWxsIHtcbiAgZm9udC1zaXplOiA4MCU7XG59XG5cbi8qKlxuICogUHJldmVudCBgc3ViYCBhbmQgYHN1cGAgZWxlbWVudHMgZnJvbSBhZmZlY3RpbmcgdGhlIGxpbmUgaGVpZ2h0IGluXG4gKiBhbGwgYnJvd3NlcnMuXG4gKi9cblxuc3ViLFxuc3VwIHtcbiAgZm9udC1zaXplOiA3NSU7XG4gIGxpbmUtaGVpZ2h0OiAwO1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIHZlcnRpY2FsLWFsaWduOiBiYXNlbGluZTtcbn1cblxuc3ViIHtcbiAgYm90dG9tOiAtMC4yNWVtO1xufVxuXG5zdXAge1xuICB0b3A6IC0wLjVlbTtcbn1cblxuLyogRW1iZWRkZWQgY29udGVudFxuICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi9cblxuLyoqXG4gKiBSZW1vdmUgdGhlIGJvcmRlciBvbiBpbWFnZXMgaW5zaWRlIGxpbmtzIGluIElFIDEwLlxuICovXG5cbmltZyB7XG4gIGJvcmRlci1zdHlsZTogbm9uZTtcbiAgd2lkdGg6IDEwMCU7XG4gIGhlaWdodDogMTAwJTtcbiAgdmVydGljYWwtYWxpZ246IGJvdHRvbTtcbn1cblxuLyogRm9ybXNcbiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovXG5cbi8qKlxuICogMS4gQ2hhbmdlIHRoZSBmb250IHN0eWxlcyBpbiBhbGwgYnJvd3NlcnMuXG4gKiAyLiBSZW1vdmUgdGhlIG1hcmdpbiBpbiBGaXJlZm94IGFuZCBTYWZhcmkuXG4gKi9cblxuYnV0dG9uLFxuaW5wdXQsXG5vcHRncm91cCxcbnNlbGVjdCxcbnRleHRhcmVhIHtcbiAgZm9udC1mYW1pbHk6IGluaGVyaXQ7IC8qIDEgKi9cbiAgZm9udC1zaXplOiAxMDAlOyAvKiAxICovXG4gIGxpbmUtaGVpZ2h0OiAxLjE1OyAvKiAxICovXG4gIG1hcmdpbjogMDsgLyogMiAqL1xufVxuXG4vKipcbiAqIFNob3cgdGhlIG92ZXJmbG93IGluIElFLlxuICogMS4gU2hvdyB0aGUgb3ZlcmZsb3cgaW4gRWRnZS5cbiAqL1xuXG5idXR0b24sXG5pbnB1dCB7IC8qIDEgKi9cbiAgb3ZlcmZsb3c6IHZpc2libGU7XG59XG5cbi8qKlxuICogUmVtb3ZlIHRoZSBpbmhlcml0YW5jZSBvZiB0ZXh0IHRyYW5zZm9ybSBpbiBFZGdlLCBGaXJlZm94LCBhbmQgSUUuXG4gKiAxLiBSZW1vdmUgdGhlIGluaGVyaXRhbmNlIG9mIHRleHQgdHJhbnNmb3JtIGluIEZpcmVmb3guXG4gKi9cblxuYnV0dG9uLFxuc2VsZWN0IHsgLyogMSAqL1xuICB0ZXh0LXRyYW5zZm9ybTogbm9uZTtcbn1cblxuLyoqXG4gKiBDb3JyZWN0IHRoZSBpbmFiaWxpdHkgdG8gc3R5bGUgY2xpY2thYmxlIHR5cGVzIGluIGlPUyBhbmQgU2FmYXJpLlxuICovXG5cbmJ1dHRvbixcblt0eXBlPVwiYnV0dG9uXCJdLFxuW3R5cGU9XCJyZXNldFwiXSxcblt0eXBlPVwic3VibWl0XCJdIHtcbiAgLXdlYmtpdC1hcHBlYXJhbmNlOiBidXR0b247XG59XG5cbi8qKlxuICogUmVtb3ZlIHRoZSBpbm5lciBib3JkZXIgYW5kIHBhZGRpbmcgaW4gRmlyZWZveC5cbiAqL1xuXG5idXR0b246Oi1tb3otZm9jdXMtaW5uZXIsXG5bdHlwZT1cImJ1dHRvblwiXTo6LW1vei1mb2N1cy1pbm5lcixcblt0eXBlPVwicmVzZXRcIl06Oi1tb3otZm9jdXMtaW5uZXIsXG5bdHlwZT1cInN1Ym1pdFwiXTo6LW1vei1mb2N1cy1pbm5lciB7XG4gIGJvcmRlci1zdHlsZTogbm9uZTtcbiAgcGFkZGluZzogMDtcbn1cblxuLyoqXG4gKiBSZXN0b3JlIHRoZSBmb2N1cyBzdHlsZXMgdW5zZXQgYnkgdGhlIHByZXZpb3VzIHJ1bGUuXG4gKi9cblxuYnV0dG9uOi1tb3otZm9jdXNyaW5nLFxuW3R5cGU9XCJidXR0b25cIl06LW1vei1mb2N1c3JpbmcsXG5bdHlwZT1cInJlc2V0XCJdOi1tb3otZm9jdXNyaW5nLFxuW3R5cGU9XCJzdWJtaXRcIl06LW1vei1mb2N1c3Jpbmcge1xuICBvdXRsaW5lOiAxcHggZG90dGVkIEJ1dHRvblRleHQ7XG59XG5cbi8qKlxuICogQ29ycmVjdCB0aGUgcGFkZGluZyBpbiBGaXJlZm94LlxuICovXG5cbmZpZWxkc2V0IHtcbiAgcGFkZGluZzogMC4zNWVtIDAuNzVlbSAwLjYyNWVtO1xufVxuXG4vKipcbiAqIDEuIENvcnJlY3QgdGhlIHRleHQgd3JhcHBpbmcgaW4gRWRnZSBhbmQgSUUuXG4gKiAyLiBDb3JyZWN0IHRoZSBjb2xvciBpbmhlcml0YW5jZSBmcm9tIGBmaWVsZHNldGAgZWxlbWVudHMgaW4gSUUuXG4gKiAzLiBSZW1vdmUgdGhlIHBhZGRpbmcgc28gZGV2ZWxvcGVycyBhcmUgbm90IGNhdWdodCBvdXQgd2hlbiB0aGV5IHplcm8gb3V0XG4gKiAgICBgZmllbGRzZXRgIGVsZW1lbnRzIGluIGFsbCBicm93c2Vycy5cbiAqL1xuXG5sZWdlbmQge1xuICAtd2Via2l0LWJveC1zaXppbmc6IGJvcmRlci1ib3g7XG4gICAgICAgICAgYm94LXNpemluZzogYm9yZGVyLWJveDsgLyogMSAqL1xuICBjb2xvcjogaW5oZXJpdDsgLyogMiAqL1xuICBkaXNwbGF5OiB0YWJsZTsgLyogMSAqL1xuICBtYXgtd2lkdGg6IDEwMCU7IC8qIDEgKi9cbiAgcGFkZGluZzogMDsgLyogMyAqL1xuICB3aGl0ZS1zcGFjZTogbm9ybWFsOyAvKiAxICovXG59XG5cbi8qKlxuICogQWRkIHRoZSBjb3JyZWN0IHZlcnRpY2FsIGFsaWdubWVudCBpbiBDaHJvbWUsIEZpcmVmb3gsIGFuZCBPcGVyYS5cbiAqL1xuXG5wcm9ncmVzcyB7XG4gIHZlcnRpY2FsLWFsaWduOiBiYXNlbGluZTtcbn1cblxuLyoqXG4gKiBSZW1vdmUgdGhlIGRlZmF1bHQgdmVydGljYWwgc2Nyb2xsYmFyIGluIElFIDEwKy5cbiAqL1xuXG50ZXh0YXJlYSB7XG4gIG92ZXJmbG93OiBhdXRvO1xufVxuXG4vKipcbiAqIDEuIEFkZCB0aGUgY29ycmVjdCBib3ggc2l6aW5nIGluIElFIDEwLlxuICogMi4gUmVtb3ZlIHRoZSBwYWRkaW5nIGluIElFIDEwLlxuICovXG5cblt0eXBlPVwiY2hlY2tib3hcIl0sXG5bdHlwZT1cInJhZGlvXCJdIHtcbiAgLXdlYmtpdC1ib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICAgICAgICAgIGJveC1zaXppbmc6IGJvcmRlci1ib3g7IC8qIDEgKi9cbiAgcGFkZGluZzogMDsgLyogMiAqL1xufVxuXG4vKipcbiAqIENvcnJlY3QgdGhlIGN1cnNvciBzdHlsZSBvZiBpbmNyZW1lbnQgYW5kIGRlY3JlbWVudCBidXR0b25zIGluIENocm9tZS5cbiAqL1xuXG5bdHlwZT1cIm51bWJlclwiXTo6LXdlYmtpdC1pbm5lci1zcGluLWJ1dHRvbixcblt0eXBlPVwibnVtYmVyXCJdOjotd2Via2l0LW91dGVyLXNwaW4tYnV0dG9uIHtcbiAgaGVpZ2h0OiBhdXRvO1xufVxuXG4vKipcbiAqIDEuIENvcnJlY3QgdGhlIG9kZCBhcHBlYXJhbmNlIGluIENocm9tZSBhbmQgU2FmYXJpLlxuICogMi4gQ29ycmVjdCB0aGUgb3V0bGluZSBzdHlsZSBpbiBTYWZhcmkuXG4gKi9cblxuW3R5cGU9XCJzZWFyY2hcIl0ge1xuICAtd2Via2l0LWFwcGVhcmFuY2U6IHRleHRmaWVsZDsgLyogMSAqL1xuICBvdXRsaW5lLW9mZnNldDogLTJweDsgLyogMiAqL1xufVxuXG4vKipcbiAqIFJlbW92ZSB0aGUgaW5uZXIgcGFkZGluZyBpbiBDaHJvbWUgYW5kIFNhZmFyaSBvbiBtYWNPUy5cbiAqL1xuXG5bdHlwZT1cInNlYXJjaFwiXTo6LXdlYmtpdC1zZWFyY2gtZGVjb3JhdGlvbiB7XG4gIC13ZWJraXQtYXBwZWFyYW5jZTogbm9uZTtcbn1cblxuLyoqXG4gKiAxLiBDb3JyZWN0IHRoZSBpbmFiaWxpdHkgdG8gc3R5bGUgY2xpY2thYmxlIHR5cGVzIGluIGlPUyBhbmQgU2FmYXJpLlxuICogMi4gQ2hhbmdlIGZvbnQgcHJvcGVydGllcyB0byBgaW5oZXJpdGAgaW4gU2FmYXJpLlxuICovXG5cbjo6LXdlYmtpdC1maWxlLXVwbG9hZC1idXR0b24ge1xuICAtd2Via2l0LWFwcGVhcmFuY2U6IGJ1dHRvbjsgLyogMSAqL1xuICBmb250OiBpbmhlcml0OyAvKiAyICovXG59XG5cbi8qIEludGVyYWN0aXZlXG4gICA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSAqL1xuXG4vKlxuICogQWRkIHRoZSBjb3JyZWN0IGRpc3BsYXkgaW4gRWRnZSwgSUUgMTArLCBhbmQgRmlyZWZveC5cbiAqL1xuXG5kZXRhaWxzIHtcbiAgZGlzcGxheTogYmxvY2s7XG59XG5cbi8qXG4gKiBBZGQgdGhlIGNvcnJlY3QgZGlzcGxheSBpbiBhbGwgYnJvd3NlcnMuXG4gKi9cblxuc3VtbWFyeSB7XG4gIGRpc3BsYXk6IGxpc3QtaXRlbTtcbn1cblxuLyogTWlzY1xuICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi9cblxuLyoqXG4gKiBBZGQgdGhlIGNvcnJlY3QgZGlzcGxheSBpbiBJRSAxMCsuXG4gKi9cblxudGVtcGxhdGUge1xuICBkaXNwbGF5OiBub25lO1xufVxuXG4vKipcbiAqIEFkZCB0aGUgY29ycmVjdCBkaXNwbGF5IGluIElFIDEwLlxuICovXG5cbltoaWRkZW5dIHtcbiAgZGlzcGxheTogbm9uZTtcbn1cbiIsImgxLCBoMiwgaDMsIHAsIHVsLCBsaSwgYSwgc3BhbntcbiAgZm9udC1mYW1pbHk6ICRsYXRvO1xuICBmb250LXdlaWdodDogbGlnaHRlcjtcbn1cblxuaGVhZGVye1xuICBhe1xuICAgIGZvbnQtc2l6ZTogJHR4dC1maWdsaTtcbiAgICBsaW5lLWhlaWdodDogJEZsaG9tZTtcbiAgfVxufVxuXG4uaG9tZXtcbiAgcHtcbiAgICBmb250LXNpemU6ICR0eHQtZmlnbGk7XG4gICAgbGluZS1oZWlnaHQ6IDNyZW07XG4gIH1cbiAgI3RleHRfZmlnbGl7XG4gICAgc3Ryb25ne1xuICAgICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgICAgJjo6YWZ0ZXJ7XG4gICAgICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgICAgIGNvbnRlbnQ6IFwiXCI7XG4gICAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgICBoZWlnaHQ6IDFweDtcbiAgICAgICAgYmFja2dyb3VuZDogYmxhY2s7XG4gICAgICAgIGJvdHRvbTogMDtcbiAgICAgIH1cbiAgICB9XG4gICAgYXtcbiAgICAgIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgICAgICY6bnRoLWNoaWxkKDEpe1xuICAgICAgfVxuICAgICAgJiNwdWJsaXF1ZXtcbiAgICAgICAgZm9udC1mYW1pbHk6ICRwbGF5ZmFpcjtcbiAgICAgIH1cbiAgICAgICYjc29jaWFsZXtcbiAgICAgICAgZm9udC1mYW1pbHk6ICRzeW5lO1xuICAgICAgfVxuICAgICAgJiNjdWx0dXJlbGxle1xuICAgICAgICBmb250LWZhbWlseTogJGF2YXJhO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG4jaXRlbXtcbiAgLmNvbnRlbnQtdGl0bGV7XG4gICAgZGlzcGxheTogZmxleDtcbiAgICBtYXJnaW46ICRtYXJnIDA7XG4gICAgJiA+ICoge1xuICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICAgIGZvbnQtc2l6ZTogMS4xNXJlbTtcbiAgICAgIGZvbnQtd2VpZ2h0OiBib2xkO1xuICAgIH1cbiAgICBoMntcbiAgICAgIG1hcmdpbjogMCAkbWFyZyAwIDA7XG4gICAgICBhe1xuICAgICAgICBmb250LXdlaWdodDogYm9sZDtcbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cblxuYS5wdWJsaXF1ZSwgc3Bhbi5wdWJsaXF1ZXtcbiAgZm9udC1mYW1pbHk6ICRwbGF5ZmFpcjtcbn1cblxuYS5zb2NpYWxlLCBzcGFuLnNvY2lhbGV7XG4gIGZvbnQtZmFtaWx5OiAkc3luZTtcbn1cblxuYS5jdWx0dXJlbGxlLCBzcGFuLmN1bHR1cmVsbGV7XG4gIGZvbnQtZmFtaWx5OiAkYXZhcmE7XG59XG5cbi5zaWRlYmFyLXJpZ2h0e1xuICB1bCwgYXtcbiAgICBmb250LXNpemU6IDAuOHJlbTtcbiAgICBsaW5lLWhlaWdodDogMS4yNXJlbTtcbiAgfVxufVxuXG4ub3RoZXJfcHJvamV0c3tcbiAgaDN7XG4gICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpO1xuICAgIG1hcmdpbjogMDtcbiAgfVxufVxuXG4uY2FyZHtcbiAgaDJ7XG4gICAgbWFyZ2luOiAwO1xuICAgIGZvbnQtc2l6ZTogMXJlbTtcbiAgfVxufVxuIiwiaGVhZGVye1xuICBwb3NpdGlvbjogZml4ZWQ7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgbWFyZ2luOiAwIDAgMCAkbWFyZztcbiAgcGFkZGluZzogJG1hcmcgMDtcbiAgd2lkdGg6IGNhbGMoICN7JFdpfSAtICN7JG1hcmd9ICk7XG4gIGJvcmRlci1ib3R0b206IDFweCBzb2xpZCBibGFjaztcbn1cbiIsIip7XG4gIGJveC1zaXppbmc6IGJvcmRlci1ib3g7XG59XG5odG1sLCBib2R5LCBoZWFkZXJ7XG4gIGJhY2tncm91bmQ6ICNmOWY5ZjlmZTtcbn1cblxuYm9keXtcbiAgI3N0YXJ0e1xuICAgICAgd2lkdGg6ICRXaTUwO1xuICAgICAgbWFyZ2luOiAkbWFyZyBhdXRvIGF1dG8gJG1hcmc7XG4gICAgICAjdGV4dF9maWdsaXtcbiAgICAgICAgcCB7XG4gICAgICAgICAgYXtcbiAgICAgICAgICAgIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgICAgICAgICAgICYubWFyZ3tcbiAgICAgICAgICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgI2l0ZW1fbGlzdHtcbiAgICAgICAgICB3aWR0aDogODB2dztcbiAgICAgICAgLmNhcmR7XG4gICAgICAgICAgd2lkdGg6IGNhbGMoKDEwMCUgLyA1KSAtICggI3skbWFyZ30gKiAyKSApO1xuICAgICAgICAgIGhlaWdodDogYXV0bztcbiAgICAgICAgICAvLyBvdmVyZmxvdzogaGlkZGVuO1xuICAgICAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgICAgICAgICB2ZXJ0aWNhbC1hbGlnbjogdG9wO1xuICAgICAgICAgIGxpbmUtaGVpZ2h0OiAwO1xuICAgICAgICAgIG1hcmdpbi1yaWdodDogJG1hcmc7XG4gICAgICAgICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgICAgICAgIG1hcmdpbi10b3A6ICRtYXJnO1xuICAgICAgICAgIGF7XG4gICAgICAgICAgICBkaXNwbGF5OiBibG9jaztcbiAgICAgICAgICB9XG4gICAgICAgICAgLmNhcmQtaGVhZGVye1xuICAgICAgICAgICAgZGlzcGxheTogbm9uZTtcbiAgICAgICAgICAgICYgPiAqe1xuICAgICAgICAgICAgICBmb250LXNpemU6IDAuOHJlbTtcbiAgICAgICAgICAgICAgbGluZS1oZWlnaHQ6IDFyZW07XG4gICAgICAgICAgICAgIG1hcmdpbi1yaWdodDogJG1hcmc7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICAgICY6aG92ZXJ7XG4gICAgICAgICAgICAuY2FyZC1oZWFkZXJ7XG4gICAgICAgICAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgICAgICAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuNSk7XG4gICAgICAgICAgICAgIHBhZGRpbmc6ICRtYXJnO1xuICAgICAgICAgICAgICBkaXNwbGF5OiBpbmxpbmUtZmxleDtcbiAgICAgICAgICAgICAgZmxleC13cmFwOiB3cmFwO1xuICAgICAgICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgICAgICAgICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlWSgtMTAwJSk7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICAgICY6bGFzdC1vZi10eXBle1xuICAgICAgICAgICAgdmVydGljYWwtYWxpZ246IGJhc2VsaW5lIWltcG9ydGFudDtcbiAgICAgICAgICB9XG4gICAgICAgICAgI2l0ZW17XG4gICAgICAgICAgICB3aWR0aDogY2FsYyggMzA3JSAtICN7JG1hcmd9ICk7XG4gICAgICAgICAgICAuY29udGVudC10aXRsZXtcbiAgICAgICAgICAgICAgbWFyZ2luOiAzMHB4IGF1dG87XG4gICAgICAgICAgICAgIGF7XG4gICAgICAgICAgICAgICAgZGlzcGxheTogYmxvY2s7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIC5lLWNvbnRlbnR7XG4gICAgICAgICAgICAgIG1hcmdpbjogMzBweCBhdXRvO1xuICAgICAgICAgICAgICBwe1xuICAgICAgICAgICAgICAgIGZvbnQtc2l6ZTogMXJlbTtcbiAgICAgICAgICAgICAgICBsaW5lLWhlaWdodDogMXJlbTtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICB9XG4gIH1cbiAgJjpub3QoLmhvbWUpe1xuICAgICNzdGFydHtcbiAgICAgIG1hcmdpbi10b3A6IGNhbGMoIDJyZW0gKyAgI3skRmxob21lfSk7XG4gICAgfVxuICB9XG59XG4iLCIuc2lkZWJhci1yaWdodHtcbiAgcG9zaXRpb246IGZpeGVkO1xuICB3aWR0aDogMjAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIHJpZ2h0OiAwO1xuICB0b3A6IDA7XG4gIGRpc3BsYXk6IGlubGluZS1mbGV4O1xuICAubGlzdC1wcm9qZXRze1xuICAgIG1hcmdpbjogYXV0bztcbiAgICB1bHtcbiAgICAgICY6bnRoLWNoaWxkKDEpe1xuICAgICAgICBtYXJnaW4tdG9wOiAwO1xuICAgICAgfVxuICAgICAgbWFyZ2luOiAkbWFyZyAkbWFyZyAwcHggMDtcbiAgICAgIHRleHQtYWxpZ246IHJpZ2h0O1xuICAgICAgbGl7XG4gICAgICAgICY6aG92ZXJ7XG4gICAgICAgICAgYXtcbiAgICAgICAgICAgIGNvbG9yOiBibGFjaztcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGF7XG4gICAgICAgIGNvbG9yOiBncmV5O1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIiwiI2l0ZW1fbGlzdHtcbiAgICBkaXNwbGF5OiBpbmxpbmUtZmxleDtcbiAgICBmbGV4LXdyYXA6IHdyYXA7XG4gICAgd2lkdGg6IDEwMCU7XG4gIC5jYXJke1xuICAgIHdpZHRoOiAkV2k1MDtcbiAgICAmOm50aC1jaGlsZChvZGQpe1xuICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZ0k7XG4gICAgfVxuICAgIC5jYXJkLWhlYWRlcntcbiAgICAgIGRpc3BsYXk6IGlubGluZS1mbGV4O1xuICAgICAgd2lkdGg6IDEwMCU7XG4gICAgICBtYXJnaW4tYm90dG9tOiAkbWFyZztcbiAgICAgICYgPiAqIHtcbiAgICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cbiIsIiNpdGVte1xuICAuZS1jb250ZW50e1xuICAgIC13ZWJraXQtY29sdW1uczogMzAwcHggMjtcbiAgICAtbW96LWNvbHVtbnM6IDMwMHB4IDI7XG4gICAgICAgIGNvbHVtbnM6IDMwMHB4IDI7XG4gICAgbWFyZ2luLWJvdHRvbTogJG1hcmc7XG4gICAgfVxuICAuY2FyZC1pbWFnZXtcbiAgICBpbWd7XG4gICAgICBtYXJnaW4tYm90dG9tOiAkbWFyZztcbiAgICB9XG4gIH1cbn1cblxuLm90aGVyX3Byb2pldHN7XG4gIGJvcmRlci10b3A6IDFweCBzb2xpZCBibGFjaztcbiAgZGlzcGxheTogZmxleDtcbiAgZmxleC13cmFwOiB3cmFwO1xuICBtYXJnaW4tdG9wOiAkbWFyZ0k7XG4gIG1hcmdpbi1ib3R0b206ICRtYXJnSTtcbiAgaDN7XG4gICAgbWFyZ2luOiAxcmVtIDAgJG1hcmcgMDtcbiAgICB3aWR0aDogMTAwJTtcbiAgfVxuICAuY2FyZHtcbiAgICB3aWR0aDogJFdpMzM7XG4gICAgJjpudGgtY2hpbGQoLTFuKzMpe1xuICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICB9XG4gICAgLmNhcmQtaGVhZGVye1xuICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgIGZsZXgtd3JhcDogd3JhcDtcbiAgICAgICYgPiAqe1xuICAgICAgICBtYXJnaW4tcmlnaHQ6ICRtYXJnO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIiwiQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNDE0cHgpIHtcbiAgLy8gdHlwb2dyYXBoaWVcbiAgaGVhZGVye1xuICAgIGF7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbTtcbiAgICAgIGxpbmUtaGVpZ2h0OiAkRmxob21lLW07XG4gICAgfVxuICB9XG4gIC5ob21le1xuICAgIHB7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbSFpbXBvcnRhbnQ7XG4gICAgICBsaW5lLWhlaWdodDogJEZsaG9tZS1tIWltcG9ydGFudDtcbiAgICB9XG4gIH1cbiAgLm90aGVyX3Byb2pldHN7XG4gICAgaDN7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbTtcbiAgICB9XG4gIH1cblxuICAvLyBtZXBcbiAgaGVhZGVye1xuICAgIHdpZHRoOiBjYWxjKCAjeyRXaS1tfSAtICgjeyRtYXJnfSAqIDIpICk7XG4gIH1cblxuICBib2R5e1xuICAgICNzdGFydHtcbiAgICAgICAgd2lkdGg6IGNhbGMoICN7JFdpLW19IC0gKCN7JG1hcmd9ICogMikgKTtcbiAgICB9XG4gICAgJjpub3QoLmhvbWUpe1xuICAgICAgI3N0YXJ0e1xuICAgICAgICBtYXJnaW4tdG9wOiAkRmxob21lO1xuICAgICAgfVxuICAgIH1cbiAgICAuc2lkZWJhci1yaWdodHtcbiAgICAgIGRpc3BsYXk6IG5vbmU7XG4gICAgfVxuICB9XG5cbiAgI2l0ZW1fbGlzdHtcbiAgICAuY2FyZHtcbiAgICAgIHdpZHRoOiBjYWxjKCAjeyRXaS1tfSAgKTtcbiAgICAgICY6bnRoLWNoaWxkKG9kZCl7XG4gICAgICAgIG1hcmdpbi1yaWdodDogMDtcbiAgICAgIH1cbiAgICAgIC5jYXJkLWhlYWRlcntcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogJG1hcmc7XG4gICAgICAgICYgPiAqIHtcbiAgICAgICAgICBtYXJnaW4tcmlnaHQ6ICRtYXJnO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLm90aGVyX3Byb2pldHN7XG4gICAgbWFyZ2luLXRvcDogJG1hcmdJO1xuICAgIC5jYXJke1xuICAgICAgd2lkdGg6ICRXaS1tO1xuICAgICAgJjpudGgtY2hpbGQoLTFuKzMpe1xuICAgICAgICBtYXJnaW4tcmlnaHQ6IDA7XG4gICAgICB9XG4gICAgfVxuICB9XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDc2OHB4KSAgYW5kIChvcmllbnRhdGlvbjogcG9ydHJhaXQpIHtcbiAgLy8gdHlwb2dyYXBoaWVcbiAgaGVhZGVye1xuICAgIGF7XG4gICAgICBmb250LXNpemU6IDIuNXJlbTtcbiAgICAgIGxpbmUtaGVpZ2h0OiAzcmVtO1xuICAgIH1cbiAgfVxuICAuaG9tZXtcbiAgICBwe1xuICAgICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpLXQhaW1wb3J0YW50O1xuICAgICAgbGluZS1oZWlnaHQ6ICRGbGhvbWUtdCFpbXBvcnRhbnQ7XG4gICAgfVxuICB9XG4gIC5vdGhlcl9wcm9qZXRze1xuICAgIGgze1xuICAgICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpLXQ7XG4gICAgfVxuICB9XG5cbiAgYm9keXtcbiAgICAmOm5vdCguaG9tZSl7XG4gICAgICAjc3RhcnR7XG4gICAgICAgIG1hcmdpbi10b3A6IDVyZW07XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgI2l0ZW1fbGlzdHtcbiAgICAuY2FyZHtcbiAgICAgIHdpZHRoOiBjYWxjKCAjeyRXaS1tfSAgKTtcbiAgICAgICY6bnRoLWNoaWxkKG9kZCl7XG4gICAgICAgIG1hcmdpbi1yaWdodDogMDtcbiAgICAgIH1cbiAgICAgIC5jYXJkLWhlYWRlcntcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogJG1hcmc7XG4gICAgICAgICYgPiAqIHtcbiAgICAgICAgICBtYXJnaW4tcmlnaHQ6ICRtYXJnO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG5cblxuXG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDgxMnB4KSBhbmQgKG9yaWVudGF0aW9uOiBsYW5kc2NhcGUpIHtcbiAgLy8gdHlwb2dyYXBoaWVcbiAgaGVhZGVye1xuICAgIGF7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbTtcbiAgICAgIGxpbmUtaGVpZ2h0OiAkRmxob21lLW07XG4gICAgfVxuICB9XG4gIC5ob21le1xuICAgIHB7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbSFpbXBvcnRhbnQ7XG4gICAgICBsaW5lLWhlaWdodDogJEZsaG9tZS1tIWltcG9ydGFudDtcbiAgICB9XG4gIH1cbiAgLm90aGVyX3Byb2pldHN7XG4gICAgaDN7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbTtcbiAgICB9XG4gIH1cblxuICAvLyBtZXBcbiAgaGVhZGVye1xuICAgIHdpZHRoOiBjYWxjKCAjeyRXaS1tfSAtICgjeyRtYXJnfSAqIDIpICk7XG4gIH1cblxuICBib2R5e1xuICAgICNzdGFydHtcbiAgICAgICAgd2lkdGg6IGNhbGMoICN7JFdpLW19IC0gKCN7JG1hcmd9ICogMikgKTtcbiAgICB9XG4gICAgJjpub3QoLmhvbWUpe1xuICAgICAgI3N0YXJ0e1xuICAgICAgICBtYXJnaW4tdG9wOiAkRmxob21lO1xuICAgICAgfVxuICAgIH1cbiAgICAuc2lkZWJhci1yaWdodHtcbiAgICAgIGRpc3BsYXk6IG5vbmU7XG4gICAgfVxuICB9XG5cbiAgI2l0ZW1fbGlzdHtcbiAgICAuY2FyZHtcbiAgICAgIHdpZHRoOiAkV2k1MDtcbiAgICAgICY6bnRoLWNoaWxkKG9kZCl7XG4gICAgICAgIG1hcmdpbi1yaWdodDogJG1hcmdJO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC5vdGhlcl9wcm9qZXRze1xuICAgIC5jYXJke1xuICAgICAgd2lkdGg6ICRXaTMzO1xuICAgICAgJjpudGgtY2hpbGQoLTFuKzMpe1xuICAgICAgICBtYXJnaW4tcmlnaHQ6ICRtYXJnO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA5MTBweCkgYW5kIChvcmllbnRhdGlvbjogcG9ydHJhaXQpIHtcbiAgaGVhZGVye1xuICAgIGF7XG4gICAgICBmb250LXNpemU6IDIuNXJlbTtcbiAgICAgIGxpbmUtaGVpZ2h0OiAzcmVtO1xuICAgIH1cbiAgfVxuICAuaG9tZXtcbiAgICBwe1xuICAgICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpLXQhaW1wb3J0YW50O1xuICAgICAgbGluZS1oZWlnaHQ6ICRGbGhvbWUtdCFpbXBvcnRhbnQ7XG4gICAgfVxuICB9XG4gIC5vdGhlcl9wcm9qZXRze1xuICAgIGgze1xuICAgICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpLXQ7XG4gICAgfVxuICB9XG4gIGJvZHl7XG4gICAgJjpub3QoLmhvbWUpe1xuICAgICAgI3N0YXJ0e1xuICAgICAgICBtYXJnaW4tdG9wOiA1cmVtO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA2NjBweCkge1xuICBoZWFkZXJ7XG4gICAgYXtcbiAgICAgIGZvbnQtc2l6ZTogJHR4dC1maWdsaS1tO1xuICAgICAgbGluZS1oZWlnaHQ6ICRGbGhvbWUtbTtcbiAgICB9XG4gIH1cbiAgLmhvbWV7XG4gICAgcHtcbiAgICAgIGZvbnQtc2l6ZTogJHR4dC1maWdsaS1tIWltcG9ydGFudDtcbiAgICAgIGxpbmUtaGVpZ2h0OiAkRmxob21lLW0haW1wb3J0YW50O1xuICAgIH1cbiAgfVxuICAub3RoZXJfcHJvamV0c3tcbiAgICBoM3tcbiAgICAgIGZvbnQtc2l6ZTogJHR4dC1maWdsaS1tO1xuICAgIH1cbiAgfVxuXG4gIGJvZHl7XG4gICAgJjpub3QoLmhvbWUpe1xuICAgICAgI3N0YXJ0e1xuICAgICAgICBtYXJnaW4tdG9wOiAkRmxob21lO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQ2dHSSxVQUFVLEdBQ04sV0FBVyxFQUFFLE1BQWMsRUFDM0IsR0FBRyxFQUFFLHFDQUF1QixFQUM1QixHQUFHLEVBQUUsNENBQThCLENBQUMsMkJBQTJCLEVBQ3pELHNDQUF3QixDQUFDLGNBQWMsRUFDdkMscUNBQXVCLENBQUUsa0JBQWtCLEVBQzNDLDBDQUF5QyxDQUFDLGFBQWE7O0FBTmpFLFVBQVUsR0FDTixXQUFXLEVBQUUsTUFBYyxFQUMzQixHQUFHLEVBQUUsa0NBQXVCLEVBQzVCLEdBQUcsRUFBRSx5Q0FBOEIsQ0FBQywyQkFBMkIsRUFDekQsbUNBQXdCLENBQUMsY0FBYyxFQUN2QyxrQ0FBdUIsQ0FBRSxrQkFBa0IsRUFDM0MsdUNBQXlDLENBQUMsYUFBYTs7QUFOakUsVUFBVSxHQUNOLFdBQVcsRUFBRSxPQUFjLEVBQzNCLEdBQUcsRUFBRSwrQ0FBdUIsRUFDNUIsR0FBRyxFQUFFLHNEQUE4QixDQUFDLDJCQUEyQixFQUN6RCxnREFBd0IsQ0FBQyxjQUFjLEVBQ3ZDLCtDQUF1QixDQUFFLGtCQUFrQixFQUMzQyxxREFBeUMsQ0FBQyxhQUFhOztBQU5qRSxVQUFVLEdBQ04sV0FBVyxFQUFFLCtCQUFjLEVBQzNCLEdBQUcsRUFBRSwwREFBdUIsRUFDNUIsR0FBRyxFQUFFLGlFQUE4QixDQUFDLDJCQUEyQixFQUN6RCwyREFBd0IsQ0FBQyxjQUFjLEVBQ3ZDLDBEQUF1QixDQUFFLGtCQUFrQixFQUMzQyx3RkFBeUMsQ0FBQyxhQUFhOztBR3RHckUsNEVBQTRFO0FBRTVFLHlGQUNnRjtBQUVoRix3SEFHRztBQUVILEFBQUEsSUFBSSxDQUFDLEVBQ0gsTUFBTSxFQUFFLENBQUMsRUFDVCxPQUFPLEVBQUUsQ0FBQyxFQUNWLFdBQVcsRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUMxQix3QkFBd0IsRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUN2QyxTQUFTLEVBQUUsSUFBSSxHQUNoQjs7QUFFRCx5RkFDZ0Y7QUFFaEYseUNBRUc7QUFFSCxBQUFBLElBQUksQ0FBQyxFQUNILE1BQU0sRUFBRSxDQUFDLEVBQ1QsT0FBTyxFQUFFLENBQUMsR0FDWDs7QUFFRCxvREFFRztBQUVILEFBQUEsSUFBSSxDQUFDLEVBQ0gsT0FBTyxFQUFFLEtBQUssR0FDZjs7QUFFRCxnSUFHRztBQUVILEFBQUEsRUFBRSxDQUFDLEVBQ0QsU0FBUyxFQUFFLElBQUksRUFDZixNQUFNLEVBQUMsQ0FBQyxHQUNUOztBQUVELGlHQUNnRjtBQUVoRixxRkFHRztBQUVILEFBQUEsRUFBRSxDQUFDLEVBQ0Qsa0JBQWtCLEVBQUUsV0FBVyxFQUN2QixVQUFVLEVBQUUsV0FBVyxFQUFFLE9BQU8sQ0FDeEMsTUFBTSxFQUFFLENBQUMsRUFBRSxPQUFPLENBQ2xCLFFBQVEsRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUMzQjs7QUFFRCxnSUFHRztBQUVILEFBQUEsR0FBRyxDQUFDLEVBQ0YsV0FBVyxFQUFFLG9CQUFvQixFQUFFLE9BQU8sQ0FDMUMsU0FBUyxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQ3hCOztBQUVELHFHQUNnRjtBQUVoRiwyREFFRztBQUNILEFBQUEsQ0FBQyxDQUFBLEVBQ0EsTUFBTSxFQUFFLENBQUMsR0FDVDs7QUFFRCxBQUFBLEVBQUUsQ0FBQSxFQUNBLFVBQVUsRUFBRSxJQUFJLEdBQ2pCOztBQUNELEFBQUEsRUFBRSxFQUFDLEVBQUUsQ0FBQSxFQUNILE1BQU0sRUFBRSxDQUFDLEVBQ1QsT0FBTyxFQUFFLENBQUMsR0FDWDs7QUFFRCxBQUFBLENBQUMsQ0FBQyxFQUNBLGdCQUFnQixFQUFFLFdBQVcsRUFDN0IsS0FBSyxFQUFFLEtBQUssRUFDWixlQUFlLEVBQUUsSUFBSSxHQUN0Qjs7QUFFRCwySEFHRztBQUVILEFBQUEsSUFBSSxDQUFBLEFBQUEsS0FBQyxBQUFBLEVBQU8sRUFDVixhQUFhLEVBQUUsSUFBSSxFQUFFLE9BQU8sQ0FDNUIsZUFBZSxFQUFFLFNBQVMsRUFBRSxPQUFPLENBQ25DLHVCQUF1QixFQUFFLGdCQUFnQixFQUNqQyxlQUFlLEVBQUUsZ0JBQWdCLEVBQUUsT0FBTyxFQUNuRDs7QUFFRCwrREFFRztBQUVILEFBQUEsQ0FBQyxFQUNELE1BQU0sQ0FBQyxFQUNMLFdBQVcsRUFBRSxNQUFNLEdBQ3BCOztBQUVELGdJQUdHO0FBRUgsQUFBQSxJQUFJLEVBQ0osR0FBRyxFQUNILElBQUksQ0FBQyxFQUNILFdBQVcsRUFBRSxvQkFBb0IsRUFBRSxPQUFPLENBQzFDLFNBQVMsRUFBRSxHQUFHLEVBQUUsT0FBTyxFQUN4Qjs7QUFFRCxpREFFRztBQUVILEFBQUEsS0FBSyxDQUFDLEVBQ0osU0FBUyxFQUFFLEdBQUcsR0FDZjs7QUFFRCx1RkFHRztBQUVILEFBQUEsR0FBRyxFQUNILEdBQUcsQ0FBQyxFQUNGLFNBQVMsRUFBRSxHQUFHLEVBQ2QsV0FBVyxFQUFFLENBQUMsRUFDZCxRQUFRLEVBQUUsUUFBUSxFQUNsQixjQUFjLEVBQUUsUUFBUSxHQUN6Qjs7QUFFRCxBQUFBLEdBQUcsQ0FBQyxFQUNGLE1BQU0sRUFBRSxPQUFPLEdBQ2hCOztBQUVELEFBQUEsR0FBRyxDQUFDLEVBQ0YsR0FBRyxFQUFFLE1BQU0sR0FDWjs7QUFFRCxpR0FDZ0Y7QUFFaEYseURBRUc7QUFFSCxBQUFBLEdBQUcsQ0FBQyxFQUNGLFlBQVksRUFBRSxJQUFJLEVBQ2xCLEtBQUssRUFBRSxJQUFJLEVBQ1gsTUFBTSxFQUFFLElBQUksRUFDWixjQUFjLEVBQUUsTUFBTSxHQUN2Qjs7QUFFRCxzRkFDZ0Y7QUFFaEYsNkZBR0c7QUFFSCxBQUFBLE1BQU0sRUFDTixLQUFLLEVBQ0wsUUFBUSxFQUNSLE1BQU0sRUFDTixRQUFRLENBQUMsRUFDUCxXQUFXLEVBQUUsT0FBTyxFQUFFLE9BQU8sQ0FDN0IsU0FBUyxFQUFFLElBQUksRUFBRSxPQUFPLENBQ3hCLFdBQVcsRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUMxQixNQUFNLEVBQUUsQ0FBQyxFQUFFLE9BQU8sRUFDbkI7O0FBRUQsNkRBR0c7QUFFSCxBQUFBLE1BQU0sRUFDTixLQUFLLENBQUMsRUFBRSxPQUFPLENBQ2IsUUFBUSxFQUFFLE9BQU8sR0FDbEI7O0FBRUQsaUlBR0c7QUFFSCxBQUFBLE1BQU0sRUFDTixNQUFNLENBQUMsRUFBRSxPQUFPLENBQ2QsY0FBYyxFQUFFLElBQUksR0FDckI7O0FBRUQsd0VBRUc7QUFFSCxBQUFBLE1BQU0sR0FDTixBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsSUFDRCxBQUFBLElBQUMsQ0FBSyxPQUFPLEFBQVosSUFDRCxBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsRUFBZSxFQUNkLGtCQUFrQixFQUFFLE1BQU0sR0FDM0I7O0FBRUQsc0RBRUc7QUFFSCxBQUFBLE1BQU0sQUFBQSxrQkFBa0IsR0FDeEIsQUFBQSxJQUFDLENBQUssUUFBUSxBQUFiLENBQWMsa0JBQWtCLEdBQ2pDLEFBQUEsSUFBQyxDQUFLLE9BQU8sQUFBWixDQUFhLGtCQUFrQixHQUNoQyxBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsQ0FBYyxrQkFBa0IsQ0FBQyxFQUNoQyxZQUFZLEVBQUUsSUFBSSxFQUNsQixPQUFPLEVBQUUsQ0FBQyxHQUNYOztBQUVELDJEQUVHO0FBRUgsQUFBQSxNQUFNLEFBQUEsZUFBZSxHQUNyQixBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsQ0FBYyxlQUFlLEdBQzlCLEFBQUEsSUFBQyxDQUFLLE9BQU8sQUFBWixDQUFhLGVBQWUsR0FDN0IsQUFBQSxJQUFDLENBQUssUUFBUSxBQUFiLENBQWMsZUFBZSxDQUFDLEVBQzdCLE9BQU8sRUFBRSxxQkFBcUIsR0FDL0I7O0FBRUQsc0NBRUc7QUFFSCxBQUFBLFFBQVEsQ0FBQyxFQUNQLE9BQU8sRUFBRSxxQkFBcUIsR0FDL0I7O0FBRUQsbU9BS0c7QUFFSCxBQUFBLE1BQU0sQ0FBQyxFQUNMLGtCQUFrQixFQUFFLFVBQVUsRUFDdEIsVUFBVSxFQUFFLFVBQVUsRUFBRSxPQUFPLENBQ3ZDLEtBQUssRUFBRSxPQUFPLEVBQUUsT0FBTyxDQUN2QixPQUFPLEVBQUUsS0FBSyxFQUFFLE9BQU8sQ0FDdkIsU0FBUyxFQUFFLElBQUksRUFBRSxPQUFPLENBQ3hCLE9BQU8sRUFBRSxDQUFDLEVBQUUsT0FBTyxDQUNuQixXQUFXLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFDN0I7O0FBRUQsd0VBRUc7QUFFSCxBQUFBLFFBQVEsQ0FBQyxFQUNQLGNBQWMsRUFBRSxRQUFRLEdBQ3pCOztBQUVELHVEQUVHO0FBRUgsQUFBQSxRQUFRLENBQUMsRUFDUCxRQUFRLEVBQUUsSUFBSSxHQUNmOztBQUVELDhFQUdHO0NBRUgsQUFBQSxBQUFBLElBQUMsQ0FBSyxVQUFVLEFBQWYsSUFDRCxBQUFBLElBQUMsQ0FBSyxPQUFPLEFBQVosRUFBYyxFQUNiLGtCQUFrQixFQUFFLFVBQVUsRUFDdEIsVUFBVSxFQUFFLFVBQVUsRUFBRSxPQUFPLENBQ3ZDLE9BQU8sRUFBRSxDQUFDLEVBQUUsT0FBTyxFQUNwQjs7QUFFRCw2RUFFRztDQUVILEFBQUEsQUFBQSxJQUFDLENBQUssUUFBUSxBQUFiLENBQWMsMkJBQTJCLEdBQzFDLEFBQUEsSUFBQyxDQUFLLFFBQVEsQUFBYixDQUFjLDJCQUEyQixDQUFDLEVBQ3pDLE1BQU0sRUFBRSxJQUFJLEdBQ2I7O0FBRUQsa0dBR0c7Q0FFSCxBQUFBLEFBQUEsSUFBQyxDQUFLLFFBQVEsQUFBYixFQUFlLEVBQ2Qsa0JBQWtCLEVBQUUsU0FBUyxFQUFFLE9BQU8sQ0FDdEMsY0FBYyxFQUFFLElBQUksRUFBRSxPQUFPLEVBQzlCOztBQUVELDhEQUVHO0NBRUgsQUFBQSxBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsQ0FBYywyQkFBMkIsQ0FBQyxFQUN6QyxrQkFBa0IsRUFBRSxJQUFJLEdBQ3pCOztBQUVELDZIQUdHO0FBRUgsQUFBQSw0QkFBNEIsQ0FBQyxFQUMzQixrQkFBa0IsRUFBRSxNQUFNLEVBQUUsT0FBTyxDQUNuQyxJQUFJLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFDdkI7O0FBRUQsNEZBQ2dGO0FBRWhGLDJEQUVHO0FBRUgsQUFBQSxPQUFPLENBQUMsRUFDTixPQUFPLEVBQUUsS0FBSyxHQUNmOztBQUVELDhDQUVHO0FBRUgsQUFBQSxPQUFPLENBQUMsRUFDTixPQUFPLEVBQUUsU0FBUyxHQUNuQjs7QUFFRCxxRkFDZ0Y7QUFFaEYseUNBRUc7QUFFSCxBQUFBLFFBQVEsQ0FBQyxFQUNQLE9BQU8sRUFBRSxJQUFJLEdBQ2Q7O0FBRUQsd0NBRUc7Q0FFSCxBQUFBLEFBQUEsTUFBQyxBQUFBLEVBQVEsRUFDUCxPQUFPLEVBQUUsSUFBSSxHQUNkOztBQ3BYRCxBQUFBLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLENBQUMsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLENBQUMsRUFBRSxJQUFJLENBQUEsRUFDNUIsV0FBVyxFSk1OLE1BQU0sRUlMWCxXQUFXLEVBQUUsT0FBTyxHQUNyQjs7QUFFRCxBQUNFLE1BREksQ0FDSixDQUFDLENBQUEsRUFDQyxTQUFTLEVIREQsTUFBTSxFR0VkLFdBQVcsRUhDTixJQUFJLEdHQVY7O0FBR0gsQUFDRSxLQURHLENBQ0gsQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFSFJELE1BQU0sRUdTZCxXQUFXLEVBQUUsSUFBSSxHQUNsQjs7QUFKSCxBQU1JLEtBTkMsQ0FLSCxXQUFXLENBQ1QsTUFBTSxDQUFBLEVBQ0osUUFBUSxFQUFFLFFBQVEsR0FVbkI7O0FBakJMLEFBUU0sS0FSRCxDQUtILFdBQVcsQ0FDVCxNQUFNLEFBRUgsT0FBTyxDQUFBLEVBQ04sT0FBTyxFQUFFLEtBQUssRUFDZCxRQUFRLEVBQUUsUUFBUSxFQUNsQixPQUFPLEVBQUUsRUFBRSxFQUNYLEtBQUssRUFBRSxJQUFJLEVBQ1gsTUFBTSxFQUFFLEdBQUcsRUFDWCxVQUFVLEVBQUUsS0FBSyxFQUNqQixNQUFNLEVBQUUsQ0FBQyxHQUNWOztBQWhCUCxBQWtCSSxLQWxCQyxDQUtILFdBQVcsQ0FhVCxDQUFDLENBQUEsRUFDQyxRQUFRLEVBQUUsUUFBUSxHQVluQjs7QUEvQkwsQUFzQk0sS0F0QkQsQ0FLSCxXQUFXLENBYVQsQ0FBQyxBQUlFLFNBQVMsQ0FBQSxFQUNSLFdBQVcsRUp1RFIsK0JBQStCLEdJdERuQzs7QUF4QlAsQUF5Qk0sS0F6QkQsQ0FLSCxXQUFXLENBYVQsQ0FBQyxBQU9FLFFBQVEsQ0FBQSxFQUNQLFdBQVcsRUpGWixNQUFNLEdJR047O0FBM0JQLEFBNEJNLEtBNUJELENBS0gsV0FBVyxDQWFULENBQUMsQUFVRSxXQUFXLENBQUEsRUFDVixXQUFXLEVKcUJYLE9BQU8sR0lwQlI7O0FBS1AsQUFDRSxLQURHLENBQ0gsY0FBYyxDQUFBLEVBQ1osT0FBTyxFQUFFLElBQUksRUFDYixNQUFNLEVIdkNILE1BQU0sQ0d1Q0ssQ0FBQyxHQVloQjs7QUFmSCxBQUlJLEtBSkMsQ0FDSCxjQUFjLEdBR1IsQ0FBQyxDQUFDLEVBQ0osWUFBWSxFSHpDWCxNQUFNLEVHMENQLFNBQVMsRUFBRSxPQUFPLEVBQ2xCLFdBQVcsRUFBRSxJQUFJLEdBQ2xCOztBQVJMLEFBU0ksS0FUQyxDQUNILGNBQWMsQ0FRWixFQUFFLENBQUEsRUFDQSxNQUFNLEVBQUUsQ0FBQyxDSDlDUixNQUFNLENHOENTLENBQUMsQ0FBQyxDQUFDLEdBSXBCOztBQWRMLEFBV00sS0FYRCxDQUNILGNBQWMsQ0FRWixFQUFFLENBRUEsQ0FBQyxDQUFBLEVBQ0MsV0FBVyxFQUFFLElBQUksR0FDbEI7O0FBS1AsQUFBQSxDQUFDLEFBQUEsU0FBUyxFQUFFLElBQUksQUFBQSxTQUFTLENBQUEsRUFDdkIsV0FBVyxFSndCRiwrQkFBK0IsR0l2QnpDOztBQUVELEFBQUEsQ0FBQyxBQUFBLFFBQVEsRUFBRSxJQUFJLEFBQUEsUUFBUSxDQUFBLEVBQ3JCLFdBQVcsRUpsQ04sTUFBTSxHSW1DWjs7QUFFRCxBQUFBLENBQUMsQUFBQSxXQUFXLEVBQUUsSUFBSSxBQUFBLFdBQVcsQ0FBQSxFQUMzQixXQUFXLEVKWkwsT0FBTyxHSWFkOztBQUVELEFBQ0UsY0FEWSxDQUNaLEVBQUUsRUFESixjQUFjLENBQ1IsQ0FBQyxDQUFBLEVBQ0gsU0FBUyxFQUFFLE1BQU0sRUFDakIsV0FBVyxFQUFFLE9BQU8sR0FDckI7O0FBR0gsQUFDRSxjQURZLENBQ1osRUFBRSxDQUFBLEVBQ0EsU0FBUyxFSGhGRCxNQUFNLEVHaUZkLE1BQU0sRUFBRSxDQUFDLEdBQ1Y7O0FBR0gsQUFDRSxLQURHLENBQ0gsRUFBRSxDQUFBLEVBQ0EsTUFBTSxFQUFFLENBQUMsRUFDVCxTQUFTLEVBQUUsSUFBSSxHQUNoQjs7QUMvRkgsQUFBQSxNQUFNLENBQUEsRUFDSixRQUFRLEVBQUUsS0FBSyxFQUNmLEdBQUcsRUFBRSxDQUFDLEVBQ04sSUFBSSxFQUFFLENBQUMsRUFDUCxNQUFNLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENKT1IsTUFBTSxFSU5YLE9BQU8sRUpNRixNQUFNLENJTkksQ0FBQyxFQUNoQixLQUFLLEVBQUUsbUJBQXlCLEVBQ2hDLGFBQWEsRUFBRSxlQUFlLEdBQy9COztBQ1JELEFBQUEsQ0FBQyxDQUFBLEVBQ0MsVUFBVSxFQUFFLFVBQVUsR0FDdkI7O0FBQ0QsQUFBQSxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQSxFQUNoQixVQUFVLEVBQUUsU0FBUyxHQUN0Qjs7QUFFRCxBQUNFLElBREUsQ0FDRixNQUFNLENBQUEsRUFDRixLQUFLLEVMTUosK0JBQW9DLEVLTHJDLE1BQU0sRUxDTCxNQUFNLENLRE8sSUFBSSxDQUFDLElBQUksQ0xDdEIsTUFBTSxHS2tFVjs7QUF0RUgsQUFNVSxJQU5OLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FDVCxDQUFDLENBQ0MsQ0FBQyxDQUFBLEVBQ0MsUUFBUSxFQUFFLFFBQVEsR0FJbkI7O0FBWFgsQUFRWSxJQVJSLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FDVCxDQUFDLENBQ0MsQ0FBQyxBQUVFLEtBQUssQ0FBQSxFQUNKLFlBQVksRUxMbkIsTUFBTSxHS01BOztBQVZiLEFBYVEsSUFiSixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUFBLEVBQ1IsS0FBSyxFQUFFLElBQUksR0FxRGQ7O0FBbkVQLEFBZVEsSUFmSixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUVWLEtBQUssQ0FBQSxFQUNILEtBQUssRUFBRSxnQ0FBbUMsRUFDMUMsTUFBTSxFQUFFLElBQUksRUFFWixPQUFPLEVBQUUsWUFBWSxFQUNyQixjQUFjLEVBQUUsR0FBRyxFQUNuQixXQUFXLEVBQUUsQ0FBQyxFQUNkLFlBQVksRUxsQmYsTUFBTSxFS21CSCxRQUFRLEVBQUUsUUFBUSxFQUNsQixVQUFVLEVMcEJiLE1BQU0sR0s4REo7O0FBbEVULEFBeUJVLElBekJOLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBRVYsS0FBSyxDQVVILENBQUMsQ0FBQSxFQUNDLE9BQU8sRUFBRSxLQUFLLEdBQ2Y7O0FBM0JYLEFBNEJVLElBNUJOLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBRVYsS0FBSyxDQWFILFlBQVksQ0FBQSxFQUNWLE9BQU8sRUFBRSxJQUFJLEdBTWQ7O0FBbkNYLEFBOEJZLElBOUJSLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBRVYsS0FBSyxDQWFILFlBQVksR0FFTixDQUFDLENBQUEsRUFDSCxTQUFTLEVBQUUsTUFBTSxFQUNqQixXQUFXLEVBQUUsSUFBSSxFQUNqQixZQUFZLEVMN0JuQixNQUFNLEdLOEJBOztBQWxDYixBQXFDWSxJQXJDUixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUVWLEtBQUssQUFxQkYsTUFBTSxDQUNMLFlBQVksQ0FBQSxFQUNWLEtBQUssRUFBRSxJQUFJLEVBQ1gsVUFBVSxFQUFFLHdCQUF3QixFQUNwQyxPQUFPLEVMcENkLE1BQU0sRUtxQ0MsT0FBTyxFQUFFLFdBQVcsRUFDcEIsU0FBUyxFQUFFLElBQUksRUFDZixRQUFRLEVBQUUsUUFBUSxFQUNsQixTQUFTLEVBQUUsaUJBQWlCLEdBQzdCOztBQTdDYixBQStDVSxJQS9DTixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUVWLEtBQUssQUFnQ0YsYUFBYSxDQUFBLEVBQ1osY0FBYyxFQUFFLFFBQVEsQ0FBQSxVQUFVLEdBQ25DOztBQWpEWCxBQWtEVSxJQWxETixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUVWLEtBQUssQ0FtQ0gsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFQUFFLG9CQUF1QixHQWMvQjs7QUFqRVgsQUFvRFksSUFwRFIsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FFVixLQUFLLENBbUNILEtBQUssQ0FFSCxjQUFjLENBQUEsRUFDWixNQUFNLEVBQUUsU0FBUyxHQUlsQjs7QUF6RGIsQUFzRGMsSUF0RFYsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FFVixLQUFLLENBbUNILEtBQUssQ0FFSCxjQUFjLENBRVosQ0FBQyxDQUFBLEVBQ0MsT0FBTyxFQUFFLEtBQUssR0FDZjs7QUF4RGYsQUEwRFksSUExRFIsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FFVixLQUFLLENBbUNILEtBQUssQ0FRSCxVQUFVLENBQUEsRUFDUixNQUFNLEVBQUUsU0FBUyxHQUtsQjs7QUFoRWIsQUE0RGMsSUE1RFYsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FFVixLQUFLLENBbUNILEtBQUssQ0FRSCxVQUFVLENBRVIsQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFQUFFLElBQUksRUFDZixXQUFXLEVBQUUsSUFBSSxHQUNsQjs7QUEvRGYsQUF3RUksSUF4RUEsQUF1RUQsSUFBSyxDQUFBLEtBQUssRUFDVCxNQUFNLENBQUEsRUFDSixVQUFVLEVBQUUsbUJBQXlCLEdBQ3RDOztBQ2pGTCxBQUFBLGNBQWMsQ0FBQSxFQUNaLFFBQVEsRUFBRSxLQUFLLEVBQ2YsS0FBSyxFQUFFLEdBQUcsRUFDVixNQUFNLEVBQUUsSUFBSSxFQUNaLEtBQUssRUFBRSxDQUFDLEVBQ1IsR0FBRyxFQUFFLENBQUMsRUFDTixPQUFPLEVBQUUsV0FBVyxHQXFCckI7O0FBM0JELEFBT0UsY0FQWSxDQU9aLGFBQWEsQ0FBQSxFQUNYLE1BQU0sRUFBRSxJQUFJLEdBa0JiOztBQTFCSCxBQVNJLGNBVFUsQ0FPWixhQUFhLENBRVgsRUFBRSxDQUFBLEVBSUEsTUFBTSxFTkZMLE1BQU0sQ0FBTixNQUFNLENNRWEsR0FBRyxDQUFDLENBQUMsRUFDekIsVUFBVSxFQUFFLEtBQUssR0FXbEI7O0FBekJMLEFBVU0sY0FWUSxDQU9aLGFBQWEsQ0FFWCxFQUFFLEFBQ0MsVUFBVyxDQUFBLENBQUMsRUFBQyxFQUNaLFVBQVUsRUFBRSxDQUFDLEdBQ2Q7O0FBWlAsQUFpQlUsY0FqQkksQ0FPWixhQUFhLENBRVgsRUFBRSxDQU1BLEVBQUUsQUFDQyxNQUFNLENBQ0wsQ0FBQyxDQUFBLEVBQ0MsS0FBSyxFQUFFLEtBQUssR0FDYjs7QUFuQlgsQUFzQk0sY0F0QlEsQ0FPWixhQUFhLENBRVgsRUFBRSxDQWFBLENBQUMsQ0FBQSxFQUNDLEtBQUssRUFBRSxJQUFJLEdBQ1o7O0FDeEJQLEFBQUEsVUFBVSxDQUFBLEVBQ04sT0FBTyxFQUFFLFdBQVcsRUFDcEIsU0FBUyxFQUFFLElBQUksRUFDZixLQUFLLEVBQUUsSUFBSSxHQWVkOztBQWxCRCxBQUlFLFVBSlEsQ0FJUixLQUFLLENBQUEsRUFDSCxLQUFLLEVQVUYsK0JBQW9DLEdPRXhDOztBQWpCSCxBQU1JLFVBTk0sQ0FJUixLQUFLLEFBRUYsVUFBVyxDQUFBLEdBQUcsRUFBQyxFQUNkLFlBQVksRVBLVixJQUFJLEdPSlA7O0FBUkwsQUFTSSxVQVRNLENBSVIsS0FBSyxDQUtILFlBQVksQ0FBQSxFQUNWLE9BQU8sRUFBRSxXQUFXLEVBQ3BCLEtBQUssRUFBRSxJQUFJLEVBQ1gsYUFBYSxFUERaLE1BQU0sR09LUjs7QUFoQkwsQUFhTSxVQWJJLENBSVIsS0FBSyxDQUtILFlBQVksR0FJTixDQUFDLENBQUMsRUFDSixZQUFZLEVQSGIsTUFBTSxHT0lOOztBQ2ZQLEFBQ0UsS0FERyxDQUNILFVBQVUsQ0FBQSxFQUNSLGVBQWUsRUFBRSxPQUFPLEVBQ3hCLFlBQVksRUFBRSxPQUFPLEVBQ2pCLE9BQU8sRUFBRSxPQUFPLEVBQ3BCLGFBQWEsRVJNVixNQUFNLEdRTFI7O0FBTkwsQUFRSSxLQVJDLENBT0gsV0FBVyxDQUNULEdBQUcsQ0FBQSxFQUNELGFBQWEsRVJFWixNQUFNLEdRRFI7O0FBSUwsQUFBQSxjQUFjLENBQUEsRUFDWixVQUFVLEVBQUUsZUFBZSxFQUMzQixPQUFPLEVBQUUsSUFBSSxFQUNiLFNBQVMsRUFBRSxJQUFJLEVBQ2YsVUFBVSxFUk5KLElBQUksRVFPVixhQUFhLEVSUFAsSUFBSSxHUXlCWDs7QUF2QkQsQUFNRSxjQU5ZLENBTVosRUFBRSxDQUFBLEVBQ0EsTUFBTSxFQUFFLElBQUksQ0FBQyxDQUFDLENSVlgsTUFBTSxDUVVZLENBQUMsRUFDdEIsS0FBSyxFQUFFLElBQUksR0FDWjs7QUFUSCxBQVVFLGNBVlksQ0FVWixLQUFLLENBQUEsRUFDSCxLQUFLLEVSVEYsK0JBQW9DLEdRb0J4Qzs7QUF0QkgsQUFZSSxjQVpVLENBVVosS0FBSyxBQUVGLFVBQVcsQ0FBQSxLQUFLLEVBQUMsRUFDaEIsWUFBWSxFUmhCWCxNQUFNLEdRaUJSOztBQWRMLEFBZUksY0FmVSxDQVVaLEtBQUssQ0FLSCxZQUFZLENBQUEsRUFDVixPQUFPLEVBQUUsSUFBSSxFQUNiLFNBQVMsRUFBRSxJQUFJLEdBSWhCOztBQXJCTCxBQWtCTSxjQWxCUSxDQVVaLEtBQUssQ0FLSCxZQUFZLEdBR04sQ0FBQyxDQUFBLEVBQ0gsWUFBWSxFUnRCYixNQUFNLEdRdUJOOztBQ2xDUCxNQUFNLENBQUMsTUFBTSxNQUFNLFNBQVMsRUFBRSxLQUFLLElBRWpDLEFBQ0UsTUFESSxDQUNKLENBQUMsQ0FBQSxFQUNDLFNBQVMsRVRlRCxNQUFNLEVTZGQsV0FBVyxFVGlCTixJQUFJLEdTaEJWO0VBRUgsQUFDRSxLQURHLENBQ0gsQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFVFNELE1BQU0sQ1NUUyxVQUFVLEVBQ2pDLFdBQVcsRVRXTixJQUFJLENTWGEsVUFBVSxHQUNqQztFQUVILEFBQ0UsY0FEWSxDQUNaLEVBQUUsQ0FBQSxFQUNBLFNBQVMsRVRHRCxNQUFNLEdTRmY7RUFJSCxBQUFBLE1BQU0sQ0FBQSxFQUNKLEtBQUssRUFBRSwwQkFBaUMsR0FDekM7RUFFRCxBQUNFLElBREUsQ0FDRixNQUFNLENBQUEsRUFDRixLQUFLLEVBQUUsMEJBQWlDLEdBQzNDO0VBSEgsQUFLSSxJQUxBLEFBSUQsSUFBSyxDQUFBLEtBQUssRUFDVCxNQUFNLENBQUEsRUFDSixVQUFVLEVUdEJULElBQUksR1N1Qk47RUFQTCxBQVNFLElBVEUsQ0FTRixjQUFjLENBQUEsRUFDWixPQUFPLEVBQUUsSUFBSSxHQUNkO0VBR0gsQUFDRSxVQURRLENBQ1IsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFQUFFLFdBQWlCLEdBVXpCO0VBWkgsQUFHSSxVQUhNLENBQ1IsS0FBSyxBQUVGLFVBQVcsQ0FBQSxHQUFHLEVBQUMsRUFDZCxZQUFZLEVBQUUsQ0FBQyxHQUNoQjtFQUxMLEFBTUksVUFOTSxDQUNSLEtBQUssQ0FLSCxZQUFZLENBQUEsRUFDVixhQUFhLEVUbkNkLE1BQU0sR1N1Q047RUFYTCxBQVFNLFVBUkksQ0FDUixLQUFLLENBS0gsWUFBWSxHQUVOLENBQUMsQ0FBQyxFQUNKLFlBQVksRVRyQ2YsTUFBTSxHU3NDSjtFQUtQLEFBQUEsY0FBYyxDQUFBLEVBQ1osVUFBVSxFVDNDTixJQUFJLEdTa0RUO0VBUkQsQUFFRSxjQUZZLENBRVosS0FBSyxDQUFBLEVBQ0gsS0FBSyxFVGxDTCxJQUFJLEdTc0NMO0VBUEgsQUFJSSxjQUpVLENBRVosS0FBSyxBQUVGLFVBQVcsQ0FBQSxLQUFLLEVBQUMsRUFDaEIsWUFBWSxFQUFFLENBQUMsR0FDaEI7O0FBS1AsTUFBTSxDQUFDLE1BQU0sTUFBTSxTQUFTLEVBQUUsS0FBSyxPQUFPLFdBQVcsRUFBRSxRQUFRLElBRTdELEFBQ0UsTUFESSxDQUNKLENBQUMsQ0FBQSxFQUNDLFNBQVMsRUFBRSxNQUFNLEVBQ2pCLFdBQVcsRUFBRSxJQUFJLEdBQ2xCO0VBRUgsQUFDRSxLQURHLENBQ0gsQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFVGpERCxNQUFNLENTaURTLFVBQVUsRUFDakMsV0FBVyxFVC9DTixJQUFJLENTK0NhLFVBQVUsR0FDakM7RUFFSCxBQUNFLGNBRFksQ0FDWixFQUFFLENBQUEsRUFDQSxTQUFTLEVUdkRELE1BQU0sR1N3RGY7RUFHSCxBQUVJLElBRkEsQUFDRCxJQUFLLENBQUEsS0FBSyxFQUNULE1BQU0sQ0FBQSxFQUNKLFVBQVUsRUFBRSxJQUFJLEdBQ2pCO0VBSUwsQUFDRSxVQURRLENBQ1IsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFQUFFLFdBQWlCLEdBVXpCO0VBWkgsQUFHSSxVQUhNLENBQ1IsS0FBSyxBQUVGLFVBQVcsQ0FBQSxHQUFHLEVBQUMsRUFDZCxZQUFZLEVBQUUsQ0FBQyxHQUNoQjtFQUxMLEFBTUksVUFOTSxDQUNSLEtBQUssQ0FLSCxZQUFZLENBQUEsRUFDVixhQUFhLEVUekZkLE1BQU0sR1M2Rk47RUFYTCxBQVFNLFVBUkksQ0FDUixLQUFLLENBS0gsWUFBWSxHQUVOLENBQUMsQ0FBQyxFQUNKLFlBQVksRVQzRmYsTUFBTSxHUzRGSjs7QUFTVCxNQUFNLENBQUMsTUFBTSxNQUFNLFNBQVMsRUFBRSxLQUFLLE9BQU8sV0FBVyxFQUFFLFNBQVMsSUFFOUQsQUFDRSxNQURJLENBQ0osQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFVGpHRCxNQUFNLEVTa0dkLFdBQVcsRVQvRk4sSUFBSSxHU2dHVjtFQUVILEFBQ0UsS0FERyxDQUNILENBQUMsQ0FBQSxFQUNDLFNBQVMsRVR2R0QsTUFBTSxDU3VHUyxVQUFVLEVBQ2pDLFdBQVcsRVRyR04sSUFBSSxDU3FHYSxVQUFVLEdBQ2pDO0VBRUgsQUFDRSxjQURZLENBQ1osRUFBRSxDQUFBLEVBQ0EsU0FBUyxFVDdHRCxNQUFNLEdTOEdmO0VBSUgsQUFBQSxNQUFNLENBQUEsRUFDSixLQUFLLEVBQUUsMEJBQWlDLEdBQ3pDO0VBRUQsQUFDRSxJQURFLENBQ0YsTUFBTSxDQUFBLEVBQ0YsS0FBSyxFQUFFLDBCQUFpQyxHQUMzQztFQUhILEFBS0ksSUFMQSxBQUlELElBQUssQ0FBQSxLQUFLLEVBQ1QsTUFBTSxDQUFBLEVBQ0osVUFBVSxFVHRJVCxJQUFJLEdTdUlOO0VBUEwsQUFTRSxJQVRFLENBU0YsY0FBYyxDQUFBLEVBQ1osT0FBTyxFQUFFLElBQUksR0FDZDtFQUdILEFBQ0UsVUFEUSxDQUNSLEtBQUssQ0FBQSxFQUNILEtBQUssRVQxSUosK0JBQW9DLEdTOEl0QztFQU5ILEFBR0ksVUFITSxDQUNSLEtBQUssQUFFRixVQUFXLENBQUEsR0FBRyxFQUFDLEVBQ2QsWUFBWSxFVC9JWixJQUFJLEdTZ0pMO0VBSUwsQUFDRSxjQURZLENBQ1osS0FBSyxDQUFBLEVBQ0gsS0FBSyxFVGxKSiwrQkFBb0MsR1NzSnRDO0VBTkgsQUFHSSxjQUhVLENBQ1osS0FBSyxBQUVGLFVBQVcsQ0FBQSxLQUFLLEVBQUMsRUFDaEIsWUFBWSxFVHpKYixNQUFNLEdTMEpOOztBQUtQLE1BQU0sQ0FBQyxNQUFNLE1BQU0sU0FBUyxFQUFFLEtBQUssT0FBTyxXQUFXLEVBQUUsUUFBUSxJQUM3RCxBQUNFLE1BREksQ0FDSixDQUFDLENBQUEsRUFDQyxTQUFTLEVBQUUsTUFBTSxFQUNqQixXQUFXLEVBQUUsSUFBSSxHQUNsQjtFQUVILEFBQ0UsS0FERyxDQUNILENBQUMsQ0FBQSxFQUNDLFNBQVMsRVR6SkQsTUFBTSxDU3lKUyxVQUFVLEVBQ2pDLFdBQVcsRVR2Sk4sSUFBSSxDU3VKYSxVQUFVLEdBQ2pDO0VBRUgsQUFDRSxjQURZLENBQ1osRUFBRSxDQUFBLEVBQ0EsU0FBUyxFVC9KRCxNQUFNLEdTZ0tmO0VBRUgsQUFFSSxJQUZBLEFBQ0QsSUFBSyxDQUFBLEtBQUssRUFDVCxNQUFNLENBQUEsRUFDSixVQUFVLEVBQUUsSUFBSSxHQUNqQjs7QUFLUCxNQUFNLENBQUMsTUFBTSxNQUFNLFNBQVMsRUFBRSxLQUFLLElBQ2pDLEFBQ0UsTUFESSxDQUNKLENBQUMsQ0FBQSxFQUNDLFNBQVMsRVRyTEQsTUFBTSxFU3NMZCxXQUFXLEVUbkxOLElBQUksR1NvTFY7RUFFSCxBQUNFLEtBREcsQ0FDSCxDQUFDLENBQUEsRUFDQyxTQUFTLEVUM0xELE1BQU0sQ1MyTFMsVUFBVSxFQUNqQyxXQUFXLEVUekxOLElBQUksQ1N5TGEsVUFBVSxHQUNqQztFQUVILEFBQ0UsY0FEWSxDQUNaLEVBQUUsQ0FBQSxFQUNBLFNBQVMsRVRqTUQsTUFBTSxHU2tNZjtFQUdILEFBRUksSUFGQSxBQUNELElBQUssQ0FBQSxLQUFLLEVBQ1QsTUFBTSxDQUFBLEVBQ0osVUFBVSxFVGxOVCxJQUFJLEdTbU5OIn0= */ +@media screen and (max-width: 1024px) and (orientation: landscape) { body #start { width: calc(( 80% )); } + body #start #text_figli #item_list .card { width: calc(( 100% / 3 ) - (1rem / 2)); } + body #start #text_figli #item_list .card #item { width: calc(300% + (.5rem * 3)); } } + +/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGhlbWUuY3NzIiwic291cmNlcyI6WyJ0aGVtZS5zY3NzIiwiY29uZmlndXJhdGlvbnMvX2ZvbnRzLnNjc3MiLCJjb25maWd1cmF0aW9ucy9fdmFyaWFibGUuc2NzcyIsImNvbmZpZ3VyYXRpb25zL19leHRlbmQuc2NzcyIsInRoZW1lL19yZXNldC5zY3NzIiwidGhlbWUvX3R5cG9ncmFwaHkuc2NzcyIsInRoZW1lL19hbmltYXRpb24uc2NzcyIsInRoZW1lL19oZWFkZXIuc2NzcyIsInRoZW1lL19zdGFydC5zY3NzIiwidGhlbWUvX3NpZGViYXIuc2NzcyIsInRoZW1lL19ibG9nLnNjc3MiLCJ0aGVtZS9faXRlbS5zY3NzIiwidGhlbWUvX21vYmlsZS5zY3NzIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIENvcmUgdmFyaWFibGVzIGFuZCBtaXhpbnNcbkBpbXBvcnQgJ2NvbmZpZ3VyYXRpb25zL19mb250cyc7XG5AaW1wb3J0ICdjb25maWd1cmF0aW9ucy9fdmFyaWFibGUnO1xuQGltcG9ydCAnY29uZmlndXJhdGlvbnMvX2V4dGVuZCc7XG5AaW1wb3J0ICd0aGVtZS9yZXNldCc7XG5AaW1wb3J0ICd0aGVtZS90eXBvZ3JhcGh5JztcblxuQGltcG9ydCAndGhlbWUvYW5pbWF0aW9uJztcbkBpbXBvcnQgJ3RoZW1lL2hlYWRlcic7XG5AaW1wb3J0ICd0aGVtZS9zdGFydCc7XG5AaW1wb3J0ICd0aGVtZS9zaWRlYmFyJztcbkBpbXBvcnQgJ3RoZW1lL2Jsb2cnO1xuQGltcG9ydCAndGhlbWUvaXRlbSc7XG5AaW1wb3J0ICd0aGVtZS9tb2JpbGUnO1xuIiwiLy8gRm9udCBGYW1pbHlcbiRmb250LWZhbWlseS1kZWZhdWx0OiAgXHRcdFx0XCJSYWxld2F5XCIsIFwiSGVsdmV0aWNhXCIsIFwiVGFob21hXCIsIFwiR2VuZXZhXCIsIFwiQXJpYWxcIiwgc2Fucy1zZXJpZjtcbiRmb250LWZhbWlseS1oZWFkZXI6ICBcdFx0XHRcIk1vbnRzZXJyYXRcIiwgXCJIZWx2ZXRpY2FcIiwgXCJUYWhvbWFcIiwgXCJHZW5ldmFcIiwgXCJBcmlhbFwiLCBzYW5zLXNlcmlmO1xuJGZvbnQtZmFtaWx5LW1vbm86IFx0XHRcdFx0XCJJbmNvbnNvbGF0YVwiLCBtb25vc3BhY2U7XG4kZm9udC1mYW1pbHktc2VyaWY6XHRcdFx0XHRcIkdlb3JnaWFcIiwgXCJUaW1lc1wiLCBcIlRpbWVzIE5ldyBSb21hblwiLCBzZXJpZjtcbi8vIEBmb250LWZhY2VcblxuJGxhdG86IFwibGF0b1wiO1xuJGZhbWlseTogXCJsYXRvXCI7XG4kZmlsZS1yZWd1bGFyOiBcIkxhdG8tUmVndWxhclwiO1xuJGNhdGVnb3J5OiBcInNhbnMtc2VyaWZcIjtcblxuQG1peGluIGZvbnQtZmFjZSgkbGF0bywgJGZpbGUtcmVndWxhciwgJGZhbWlseSwgJGNhdGVnb3J5OlwiXCIpIHtcbiAgICAkZmlsZXBhdGg6IFwiLi4vZm9udHMvXCIgKyAkZmFtaWx5ICsgXCIvXCIgKyAkZmlsZS1yZWd1bGFyO1xuICAgIEBmb250LWZhY2Uge1xuICAgICAgICBmb250LWZhbWlseTogXCIjeyRsYXRvfVwiO1xuICAgICAgICBzcmM6IHVybCgkZmlsZXBhdGggKyBcIi5lb3RcIik7XG4gICAgICAgIHNyYzogdXJsKCRmaWxlcGF0aCArIFwiLmVvdD8jaWVmaXhcIikgZm9ybWF0KCdlbWJlZGRlZC1vcGVudHlwZScpLFxuICAgICAgICAgICAgICB1cmwoJGZpbGVwYXRoICsgXCIud29mZlwiKSBmb3JtYXQoJ3dvZmYnKSxcbiAgICAgICAgICAgICAgdXJsKCRmaWxlcGF0aCArIFwiLnR0ZlwiKSAgZm9ybWF0KCd0cnVldHlwZScpLFxuICAgICAgICAgICAgICB1cmwoJGZpbGVwYXRoICsgXCIuc3ZnI1wiICsgJGxhdG8gKyBcIlwiKSBmb3JtYXQoJ3N2ZycpO1xuICAgIH1cblxuICAgICUjeyRsYXRvfSB7XG4gICAgICAgIGZvbnQ6IHtcbiAgICAgICAgICAgIEBpZiAkY2F0ZWdvcnkgIT0gXCJcIiB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JGxhdG99XCIsICN7JGNhdGVnb3J5fTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIEBlbHNlIHtcbiAgICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skbGF0b31cIjtcbiAgICAgICAgICAgICAgICB3ZWlnaHQ6IG5vcm1hbDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbn1cblxuJHN5bmU6IFwic3luZVwiO1xuJGZhbWlseS1ib2xkOiBcInN5bmVcIjtcbiRmaWxlLWJvbGQ6IFwiU3luZS1Cb2xkXCI7XG5cbkBtaXhpbiBmb250LWZhY2UoJHN5bmUsICRmaWxlLWJvbGQsICRmYW1pbHktYm9sZCwgJGNhdGVnb3J5OlwiXCIpIHtcbiAgICAkZmlsZXBhdGg6IFwiLi4vZm9udHMvXCIgKyAkZmFtaWx5LWJvbGQgKyBcIi9cIiArICRmaWxlLWJvbGQ7XG4gICAgQGZvbnQtZmFjZSB7XG4gICAgICAgIGZvbnQtZmFtaWx5OiBcIiN7JHN5bmV9XCI7XG4gICAgICAgIHNyYzogdXJsKCRmaWxlcGF0aCArIFwiLmVvdFwiKTtcbiAgICAgICAgc3JjOiB1cmwoJGZpbGVwYXRoICsgXCIuZW90PyNpZWZpeFwiKSBmb3JtYXQoJ2VtYmVkZGVkLW9wZW50eXBlJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi53b2ZmXCIpIGZvcm1hdCgnd29mZicpLFxuICAgIH1cblxuICAgICUjeyRzeW5lfSB7XG4gICAgICAgIGZvbnQ6IHtcbiAgICAgICAgICAgIEBpZiAkY2F0ZWdvcnkgIT0gXCJcIiB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JHN5bmV9XCIsICN7JGNhdGVnb3J5fTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIEBlbHNlIHtcbiAgICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skc3luZX1cIjtcbiAgICAgICAgICAgICAgICB3ZWlnaHQ6IG5vcm1hbDtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbn1cblxuJGF2YXJhOiBcIkF2YXJhXCI7XG4kZmFtaWx5LWJvbGQtaXRhbGljOiBcImF2YXJhXCI7XG4kZmlsZS1ib2xkLWl0YWxpYzogXCJBdmFyYS1Cb2xkX0l0YWxpY193ZWJcIjtcblxuQG1peGluIGZvbnQtZmFjZSgkYXZhcmEsICRmaWxlLWJvbGQtaXRhbGljLCAkZmFtaWx5LWJvbGQtaXRhbGljLCAkY2F0ZWdvcnk6XCJcIikge1xuICAgICRmaWxlcGF0aDogXCIuLi9mb250cy9cIiArICRmYW1pbHktYm9sZC1pdGFsaWMgKyBcIi9cIiArICRmaWxlLWJvbGQtaXRhbGljO1xuICAgIEBmb250LWZhY2Uge1xuICAgICAgICBmb250LWZhbWlseTogXCIjeyRhdmFyYX1cIjtcbiAgICAgICAgc3JjOiB1cmwoJGZpbGVwYXRoICsgXCIuZW90XCIpO1xuICAgICAgICBzcmM6IHVybCgkZmlsZXBhdGggKyBcIi5lb3Q/I2llZml4XCIpIGZvcm1hdCgnZW1iZWRkZWQtb3BlbnR5cGUnKSxcbiAgICAgICAgICAgICAgdXJsKCRmaWxlcGF0aCArIFwiLndvZmZcIikgZm9ybWF0KCd3b2ZmJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi50dGZcIikgIGZvcm1hdCgndHJ1ZXR5cGUnKSxcbiAgICAgICAgICAgICAgdXJsKCRmaWxlcGF0aCArIFwiLnN2ZyNcIiArICRhdmFyYSArIFwiXCIpIGZvcm1hdCgnc3ZnJyk7XG4gICAgfVxuXG4gICAgJSN7JGF2YXJhfSB7XG4gICAgICAgIGZvbnQ6IHtcbiAgICAgICAgICAgIEBpZiAkY2F0ZWdvcnkgIT0gXCJcIiB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JGF2YXJhfVwiLCAjeyRjYXRlZ29yeX07XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBAZWxzZSB7XG4gICAgICAgICAgICAgICAgZmFtaWx5OiBcIiN7JGF2YXJhfVwiO1xuICAgICAgICAgICAgICAgIHdlaWdodDogbm9ybWFsO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfVxufVxuXG4kcGxheWZhaXI6IFwicGxheWZhaXJkaXNwbGF5LXJlZ3VsYXJpdGFsaWNcIjtcbiRmYW1pbHktaXRhbGljOiBcInBsYXlmYWlyXCI7XG4kZmlsZS1pdGFsaWM6IFwicGxheWZhaXJkaXNwbGF5LXJlZ3VsYXJpdGFsaWNcIjtcblxuQG1peGluIGZvbnQtZmFjZSgkcGxheWZhaXIsICRmaWxlLWl0YWxpYywgJGZhbWlseS1pdGFsaWMsICRjYXRlZ29yeTpcIlwiKSB7XG4gICAgJGZpbGVwYXRoOiBcIi4uL2ZvbnRzL1wiICsgJGZhbWlseS1pdGFsaWMgKyBcIi9cIiArICRmaWxlLWl0YWxpYztcbiAgICBAZm9udC1mYWNlIHtcbiAgICAgICAgZm9udC1mYW1pbHk6IFwiI3skcGxheWZhaXJ9XCI7XG4gICAgICAgIHNyYzogdXJsKCRmaWxlcGF0aCArIFwiLmVvdFwiKTtcbiAgICAgICAgc3JjOiB1cmwoJGZpbGVwYXRoICsgXCIuZW90PyNpZWZpeFwiKSBmb3JtYXQoJ2VtYmVkZGVkLW9wZW50eXBlJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi53b2ZmXCIpIGZvcm1hdCgnd29mZicpLFxuICAgICAgICAgICAgICB1cmwoJGZpbGVwYXRoICsgXCIudHRmXCIpICBmb3JtYXQoJ3RydWV0eXBlJyksXG4gICAgICAgICAgICAgIHVybCgkZmlsZXBhdGggKyBcIi5zdmcjXCIgKyAkcGxheWZhaXIgKyBcIlwiKSBmb3JtYXQoJ3N2ZycpO1xuICAgIH1cblxuICAgICUjeyRwbGF5ZmFpcn0ge1xuICAgICAgICBmb250OiB7XG4gICAgICAgICAgICBAaWYgJGNhdGVnb3J5ICE9IFwiXCIge1xuICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skcGxheWZhaXJ9XCIsICN7JGNhdGVnb3J5fTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIEBlbHNlIHtcbiAgICAgICAgICAgICAgICBmYW1pbHk6IFwiI3skcGxheWZhaXJ9XCI7XG4gICAgICAgICAgICAgICAgd2VpZ2h0OiBub3JtYWw7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG59XG4iLCJAaW5jbHVkZSBmb250LWZhY2UoJGxhdG8sICRmaWxlLXJlZ3VsYXIsICRmYW1pbHksICRjYXRlZ29yeSk7Ly8xXG5AaW5jbHVkZSBmb250LWZhY2UoJHN5bmUsICRmaWxlLWJvbGQsICRmYW1pbHktYm9sZCwgJGNhdGVnb3J5KTsvLzJcbkBpbmNsdWRlIGZvbnQtZmFjZSgkYXZhcmEsICRmaWxlLWJvbGQtaXRhbGljLCAkZmFtaWx5LWJvbGQtaXRhbGljLCAkY2F0ZWdvcnkpOy8vMlxuQGluY2x1ZGUgZm9udC1mYWNlKCRwbGF5ZmFpciwgJGZpbGUtaXRhbGljLCAkZmFtaWx5LWl0YWxpYywgJGNhdGVnb3J5KTsvLzJcblxuLy8gZm9udC1zaXplXG4kdHh0LWZpZ2xpOiAyLjVyZW07XG5cbi8vIGxpbmUtaGVpZ2h0XG4kRmxob21lOiAzcmVtO1xuXG4kbWFyZzogMC41cmVtO1xuJG1hcmdJOiAxcmVtO1xuJFdpOiA4MCU7XG5cbiRXaTUwOiBjYWxjKCggMTAwJSAvIDIgKSAtICgjeyRtYXJnSX0gLyAyKSk7XG4kV2kzMzogY2FsYygoIDEwMCUgLyAzICkgLSAoI3skbWFyZ0l9IC8gMykpO1xuXG4vLyBtb2JpbGVcbiR0eHQtZmlnbGktbTogMS41cmVtO1xuXG4vLyBsaW5lLWhlaWdodFxuJEZsaG9tZS1tOiAycmVtO1xuJFdpLW06MTAwJTtcbiRXaTgwLW06IGNhbGMoKCA4MCUgKSk7XG4kV2kzMy1tOiBjYWxjKCggMTAwJSAvIDMgKSAtICgjeyRtYXJnSX0gLyAyKSk7XG5cblxuLy8gdGFibGV0dGVcbiR0eHQtZmlnbGktdDogMi41cmVtO1xuXG4vLyBsaW5lLWhlaWdodFxuJEZsaG9tZS10OiAzcmVtO1xuXG4vLyBjb2xvclxuJGNvbG9yX3B1YmxpcXVlOiBsaWdodHllbGxvdztcbiRjb2xvcl9zb2NpYWxlOiBsaWdodGdyZWVuO1xuJGNvbG9yX2N1bHR1cmVsbGU6IGxpZ2h0Ymx1ZTtcbiIsIiIsIi8qISBub3JtYWxpemUuY3NzIHY4LjAuMSB8IE1JVCBMaWNlbnNlIHwgZ2l0aHViLmNvbS9uZWNvbGFzL25vcm1hbGl6ZS5jc3MgKi9cblxuLyogRG9jdW1lbnRcbiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovXG5cbi8qKlxuICogMS4gQ29ycmVjdCB0aGUgbGluZSBoZWlnaHQgaW4gYWxsIGJyb3dzZXJzLlxuICogMi4gUHJldmVudCBhZGp1c3RtZW50cyBvZiBmb250IHNpemUgYWZ0ZXIgb3JpZW50YXRpb24gY2hhbmdlcyBpbiBpT1MuXG4gKi9cblxuaHRtbCB7XG4gIG1hcmdpbjogMDtcbiAgcGFkZGluZzogMDtcbiAgbGluZS1oZWlnaHQ6IDEuMTU7IC8qIDEgKi9cbiAgLXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OiAxMDAlOyAvKiAyICovXG4gIGZvbnQtc2l6ZTogMTZweDtcbn1cblxuLyogU2VjdGlvbnNcbiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovXG5cbi8qKlxuICogUmVtb3ZlIHRoZSBtYXJnaW4gaW4gYWxsIGJyb3dzZXJzLlxuICovXG5cbmJvZHkge1xuICBtYXJnaW46IDA7XG4gIHBhZGRpbmc6IDA7XG59XG5cbi8qKlxuICogUmVuZGVyIHRoZSBgbWFpbmAgZWxlbWVudCBjb25zaXN0ZW50bHkgaW4gSUUuXG4gKi9cblxubWFpbiB7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuXG4vKipcbiAqIENvcnJlY3QgdGhlIGZvbnQgc2l6ZSBhbmQgbWFyZ2luIG9uIGBoMWAgZWxlbWVudHMgd2l0aGluIGBzZWN0aW9uYCBhbmRcbiAqIGBhcnRpY2xlYCBjb250ZXh0cyBpbiBDaHJvbWUsIEZpcmVmb3gsIGFuZCBTYWZhcmkuXG4gKi9cblxuaDEge1xuICBmb250LXNpemU6IGF1dG87XG4gIG1hcmdpbjowO1xufVxuXG4vKiBHcm91cGluZyBjb250ZW50XG4gICA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSAqL1xuXG4vKipcbiAqIDEuIEFkZCB0aGUgY29ycmVjdCBib3ggc2l6aW5nIGluIEZpcmVmb3guXG4gKiAyLiBTaG93IHRoZSBvdmVyZmxvdyBpbiBFZGdlIGFuZCBJRS5cbiAqL1xuXG5ociB7XG4gIC13ZWJraXQtYm94LXNpemluZzogY29udGVudC1ib3g7XG4gICAgICAgICAgYm94LXNpemluZzogY29udGVudC1ib3g7IC8qIDEgKi9cbiAgaGVpZ2h0OiAwOyAvKiAxICovXG4gIG92ZXJmbG93OiB2aXNpYmxlOyAvKiAyICovXG59XG5cbi8qKlxuICogMS4gQ29ycmVjdCB0aGUgaW5oZXJpdGFuY2UgYW5kIHNjYWxpbmcgb2YgZm9udCBzaXplIGluIGFsbCBicm93c2Vycy5cbiAqIDIuIENvcnJlY3QgdGhlIG9kZCBgZW1gIGZvbnQgc2l6aW5nIGluIGFsbCBicm93c2Vycy5cbiAqL1xuXG5wcmUge1xuICBmb250LWZhbWlseTogbW9ub3NwYWNlLCBtb25vc3BhY2U7IC8qIDEgKi9cbiAgZm9udC1zaXplOiAxZW07IC8qIDIgKi9cbn1cblxuLyogVGV4dC1sZXZlbCBzZW1hbnRpY3NcbiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovXG5cbi8qKlxuICogUmVtb3ZlIHRoZSBncmF5IGJhY2tncm91bmQgb24gYWN0aXZlIGxpbmtzIGluIElFIDEwLlxuICovXG5we1xuIG1hcmdpbjogMDtcbn1cblxudWx7XG4gIGxpc3Qtc3R5bGU6IG5vbmU7XG59XG51bCxsaXtcbiAgbWFyZ2luOiAwO1xuICBwYWRkaW5nOiAwO1xufVxuXG5hIHtcbiAgYmFja2dyb3VuZC1jb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGNvbG9yOiBibGFjaztcbiAgdGV4dC1kZWNvcmF0aW9uOiBub25lO1xufVxuXG4vKipcbiAqIDEuIFJlbW92ZSB0aGUgYm90dG9tIGJvcmRlciBpbiBDaHJvbWUgNTctXG4gKiAyLiBBZGQgdGhlIGNvcnJlY3QgdGV4dCBkZWNvcmF0aW9uIGluIENocm9tZSwgRWRnZSwgSUUsIE9wZXJhLCBhbmQgU2FmYXJpLlxuICovXG5cbmFiYnJbdGl0bGVdIHtcbiAgYm9yZGVyLWJvdHRvbTogbm9uZTsgLyogMSAqL1xuICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsgLyogMiAqL1xuICAtd2Via2l0LXRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lIGRvdHRlZDtcbiAgICAgICAgICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZSBkb3R0ZWQ7IC8qIDIgKi9cbn1cblxuLyoqXG4gKiBBZGQgdGhlIGNvcnJlY3QgZm9udCB3ZWlnaHQgaW4gQ2hyb21lLCBFZGdlLCBhbmQgU2FmYXJpLlxuICovXG5cbmIsXG5zdHJvbmcge1xuICBmb250LXdlaWdodDogYm9sZGVyO1xufVxuXG4vKipcbiAqIDEuIENvcnJlY3QgdGhlIGluaGVyaXRhbmNlIGFuZCBzY2FsaW5nIG9mIGZvbnQgc2l6ZSBpbiBhbGwgYnJvd3NlcnMuXG4gKiAyLiBDb3JyZWN0IHRoZSBvZGQgYGVtYCBmb250IHNpemluZyBpbiBhbGwgYnJvd3NlcnMuXG4gKi9cblxuY29kZSxcbmtiZCxcbnNhbXAge1xuICBmb250LWZhbWlseTogbW9ub3NwYWNlLCBtb25vc3BhY2U7IC8qIDEgKi9cbiAgZm9udC1zaXplOiAxZW07IC8qIDIgKi9cbn1cblxuLyoqXG4gKiBBZGQgdGhlIGNvcnJlY3QgZm9udCBzaXplIGluIGFsbCBicm93c2Vycy5cbiAqL1xuXG5zbWFsbCB7XG4gIGZvbnQtc2l6ZTogODAlO1xufVxuXG4vKipcbiAqIFByZXZlbnQgYHN1YmAgYW5kIGBzdXBgIGVsZW1lbnRzIGZyb20gYWZmZWN0aW5nIHRoZSBsaW5lIGhlaWdodCBpblxuICogYWxsIGJyb3dzZXJzLlxuICovXG5cbnN1YixcbnN1cCB7XG4gIGZvbnQtc2l6ZTogNzUlO1xuICBsaW5lLWhlaWdodDogMDtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICB2ZXJ0aWNhbC1hbGlnbjogYmFzZWxpbmU7XG59XG5cbnN1YiB7XG4gIGJvdHRvbTogLTAuMjVlbTtcbn1cblxuc3VwIHtcbiAgdG9wOiAtMC41ZW07XG59XG5cbi8qIEVtYmVkZGVkIGNvbnRlbnRcbiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovXG5cbi8qKlxuICogUmVtb3ZlIHRoZSBib3JkZXIgb24gaW1hZ2VzIGluc2lkZSBsaW5rcyBpbiBJRSAxMC5cbiAqL1xuXG5pbWcge1xuICBib3JkZXItc3R5bGU6IG5vbmU7XG4gIHdpZHRoOiAxMDAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIHZlcnRpY2FsLWFsaWduOiBib3R0b207XG59XG5cbi8qIEZvcm1zXG4gICA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSAqL1xuXG4vKipcbiAqIDEuIENoYW5nZSB0aGUgZm9udCBzdHlsZXMgaW4gYWxsIGJyb3dzZXJzLlxuICogMi4gUmVtb3ZlIHRoZSBtYXJnaW4gaW4gRmlyZWZveCBhbmQgU2FmYXJpLlxuICovXG5cbmJ1dHRvbixcbmlucHV0LFxub3B0Z3JvdXAsXG5zZWxlY3QsXG50ZXh0YXJlYSB7XG4gIGZvbnQtZmFtaWx5OiBpbmhlcml0OyAvKiAxICovXG4gIGZvbnQtc2l6ZTogMTAwJTsgLyogMSAqL1xuICBsaW5lLWhlaWdodDogMS4xNTsgLyogMSAqL1xuICBtYXJnaW46IDA7IC8qIDIgKi9cbn1cblxuLyoqXG4gKiBTaG93IHRoZSBvdmVyZmxvdyBpbiBJRS5cbiAqIDEuIFNob3cgdGhlIG92ZXJmbG93IGluIEVkZ2UuXG4gKi9cblxuYnV0dG9uLFxuaW5wdXQgeyAvKiAxICovXG4gIG92ZXJmbG93OiB2aXNpYmxlO1xufVxuXG4vKipcbiAqIFJlbW92ZSB0aGUgaW5oZXJpdGFuY2Ugb2YgdGV4dCB0cmFuc2Zvcm0gaW4gRWRnZSwgRmlyZWZveCwgYW5kIElFLlxuICogMS4gUmVtb3ZlIHRoZSBpbmhlcml0YW5jZSBvZiB0ZXh0IHRyYW5zZm9ybSBpbiBGaXJlZm94LlxuICovXG5cbmJ1dHRvbixcbnNlbGVjdCB7IC8qIDEgKi9cbiAgdGV4dC10cmFuc2Zvcm06IG5vbmU7XG59XG5cbi8qKlxuICogQ29ycmVjdCB0aGUgaW5hYmlsaXR5IHRvIHN0eWxlIGNsaWNrYWJsZSB0eXBlcyBpbiBpT1MgYW5kIFNhZmFyaS5cbiAqL1xuXG5idXR0b24sXG5bdHlwZT1cImJ1dHRvblwiXSxcblt0eXBlPVwicmVzZXRcIl0sXG5bdHlwZT1cInN1Ym1pdFwiXSB7XG4gIC13ZWJraXQtYXBwZWFyYW5jZTogYnV0dG9uO1xufVxuXG4vKipcbiAqIFJlbW92ZSB0aGUgaW5uZXIgYm9yZGVyIGFuZCBwYWRkaW5nIGluIEZpcmVmb3guXG4gKi9cblxuYnV0dG9uOjotbW96LWZvY3VzLWlubmVyLFxuW3R5cGU9XCJidXR0b25cIl06Oi1tb3otZm9jdXMtaW5uZXIsXG5bdHlwZT1cInJlc2V0XCJdOjotbW96LWZvY3VzLWlubmVyLFxuW3R5cGU9XCJzdWJtaXRcIl06Oi1tb3otZm9jdXMtaW5uZXIge1xuICBib3JkZXItc3R5bGU6IG5vbmU7XG4gIHBhZGRpbmc6IDA7XG59XG5cbi8qKlxuICogUmVzdG9yZSB0aGUgZm9jdXMgc3R5bGVzIHVuc2V0IGJ5IHRoZSBwcmV2aW91cyBydWxlLlxuICovXG5cbmJ1dHRvbjotbW96LWZvY3VzcmluZyxcblt0eXBlPVwiYnV0dG9uXCJdOi1tb3otZm9jdXNyaW5nLFxuW3R5cGU9XCJyZXNldFwiXTotbW96LWZvY3VzcmluZyxcblt0eXBlPVwic3VibWl0XCJdOi1tb3otZm9jdXNyaW5nIHtcbiAgb3V0bGluZTogMXB4IGRvdHRlZCBCdXR0b25UZXh0O1xufVxuXG4vKipcbiAqIENvcnJlY3QgdGhlIHBhZGRpbmcgaW4gRmlyZWZveC5cbiAqL1xuXG5maWVsZHNldCB7XG4gIHBhZGRpbmc6IDAuMzVlbSAwLjc1ZW0gMC42MjVlbTtcbn1cblxuLyoqXG4gKiAxLiBDb3JyZWN0IHRoZSB0ZXh0IHdyYXBwaW5nIGluIEVkZ2UgYW5kIElFLlxuICogMi4gQ29ycmVjdCB0aGUgY29sb3IgaW5oZXJpdGFuY2UgZnJvbSBgZmllbGRzZXRgIGVsZW1lbnRzIGluIElFLlxuICogMy4gUmVtb3ZlIHRoZSBwYWRkaW5nIHNvIGRldmVsb3BlcnMgYXJlIG5vdCBjYXVnaHQgb3V0IHdoZW4gdGhleSB6ZXJvIG91dFxuICogICAgYGZpZWxkc2V0YCBlbGVtZW50cyBpbiBhbGwgYnJvd3NlcnMuXG4gKi9cblxubGVnZW5kIHtcbiAgLXdlYmtpdC1ib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICAgICAgICAgIGJveC1zaXppbmc6IGJvcmRlci1ib3g7IC8qIDEgKi9cbiAgY29sb3I6IGluaGVyaXQ7IC8qIDIgKi9cbiAgZGlzcGxheTogdGFibGU7IC8qIDEgKi9cbiAgbWF4LXdpZHRoOiAxMDAlOyAvKiAxICovXG4gIHBhZGRpbmc6IDA7IC8qIDMgKi9cbiAgd2hpdGUtc3BhY2U6IG5vcm1hbDsgLyogMSAqL1xufVxuXG4vKipcbiAqIEFkZCB0aGUgY29ycmVjdCB2ZXJ0aWNhbCBhbGlnbm1lbnQgaW4gQ2hyb21lLCBGaXJlZm94LCBhbmQgT3BlcmEuXG4gKi9cblxucHJvZ3Jlc3Mge1xuICB2ZXJ0aWNhbC1hbGlnbjogYmFzZWxpbmU7XG59XG5cbi8qKlxuICogUmVtb3ZlIHRoZSBkZWZhdWx0IHZlcnRpY2FsIHNjcm9sbGJhciBpbiBJRSAxMCsuXG4gKi9cblxudGV4dGFyZWEge1xuICBvdmVyZmxvdzogYXV0bztcbn1cblxuLyoqXG4gKiAxLiBBZGQgdGhlIGNvcnJlY3QgYm94IHNpemluZyBpbiBJRSAxMC5cbiAqIDIuIFJlbW92ZSB0aGUgcGFkZGluZyBpbiBJRSAxMC5cbiAqL1xuXG5bdHlwZT1cImNoZWNrYm94XCJdLFxuW3R5cGU9XCJyYWRpb1wiXSB7XG4gIC13ZWJraXQtYm94LXNpemluZzogYm9yZGVyLWJveDtcbiAgICAgICAgICBib3gtc2l6aW5nOiBib3JkZXItYm94OyAvKiAxICovXG4gIHBhZGRpbmc6IDA7IC8qIDIgKi9cbn1cblxuLyoqXG4gKiBDb3JyZWN0IHRoZSBjdXJzb3Igc3R5bGUgb2YgaW5jcmVtZW50IGFuZCBkZWNyZW1lbnQgYnV0dG9ucyBpbiBDaHJvbWUuXG4gKi9cblxuW3R5cGU9XCJudW1iZXJcIl06Oi13ZWJraXQtaW5uZXItc3Bpbi1idXR0b24sXG5bdHlwZT1cIm51bWJlclwiXTo6LXdlYmtpdC1vdXRlci1zcGluLWJ1dHRvbiB7XG4gIGhlaWdodDogYXV0bztcbn1cblxuLyoqXG4gKiAxLiBDb3JyZWN0IHRoZSBvZGQgYXBwZWFyYW5jZSBpbiBDaHJvbWUgYW5kIFNhZmFyaS5cbiAqIDIuIENvcnJlY3QgdGhlIG91dGxpbmUgc3R5bGUgaW4gU2FmYXJpLlxuICovXG5cblt0eXBlPVwic2VhcmNoXCJdIHtcbiAgLXdlYmtpdC1hcHBlYXJhbmNlOiB0ZXh0ZmllbGQ7IC8qIDEgKi9cbiAgb3V0bGluZS1vZmZzZXQ6IC0ycHg7IC8qIDIgKi9cbn1cblxuLyoqXG4gKiBSZW1vdmUgdGhlIGlubmVyIHBhZGRpbmcgaW4gQ2hyb21lIGFuZCBTYWZhcmkgb24gbWFjT1MuXG4gKi9cblxuW3R5cGU9XCJzZWFyY2hcIl06Oi13ZWJraXQtc2VhcmNoLWRlY29yYXRpb24ge1xuICAtd2Via2l0LWFwcGVhcmFuY2U6IG5vbmU7XG59XG5cbi8qKlxuICogMS4gQ29ycmVjdCB0aGUgaW5hYmlsaXR5IHRvIHN0eWxlIGNsaWNrYWJsZSB0eXBlcyBpbiBpT1MgYW5kIFNhZmFyaS5cbiAqIDIuIENoYW5nZSBmb250IHByb3BlcnRpZXMgdG8gYGluaGVyaXRgIGluIFNhZmFyaS5cbiAqL1xuXG46Oi13ZWJraXQtZmlsZS11cGxvYWQtYnV0dG9uIHtcbiAgLXdlYmtpdC1hcHBlYXJhbmNlOiBidXR0b247IC8qIDEgKi9cbiAgZm9udDogaW5oZXJpdDsgLyogMiAqL1xufVxuXG4vKiBJbnRlcmFjdGl2ZVxuICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi9cblxuLypcbiAqIEFkZCB0aGUgY29ycmVjdCBkaXNwbGF5IGluIEVkZ2UsIElFIDEwKywgYW5kIEZpcmVmb3guXG4gKi9cblxuZGV0YWlscyB7XG4gIGRpc3BsYXk6IGJsb2NrO1xufVxuXG4vKlxuICogQWRkIHRoZSBjb3JyZWN0IGRpc3BsYXkgaW4gYWxsIGJyb3dzZXJzLlxuICovXG5cbnN1bW1hcnkge1xuICBkaXNwbGF5OiBsaXN0LWl0ZW07XG59XG5cbi8qIE1pc2NcbiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovXG5cbi8qKlxuICogQWRkIHRoZSBjb3JyZWN0IGRpc3BsYXkgaW4gSUUgMTArLlxuICovXG5cbnRlbXBsYXRlIHtcbiAgZGlzcGxheTogbm9uZTtcbn1cblxuLyoqXG4gKiBBZGQgdGhlIGNvcnJlY3QgZGlzcGxheSBpbiBJRSAxMC5cbiAqL1xuXG5baGlkZGVuXSB7XG4gIGRpc3BsYXk6IG5vbmU7XG59XG4iLCJoMSwgaDIsIGgzLCBwLCB1bCwgbGksIGEsIHNwYW57XG4gIGZvbnQtZmFtaWx5OiAkbGF0bztcbiAgZm9udC13ZWlnaHQ6IGxpZ2h0ZXI7XG59XG5cbmhlYWRlcntcbiAgYXtcbiAgICBmb250LXNpemU6ICR0eHQtZmlnbGk7XG4gICAgbGluZS1oZWlnaHQ6ICRGbGhvbWU7XG4gIH1cbn1cblxuLmhvbWV7XG4gIHB7XG4gICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpO1xuICAgIGxpbmUtaGVpZ2h0OiAzcmVtO1xuICB9XG4gICN0ZXh0X2ZpZ2xpe1xuICAgIHN0cm9uZ3tcbiAgICAgIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgICAgICY6OmFmdGVye1xuICAgICAgICBkaXNwbGF5OiBibG9jaztcbiAgICAgICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgICAgICBjb250ZW50OiBcIlwiO1xuICAgICAgICB3aWR0aDogMTAwJTtcbiAgICAgICAgaGVpZ2h0OiAxcHg7XG4gICAgICAgIGJhY2tncm91bmQ6IGJsYWNrO1xuICAgICAgICBib3R0b206IDA7XG4gICAgICB9XG4gICAgfVxuICAgIGF7XG4gICAgICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gICAgICAmOm50aC1jaGlsZCgxKXtcbiAgICAgIH1cbiAgICAgICYjcHVibGlxdWV7XG4gICAgICAgIGZvbnQtZmFtaWx5OiAkcGxheWZhaXI7XG4gICAgICB9XG4gICAgICAmI3NvY2lhbGV7XG4gICAgICAgIGZvbnQtZmFtaWx5OiAkc3luZTtcbiAgICAgIH1cbiAgICAgICYjY3VsdHVyZWxsZXtcbiAgICAgICAgZm9udC1mYW1pbHk6ICRhdmFyYTtcbiAgICAgIH1cbiAgICB9XG4gICAgI2l0ZW17XG4gICAgICAuZS1jb250ZW50e1xuICAgICAgICBwe1xuICAgICAgICAgIGZvbnQtc2l6ZTogMXJlbTtcbiAgICAgICAgICBsaW5lLWhlaWdodDogMS4ycmVtO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG59XG5cbiNpdGVte1xuICBoMntcbiAgICB3aWR0aDogYXV0bztcbiAgICBsaW5lLWhlaWdodDogNHJlbSFpbXBvcnRhbnQ7XG4gICAgZm9udC1zaXplOiA0cmVtIWltcG9ydGFudDtcbiAgICBmb250LXdlaWdodDogbm9ybWFsIWltcG9ydGFudDtcbiAgfVxuICBzcGFue1xuICAgIGZvbnQtc2l6ZTogMXJlbTtcbiB9XG4gJi5wdWJsaXF1ZXtcbiAgIGgye1xuICAgICBmb250LWZhbWlseTogJHBsYXlmYWlyO1xuICAgfVxuICAgc3BhbntcbiAgICAgZm9udC1mYW1pbHk6ICRwbGF5ZmFpcjtcbiAgICAgZm9udC13ZWlnaHQ6IG5vcm1hbDtcbiAgIH1cbiB9XG4gJi5zb2NpYWxle1xuICAgaDJ7XG4gICAgIGZvbnQtZmFtaWx5OiAkc3luZTtcbiAgIH1cbiAgIHNwYW57XG4gICAgIGZvbnQtZmFtaWx5OiAkc3luZTtcbiAgICAgZm9udC13ZWlnaHQ6IG5vcm1hbDtcbiAgIH1cbiB9XG4gICYuY3VsdHVyZWxsZXtcbiAgICBoMntcbiAgICAgIGZvbnQtZmFtaWx5OiAkYXZhcmE7XG4gICAgfVxuICAgIHNwYW57XG4gICAgICBmb250LWZhbWlseTogJGF2YXJhO1xuICAgICAgZm9udC13ZWlnaHQ6IG5vcm1hbDtcbiAgICB9XG4gIH1cbn1cblxuYS5wdWJsaXF1ZSwgc3Bhbi5wdWJsaXF1ZXtcbiAgZm9udC1mYW1pbHk6ICRwbGF5ZmFpcjtcbn1cblxuYS5zb2NpYWxlLCBzcGFuLnNvY2lhbGV7XG4gIGZvbnQtZmFtaWx5OiAkc3luZTtcbn1cblxuYS5jdWx0dXJlbGxlLCBzcGFuLmN1bHR1cmVsbGV7XG4gIGZvbnQtZmFtaWx5OiAkYXZhcmE7XG59XG5cbi5zaWRlYmFyLXJpZ2h0e1xuICB1bCwgYXtcbiAgICBmb250LXNpemU6IDAuN3JlbTtcbiAgICBsaW5lLWhlaWdodDogMC45cmVtO1xuICB9XG59XG5cbi5vdGhlcl9wcm9qZXRze1xuICBoM3tcbiAgICBmb250LXNpemU6ICR0eHQtZmlnbGk7XG4gICAgbWFyZ2luOiAwO1xuICB9XG59XG5cbi5jYXJke1xuICBoMntcbiAgICBtYXJnaW46IDA7XG4gICAgZm9udC1zaXplOiAxcmVtO1xuICB9XG59XG4iLCIkYW5pbWF0ZTogYWxsIDAuMnMgZWFzZS1pbi1vdXQ7XG5cbiN0ZXh0X2ZpZ2xpe1xuICB6LWluZGV4OiA5OTk7XG4gIHB7XG4gICAgJiA+IGF7XG4gICAgICB6LWluZGV4OiA5OTk7XG4gICAgICAmOmJlZm9yZSxcbiAgICAgICY6YWZ0ZXIge1xuICAgICAgICBjb250ZW50OiBcIlwiO1xuICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgICAgIGJvdHRvbTogLTFweDtcbiAgICAgICAgd2lkdGg6IDBweDtcbiAgICAgICAgaGVpZ2h0OiAxcHg7XG4gICAgICAgIG1hcmdpbjogMXB4IDAgMDtcbiAgICAgICAgdHJhbnNpdGlvbjogJGFuaW1hdGU7XG4gICAgICAgIHRyYW5zaXRpb24tZHVyYXRpb246IDAuM3M7XG4gICAgICAgIG9wYWNpdHk6IDA7XG4gICAgICAgIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xuICAgICAgfVxuICAgICAgJjo6YmVmb3Jle1xuICAgICAgICBsZWZ0OiA1MCU7XG4gICAgICB9XG4gICAgICAmOmFmdGVye1xuICAgICAgICByaWdodDogNTAlO1xuICAgICAgfVxuICAgICAgJjpob3ZlciB7XG4gICAgICAgIGN1cnNvcjogcG9pbnRlcjtcbiAgICAgICAgJjpiZWZvcmUsXG4gICAgICAgICY6YWZ0ZXIge1xuICAgICAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgICAgIG9wYWNpdHk6IDE7XG4gICAgICAgIH1cbiAgICAgICAgJjpiZWZvcmUsXG4gICAgICAgICY6YWZ0ZXIge1xuICAgICAgICAgIHdpZHRoOiA1MCU7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cblxuLmNpcmNsZV9ob3ZlcntcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB3aWR0aDogMHB4O1xuICBoZWlnaHQ6IDBweDtcbiAgYmFja2dyb3VuZDogJGNvbG9yX2N1bHR1cmVsbGU7XG4gIGJvcmRlci1yYWRpdXM6IDUwMHB4O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTtcbiAgei1pbmRleDogMDtcbiAgdHJhbnNpdGlvbjogaGVpZ2h0IC4ycywgd2lkdGggLjJzO1xuICAmLm9wZW57XG4gICAgd2lkdGg6IDIwMHB4O1xuICAgIGhlaWdodDogMjAwcHg7XG4gICAgdHJhbnNpdGlvbjogaGVpZ2h0IC41cywgd2lkdGggLjVzO1xuICB9XG4gICYucHVibGlxdWV7XG4gICAgYmFja2dyb3VuZDogJGNvbG9yX3B1YmxpcXVlO1xuICB9XG4gICYuc29jaWFsZXtcbiAgICBiYWNrZ3JvdW5kOiAkY29sb3Jfc29jaWFsZTtcbiAgfVxuICAmLmN1bHR1cmVsbGV7XG4gICAgYmFja2dyb3VuZDogJGNvbG9yX2N1bHR1cmVsbGU7XG4gIH1cbn1cbiIsImhlYWRlcntcbiAgcG9zaXRpb246IGZpeGVkO1xuICB0b3A6IDA7XG4gIGxlZnQ6IDA7XG4gIG1hcmdpbjogMCAwIDAgJG1hcmc7XG4gIHBhZGRpbmc6ICRtYXJnIDA7XG4gIHdpZHRoOiBjYWxjKCAjeyRXaX0gLSAjeyRtYXJnfSApO1xuICBib3JkZXItYm90dG9tOiAxcHggc29saWQgYmxhY2s7XG59XG4iLCIqe1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xufVxuXG5odG1sLCBib2R5LCBoZWFkZXJ7XG4gIGJhY2tncm91bmQ6ICNmOWY5ZjlmZTtcbn1cblxuYm9keXtcbiAgI3N0YXJ0e1xuICAgICAgd2lkdGg6ICRXaTUwO1xuICAgICAgbWFyZ2luOiAkbWFyZyBhdXRvIGF1dG8gJG1hcmc7XG4gICAgICAjdGV4dF9maWdsaXtcbiAgICAgICAgcCB7XG4gICAgICAgICAgYXtcbiAgICAgICAgICAgIC8vIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgICAgICAgICAgICYubWFyZ3tcbiAgICAgICAgICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgI2l0ZW1fbGlzdHtcbiAgICAgICAgICB3aWR0aDogODB2dztcbiAgICAgICAgICBtYXJnaW4tdG9wOiAwLjVyZW07XG4gICAgICAgIC5jYXJke1xuICAgICAgICAgIHdpZHRoOiBjYWxjKCgxMDAlIC8gNSkgLSAoICN7JG1hcmd9ICogMikgKTtcbiAgICAgICAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gICAgICAgICAgdmVydGljYWwtYWxpZ246IHRvcDtcbiAgICAgICAgICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gICAgICAgICAgbWFyZ2luOiAwICRtYXJnICRtYXJnIDA7XG4gICAgICAgICAgaGVpZ2h0OiAxMDAlO1xuICAgICAgICAgICYgPiAuY2FyZC1pbWFnZXtcbiAgICAgICAgICAgICYub3BlbntcbiAgICAgICAgICAgICAgYXtcbiAgICAgICAgICAgICAgICBpbWd7XG4gICAgICAgICAgICAgICAgICBmaWx0ZXI6IGdyYXlzY2FsZSgwKTtcbiAgICAgICAgICAgICAgICAgIHRyYW5zaXRpb246IGZpbHRlciAwLjNzIGVhc2U7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICAgIGF7XG4gICAgICAgICAgICBkaXNwbGF5OiBibG9jaztcbiAgICAgICAgICAgIGJvcmRlci1yYWRpdXM6IDE1cHg7XG4gICAgICAgICAgICBvdmVyZmxvdzogYXV0bztcbiAgICAgICAgICAgIGltZ3tcbiAgICAgICAgICAgICAgZmlsdGVyOiBncmF5c2NhbGUoMSk7XG4gICAgICAgICAgICAgIHRyYW5zaXRpb246IGZpbHRlciAwLjNzIGVhc2U7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICAgIC5jYXJkLWhlYWRlcntcbiAgICAgICAgICAgIHdpZHRoOiAxMDAlO1xuICAgICAgICAgICAgcGFkZGluZzogJG1hcmc7XG4gICAgICAgICAgICBmbGV4LXdyYXA6IHdyYXA7XG4gICAgICAgICAgICBkaXNwbGF5OiBub25lO1xuICAgICAgICAgICAgJiA+ICp7XG4gICAgICAgICAgICAgIGZvbnQtc2l6ZTogMC44cmVtO1xuICAgICAgICAgICAgICBsaW5lLWhlaWdodDogMXJlbTtcbiAgICAgICAgICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgICAgJjpob3ZlcntcbiAgICAgICAgICAgIGF7XG4gICAgICAgICAgICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgICAgICAgICAgICBpbWd7XG4gICAgICAgICAgICAgICAgZmlsdGVyOiBncmF5c2NhbGUoMCk7XG4gICAgICAgICAgICAgICAgdHJhbnNpdGlvbjogZmlsdGVyIDAuM3MgZWFzZTtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgICAgLmNhcmQtaGVhZGVye1xuICAgICAgICAgICAgICBkaXNwbGF5OiBpbmxpbmUtZmxleDtcbiAgICAgICAgICAgICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgICAgICAgICAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoLTEwMCUpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgICAmOmxhc3Qtb2YtdHlwZXtcbiAgICAgICAgICAgIHZlcnRpY2FsLWFsaWduOiBiYXNlbGluZSFpbXBvcnRhbnQ7XG4gICAgICAgICAgfVxuICAgICAgICAgICNpdGVte1xuICAgICAgICAgICAgd2lkdGg6IGNhbGMoIDUwMCUgKyAoICN7JG1hcmd9ICogNSkgKTtcbiAgICAgICAgICAgIHBhZGRpbmc6IDJyZW07XG4gICAgICAgICAgICBtYXJnaW4tdG9wOiAkbWFyZztcbiAgICAgICAgICAgIGJvcmRlci1yYWRpdXM6IDE1cHg7XG4gICAgICAgICAgICAuY29udGVudC10aXRsZXtcbiAgICAgICAgICAgICAgbWFyZ2luOiAwIGF1dG8gMCBhdXRvO1xuICAgICAgICAgICAgICBkaXNwbGF5OiBmbGV4O1xuICAgICAgICAgICAgICBmbGV4LXdyYXA6IHdyYXA7XG4gICAgICAgICAgICAgIGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2VlbjtcbiAgICAgICAgICAgICAgYWxpZ24taXRlbXM6IGJhc2VsaW5lO1xuICAgICAgICAgICAgICBhe1xuICAgICAgICAgICAgICAgIGRpc3BsYXk6IGJsb2NrO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAuZS1jb250ZW50e1xuICAgICAgICAgICAgICBtYXJnaW46IDFyZW0gYXV0byAycmVtIGF1dG87XG4gICAgICAgICAgICAgIHB7XG4gICAgICAgICAgICAgICAgZm9udC1zaXplOiAxcmVtO1xuICAgICAgICAgICAgICAgIGxpbmUtaGVpZ2h0OiAxLjJyZW07XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgICYucHVibGlxdWV7XG4gICAgICAgICAgICAgIGJhY2tncm91bmQ6ICRjb2xvcl9wdWJsaXF1ZTtcblxuICAgICAgICAgICAgfVxuICAgICAgICAgICAgJi5zb2NpYWxle1xuICAgICAgICAgICAgICBiYWNrZ3JvdW5kOiAkY29sb3Jfc29jaWFsZTtcblxuICAgICAgICAgICAgfVxuICAgICAgICAgICAgJi5jdWx0dXJlbGxle1xuICAgICAgICAgICAgICBiYWNrZ3JvdW5kOiAkY29sb3JfY3VsdHVyZWxsZTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgJi5wdWJsaXF1ZXtcbiAgICAgICAgICAuY2FyZHtcbiAgICAgICAgICAgICY6aG92ZXJ7XG4gICAgICAgICAgICAgIC5jYXJkLWhlYWRlcntcbiAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kOiAkY29sb3JfcHVibGlxdWU7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgJi5zb2NpYWxle1xuICAgICAgICAgIC5jYXJke1xuICAgICAgICAgICAgJjpob3ZlcntcbiAgICAgICAgICAgICAgLmNhcmQtaGVhZGVye1xuICAgICAgICAgICAgICAgIGJhY2tncm91bmQ6ICRjb2xvcl9zb2NpYWxlO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgICYuY3VsdHVyZWxsZXtcbiAgICAgICAgICAuY2FyZHtcbiAgICAgICAgICAgICY6aG92ZXJ7XG4gICAgICAgICAgICAgIC5jYXJkLWhlYWRlcntcbiAgICAgICAgICAgICAgICBiYWNrZ3JvdW5kOiAkY29sb3JfY3VsdHVyZWxsZTtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxuICAmOm5vdCguaG9tZSl7XG4gICAgI3N0YXJ0e1xuICAgICAgbWFyZ2luLXRvcDogY2FsYyggMnJlbSArICAjeyRGbGhvbWV9KTtcbiAgICB9XG4gIH1cbn1cbiIsIi5zaWRlYmFyLXJpZ2h0e1xuICBwb3NpdGlvbjogZml4ZWQ7XG4gIHdpZHRoOiAxNSU7XG4gIGhlaWdodDogMTAwJTtcbiAgcmlnaHQ6IDA7XG4gIHRvcDogMDtcbiAgZGlzcGxheTogaW5saW5lLWZsZXg7XG4gIC5saXN0LXByb2pldHN7XG4gICAgbWFyZ2luOiBhdXRvO1xuICAgIHVse1xuICAgICAgJjpudGgtY2hpbGQoMSl7XG4gICAgICAgIG1hcmdpbi10b3A6IDA7XG4gICAgICB9XG4gICAgICBtYXJnaW46ICRtYXJnICRtYXJnIDBweCAwO1xuICAgICAgdGV4dC1hbGlnbjogcmlnaHQ7XG4gICAgICBsaXtcbiAgICAgICAgJjpob3ZlcntcbiAgICAgICAgICBhe1xuICAgICAgICAgICAgY29sb3I6IGJsYWNrO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgICAgYXtcbiAgICAgICAgY29sb3I6IGdyZXk7XG4gICAgICAgICY6OmJlZm9yZXtcbiAgICAgICAgICBjb250ZW50OiBcIiBcIjtcbiAgICAgICAgICB3aWR0aDogNXB4O1xuICAgICAgICAgIGhlaWdodDogNXB4O1xuICAgICAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgICAgICAgICBib3JkZXItcmFkaXVzOiA1cHg7XG4gICAgICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTtcbiAgICAgICAgICBtYXJnaW4tcmlnaHQ6IDVweDtcbiAgICAgICAgfVxuICAgICAgICAmI2N1bHR1cmVsbGV7XG4gICAgICAgICAgJjo6YmVmb3Jle1xuICAgICAgICAgICAgYmFja2dyb3VuZDogJGNvbG9yX2N1bHR1cmVsbGU7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgICYjc29jaWFsZXtcbiAgICAgICAgICAmOjpiZWZvcmV7XG4gICAgICAgICAgYmFja2dyb3VuZDogJGNvbG9yX3NvY2lhbGU7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgICYjcHVibGlxdWV7XG4gICAgICAgICAgJjo6YmVmb3Jle1xuICAgICAgICAgIGJhY2tncm91bmQ6ICRjb2xvcl9wdWJsaXF1ZTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgJi5hY3RpdmV7XG4gICAgICAgICAgY29sb3I6IGJsYWNrO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG59XG4iLCIjaXRlbV9saXN0e1xuICAgIGRpc3BsYXk6IGlubGluZS1mbGV4O1xuICAgIGZsZXgtd3JhcDogd3JhcDtcbiAgICB3aWR0aDogMTAwJTtcbiAgLmNhcmR7XG4gICAgd2lkdGg6ICRXaTUwO1xuICAgICY6bnRoLWNoaWxkKG9kZCl7XG4gICAgICBtYXJnaW4tcmlnaHQ6ICRtYXJnSTtcbiAgICB9XG4gICAgLmNhcmQtaGVhZGVye1xuICAgICAgZGlzcGxheTogaW5saW5lLWZsZXg7XG4gICAgICB3aWR0aDogMTAwJTtcbiAgICAgIG1hcmdpbi1ib3R0b206ICRtYXJnO1xuICAgICAgJiA+ICoge1xuICAgICAgICBtYXJnaW4tcmlnaHQ6ICRtYXJnO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIiwiI2l0ZW17XG4gIC5lLWNvbnRlbnR7XG4gICAgLXdlYmtpdC1jb2x1bW5zOiAzMDBweCAyO1xuICAgIC1tb3otY29sdW1uczogMzAwcHggMjtcbiAgICAgICAgY29sdW1uczogMzAwcHggMjtcbiAgICBtYXJnaW4tYm90dG9tOiAkbWFyZztcbiAgICB9XG4gIC5jYXJkLWltYWdle1xuICAgIGltZzpub3QoOmxhc3Qtb2YtdHlwZSl7XG4gICAgICBtYXJnaW4tYm90dG9tOiAkbWFyZztcbiAgICB9XG4gIH1cbn1cblxuLm90aGVyX3Byb2pldHN7XG4gIGJvcmRlci10b3A6IDFweCBzb2xpZCBibGFjaztcbiAgZGlzcGxheTogZmxleDtcbiAgZmxleC13cmFwOiB3cmFwO1xuICBtYXJnaW4tdG9wOiAkbWFyZ0k7XG4gIG1hcmdpbi1ib3R0b206ICRtYXJnSTtcbiAgaDN7XG4gICAgbWFyZ2luOiAxcmVtIDAgJG1hcmcgMDtcbiAgICB3aWR0aDogMTAwJTtcbiAgfVxuICAuY2FyZHtcbiAgICB3aWR0aDogJFdpMzM7XG4gICAgJjpudGgtY2hpbGQoLTFuKzMpe1xuICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICB9XG4gICAgLmNhcmQtaGVhZGVye1xuICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgIGZsZXgtd3JhcDogd3JhcDtcbiAgICAgICYgPiAqe1xuICAgICAgICBtYXJnaW4tcmlnaHQ6ICRtYXJnO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuIiwiQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogNDE0cHgpIHtcbiAgYm9keXtcbiAgICAjc3RhcnR7XG4gICAgICB3aWR0aDogJFdpLW07XG4gICAgICAjdGV4dF9maWdsaXtcbiAgICAgICAgI2l0ZW1fbGlzdHtcbiAgICAgICAgICAuY2FyZHtcbiAgICAgICAgICAgIHdpZHRoOiAkV2ktbTtcbiAgICAgICAgICAgICNpdGVte1xuICAgICAgICAgICAgICB3aWR0aDogY2FsYygxMDAlICsgKC41cmVtICogMykpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG5cbiAgLy8gdHlwb2dyYXBoaWVcbiAgaGVhZGVye1xuICAgIGF7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbTtcbiAgICAgIGxpbmUtaGVpZ2h0OiAkRmxob21lLW07XG4gICAgfVxuICB9XG4gIC5ob21le1xuICAgIHB7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbSFpbXBvcnRhbnQ7XG4gICAgICBsaW5lLWhlaWdodDogJEZsaG9tZS1tIWltcG9ydGFudDtcbiAgICB9XG4gIH1cbiAgLm90aGVyX3Byb2pldHN7XG4gICAgaDN7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktbTtcbiAgICB9XG4gIH1cblxuICAvLyBtZXBcbiAgaGVhZGVye1xuICAgIHdpZHRoOiBjYWxjKCAjeyRXaS1tfSAtICgjeyRtYXJnfSAqIDIpICk7XG4gIH1cblxuICBib2R5e1xuICAgICNzdGFydHtcbiAgICAgICAgd2lkdGg6IGNhbGMoICN7JFdpLW19IC0gKCN7JG1hcmd9ICogMikgKTtcbiAgICB9XG4gICAgJjpub3QoLmhvbWUpe1xuICAgICAgI3N0YXJ0e1xuICAgICAgICBtYXJnaW4tdG9wOiAkRmxob21lO1xuICAgICAgfVxuICAgIH1cbiAgICAuc2lkZWJhci1yaWdodHtcbiAgICAgIGRpc3BsYXk6IG5vbmU7XG4gICAgfVxuICB9XG5cbiAgI2l0ZW1fbGlzdHtcbiAgICAuY2FyZHtcbiAgICAgIHdpZHRoOiBjYWxjKCAjeyRXaS1tfSAgKSFpbXBvcnRhbnQ7XG4gICAgICAmOm50aC1jaGlsZChvZGQpe1xuICAgICAgICBtYXJnaW4tcmlnaHQ6IDA7XG4gICAgICB9XG4gICAgICAuY2FyZC1oZWFkZXJ7XG4gICAgICAgIG1hcmdpbi1ib3R0b206ICRtYXJnO1xuICAgICAgICAmID4gKiB7XG4gICAgICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZztcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIC5vdGhlcl9wcm9qZXRze1xuICAgIG1hcmdpbi10b3A6ICRtYXJnSTtcbiAgICAuY2FyZHtcbiAgICAgIHdpZHRoOiAkV2ktbTtcbiAgICAgICY6bnRoLWNoaWxkKC0xbiszKXtcbiAgICAgICAgbWFyZ2luLXJpZ2h0OiAwO1xuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG5AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOiA3NjhweCkgIGFuZCAob3JpZW50YXRpb246IHBvcnRyYWl0KSB7XG5cbiAgYm9keXtcbiAgICAjc3RhcnR7XG4gICAgICB3aWR0aDogJFdpODAtbTtcbiAgICAgICN0ZXh0X2ZpZ2xpe1xuICAgICAgICAjaXRlbV9saXN0e1xuICAgICAgICAgIC5jYXJke1xuICAgICAgICAgICAgd2lkdGg6ICRXaTMzLW07XG4gICAgICAgICAgICAjaXRlbXtcbiAgICAgICAgICAgICAgd2lkdGg6IGNhbGMoMzAwJSArICguNXJlbSAqIDMpKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAvLyB0eXBvZ3JhcGhpZVxuICBoZWFkZXJ7XG4gICAgYXtcbiAgICAgIGZvbnQtc2l6ZTogMi41cmVtO1xuICAgICAgbGluZS1oZWlnaHQ6IDNyZW07XG4gICAgfVxuICB9XG4gIC8vIC5ob21le1xuICAvLyAgIHB7XG4gIC8vICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktdCFpbXBvcnRhbnQ7XG4gIC8vICAgICBsaW5lLWhlaWdodDogJEZsaG9tZS10IWltcG9ydGFudDtcbiAgLy8gICB9XG4gIC8vIH1cbiAgLm90aGVyX3Byb2pldHN7XG4gICAgaDN7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktdDtcbiAgICB9XG4gIH1cblxuICBib2R5e1xuICAgICY6bm90KC5ob21lKXtcbiAgICAgICNzdGFydHtcbiAgICAgICAgbWFyZ2luLXRvcDogNXJlbTtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAjaXRlbV9saXN0e1xuICAgIC5jYXJke1xuICAgICAgd2lkdGg6IGNhbGMoICN7JFdpLW19ICApO1xuICAgICAgJjpudGgtY2hpbGQob2RkKXtcbiAgICAgICAgbWFyZ2luLXJpZ2h0OiAwO1xuICAgICAgfVxuICAgICAgLmNhcmQtaGVhZGVye1xuICAgICAgICBtYXJnaW4tYm90dG9tOiAkbWFyZztcbiAgICAgICAgJiA+ICoge1xuICAgICAgICAgIG1hcmdpbi1yaWdodDogJG1hcmc7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9XG4gIH1cbn1cblxuQG1lZGlhIHNjcmVlbiBhbmQgKG1heC13aWR0aDogODEycHgpIGFuZCAob3JpZW50YXRpb246IGxhbmRzY2FwZSkge1xuICAvLyB0eXBvZ3JhcGhpZVxuICBoZWFkZXJ7XG4gICAgYXtcbiAgICAgIGZvbnQtc2l6ZTogJHR4dC1maWdsaS1tO1xuICAgICAgbGluZS1oZWlnaHQ6ICRGbGhvbWUtbTtcbiAgICB9XG4gIH1cbiAgLmhvbWV7XG4gICAgcHtcbiAgICAgIGZvbnQtc2l6ZTogJHR4dC1maWdsaS1tIWltcG9ydGFudDtcbiAgICAgIGxpbmUtaGVpZ2h0OiAkRmxob21lLW0haW1wb3J0YW50O1xuICAgIH1cbiAgfVxuICAub3RoZXJfcHJvamV0c3tcbiAgICBoM3tcbiAgICAgIGZvbnQtc2l6ZTogJHR4dC1maWdsaS1tO1xuICAgIH1cbiAgfVxuXG4gIC8vIG1lcFxuICBoZWFkZXJ7XG4gICAgd2lkdGg6IGNhbGMoICN7JFdpLW19IC0gKCN7JG1hcmd9ICogMikgKTtcbiAgfVxuXG4gIGJvZHl7XG4gICAgI3N0YXJ0e1xuICAgICAgICB3aWR0aDogY2FsYyggI3skV2ktbX0gLSAoI3skbWFyZ30gKiAyKSApO1xuICAgIH1cbiAgICAmOm5vdCguaG9tZSl7XG4gICAgICAjc3RhcnR7XG4gICAgICAgIG1hcmdpbi10b3A6ICRGbGhvbWU7XG4gICAgICB9XG4gICAgfVxuICAgIC5zaWRlYmFyLXJpZ2h0e1xuICAgICAgZGlzcGxheTogbm9uZTtcbiAgICB9XG4gIH1cblxuICAjaXRlbV9saXN0e1xuICAgIC5jYXJke1xuICAgICAgd2lkdGg6ICRXaTUwO1xuICAgICAgJjpudGgtY2hpbGQob2RkKXtcbiAgICAgICAgbWFyZ2luLXJpZ2h0OiAkbWFyZ0k7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLm90aGVyX3Byb2pldHN7XG4gICAgLmNhcmR7XG4gICAgICB3aWR0aDogJFdpMzM7XG4gICAgICAmOm50aC1jaGlsZCgtMW4rMyl7XG4gICAgICAgIG1hcmdpbi1yaWdodDogJG1hcmc7XG4gICAgICB9XG4gICAgfVxuICB9XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDkxMHB4KSBhbmQgKG9yaWVudGF0aW9uOiBwb3J0cmFpdCkge1xuICBoZWFkZXJ7XG4gICAgYXtcbiAgICAgIGZvbnQtc2l6ZTogMi41cmVtO1xuICAgICAgbGluZS1oZWlnaHQ6IDNyZW07XG4gICAgfVxuICB9XG4gIC5ob21le1xuICAgIHB7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktdCFpbXBvcnRhbnQ7XG4gICAgICBsaW5lLWhlaWdodDogJEZsaG9tZS10IWltcG9ydGFudDtcbiAgICB9XG4gIH1cbiAgLm90aGVyX3Byb2pldHN7XG4gICAgaDN7XG4gICAgICBmb250LXNpemU6ICR0eHQtZmlnbGktdDtcbiAgICB9XG4gIH1cbiAgYm9keXtcbiAgICAmOm5vdCguaG9tZSl7XG4gICAgICAjc3RhcnR7XG4gICAgICAgIG1hcmdpbi10b3A6IDVyZW07XG4gICAgICB9XG4gICAgfVxuICB9XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDY2MHB4KSB7XG4gIGhlYWRlcntcbiAgICBhe1xuICAgICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpLW07XG4gICAgICBsaW5lLWhlaWdodDogJEZsaG9tZS1tO1xuICAgIH1cbiAgfVxuICAuaG9tZXtcbiAgICBwe1xuICAgICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpLW0haW1wb3J0YW50O1xuICAgICAgbGluZS1oZWlnaHQ6ICRGbGhvbWUtbSFpbXBvcnRhbnQ7XG4gICAgfVxuICB9XG4gIC5vdGhlcl9wcm9qZXRze1xuICAgIGgze1xuICAgICAgZm9udC1zaXplOiAkdHh0LWZpZ2xpLW07XG4gICAgfVxuICB9XG5cbiAgYm9keXtcbiAgICAmOm5vdCguaG9tZSl7XG4gICAgICAjc3RhcnR7XG4gICAgICAgIG1hcmdpbi10b3A6ICRGbGhvbWU7XG4gICAgICB9XG4gICAgfVxuICB9XG59XG5cbkBtZWRpYSBzY3JlZW4gYW5kIChtYXgtd2lkdGg6IDEwMjRweCkgYW5kIChvcmllbnRhdGlvbjogbGFuZHNjYXBlKSB7XG4gIGJvZHl7XG4gICAgI3N0YXJ0e1xuICAgICAgd2lkdGg6ICRXaTgwLW07XG4gICAgICAjdGV4dF9maWdsaXtcbiAgICAgICAgI2l0ZW1fbGlzdHtcbiAgICAgICAgICAuY2FyZHtcbiAgICAgICAgICAgIHdpZHRoOiAkV2kzMy1tO1xuICAgICAgICAgICAgI2l0ZW17XG4gICAgICAgICAgICAgIHdpZHRoOiBjYWxjKDMwMCUgKyAoLjVyZW0gKiAzKSk7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuICB9XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFDZ0dJLFVBQVUsR0FDTixXQUFXLEVBQUUsTUFBYyxFQUMzQixHQUFHLEVBQUUscUNBQXVCLEVBQzVCLEdBQUcsRUFBRSw0Q0FBOEIsQ0FBQywyQkFBMkIsRUFDekQsc0NBQXdCLENBQUMsY0FBYyxFQUN2QyxxQ0FBdUIsQ0FBRSxrQkFBa0IsRUFDM0MsMENBQXlDLENBQUMsYUFBYTs7QUFOakUsVUFBVSxHQUNOLFdBQVcsRUFBRSxNQUFjLEVBQzNCLEdBQUcsRUFBRSxrQ0FBdUIsRUFDNUIsR0FBRyxFQUFFLHlDQUE4QixDQUFDLDJCQUEyQixFQUN6RCxtQ0FBd0IsQ0FBQyxjQUFjLEVBQ3ZDLGtDQUF1QixDQUFFLGtCQUFrQixFQUMzQyx1Q0FBeUMsQ0FBQyxhQUFhOztBQU5qRSxVQUFVLEdBQ04sV0FBVyxFQUFFLE9BQWMsRUFDM0IsR0FBRyxFQUFFLCtDQUF1QixFQUM1QixHQUFHLEVBQUUsc0RBQThCLENBQUMsMkJBQTJCLEVBQ3pELGdEQUF3QixDQUFDLGNBQWMsRUFDdkMsK0NBQXVCLENBQUUsa0JBQWtCLEVBQzNDLHFEQUF5QyxDQUFDLGFBQWE7O0FBTmpFLFVBQVUsR0FDTixXQUFXLEVBQUUsK0JBQWMsRUFDM0IsR0FBRyxFQUFFLDBEQUF1QixFQUM1QixHQUFHLEVBQUUsaUVBQThCLENBQUMsMkJBQTJCLEVBQ3pELDJEQUF3QixDQUFDLGNBQWMsRUFDdkMsMERBQXVCLENBQUUsa0JBQWtCLEVBQzNDLHdGQUF5QyxDQUFDLGFBQWE7O0FHdEdyRSw0RUFBNEU7QUFFNUUseUZBQ2dGO0FBRWhGLHdIQUdHO0FBRUgsQUFBQSxJQUFJLENBQUMsRUFDSCxNQUFNLEVBQUUsQ0FBQyxFQUNULE9BQU8sRUFBRSxDQUFDLEVBQ1YsV0FBVyxFQUFFLElBQUksRUFBRSxPQUFPLENBQzFCLHdCQUF3QixFQUFFLElBQUksRUFBRSxPQUFPLENBQ3ZDLFNBQVMsRUFBRSxJQUFJLEdBQ2hCOztBQUVELHlGQUNnRjtBQUVoRix5Q0FFRztBQUVILEFBQUEsSUFBSSxDQUFDLEVBQ0gsTUFBTSxFQUFFLENBQUMsRUFDVCxPQUFPLEVBQUUsQ0FBQyxHQUNYOztBQUVELG9EQUVHO0FBRUgsQUFBQSxJQUFJLENBQUMsRUFDSCxPQUFPLEVBQUUsS0FBSyxHQUNmOztBQUVELGdJQUdHO0FBRUgsQUFBQSxFQUFFLENBQUMsRUFDRCxTQUFTLEVBQUUsSUFBSSxFQUNmLE1BQU0sRUFBQyxDQUFDLEdBQ1Q7O0FBRUQsaUdBQ2dGO0FBRWhGLHFGQUdHO0FBRUgsQUFBQSxFQUFFLENBQUMsRUFDRCxrQkFBa0IsRUFBRSxXQUFXLEVBQ3ZCLFVBQVUsRUFBRSxXQUFXLEVBQUUsT0FBTyxDQUN4QyxNQUFNLEVBQUUsQ0FBQyxFQUFFLE9BQU8sQ0FDbEIsUUFBUSxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQzNCOztBQUVELGdJQUdHO0FBRUgsQUFBQSxHQUFHLENBQUMsRUFDRixXQUFXLEVBQUUsb0JBQW9CLEVBQUUsT0FBTyxDQUMxQyxTQUFTLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFDeEI7O0FBRUQscUdBQ2dGO0FBRWhGLDJEQUVHO0FBQ0gsQUFBQSxDQUFDLENBQUEsRUFDQSxNQUFNLEVBQUUsQ0FBQyxHQUNUOztBQUVELEFBQUEsRUFBRSxDQUFBLEVBQ0EsVUFBVSxFQUFFLElBQUksR0FDakI7O0FBQ0QsQUFBQSxFQUFFLEVBQUMsRUFBRSxDQUFBLEVBQ0gsTUFBTSxFQUFFLENBQUMsRUFDVCxPQUFPLEVBQUUsQ0FBQyxHQUNYOztBQUVELEFBQUEsQ0FBQyxDQUFDLEVBQ0EsZ0JBQWdCLEVBQUUsV0FBVyxFQUM3QixLQUFLLEVBQUUsS0FBSyxFQUNaLGVBQWUsRUFBRSxJQUFJLEdBQ3RCOztBQUVELDJIQUdHO0FBRUgsQUFBQSxJQUFJLENBQUEsQUFBQSxLQUFDLEFBQUEsRUFBTyxFQUNWLGFBQWEsRUFBRSxJQUFJLEVBQUUsT0FBTyxDQUM1QixlQUFlLEVBQUUsU0FBUyxFQUFFLE9BQU8sQ0FDbkMsdUJBQXVCLEVBQUUsZ0JBQWdCLEVBQ2pDLGVBQWUsRUFBRSxnQkFBZ0IsRUFBRSxPQUFPLEVBQ25EOztBQUVELCtEQUVHO0FBRUgsQUFBQSxDQUFDLEVBQ0QsTUFBTSxDQUFDLEVBQ0wsV0FBVyxFQUFFLE1BQU0sR0FDcEI7O0FBRUQsZ0lBR0c7QUFFSCxBQUFBLElBQUksRUFDSixHQUFHLEVBQ0gsSUFBSSxDQUFDLEVBQ0gsV0FBVyxFQUFFLG9CQUFvQixFQUFFLE9BQU8sQ0FDMUMsU0FBUyxFQUFFLEdBQUcsRUFBRSxPQUFPLEVBQ3hCOztBQUVELGlEQUVHO0FBRUgsQUFBQSxLQUFLLENBQUMsRUFDSixTQUFTLEVBQUUsR0FBRyxHQUNmOztBQUVELHVGQUdHO0FBRUgsQUFBQSxHQUFHLEVBQ0gsR0FBRyxDQUFDLEVBQ0YsU0FBUyxFQUFFLEdBQUcsRUFDZCxXQUFXLEVBQUUsQ0FBQyxFQUNkLFFBQVEsRUFBRSxRQUFRLEVBQ2xCLGNBQWMsRUFBRSxRQUFRLEdBQ3pCOztBQUVELEFBQUEsR0FBRyxDQUFDLEVBQ0YsTUFBTSxFQUFFLE9BQU8sR0FDaEI7O0FBRUQsQUFBQSxHQUFHLENBQUMsRUFDRixHQUFHLEVBQUUsTUFBTSxHQUNaOztBQUVELGlHQUNnRjtBQUVoRix5REFFRztBQUVILEFBQUEsR0FBRyxDQUFDLEVBQ0YsWUFBWSxFQUFFLElBQUksRUFDbEIsS0FBSyxFQUFFLElBQUksRUFDWCxNQUFNLEVBQUUsSUFBSSxFQUNaLGNBQWMsRUFBRSxNQUFNLEdBQ3ZCOztBQUVELHNGQUNnRjtBQUVoRiw2RkFHRztBQUVILEFBQUEsTUFBTSxFQUNOLEtBQUssRUFDTCxRQUFRLEVBQ1IsTUFBTSxFQUNOLFFBQVEsQ0FBQyxFQUNQLFdBQVcsRUFBRSxPQUFPLEVBQUUsT0FBTyxDQUM3QixTQUFTLEVBQUUsSUFBSSxFQUFFLE9BQU8sQ0FDeEIsV0FBVyxFQUFFLElBQUksRUFBRSxPQUFPLENBQzFCLE1BQU0sRUFBRSxDQUFDLEVBQUUsT0FBTyxFQUNuQjs7QUFFRCw2REFHRztBQUVILEFBQUEsTUFBTSxFQUNOLEtBQUssQ0FBQyxFQUFFLE9BQU8sQ0FDYixRQUFRLEVBQUUsT0FBTyxHQUNsQjs7QUFFRCxpSUFHRztBQUVILEFBQUEsTUFBTSxFQUNOLE1BQU0sQ0FBQyxFQUFFLE9BQU8sQ0FDZCxjQUFjLEVBQUUsSUFBSSxHQUNyQjs7QUFFRCx3RUFFRztBQUVILEFBQUEsTUFBTSxHQUNOLEFBQUEsSUFBQyxDQUFLLFFBQVEsQUFBYixJQUNELEFBQUEsSUFBQyxDQUFLLE9BQU8sQUFBWixJQUNELEFBQUEsSUFBQyxDQUFLLFFBQVEsQUFBYixFQUFlLEVBQ2Qsa0JBQWtCLEVBQUUsTUFBTSxHQUMzQjs7QUFFRCxzREFFRztBQUVILEFBQUEsTUFBTSxBQUFBLGtCQUFrQixHQUN4QixBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsQ0FBYyxrQkFBa0IsR0FDakMsQUFBQSxJQUFDLENBQUssT0FBTyxBQUFaLENBQWEsa0JBQWtCLEdBQ2hDLEFBQUEsSUFBQyxDQUFLLFFBQVEsQUFBYixDQUFjLGtCQUFrQixDQUFDLEVBQ2hDLFlBQVksRUFBRSxJQUFJLEVBQ2xCLE9BQU8sRUFBRSxDQUFDLEdBQ1g7O0FBRUQsMkRBRUc7QUFFSCxBQUFBLE1BQU0sQUFBQSxlQUFlLEdBQ3JCLEFBQUEsSUFBQyxDQUFLLFFBQVEsQUFBYixDQUFjLGVBQWUsR0FDOUIsQUFBQSxJQUFDLENBQUssT0FBTyxBQUFaLENBQWEsZUFBZSxHQUM3QixBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsQ0FBYyxlQUFlLENBQUMsRUFDN0IsT0FBTyxFQUFFLHFCQUFxQixHQUMvQjs7QUFFRCxzQ0FFRztBQUVILEFBQUEsUUFBUSxDQUFDLEVBQ1AsT0FBTyxFQUFFLHFCQUFxQixHQUMvQjs7QUFFRCxtT0FLRztBQUVILEFBQUEsTUFBTSxDQUFDLEVBQ0wsa0JBQWtCLEVBQUUsVUFBVSxFQUN0QixVQUFVLEVBQUUsVUFBVSxFQUFFLE9BQU8sQ0FDdkMsS0FBSyxFQUFFLE9BQU8sRUFBRSxPQUFPLENBQ3ZCLE9BQU8sRUFBRSxLQUFLLEVBQUUsT0FBTyxDQUN2QixTQUFTLEVBQUUsSUFBSSxFQUFFLE9BQU8sQ0FDeEIsT0FBTyxFQUFFLENBQUMsRUFBRSxPQUFPLENBQ25CLFdBQVcsRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUM3Qjs7QUFFRCx3RUFFRztBQUVILEFBQUEsUUFBUSxDQUFDLEVBQ1AsY0FBYyxFQUFFLFFBQVEsR0FDekI7O0FBRUQsdURBRUc7QUFFSCxBQUFBLFFBQVEsQ0FBQyxFQUNQLFFBQVEsRUFBRSxJQUFJLEdBQ2Y7O0FBRUQsOEVBR0c7Q0FFSCxBQUFBLEFBQUEsSUFBQyxDQUFLLFVBQVUsQUFBZixJQUNELEFBQUEsSUFBQyxDQUFLLE9BQU8sQUFBWixFQUFjLEVBQ2Isa0JBQWtCLEVBQUUsVUFBVSxFQUN0QixVQUFVLEVBQUUsVUFBVSxFQUFFLE9BQU8sQ0FDdkMsT0FBTyxFQUFFLENBQUMsRUFBRSxPQUFPLEVBQ3BCOztBQUVELDZFQUVHO0NBRUgsQUFBQSxBQUFBLElBQUMsQ0FBSyxRQUFRLEFBQWIsQ0FBYywyQkFBMkIsR0FDMUMsQUFBQSxJQUFDLENBQUssUUFBUSxBQUFiLENBQWMsMkJBQTJCLENBQUMsRUFDekMsTUFBTSxFQUFFLElBQUksR0FDYjs7QUFFRCxrR0FHRztDQUVILEFBQUEsQUFBQSxJQUFDLENBQUssUUFBUSxBQUFiLEVBQWUsRUFDZCxrQkFBa0IsRUFBRSxTQUFTLEVBQUUsT0FBTyxDQUN0QyxjQUFjLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFDOUI7O0FBRUQsOERBRUc7Q0FFSCxBQUFBLEFBQUEsSUFBQyxDQUFLLFFBQVEsQUFBYixDQUFjLDJCQUEyQixDQUFDLEVBQ3pDLGtCQUFrQixFQUFFLElBQUksR0FDekI7O0FBRUQsNkhBR0c7QUFFSCxBQUFBLDRCQUE0QixDQUFDLEVBQzNCLGtCQUFrQixFQUFFLE1BQU0sRUFBRSxPQUFPLENBQ25DLElBQUksRUFBRSxPQUFPLEVBQUUsT0FBTyxFQUN2Qjs7QUFFRCw0RkFDZ0Y7QUFFaEYsMkRBRUc7QUFFSCxBQUFBLE9BQU8sQ0FBQyxFQUNOLE9BQU8sRUFBRSxLQUFLLEdBQ2Y7O0FBRUQsOENBRUc7QUFFSCxBQUFBLE9BQU8sQ0FBQyxFQUNOLE9BQU8sRUFBRSxTQUFTLEdBQ25COztBQUVELHFGQUNnRjtBQUVoRix5Q0FFRztBQUVILEFBQUEsUUFBUSxDQUFDLEVBQ1AsT0FBTyxFQUFFLElBQUksR0FDZDs7QUFFRCx3Q0FFRztDQUVILEFBQUEsQUFBQSxNQUFDLEFBQUEsRUFBUSxFQUNQLE9BQU8sRUFBRSxJQUFJLEdBQ2Q7O0FDcFhELEFBQUEsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FBQyxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsQ0FBQyxFQUFFLElBQUksQ0FBQSxFQUM1QixXQUFXLEVKTU4sTUFBTSxFSUxYLFdBQVcsRUFBRSxPQUFPLEdBQ3JCOztBQUVELEFBQ0UsTUFESSxDQUNKLENBQUMsQ0FBQSxFQUNDLFNBQVMsRUhERCxNQUFNLEVHRWQsV0FBVyxFSENOLElBQUksR0dBVjs7QUFHSCxBQUNFLEtBREcsQ0FDSCxDQUFDLENBQUEsRUFDQyxTQUFTLEVIUkQsTUFBTSxFR1NkLFdBQVcsRUFBRSxJQUFJLEdBQ2xCOztBQUpILEFBTUksS0FOQyxDQUtILFdBQVcsQ0FDVCxNQUFNLENBQUEsRUFDSixRQUFRLEVBQUUsUUFBUSxHQVVuQjs7QUFqQkwsQUFRTSxLQVJELENBS0gsV0FBVyxDQUNULE1BQU0sQUFFSCxPQUFPLENBQUEsRUFDTixPQUFPLEVBQUUsS0FBSyxFQUNkLFFBQVEsRUFBRSxRQUFRLEVBQ2xCLE9BQU8sRUFBRSxFQUFFLEVBQ1gsS0FBSyxFQUFFLElBQUksRUFDWCxNQUFNLEVBQUUsR0FBRyxFQUNYLFVBQVUsRUFBRSxLQUFLLEVBQ2pCLE1BQU0sRUFBRSxDQUFDLEdBQ1Y7O0FBaEJQLEFBa0JJLEtBbEJDLENBS0gsV0FBVyxDQWFULENBQUMsQ0FBQSxFQUNDLFFBQVEsRUFBRSxRQUFRLEdBWW5COztBQS9CTCxBQXNCTSxLQXRCRCxDQUtILFdBQVcsQ0FhVCxDQUFDLEFBSUUsU0FBUyxDQUFBLEVBQ1IsV0FBVyxFSnVEUiwrQkFBK0IsR0l0RG5DOztBQXhCUCxBQXlCTSxLQXpCRCxDQUtILFdBQVcsQ0FhVCxDQUFDLEFBT0UsUUFBUSxDQUFBLEVBQ1AsV0FBVyxFSkZaLE1BQU0sR0lHTjs7QUEzQlAsQUE0Qk0sS0E1QkQsQ0FLSCxXQUFXLENBYVQsQ0FBQyxBQVVFLFdBQVcsQ0FBQSxFQUNWLFdBQVcsRUpxQlgsT0FBTyxHSXBCUjs7QUE5QlAsQUFrQ1EsS0FsQ0gsQ0FLSCxXQUFXLENBMkJULEtBQUssQ0FDSCxVQUFVLENBQ1IsQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFQUFFLElBQUksRUFDZixXQUFXLEVBQUUsTUFBTSxHQUNwQjs7QUFNVCxBQUNFLEtBREcsQ0FDSCxFQUFFLENBQUEsRUFDQSxLQUFLLEVBQUUsSUFBSSxFQUNYLFdBQVcsRUFBRSxJQUFJLENBQUEsVUFBVSxFQUMzQixTQUFTLEVBQUUsSUFBSSxDQUFBLFVBQVUsRUFDekIsV0FBVyxFQUFFLE1BQU0sQ0FBQSxVQUFVLEdBQzlCOztBQU5ILEFBT0UsS0FQRyxDQU9ILElBQUksQ0FBQSxFQUNGLFNBQVMsRUFBRSxJQUFJLEdBQ2pCOztBQVRGLEFBV0csS0FYRSxBQVVILFNBQVMsQ0FDUixFQUFFLENBQUEsRUFDQSxXQUFXLEVKdUJMLCtCQUErQixHSXRCdEM7O0FBYkosQUFjRyxLQWRFLEFBVUgsU0FBUyxDQUlSLElBQUksQ0FBQSxFQUNGLFdBQVcsRUpvQkwsK0JBQStCLEVJbkJyQyxXQUFXLEVBQUUsTUFBTSxHQUNwQjs7QUFqQkosQUFvQkcsS0FwQkUsQUFtQkgsUUFBUSxDQUNQLEVBQUUsQ0FBQSxFQUNBLFdBQVcsRUp4Q1QsTUFBTSxHSXlDVDs7QUF0QkosQUF1QkcsS0F2QkUsQUFtQkgsUUFBUSxDQUlQLElBQUksQ0FBQSxFQUNGLFdBQVcsRUozQ1QsTUFBTSxFSTRDUixXQUFXLEVBQUUsTUFBTSxHQUNwQjs7QUExQkosQUE2QkksS0E3QkMsQUE0QkYsV0FBVyxDQUNWLEVBQUUsQ0FBQSxFQUNBLFdBQVcsRUp2QlQsT0FBTyxHSXdCVjs7QUEvQkwsQUFnQ0ksS0FoQ0MsQUE0QkYsV0FBVyxDQUlWLElBQUksQ0FBQSxFQUNGLFdBQVcsRUoxQlQsT0FBTyxFSTJCVCxXQUFXLEVBQUUsTUFBTSxHQUNwQjs7QUFJTCxBQUFBLENBQUMsQUFBQSxTQUFTLEVBQUUsSUFBSSxBQUFBLFNBQVMsQ0FBQSxFQUN2QixXQUFXLEVKTEYsK0JBQStCLEdJTXpDOztBQUVELEFBQUEsQ0FBQyxBQUFBLFFBQVEsRUFBRSxJQUFJLEFBQUEsUUFBUSxDQUFBLEVBQ3JCLFdBQVcsRUovRE4sTUFBTSxHSWdFWjs7QUFFRCxBQUFBLENBQUMsQUFBQSxXQUFXLEVBQUUsSUFBSSxBQUFBLFdBQVcsQ0FBQSxFQUMzQixXQUFXLEVKekNMLE9BQU8sR0kwQ2Q7O0FBRUQsQUFDRSxjQURZLENBQ1osRUFBRSxFQURKLGNBQWMsQ0FDUixDQUFDLENBQUEsRUFDSCxTQUFTLEVBQUUsTUFBTSxFQUNqQixXQUFXLEVBQUUsTUFBTSxHQUNwQjs7QUFHSCxBQUNFLGNBRFksQ0FDWixFQUFFLENBQUEsRUFDQSxTQUFTLEVIN0dELE1BQU0sRUc4R2QsTUFBTSxFQUFFLENBQUMsR0FDVjs7QUFHSCxBQUNFLEtBREcsQ0FDSCxFQUFFLENBQUEsRUFDQSxNQUFNLEVBQUUsQ0FBQyxFQUNULFNBQVMsRUFBRSxJQUFJLEdBQ2hCOztBQzFISCxBQUFBLFdBQVcsQ0FBQSxFQUNULE9BQU8sRUFBRSxHQUFHLEdBcUNiOztBQXRDRCxBQUdJLFdBSE8sQ0FFVCxDQUFDLEdBQ0ssQ0FBQyxDQUFBLEVBQ0gsT0FBTyxFQUFFLEdBQUcsR0FnQ2I7O0FBcENMLEFBS00sV0FMSyxDQUVULENBQUMsR0FDSyxDQUFDLEFBRUYsT0FBTyxFQUxkLFdBQVcsQ0FFVCxDQUFDLEdBQ0ssQ0FBQyxBQUdGLE1BQU0sQ0FBQyxFQUNOLE9BQU8sRUFBRSxFQUFFLEVBQ1gsUUFBUSxFQUFFLFFBQVEsRUFDbEIsTUFBTSxFQUFFLElBQUksRUFDWixLQUFLLEVBQUUsR0FBRyxFQUNWLE1BQU0sRUFBRSxHQUFHLEVBQ1gsTUFBTSxFQUFFLE9BQU8sRUFDZixVQUFVLEVBZlIsR0FBRyxDQUFDLElBQUksQ0FBQyxXQUFXLEVBZ0J0QixtQkFBbUIsRUFBRSxJQUFJLEVBQ3pCLE9BQU8sRUFBRSxDQUFDLEVBQ1YsZ0JBQWdCLEVBQUUsS0FBSyxHQUN4Qjs7QUFqQlAsQUFrQk0sV0FsQkssQ0FFVCxDQUFDLEdBQ0ssQ0FBQyxBQWVGLFFBQVEsQ0FBQSxFQUNQLElBQUksRUFBRSxHQUFHLEdBQ1Y7O0FBcEJQLEFBcUJNLFdBckJLLENBRVQsQ0FBQyxHQUNLLENBQUMsQUFrQkYsTUFBTSxDQUFBLEVBQ0wsS0FBSyxFQUFFLEdBQUcsR0FDWDs7QUF2QlAsQUF3Qk0sV0F4QkssQ0FFVCxDQUFDLEdBQ0ssQ0FBQyxBQXFCRixNQUFNLENBQUMsRUFDTixNQUFNLEVBQUUsT0FBTyxHQVVoQjs7QUFuQ1AsQUEwQlEsV0ExQkcsQ0FFVCxDQUFDLEdBQ0ssQ0FBQyxBQXFCRixNQUFNLEFBRUosT0FBTyxFQTFCaEIsV0FBVyxDQUVULENBQUMsR0FDSyxDQUFDLEFBcUJGLE1BQU0sQUFHSixNQUFNLENBQUMsRUFDTixLQUFLLEVBQUUsSUFBSSxFQUNYLE9BQU8sRUFBRSxDQUFDLEdBQ1g7O0FBOUJULEFBK0JRLFdBL0JHLENBRVQsQ0FBQyxHQUNLLENBQUMsQUFxQkYsTUFBTSxBQU9KLE9BQU8sRUEvQmhCLFdBQVcsQ0FFVCxDQUFDLEdBQ0ssQ0FBQyxBQXFCRixNQUFNLEFBUUosTUFBTSxDQUFDLEVBQ04sS0FBSyxFQUFFLEdBQUcsR0FDWDs7QUFNVCxBQUFBLGFBQWEsQ0FBQSxFQUNYLFFBQVEsRUFBRSxRQUFRLEVBQ2xCLEtBQUssRUFBRSxHQUFHLEVBQ1YsTUFBTSxFQUFFLEdBQUcsRUFDWCxVQUFVLEVKVE8sU0FBUyxFSVUxQixhQUFhLEVBQUUsS0FBSyxFQUNwQixTQUFTLEVBQUUscUJBQXFCLEVBQ2hDLE9BQU8sRUFBRSxDQUFDLEVBQ1YsVUFBVSxFQUFFLHFCQUFxQixHQWVsQzs7QUF2QkQsQUFTRSxhQVRXLEFBU1YsS0FBSyxDQUFBLEVBQ0osS0FBSyxFQUFFLEtBQUssRUFDWixNQUFNLEVBQUUsS0FBSyxFQUNiLFVBQVUsRUFBRSxxQkFBcUIsR0FDbEM7O0FBYkgsQUFjRSxhQWRXLEFBY1YsU0FBUyxDQUFBLEVBQ1IsVUFBVSxFSnRCRyxXQUFXLEdJdUJ6Qjs7QUFoQkgsQUFpQkUsYUFqQlcsQUFpQlYsUUFBUSxDQUFBLEVBQ1AsVUFBVSxFSnhCRSxVQUFVLEdJeUJ2Qjs7QUFuQkgsQUFvQkUsYUFwQlcsQUFvQlYsV0FBVyxDQUFBLEVBQ1YsVUFBVSxFSjFCSyxTQUFTLEdJMkJ6Qjs7QUNoRUgsQUFBQSxNQUFNLENBQUEsRUFDSixRQUFRLEVBQUUsS0FBSyxFQUNmLEdBQUcsRUFBRSxDQUFDLEVBQ04sSUFBSSxFQUFFLENBQUMsRUFDUCxNQUFNLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENMT1IsTUFBTSxFS05YLE9BQU8sRUxNRixNQUFNLENLTkksQ0FBQyxFQUNoQixLQUFLLEVBQUUsbUJBQXlCLEVBQ2hDLGFBQWEsRUFBRSxlQUFlLEdBQy9COztBQ1JELEFBQUEsQ0FBQyxDQUFBLEVBQ0MsVUFBVSxFQUFFLFVBQVUsR0FDdkI7O0FBRUQsQUFBQSxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQSxFQUNoQixVQUFVLEVBQUUsU0FBUyxHQUN0Qjs7QUFFRCxBQUNFLElBREUsQ0FDRixNQUFNLENBQUEsRUFDRixLQUFLLEVOS0osK0JBQW9DLEVNSnJDLE1BQU0sRU5BTCxNQUFNLENNQU8sSUFBSSxDQUFDLElBQUksQ05BdEIsTUFBTSxHTW1JVjs7QUF0SUgsQUFRWSxJQVJSLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FDVCxDQUFDLENBQ0MsQ0FBQyxBQUVFLEtBQUssQ0FBQSxFQUNKLFlBQVksRU5ObkIsTUFBTSxHTU9BOztBQVZiLEFBYVEsSUFiSixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUFBLEVBQ1IsS0FBSyxFQUFFLElBQUksRUFDWCxVQUFVLEVBQUUsTUFBTSxHQXFIckI7O0FBcElQLEFBZ0JRLElBaEJKLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFQUFFLGdDQUFtQyxFQUMxQyxPQUFPLEVBQUUsWUFBWSxFQUNyQixjQUFjLEVBQUUsR0FBRyxFQUNuQixRQUFRLEVBQUUsUUFBUSxFQUNsQixNQUFNLEVBQUUsQ0FBQyxDTmxCWixNQUFNLENBQU4sTUFBTSxDTWtCbUIsQ0FBQyxFQUN2QixNQUFNLEVBQUUsSUFBSSxHQWtGYjs7QUF4R1QsQUEwQmdCLElBMUJaLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxHQU9DLFdBQVcsQUFDWixLQUFLLENBQ0osQ0FBQyxDQUNDLEdBQUcsQ0FBQSxFQUNELE1BQU0sRUFBRSxZQUFZLEVBQ3BCLFVBQVUsRUFBRSxnQkFBZ0IsR0FDN0I7O0FBN0JqQixBQWlDVSxJQWpDTixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUdWLEtBQUssQ0FpQkgsQ0FBQyxDQUFBLEVBQ0MsT0FBTyxFQUFFLEtBQUssRUFDZCxhQUFhLEVBQUUsSUFBSSxFQUNuQixRQUFRLEVBQUUsSUFBSSxHQUtmOztBQXpDWCxBQXFDWSxJQXJDUixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUdWLEtBQUssQ0FpQkgsQ0FBQyxDQUlDLEdBQUcsQ0FBQSxFQUNELE1BQU0sRUFBRSxZQUFZLEVBQ3BCLFVBQVUsRUFBRSxnQkFBZ0IsR0FDN0I7O0FBeENiLEFBMENVLElBMUNOLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxDQTBCSCxZQUFZLENBQUEsRUFDVixLQUFLLEVBQUUsSUFBSSxFQUNYLE9BQU8sRU56Q1osTUFBTSxFTTBDRCxTQUFTLEVBQUUsSUFBSSxFQUNmLE9BQU8sRUFBRSxJQUFJLEdBTWQ7O0FBcERYLEFBK0NZLElBL0NSLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxDQTBCSCxZQUFZLEdBS04sQ0FBQyxDQUFBLEVBQ0gsU0FBUyxFQUFFLE1BQU0sRUFDakIsV0FBVyxFQUFFLElBQUksRUFDakIsWUFBWSxFTi9DbkIsTUFBTSxHTWdEQTs7QUFuRGIsQUFzRFksSUF0RFIsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FHVixLQUFLLEFBcUNGLE1BQU0sQ0FDTCxDQUFDLENBQUEsRUFDQyxPQUFPLEVBQUUsS0FBSyxHQUtmOztBQTVEYixBQXdEYyxJQXhEVixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUdWLEtBQUssQUFxQ0YsTUFBTSxDQUNMLENBQUMsQ0FFQyxHQUFHLENBQUEsRUFDRCxNQUFNLEVBQUUsWUFBWSxFQUNwQixVQUFVLEVBQUUsZ0JBQWdCLEdBQzdCOztBQTNEZixBQTZEWSxJQTdEUixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUdWLEtBQUssQUFxQ0YsTUFBTSxDQVFMLFlBQVksQ0FBQSxFQUNWLE9BQU8sRUFBRSxXQUFXLEVBQ3BCLFFBQVEsRUFBRSxRQUFRLEVBQ2xCLFNBQVMsRUFBRSxpQkFBaUIsR0FDN0I7O0FBakViLEFBbUVVLElBbkVOLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxBQW1ERixhQUFhLENBQUEsRUFDWixjQUFjLEVBQUUsUUFBUSxDQUFBLFVBQVUsR0FDbkM7O0FBckVYLEFBc0VVLElBdEVOLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxDQXNESCxLQUFLLENBQUEsRUFDSCxLQUFLLEVBQUUsMkJBQThCLEVBQ3JDLE9BQU8sRUFBRSxJQUFJLEVBQ2IsVUFBVSxFTnRFZixNQUFNLEVNdUVELGFBQWEsRUFBRSxJQUFJLEdBNkJwQjs7QUF2R1gsQUEyRVksSUEzRVIsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FHVixLQUFLLENBc0RILEtBQUssQ0FLSCxjQUFjLENBQUEsRUFDWixNQUFNLEVBQUUsYUFBYSxFQUNyQixPQUFPLEVBQUUsSUFBSSxFQUNiLFNBQVMsRUFBRSxJQUFJLEVBQ2YsZUFBZSxFQUFFLGFBQWEsRUFDOUIsV0FBVyxFQUFFLFFBQVEsR0FJdEI7O0FBcEZiLEFBaUZjLElBakZWLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxDQXNESCxLQUFLLENBS0gsY0FBYyxDQU1aLENBQUMsQ0FBQSxFQUNDLE9BQU8sRUFBRSxLQUFLLEdBQ2Y7O0FBbkZmLEFBcUZZLElBckZSLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxDQXNESCxLQUFLLENBZUgsVUFBVSxDQUFBLEVBQ1IsTUFBTSxFQUFFLG1CQUFtQixHQUs1Qjs7QUEzRmIsQUF1RmMsSUF2RlYsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FHVixLQUFLLENBc0RILEtBQUssQ0FlSCxVQUFVLENBRVIsQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFQUFFLElBQUksRUFDZixXQUFXLEVBQUUsTUFBTSxHQUNwQjs7QUExRmYsQUE0RlksSUE1RlIsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQ0FHVixLQUFLLENBc0RILEtBQUssQUFzQkYsU0FBUyxDQUFBLEVBQ1IsVUFBVSxFTmxFUCxXQUFXLEdNb0VmOztBQS9GYixBQWdHWSxJQWhHUixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxDQUdWLEtBQUssQ0FzREgsS0FBSyxBQTBCRixRQUFRLENBQUEsRUFDUCxVQUFVLEVOckVSLFVBQVUsR011RWI7O0FBbkdiLEFBb0dZLElBcEdSLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLENBR1YsS0FBSyxDQXNESCxLQUFLLEFBOEJGLFdBQVcsQ0FBQSxFQUNWLFVBQVUsRU54RUwsU0FBUyxHTXlFZjs7QUF0R2IsQUE0R2MsSUE1R1YsQ0FDRixNQUFNLENBR0YsV0FBVyxDQVNULFVBQVUsQUE0RlQsU0FBUyxDQUNSLEtBQUssQUFDRixNQUFNLENBQ0wsWUFBWSxDQUFBLEVBQ1YsVUFBVSxFTmxGVCxXQUFXLEdNbUZiOztBQTlHZixBQXFIYyxJQXJIVixDQUNGLE1BQU0sQ0FHRixXQUFXLENBU1QsVUFBVSxBQXFHVCxRQUFRLENBQ1AsS0FBSyxBQUNGLE1BQU0sQ0FDTCxZQUFZLENBQUEsRUFDVixVQUFVLEVOMUZWLFVBQVUsR00yRlg7O0FBdkhmLEFBOEhjLElBOUhWLENBQ0YsTUFBTSxDQUdGLFdBQVcsQ0FTVCxVQUFVLEFBOEdULFdBQVcsQ0FDVixLQUFLLEFBQ0YsTUFBTSxDQUNMLFlBQVksQ0FBQSxFQUNWLFVBQVUsRU5sR1AsU0FBUyxHTW1HYjs7QUFoSWYsQUF3SUksSUF4SUEsQUF1SUQsSUFBSyxDQUFBLEtBQUssRUFDVCxNQUFNLENBQUEsRUFDSixVQUFVLEVBQUUsbUJBQXlCLEdBQ3RDOztBQ2xKTCxBQUFBLGNBQWMsQ0FBQSxFQUNaLFFBQVEsRUFBRSxLQUFLLEVBQ2YsS0FBSyxFQUFFLEdBQUcsRUFDVixNQUFNLEVBQUUsSUFBSSxFQUNaLEtBQUssRUFBRSxDQUFDLEVBQ1IsR0FBRyxFQUFFLENBQUMsRUFDTixPQUFPLEVBQUUsV0FBVyxHQWdEckI7O0FBdERELEFBT0UsY0FQWSxDQU9aLGFBQWEsQ0FBQSxFQUNYLE1BQU0sRUFBRSxJQUFJLEdBNkNiOztBQXJESCxBQVNJLGNBVFUsQ0FPWixhQUFhLENBRVgsRUFBRSxDQUFBLEVBSUEsTUFBTSxFUEZMLE1BQU0sQ0FBTixNQUFNLENPRWEsR0FBRyxDQUFDLENBQUMsRUFDekIsVUFBVSxFQUFFLEtBQUssR0FzQ2xCOztBQXBETCxBQVVNLGNBVlEsQ0FPWixhQUFhLENBRVgsRUFBRSxBQUNDLFVBQVcsQ0FBQSxDQUFDLEVBQUMsRUFDWixVQUFVLEVBQUUsQ0FBQyxHQUNkOztBQVpQLEFBaUJVLGNBakJJLENBT1osYUFBYSxDQUVYLEVBQUUsQ0FNQSxFQUFFLEFBQ0MsTUFBTSxDQUNMLENBQUMsQ0FBQSxFQUNDLEtBQUssRUFBRSxLQUFLLEdBQ2I7O0FBbkJYLEFBc0JNLGNBdEJRLENBT1osYUFBYSxDQUVYLEVBQUUsQ0FhQSxDQUFDLENBQUEsRUFDQyxLQUFLLEVBQUUsSUFBSSxHQTRCWjs7QUFuRFAsQUF3QlEsY0F4Qk0sQ0FPWixhQUFhLENBRVgsRUFBRSxDQWFBLENBQUMsQUFFRSxRQUFRLENBQUEsRUFDUCxPQUFPLEVBQUUsR0FBRyxFQUNaLEtBQUssRUFBRSxHQUFHLEVBQ1YsTUFBTSxFQUFFLEdBQUcsRUFDWCxPQUFPLEVBQUUsWUFBWSxFQUNyQixhQUFhLEVBQUUsR0FBRyxFQUNsQixjQUFjLEVBQUUsTUFBTSxFQUN0QixZQUFZLEVBQUUsR0FBRyxHQUNsQjs7QUFoQ1QsQUFrQ1UsY0FsQ0ksQ0FPWixhQUFhLENBRVgsRUFBRSxDQWFBLENBQUMsQUFXRSxXQUFXLEFBQ1QsUUFBUSxDQUFBLEVBQ1AsVUFBVSxFUEVILFNBQVMsR09EakI7O0FBcENYLEFBdUNVLGNBdkNJLENBT1osYUFBYSxDQUVYLEVBQUUsQ0FhQSxDQUFDLEFBZ0JFLFFBQVEsQUFDTixRQUFRLENBQUEsRUFDVCxVQUFVLEVQSkosVUFBVSxHT0tmOztBQXpDWCxBQTRDVSxjQTVDSSxDQU9aLGFBQWEsQ0FFWCxFQUFFLENBYUEsQ0FBQyxBQXFCRSxTQUFTLEFBQ1AsUUFBUSxDQUFBLEVBQ1QsVUFBVSxFUFZILFdBQVcsR09XakI7O0FBOUNYLEFBZ0RRLGNBaERNLENBT1osYUFBYSxDQUVYLEVBQUUsQ0FhQSxDQUFDLEFBMEJFLE9BQU8sQ0FBQSxFQUNOLEtBQUssRUFBRSxLQUFLLEdBQ2I7O0FDbERULEFBQUEsVUFBVSxDQUFBLEVBQ04sT0FBTyxFQUFFLFdBQVcsRUFDcEIsU0FBUyxFQUFFLElBQUksRUFDZixLQUFLLEVBQUUsSUFBSSxHQWVkOztBQWxCRCxBQUlFLFVBSlEsQ0FJUixLQUFLLENBQUEsRUFDSCxLQUFLLEVSVUYsK0JBQW9DLEdRRXhDOztBQWpCSCxBQU1JLFVBTk0sQ0FJUixLQUFLLEFBRUYsVUFBVyxDQUFBLEdBQUcsRUFBQyxFQUNkLFlBQVksRVJLVixJQUFJLEdRSlA7O0FBUkwsQUFTSSxVQVRNLENBSVIsS0FBSyxDQUtILFlBQVksQ0FBQSxFQUNWLE9BQU8sRUFBRSxXQUFXLEVBQ3BCLEtBQUssRUFBRSxJQUFJLEVBQ1gsYUFBYSxFUkRaLE1BQU0sR1FLUjs7QUFoQkwsQUFhTSxVQWJJLENBSVIsS0FBSyxDQUtILFlBQVksR0FJTixDQUFDLENBQUMsRUFDSixZQUFZLEVSSGIsTUFBTSxHUUlOOztBQ2ZQLEFBQ0UsS0FERyxDQUNILFVBQVUsQ0FBQSxFQUNSLGVBQWUsRUFBRSxPQUFPLEVBQ3hCLFlBQVksRUFBRSxPQUFPLEVBQ2pCLE9BQU8sRUFBRSxPQUFPLEVBQ3BCLGFBQWEsRVRNVixNQUFNLEdTTFI7O0FBTkwsQUFRSSxLQVJDLENBT0gsV0FBVyxDQUNULEdBQUcsQUFBQSxJQUFLLENBQUEsYUFBYSxFQUFDLEVBQ3BCLGFBQWEsRVRFWixNQUFNLEdTRFI7O0FBSUwsQUFBQSxjQUFjLENBQUEsRUFDWixVQUFVLEVBQUUsZUFBZSxFQUMzQixPQUFPLEVBQUUsSUFBSSxFQUNiLFNBQVMsRUFBRSxJQUFJLEVBQ2YsVUFBVSxFVE5KLElBQUksRVNPVixhQUFhLEVUUFAsSUFBSSxHU3lCWDs7QUF2QkQsQUFNRSxjQU5ZLENBTVosRUFBRSxDQUFBLEVBQ0EsTUFBTSxFQUFFLElBQUksQ0FBQyxDQUFDLENUVlgsTUFBTSxDU1VZLENBQUMsRUFDdEIsS0FBSyxFQUFFLElBQUksR0FDWjs7QUFUSCxBQVVFLGNBVlksQ0FVWixLQUFLLENBQUEsRUFDSCxLQUFLLEVUVEYsK0JBQW9DLEdTb0J4Qzs7QUF0QkgsQUFZSSxjQVpVLENBVVosS0FBSyxBQUVGLFVBQVcsQ0FBQSxLQUFLLEVBQUMsRUFDaEIsWUFBWSxFVGhCWCxNQUFNLEdTaUJSOztBQWRMLEFBZUksY0FmVSxDQVVaLEtBQUssQ0FLSCxZQUFZLENBQUEsRUFDVixPQUFPLEVBQUUsSUFBSSxFQUNiLFNBQVMsRUFBRSxJQUFJLEdBSWhCOztBQXJCTCxBQWtCTSxjQWxCUSxDQVVaLEtBQUssQ0FLSCxZQUFZLEdBR04sQ0FBQyxDQUFBLEVBQ0gsWUFBWSxFVHRCYixNQUFNLEdTdUJOOztBQ2xDUCxNQUFNLENBQUMsTUFBTSxNQUFNLFNBQVMsRUFBRSxLQUFLLElBQ2pDLEFBQ0UsSUFERSxDQUNGLE1BQU0sQ0FBQSxFQUNKLEtBQUssRVZvQkwsSUFBSSxHVVRMO0VBYkgsQUFLUSxJQUxKLENBQ0YsTUFBTSxDQUVKLFdBQVcsQ0FDVCxVQUFVLENBQ1IsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFVmdCWCxJQUFJLEdVWkM7RUFWVCxBQU9VLElBUE4sQ0FDRixNQUFNLENBRUosV0FBVyxDQUNULFVBQVUsQ0FDUixLQUFLLENBRUgsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFQUFFLHdCQUF3QixHQUNoQztFQVNYLEFBQ0UsTUFESSxDQUNKLENBQUMsQ0FBQSxFQUNDLFNBQVMsRVZGRCxNQUFNLEVVR2QsV0FBVyxFVkFOLElBQUksR1VDVjtFQUVILEFBQ0UsS0FERyxDQUNILENBQUMsQ0FBQSxFQUNDLFNBQVMsRVZSRCxNQUFNLENVUVMsVUFBVSxFQUNqQyxXQUFXLEVWTk4sSUFBSSxDVU1hLFVBQVUsR0FDakM7RUFFSCxBQUNFLGNBRFksQ0FDWixFQUFFLENBQUEsRUFDQSxTQUFTLEVWZEQsTUFBTSxHVWVmO0VBSUgsQUFBQSxNQUFNLENBQUEsRUFDSixLQUFLLEVBQUUsMEJBQWlDLEdBQ3pDO0VBRUQsQUFDRSxJQURFLENBQ0YsTUFBTSxDQUFBLEVBQ0YsS0FBSyxFQUFFLDBCQUFpQyxHQUMzQztFQUhILEFBS0ksSUFMQSxBQUlELElBQUssQ0FBQSxLQUFLLEVBQ1QsTUFBTSxDQUFBLEVBQ0osVUFBVSxFVnZDVCxJQUFJLEdVd0NOO0VBUEwsQUFTRSxJQVRFLENBU0YsY0FBYyxDQUFBLEVBQ1osT0FBTyxFQUFFLElBQUksR0FDZDtFQUdILEFBQ0UsVUFEUSxDQUNSLEtBQUssQ0FBQSxFQUNILEtBQUssRUFBRSxXQUFpQixDQUFBLFVBQVUsR0FVbkM7RUFaSCxBQUdJLFVBSE0sQ0FDUixLQUFLLEFBRUYsVUFBVyxDQUFBLEdBQUcsRUFBQyxFQUNkLFlBQVksRUFBRSxDQUFDLEdBQ2hCO0VBTEwsQUFNSSxVQU5NLENBQ1IsS0FBSyxDQUtILFlBQVksQ0FBQSxFQUNWLGFBQWEsRVZwRGQsTUFBTSxHVXdETjtFQVhMLEFBUU0sVUFSSSxDQUNSLEtBQUssQ0FLSCxZQUFZLEdBRU4sQ0FBQyxDQUFDLEVBQ0osWUFBWSxFVnREZixNQUFNLEdVdURKO0VBS1AsQUFBQSxjQUFjLENBQUEsRUFDWixVQUFVLEVWNUROLElBQUksR1VtRVQ7RUFSRCxBQUVFLGNBRlksQ0FFWixLQUFLLENBQUEsRUFDSCxLQUFLLEVWbkRMLElBQUksR1V1REw7RUFQSCxBQUlJLGNBSlUsQ0FFWixLQUFLLEFBRUYsVUFBVyxDQUFBLEtBQUssRUFBQyxFQUNoQixZQUFZLEVBQUUsQ0FBQyxHQUNoQjs7QUFLUCxNQUFNLENBQUMsTUFBTSxNQUFNLFNBQVMsRUFBRSxLQUFLLE9BQU8sV0FBVyxFQUFFLFFBQVEsSUFFN0QsQUFDRSxJQURFLENBQ0YsTUFBTSxDQUFBLEVBQ0osS0FBSyxFVjlERixhQUFhLEdVeUVqQjtFQWJILEFBS1EsSUFMSixDQUNGLE1BQU0sQ0FFSixXQUFXLENBQ1QsVUFBVSxDQUNSLEtBQUssQ0FBQSxFQUNILEtBQUssRVZqRVIsK0JBQW9DLEdVcUVsQztFQVZULEFBT1UsSUFQTixDQUNGLE1BQU0sQ0FFSixXQUFXLENBQ1QsVUFBVSxDQUNSLEtBQUssQ0FFSCxLQUFLLENBQUEsRUFDSCxLQUFLLEVBQUUsd0JBQXdCLEdBQ2hDO0VBUVgsQUFDRSxNQURJLENBQ0osQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFQUFFLE1BQU0sRUFDakIsV0FBVyxFQUFFLElBQUksR0FDbEI7RUFRSCxBQUNFLGNBRFksQ0FDWixFQUFFLENBQUEsRUFDQSxTQUFTLEVWdEZELE1BQU0sR1V1RmY7RUFHSCxBQUVJLElBRkEsQUFDRCxJQUFLLENBQUEsS0FBSyxFQUNULE1BQU0sQ0FBQSxFQUNKLFVBQVUsRUFBRSxJQUFJLEdBQ2pCO0VBSUwsQUFDRSxVQURRLENBQ1IsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFQUFFLFdBQWlCLEdBVXpCO0VBWkgsQUFHSSxVQUhNLENBQ1IsS0FBSyxBQUVGLFVBQVcsQ0FBQSxHQUFHLEVBQUMsRUFDZCxZQUFZLEVBQUUsQ0FBQyxHQUNoQjtFQUxMLEFBTUksVUFOTSxDQUNSLEtBQUssQ0FLSCxZQUFZLENBQUEsRUFDVixhQUFhLEVWM0hkLE1BQU0sR1UrSE47RUFYTCxBQVFNLFVBUkksQ0FDUixLQUFLLENBS0gsWUFBWSxHQUVOLENBQUMsQ0FBQyxFQUNKLFlBQVksRVY3SGYsTUFBTSxHVThISjs7QUFNVCxNQUFNLENBQUMsTUFBTSxNQUFNLFNBQVMsRUFBRSxLQUFLLE9BQU8sV0FBVyxFQUFFLFNBQVMsSUFFOUQsQUFDRSxNQURJLENBQ0osQ0FBQyxDQUFBLEVBQ0MsU0FBUyxFVmhJRCxNQUFNLEVVaUlkLFdBQVcsRVY5SE4sSUFBSSxHVStIVjtFQUVILEFBQ0UsS0FERyxDQUNILENBQUMsQ0FBQSxFQUNDLFNBQVMsRVZ0SUQsTUFBTSxDVXNJUyxVQUFVLEVBQ2pDLFdBQVcsRVZwSU4sSUFBSSxDVW9JYSxVQUFVLEdBQ2pDO0VBRUgsQUFDRSxjQURZLENBQ1osRUFBRSxDQUFBLEVBQ0EsU0FBUyxFVjVJRCxNQUFNLEdVNklmO0VBSUgsQUFBQSxNQUFNLENBQUEsRUFDSixLQUFLLEVBQUUsMEJBQWlDLEdBQ3pDO0VBRUQsQUFDRSxJQURFLENBQ0YsTUFBTSxDQUFBLEVBQ0YsS0FBSyxFQUFFLDBCQUFpQyxHQUMzQztFQUhILEFBS0ksSUFMQSxBQUlELElBQUssQ0FBQSxLQUFLLEVBQ1QsTUFBTSxDQUFBLEVBQ0osVUFBVSxFVnJLVCxJQUFJLEdVc0tOO0VBUEwsQUFTRSxJQVRFLENBU0YsY0FBYyxDQUFBLEVBQ1osT0FBTyxFQUFFLElBQUksR0FDZDtFQUdILEFBQ0UsVUFEUSxDQUNSLEtBQUssQ0FBQSxFQUNILEtBQUssRVZ6S0osK0JBQW9DLEdVNkt0QztFQU5ILEFBR0ksVUFITSxDQUNSLEtBQUssQUFFRixVQUFXLENBQUEsR0FBRyxFQUFDLEVBQ2QsWUFBWSxFVjlLWixJQUFJLEdVK0tMO0VBSUwsQUFDRSxjQURZLENBQ1osS0FBSyxDQUFBLEVBQ0gsS0FBSyxFVmpMSiwrQkFBb0MsR1VxTHRDO0VBTkgsQUFHSSxjQUhVLENBQ1osS0FBSyxBQUVGLFVBQVcsQ0FBQSxLQUFLLEVBQUMsRUFDaEIsWUFBWSxFVnhMYixNQUFNLEdVeUxOOztBQUtQLE1BQU0sQ0FBQyxNQUFNLE1BQU0sU0FBUyxFQUFFLEtBQUssT0FBTyxXQUFXLEVBQUUsUUFBUSxJQUM3RCxBQUNFLE1BREksQ0FDSixDQUFDLENBQUEsRUFDQyxTQUFTLEVBQUUsTUFBTSxFQUNqQixXQUFXLEVBQUUsSUFBSSxHQUNsQjtFQUVILEFBQ0UsS0FERyxDQUNILENBQUMsQ0FBQSxFQUNDLFNBQVMsRVZyTEQsTUFBTSxDVXFMUyxVQUFVLEVBQ2pDLFdBQVcsRVZuTE4sSUFBSSxDVW1MYSxVQUFVLEdBQ2pDO0VBRUgsQUFDRSxjQURZLENBQ1osRUFBRSxDQUFBLEVBQ0EsU0FBUyxFVjNMRCxNQUFNLEdVNExmO0VBRUgsQUFFSSxJQUZBLEFBQ0QsSUFBSyxDQUFBLEtBQUssRUFDVCxNQUFNLENBQUEsRUFDSixVQUFVLEVBQUUsSUFBSSxHQUNqQjs7QUFLUCxNQUFNLENBQUMsTUFBTSxNQUFNLFNBQVMsRUFBRSxLQUFLLElBQ2pDLEFBQ0UsTUFESSxDQUNKLENBQUMsQ0FBQSxFQUNDLFNBQVMsRVZwTkQsTUFBTSxFVXFOZCxXQUFXLEVWbE5OLElBQUksR1VtTlY7RUFFSCxBQUNFLEtBREcsQ0FDSCxDQUFDLENBQUEsRUFDQyxTQUFTLEVWMU5ELE1BQU0sQ1UwTlMsVUFBVSxFQUNqQyxXQUFXLEVWeE5OLElBQUksQ1V3TmEsVUFBVSxHQUNqQztFQUVILEFBQ0UsY0FEWSxDQUNaLEVBQUUsQ0FBQSxFQUNBLFNBQVMsRVZoT0QsTUFBTSxHVWlPZjtFQUdILEFBRUksSUFGQSxBQUNELElBQUssQ0FBQSxLQUFLLEVBQ1QsTUFBTSxDQUFBLEVBQ0osVUFBVSxFVmpQVCxJQUFJLEdVa1BOOztBQUtQLE1BQU0sQ0FBQyxNQUFNLE1BQU0sU0FBUyxFQUFFLE1BQU0sT0FBTyxXQUFXLEVBQUUsU0FBUyxJQUMvRCxBQUNFLElBREUsQ0FDRixNQUFNLENBQUEsRUFDSixLQUFLLEVWM09GLGFBQWEsR1VzUGpCO0VBYkgsQUFLUSxJQUxKLENBQ0YsTUFBTSxDQUVKLFdBQVcsQ0FDVCxVQUFVLENBQ1IsS0FBSyxDQUFBLEVBQ0gsS0FBSyxFVjlPUiwrQkFBb0MsR1VrUGxDO0VBVlQsQUFPVSxJQVBOLENBQ0YsTUFBTSxDQUVKLFdBQVcsQ0FDVCxVQUFVLENBQ1IsS0FBSyxDQUVILEtBQUssQ0FBQSxFQUNILEtBQUssRUFBRSx3QkFBd0IsR0FDaEMifQ== */ diff --git a/user/themes/figureslibres/css-compiled/theme.min.css b/user/themes/figureslibres/css-compiled/theme.min.css index 953c28e..e11ee39 100644 --- a/user/themes/figureslibres/css-compiled/theme.min.css +++ b/user/themes/figureslibres/css-compiled/theme.min.css @@ -1 +1 @@ -@font-face{font-family:lato;src:url(../fonts/lato/Lato-Regular.eot);src:url(../fonts/lato/Lato-Regular.eot?#iefix) format('embedded-opentype'),url(../fonts/lato/Lato-Regular.woff) format('woff'),url(../fonts/lato/Lato-Regular.ttf) format('truetype'),url(../fonts/lato/Lato-Regular.svg#lato) format('svg')}@font-face{font-family:syne;src:url(../fonts/syne/Syne-Bold.eot);src:url(../fonts/syne/Syne-Bold.eot?#iefix) format('embedded-opentype'),url(../fonts/syne/Syne-Bold.woff) format('woff'),url(../fonts/syne/Syne-Bold.ttf) format('truetype'),url(../fonts/syne/Syne-Bold.svg#syne) format('svg')}@font-face{font-family:Avara;src:url(../fonts/avara/Avara-Bold_Italic_web.eot);src:url(../fonts/avara/Avara-Bold_Italic_web.eot?#iefix) format('embedded-opentype'),url(../fonts/avara/Avara-Bold_Italic_web.woff) format('woff'),url(../fonts/avara/Avara-Bold_Italic_web.ttf) format('truetype'),url(../fonts/avara/Avara-Bold_Italic_web.svg#Avara) format('svg')}@font-face{font-family:playfairdisplay-regularitalic;src:url(../fonts/playfair/playfairdisplay-regularitalic.eot);src:url(../fonts/playfair/playfairdisplay-regularitalic.eot?#iefix) format('embedded-opentype'),url(../fonts/playfair/playfairdisplay-regularitalic.woff) format('woff'),url(../fonts/playfair/playfairdisplay-regularitalic.ttf) format('truetype'),url(../fonts/playfair/playfairdisplay-regularitalic.svg#playfairdisplay-regularitalic) format('svg')}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{font-size:16px;line-height:1.15;margin:0;padding:0;-webkit-text-size-adjust:100%}body{margin:0;padding:0}main{display:block}h1{font-size:auto;margin:0}hr{overflow:visible;box-sizing:content-box;height:0}pre{font-family:monospace,monospace;font-size:1em}p{margin:0}ul{list-style:none}li,ul{margin:0;padding:0}a{text-decoration:none;color:#000;background-color:transparent}abbr[title]{text-decoration:underline;text-decoration:underline dotted;border-bottom:none;-webkit-text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{width:100%;height:100%;vertical-align:bottom;border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{display:table;box-sizing:border-box;max-width:100%;padding:0;white-space:normal;color:inherit}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}a,h1,h2,h3,li,p,span,ul{font-family:lato;font-weight:lighter}header a{font-size:2.5rem;line-height:3rem}.home p{font-size:2.5rem;line-height:3rem}.home #text_figli strong{position:relative}.home #text_figli strong::after{position:absolute;bottom:0;display:block;width:100%;height:1px;content:'';background:#000}.home #text_figli a{position:relative}.home #text_figli a#publique{font-family:playfairdisplay-regularitalic}.home #text_figli a#sociale{font-family:syne}.home #text_figli a#culturelle{font-family:Avara}#item .content-title{display:-ms-flexbox;display:flex;margin:.5rem 0}#item .content-title>*{font-size:1.15rem;font-weight:700;margin-right:.5rem}#item .content-title h2{margin:0 .5rem 0 0}#item .content-title h2 a{font-weight:700}a.publique,span.publique{font-family:playfairdisplay-regularitalic}a.sociale,span.sociale{font-family:syne}a.culturelle,span.culturelle{font-family:Avara}.sidebar-right a,.sidebar-right ul{font-size:.8rem;line-height:1.25rem}.other_projets h3{font-size:2.5rem;margin:0}.card h2{font-size:1rem;margin:0}header{position:fixed;top:0;left:0;width:calc(80% - .5rem);margin:0 0 0 .5rem;padding:.5rem 0;border-bottom:1px solid #000}*{box-sizing:border-box}body,header,html{background:#f9f9f9fe}body #start{width:calc((100% / 2) - (1rem / 2));margin:.5rem auto auto .5rem}body #start #text_figli p a{position:relative}body #start #text_figli p a.marg{margin-right:.5rem}body #start #text_figli #item_list{width:80vw}body #start #text_figli #item_list .card{line-height:0;position:relative;display:inline-block;width:calc((100% / 5) - (.5rem * 2));height:auto;margin-top:.5rem;margin-right:.5rem;vertical-align:top}body #start #text_figli #item_list .card a{display:block}body #start #text_figli #item_list .card .card-header{display:none}body #start #text_figli #item_list .card .card-header>*{font-size:.8rem;line-height:1rem;margin-right:.5rem}body #start #text_figli #item_list .card:hover .card-header{position:absolute;display:-ms-inline-flexbox;display:inline-flex;width:100%;padding:.5rem;transform:translateY(-100%);background:rgba(255,255,255,.5);-ms-flex-wrap:wrap;flex-wrap:wrap}body #start #text_figli #item_list .card:last-of-type{vertical-align:baseline!important}body #start #text_figli #item_list .card #item{width:calc(307% - .5rem)}body #start #text_figli #item_list .card #item .content-title{margin:30px auto}body #start #text_figli #item_list .card #item .content-title a{display:block}body #start #text_figli #item_list .card #item .e-content{margin:30px auto}body #start #text_figli #item_list .card #item .e-content p{font-size:1rem;line-height:1rem}body:not(.home) #start{margin-top:calc(2rem + 3rem)}.sidebar-right{position:fixed;top:0;right:0;display:-ms-inline-flexbox;display:inline-flex;width:20%;height:100%}.sidebar-right .list-projets{margin:auto}.sidebar-right .list-projets ul{margin:.5rem .5rem 0 0;text-align:right}.sidebar-right .list-projets ul:nth-child(1){margin-top:0}.sidebar-right .list-projets ul li:hover a{color:#000}.sidebar-right .list-projets ul a{color:grey}#item_list{display:-ms-inline-flexbox;display:inline-flex;width:100%;-ms-flex-wrap:wrap;flex-wrap:wrap}#item_list .card{width:calc((100% / 2) - (1rem / 2))}#item_list .card:nth-child(odd){margin-right:1rem}#item_list .card .card-header{display:-ms-inline-flexbox;display:inline-flex;width:100%;margin-bottom:.5rem}#item_list .card .card-header>*{margin-right:.5rem}#item .e-content{margin-bottom:.5rem;columns:300px 2}#item .card-image img{margin-bottom:.5rem}.other_projets{display:-ms-flexbox;display:flex;margin-top:1rem;margin-bottom:1rem;border-top:1px solid #000;-ms-flex-wrap:wrap;flex-wrap:wrap}.other_projets h3{width:100%;margin:1rem 0 .5rem 0}.other_projets .card{width:calc((100% / 3) - (1rem / 3))}.other_projets .card:nth-child(-1n+3){margin-right:.5rem}.other_projets .card .card-header{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.other_projets .card .card-header>*{margin-right:.5rem}@media screen and (max-width:414px){header a{font-size:1.5rem;line-height:2rem}.home p{font-size:1.5rem!important;line-height:2rem!important}.other_projets h3{font-size:1.5rem}header{width:calc(100% - (.5rem * 2))}body #start{width:calc(100% - (.5rem * 2))}body:not(.home) #start{margin-top:3rem}body .sidebar-right{display:none}#item_list .card{width:calc(100%)}#item_list .card:nth-child(odd){margin-right:0}#item_list .card .card-header{margin-bottom:.5rem}#item_list .card .card-header>*{margin-right:.5rem}.other_projets{margin-top:1rem}.other_projets .card{width:100%}.other_projets .card:nth-child(-1n+3){margin-right:0}}@media screen and (max-width:768px) and (orientation:portrait){header a{font-size:2.5rem;line-height:3rem}.home p{font-size:2.5rem!important;line-height:3rem!important}.other_projets h3{font-size:2.5rem}body:not(.home) #start{margin-top:5rem}#item_list .card{width:calc(100%)}#item_list .card:nth-child(odd){margin-right:0}#item_list .card .card-header{margin-bottom:.5rem}#item_list .card .card-header>*{margin-right:.5rem}}@media screen and (max-width:812px) and (orientation:landscape){header a{font-size:1.5rem;line-height:2rem}.home p{font-size:1.5rem!important;line-height:2rem!important}.other_projets h3{font-size:1.5rem}header{width:calc(100% - (.5rem * 2))}body #start{width:calc(100% - (.5rem * 2))}body:not(.home) #start{margin-top:3rem}body .sidebar-right{display:none}#item_list .card{width:calc((100% / 2) - (1rem / 2))}#item_list .card:nth-child(odd){margin-right:1rem}.other_projets .card{width:calc((100% / 3) - (1rem / 3))}.other_projets .card:nth-child(-1n+3){margin-right:.5rem}}@media screen and (max-width:910px) and (orientation:portrait){header a{font-size:2.5rem;line-height:3rem}.home p{font-size:2.5rem!important;line-height:3rem!important}.other_projets h3{font-size:2.5rem}body:not(.home) #start{margin-top:5rem}}@media screen and (max-width:660px){header a{font-size:1.5rem;line-height:2rem}.home p{font-size:1.5rem!important;line-height:2rem!important}.other_projets h3{font-size:1.5rem}body:not(.home) #start{margin-top:3rem}} \ No newline at end of file +@font-face{font-family:lato;src:url(../fonts/lato/Lato-Regular.eot);src:url(../fonts/lato/Lato-Regular.eot?#iefix) format('embedded-opentype'),url(../fonts/lato/Lato-Regular.woff) format('woff'),url(../fonts/lato/Lato-Regular.ttf) format('truetype'),url(../fonts/lato/Lato-Regular.svg#lato) format('svg')}@font-face{font-family:syne;src:url(../fonts/syne/Syne-Bold.eot);src:url(../fonts/syne/Syne-Bold.eot?#iefix) format('embedded-opentype'),url(../fonts/syne/Syne-Bold.woff) format('woff'),url(../fonts/syne/Syne-Bold.ttf) format('truetype'),url(../fonts/syne/Syne-Bold.svg#syne) format('svg')}@font-face{font-family:Avara;src:url(../fonts/avara/Avara-Bold_Italic_web.eot);src:url(../fonts/avara/Avara-Bold_Italic_web.eot?#iefix) format('embedded-opentype'),url(../fonts/avara/Avara-Bold_Italic_web.woff) format('woff'),url(../fonts/avara/Avara-Bold_Italic_web.ttf) format('truetype'),url(../fonts/avara/Avara-Bold_Italic_web.svg#Avara) format('svg')}@font-face{font-family:playfairdisplay-regularitalic;src:url(../fonts/playfair/playfairdisplay-regularitalic.eot);src:url(../fonts/playfair/playfairdisplay-regularitalic.eot?#iefix) format('embedded-opentype'),url(../fonts/playfair/playfairdisplay-regularitalic.woff) format('woff'),url(../fonts/playfair/playfairdisplay-regularitalic.ttf) format('truetype'),url(../fonts/playfair/playfairdisplay-regularitalic.svg#playfairdisplay-regularitalic) format('svg')}/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{font-size:16px;line-height:1.15;margin:0;padding:0;-webkit-text-size-adjust:100%}body{margin:0;padding:0}main{display:block}h1{font-size:auto;margin:0}hr{overflow:visible;box-sizing:content-box;height:0}pre{font-family:monospace,monospace;font-size:1em}p{margin:0}ul{list-style:none}li,ul{margin:0;padding:0}a{text-decoration:none;color:#000;background-color:transparent}abbr[title]{text-decoration:underline;text-decoration:underline dotted;border-bottom:none;-webkit-text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{width:100%;height:100%;vertical-align:bottom;border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{display:table;box-sizing:border-box;max-width:100%;padding:0;white-space:normal;color:inherit}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}a,h1,h2,h3,li,p,span,ul{font-family:lato;font-weight:lighter}header a{font-size:2.5rem;line-height:3rem}.home p{font-size:2.5rem;line-height:3rem}.home #text_figli strong{position:relative}.home #text_figli strong::after{position:absolute;bottom:0;display:block;width:100%;height:1px;content:'';background:#000}.home #text_figli a{position:relative}.home #text_figli a#publique{font-family:playfairdisplay-regularitalic}.home #text_figli a#sociale{font-family:syne}.home #text_figli a#culturelle{font-family:Avara}.home #text_figli #item .e-content p{font-size:1rem;line-height:1.2rem}#item h2{font-size:4rem!important;font-weight:400!important;line-height:4rem!important;width:auto}#item span{font-size:1rem}#item.publique h2{font-family:playfairdisplay-regularitalic}#item.publique span{font-family:playfairdisplay-regularitalic;font-weight:400}#item.sociale h2{font-family:syne}#item.sociale span{font-family:syne;font-weight:400}#item.culturelle h2{font-family:Avara}#item.culturelle span{font-family:Avara;font-weight:400}a.publique,span.publique{font-family:playfairdisplay-regularitalic}a.sociale,span.sociale{font-family:syne}a.culturelle,span.culturelle{font-family:Avara}.sidebar-right a,.sidebar-right ul{font-size:.7rem;line-height:.9rem}.other_projets h3{font-size:2.5rem;margin:0}.card h2{font-size:1rem;margin:0}#text_figli{z-index:999}#text_figli p>a{z-index:999}#text_figli p>a:after,#text_figli p>a:before{position:absolute;bottom:-1px;width:0;height:1px;margin:1px 0 0;content:'';transition:all .2s ease-in-out;transition-duration:.3s;opacity:0;background-color:#000}#text_figli p>a::before{left:50%}#text_figli p>a:after{right:50%}#text_figli p>a:hover{cursor:pointer}#text_figli p>a:hover:after,#text_figli p>a:hover:before{width:100%;opacity:1}#text_figli p>a:hover:after,#text_figli p>a:hover:before{width:50%}.circle_hover{position:absolute;z-index:0;width:0;height:0;transition:height .2s,width .2s;transform:translate(-50%,-50%);border-radius:500px;background:#add8e6}.circle_hover.open{width:200px;height:200px;transition:height .5s,width .5s}.circle_hover.publique{background:#ffffe0}.circle_hover.sociale{background:#90ee90}.circle_hover.culturelle{background:#add8e6}header{position:fixed;top:0;left:0;width:calc(80% - .5rem);margin:0 0 0 .5rem;padding:.5rem 0;border-bottom:1px solid #000}*{box-sizing:border-box}body,header,html{background:#f9f9f9fe}body #start{width:calc((100% / 2) - (1rem / 2));margin:.5rem auto auto .5rem}body #start #text_figli p a.marg{margin-right:.5rem}body #start #text_figli #item_list{width:80vw;margin-top:.5rem}body #start #text_figli #item_list .card{position:relative;display:inline-block;width:calc((100% / 5) - (.5rem * 2));height:100%;margin:0 .5rem .5rem 0;vertical-align:top}body #start #text_figli #item_list .card>.card-image.open a img{transition:filter .3s ease;filter:grayscale(0)}body #start #text_figli #item_list .card a{display:block;overflow:auto;border-radius:15px}body #start #text_figli #item_list .card a img{transition:filter .3s ease;filter:grayscale(1)}body #start #text_figli #item_list .card .card-header{display:none;width:100%;padding:.5rem;-ms-flex-wrap:wrap;flex-wrap:wrap}body #start #text_figli #item_list .card .card-header>*{font-size:.8rem;line-height:1rem;margin-right:.5rem}body #start #text_figli #item_list .card:hover a{display:block}body #start #text_figli #item_list .card:hover a img{transition:filter .3s ease;filter:grayscale(0)}body #start #text_figli #item_list .card:hover .card-header{position:absolute;display:-ms-inline-flexbox;display:inline-flex;transform:translateY(-100%)}body #start #text_figli #item_list .card:last-of-type{vertical-align:baseline!important}body #start #text_figli #item_list .card #item{width:calc(500% + (.5rem * 5));margin-top:.5rem;padding:2rem;border-radius:15px}body #start #text_figli #item_list .card #item .content-title{display:-ms-flexbox;display:flex;margin:0 auto 0 auto;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:baseline;align-items:baseline}body #start #text_figli #item_list .card #item .content-title a{display:block}body #start #text_figli #item_list .card #item .e-content{margin:1rem auto 2rem auto}body #start #text_figli #item_list .card #item .e-content p{font-size:1rem;line-height:1.2rem}body #start #text_figli #item_list .card #item.publique{background:#ffffe0}body #start #text_figli #item_list .card #item.sociale{background:#90ee90}body #start #text_figli #item_list .card #item.culturelle{background:#add8e6}body #start #text_figli #item_list.publique .card:hover .card-header{background:#ffffe0}body #start #text_figli #item_list.sociale .card:hover .card-header{background:#90ee90}body #start #text_figli #item_list.culturelle .card:hover .card-header{background:#add8e6}body:not(.home) #start{margin-top:calc(2rem + 3rem)}.sidebar-right{position:fixed;top:0;right:0;display:-ms-inline-flexbox;display:inline-flex;width:15%;height:100%}.sidebar-right .list-projets{margin:auto}.sidebar-right .list-projets ul{margin:.5rem .5rem 0 0;text-align:right}.sidebar-right .list-projets ul:nth-child(1){margin-top:0}.sidebar-right .list-projets ul li:hover a{color:#000}.sidebar-right .list-projets ul a{color:grey}.sidebar-right .list-projets ul a::before{display:inline-block;width:5px;height:5px;margin-right:5px;content:' ';vertical-align:middle;border-radius:5px}.sidebar-right .list-projets ul a#culturelle::before{background:#add8e6}.sidebar-right .list-projets ul a#sociale::before{background:#90ee90}.sidebar-right .list-projets ul a#publique::before{background:#ffffe0}.sidebar-right .list-projets ul a.active{color:#000}#item_list{display:-ms-inline-flexbox;display:inline-flex;width:100%;-ms-flex-wrap:wrap;flex-wrap:wrap}#item_list .card{width:calc((100% / 2) - (1rem / 2))}#item_list .card:nth-child(odd){margin-right:1rem}#item_list .card .card-header{display:-ms-inline-flexbox;display:inline-flex;width:100%;margin-bottom:.5rem}#item_list .card .card-header>*{margin-right:.5rem}#item .e-content{margin-bottom:.5rem;columns:300px 2}#item .card-image img:not(:last-of-type){margin-bottom:.5rem}.other_projets{display:-ms-flexbox;display:flex;margin-top:1rem;margin-bottom:1rem;border-top:1px solid #000;-ms-flex-wrap:wrap;flex-wrap:wrap}.other_projets h3{width:100%;margin:1rem 0 .5rem 0}.other_projets .card{width:calc((100% / 3) - (1rem / 3))}.other_projets .card:nth-child(-1n+3){margin-right:.5rem}.other_projets .card .card-header{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.other_projets .card .card-header>*{margin-right:.5rem}@media screen and (max-width:414px){body #start{width:100%}body #start #text_figli #item_list .card{width:100%}body #start #text_figli #item_list .card #item{width:calc(100% + (.5rem * 3))}header a{font-size:1.5rem;line-height:2rem}.home p{font-size:1.5rem!important;line-height:2rem!important}.other_projets h3{font-size:1.5rem}header{width:calc(100% - (.5rem * 2))}body #start{width:calc(100% - (.5rem * 2))}body:not(.home) #start{margin-top:3rem}body .sidebar-right{display:none}#item_list .card{width:calc(100%)!important}#item_list .card:nth-child(odd){margin-right:0}#item_list .card .card-header{margin-bottom:.5rem}#item_list .card .card-header>*{margin-right:.5rem}.other_projets{margin-top:1rem}.other_projets .card{width:100%}.other_projets .card:nth-child(-1n+3){margin-right:0}}@media screen and (max-width:768px) and (orientation:portrait){body #start{width:calc((80%))}body #start #text_figli #item_list .card{width:calc((100% / 3) - (1rem / 2))}body #start #text_figli #item_list .card #item{width:calc(300% + (.5rem * 3))}header a{font-size:2.5rem;line-height:3rem}.other_projets h3{font-size:2.5rem}body:not(.home) #start{margin-top:5rem}#item_list .card{width:calc(100%)}#item_list .card:nth-child(odd){margin-right:0}#item_list .card .card-header{margin-bottom:.5rem}#item_list .card .card-header>*{margin-right:.5rem}}@media screen and (max-width:812px) and (orientation:landscape){header a{font-size:1.5rem;line-height:2rem}.home p{font-size:1.5rem!important;line-height:2rem!important}.other_projets h3{font-size:1.5rem}header{width:calc(100% - (.5rem * 2))}body #start{width:calc(100% - (.5rem * 2))}body:not(.home) #start{margin-top:3rem}body .sidebar-right{display:none}#item_list .card{width:calc((100% / 2) - (1rem / 2))}#item_list .card:nth-child(odd){margin-right:1rem}.other_projets .card{width:calc((100% / 3) - (1rem / 3))}.other_projets .card:nth-child(-1n+3){margin-right:.5rem}}@media screen and (max-width:910px) and (orientation:portrait){header a{font-size:2.5rem;line-height:3rem}.home p{font-size:2.5rem!important;line-height:3rem!important}.other_projets h3{font-size:2.5rem}body:not(.home) #start{margin-top:5rem}}@media screen and (max-width:660px){header a{font-size:1.5rem;line-height:2rem}.home p{font-size:1.5rem!important;line-height:2rem!important}.other_projets h3{font-size:1.5rem}body:not(.home) #start{margin-top:3rem}}@media screen and (max-width:1024px) and (orientation:landscape){body #start{width:calc((80%))}body #start #text_figli #item_list .card{width:calc((100% / 3) - (1rem / 2))}body #start #text_figli #item_list .card #item{width:calc(300% + (.5rem * 3))}} \ No newline at end of file diff --git a/user/themes/figureslibres/js/site.js b/user/themes/figureslibres/js/site.js index 1313960..dda739a 100644 --- a/user/themes/figureslibres/js/site.js +++ b/user/themes/figureslibres/js/site.js @@ -27,6 +27,7 @@ function click_link() { }, complete: function(data) { console.log('cat', cat); + // animate(); click_img(e, $this, $href, url, page, cat, project_name); } }); @@ -39,7 +40,6 @@ function click_list() { $link.click(function(e) { e.preventDefault(); - var $this = $(this); $href = $this.attr("href"); url = $href.split("/"); @@ -53,18 +53,24 @@ function click_list() { if ($link_txt.hasClass('open') && $link_card.parent().hasClass('open') ) { - console.log('anchor'); anchor($href); }else if ($link_txt.hasClass('open')) { - + anchor($href); $.ajax({ url : '/' + page + '/' + cat + '/' + project_name, // La ressource ciblée type : 'GET', // Le type de la requête HTTP dataType:'html', success: function(data) { + $('#item_list.'+cat+' .card-image.open #item').remove(); + $('#item_list.'+cat+' .card-image').removeClass('open'); $(data).find('#item').addClass(cat).insertAfter($link_card); $link_card.parent().addClass('open'); + + }, + complete: function(data) { + // animate(); + marg_item(project_name); }, }); @@ -77,7 +83,9 @@ function click_list() { success: function(data) { $(data).find('#item_list').addClass(cat).insertAfter($link_txt); $link_txt.addClass('open'); + anchor($href); + $('.card a').click(function (e) { e.preventDefault() }) @@ -104,11 +112,13 @@ function click_list() { $(data).find('#item').addClass(cat).insertAfter($link_card); $link_card.parent().addClass('open'); }, + complete: function(data) { + // animate(); + marg_item(project_name); + }, }); } - - }, 1500); - + }, 900); } }); } @@ -116,7 +126,6 @@ function click_list() { } function click_img(e, $this, $href, url, page, cat, project_name) { - // $link_img = $('.card .card-image > a'); $('section.' + cat +' .card-image > a').click(function(e) { var $this = $(this); $href = $this.attr("href"); @@ -131,14 +140,18 @@ function click_img(e, $this, $href, url, page, cat, project_name) { $this.parent().removeClass('open'); $($this.parent().find('#item.'+ cat)).remove(); } else { - $.ajax({ url : '/' + page + '/' + cat + '/' + project_name, // La ressource ciblée type : 'GET', // Le type de la requête HTTP dataType:'html', success: function(data) { + $('#item_list.'+cat+' .card-image.open #item').remove(); + $('#item_list.'+cat+' .card-image').removeClass('open'); + $(data).find('#item').addClass(cat).insertAfter($this); $this.parent().addClass('open'); + marg_item(project_name); + anchor($href); }, }); @@ -149,11 +162,74 @@ function click_img(e, $this, $href, url, page, cat, project_name) { function anchor($href) { var aTag = $("a[href='"+ $href +"']"); - $('html,body').animate({scrollTop: aTag.offset().top},'slow'); + // console.log('aTag.offset()', aTag.offset().top+10); + $('html,body').animate({scrollTop: aTag.offset().top-7},'slow'); } +function animate() { + var $item_list = $('#item_list .card > .card-image'); + $item_list.fadeIn( "slow" ); +} + +function marg_item(project_name) { + + var $item = $('.card-image#'+project_name+' #item'); + var $item_offset = $item.offset().left-8; + var $card = $item.parents('.card'); + + var $item_list = $item.parents('#item_list'); + var pos_card = $card.offset().left; + + var margin_right = $item_list.width() - (pos_card + $card.width()); + +console.log('$card.width()',$card.width()); +console.log('pos_card',pos_card); + +console.log('$item_list',$item_list.width()); + + $item.css('margin-left', '-'+$item_offset +'px'); + // $item.parents('.card').css('margin-right', margin_right); +} + +function mouseHover() { + + $link_cat = $('#text_figli p > a'); + + $link_cat.mouseenter(function() { + link_id = $(this).attr("id"); + + $('html').append("
"); + console.log('link_id',link_id); + + setTimeout(function () { + $('.circle_hover').addClass('open '+ link_id); + }, 10); + + mousePos(); + + }) + .mouseleave(function() { + $('html').find(".circle_hover").remove(); + }); + + function mousePos() { + var currentMousePos = { x: 0, y: 0 }; + var $circle = $('.circle_hover'); + + $(document).mousemove(function(event) { + currentMousePos.x = event.pageX; + currentMousePos.y = event.pageY; + + $circle.css('left', currentMousePos.x ); + $circle.css('top', currentMousePos.y ); + }); + } + +}; + $(document).ready(function($){ click_link(); click_list(); + mouseHover(); }); diff --git a/user/themes/figureslibres/scss/configurations/_variable.scss b/user/themes/figureslibres/scss/configurations/_variable.scss index 28f916c..a36597c 100644 --- a/user/themes/figureslibres/scss/configurations/_variable.scss +++ b/user/themes/figureslibres/scss/configurations/_variable.scss @@ -8,7 +8,7 @@ $txt-figli: 2.5rem; // line-height $Flhome: 3rem; - + $marg: 0.5rem; $margI: 1rem; $Wi: 80%; @@ -22,6 +22,9 @@ $txt-figli-m: 1.5rem; // line-height $Flhome-m: 2rem; $Wi-m:100%; +$Wi80-m: calc(( 80% )); +$Wi33-m: calc(( 100% / 3 ) - (#{$margI} / 2)); + // tablette $txt-figli-t: 2.5rem; @@ -29,18 +32,7 @@ $txt-figli-t: 2.5rem; // line-height $Flhome-t: 3rem; -// :root { -// --color: red; -// } -// -// :root .category{ -// --color:blue; -// } -// -// .card{ -// color: var(--color); -// } -// -// :root .category{ -// color: var(--color); -// } +// color +$color_publique: lightyellow; +$color_sociale: lightgreen; +$color_culturelle: lightblue; diff --git a/user/themes/figureslibres/scss/theme.scss b/user/themes/figureslibres/scss/theme.scss index aedac93..9b56215 100644 --- a/user/themes/figureslibres/scss/theme.scss +++ b/user/themes/figureslibres/scss/theme.scss @@ -5,9 +5,10 @@ @import 'theme/reset'; @import 'theme/typography'; +@import 'theme/animation'; @import 'theme/header'; @import 'theme/start'; @import 'theme/sidebar'; @import 'theme/blog'; @import 'theme/item'; -@import 'theme/mobile'; +@import 'theme/mobile'; diff --git a/user/themes/figureslibres/scss/theme/_animation.scss b/user/themes/figureslibres/scss/theme/_animation.scss index 3809282..c5fb584 100644 --- a/user/themes/figureslibres/scss/theme/_animation.scss +++ b/user/themes/figureslibres/scss/theme/_animation.scss @@ -1,23 +1,66 @@ -.default-animation { - transition: all 0.5s ease; +$animate: all 0.2s ease-in-out; + +#text_figli{ + z-index: 999; + p{ + & > a{ + z-index: 999; + &:before, + &:after { + content: ""; + position: absolute; + bottom: -1px; + width: 0px; + height: 1px; + margin: 1px 0 0; + transition: $animate; + transition-duration: 0.3s; + opacity: 0; + background-color: black; + } + &::before{ + left: 50%; + } + &:after{ + right: 50%; + } + &:hover { + cursor: pointer; + &:before, + &:after { + width: 100%; + opacity: 1; + } + &:before, + &:after { + width: 50%; + } + } + } + } } -// Pulse Animation -.pulse { - animation-name: pulse_animation; - animation-duration: 2000ms; - transform-origin:70% 70%; - animation-iteration-count: infinite; - animation-timing-function: linear; -} - -@keyframes pulse_animation { - 0% { transform: scale(1); } - 30% { transform: scale(1); } - 40% { transform: scale(1.08); } - 50% { transform: scale(1); } - 60% { transform: scale(1); } - 70% { transform: scale(1.05); } - 80% { transform: scale(1); } - 100% { transform: scale(1); } +.circle_hover{ + position: absolute; + width: 0px; + height: 0px; + background: $color_culturelle; + border-radius: 500px; + transform: translate(-50%, -50%); + z-index: 0; + transition: height .2s, width .2s; + &.open{ + width: 200px; + height: 200px; + transition: height .5s, width .5s; + } + &.publique{ + background: $color_publique; + } + &.sociale{ + background: $color_sociale; + } + &.culturelle{ + background: $color_culturelle; + } } diff --git a/user/themes/figureslibres/scss/theme/_item.scss b/user/themes/figureslibres/scss/theme/_item.scss index a82c7b7..0b69cde 100644 --- a/user/themes/figureslibres/scss/theme/_item.scss +++ b/user/themes/figureslibres/scss/theme/_item.scss @@ -6,7 +6,7 @@ margin-bottom: $marg; } .card-image{ - img{ + img:not(:last-of-type){ margin-bottom: $marg; } } diff --git a/user/themes/figureslibres/scss/theme/_mobile.scss b/user/themes/figureslibres/scss/theme/_mobile.scss index 756aeb5..e48e338 100644 --- a/user/themes/figureslibres/scss/theme/_mobile.scss +++ b/user/themes/figureslibres/scss/theme/_mobile.scss @@ -1,4 +1,21 @@ @media screen and (max-width: 414px) { + body{ + #start{ + width: $Wi-m; + #text_figli{ + #item_list{ + .card{ + width: $Wi-m; + #item{ + width: calc(100% + (.5rem * 3)); + } + } + } + } + } + } + + // typographie header{ a{ @@ -39,7 +56,7 @@ #item_list{ .card{ - width: calc( #{$Wi-m} ); + width: calc( #{$Wi-m} )!important; &:nth-child(odd){ margin-right: 0; } @@ -64,6 +81,23 @@ } @media screen and (max-width: 768px) and (orientation: portrait) { + + body{ + #start{ + width: $Wi80-m; + #text_figli{ + #item_list{ + .card{ + width: $Wi33-m; + #item{ + width: calc(300% + (.5rem * 3)); + } + } + } + } + } + } + // typographie header{ a{ @@ -71,12 +105,12 @@ line-height: 3rem; } } - .home{ - p{ - font-size: $txt-figli-t!important; - line-height: $Flhome-t!important; - } - } + // .home{ + // p{ + // font-size: $txt-figli-t!important; + // line-height: $Flhome-t!important; + // } + // } .other_projets{ h3{ font-size: $txt-figli-t; @@ -105,9 +139,6 @@ } } } - - - } @media screen and (max-width: 812px) and (orientation: landscape) { @@ -222,3 +253,21 @@ } } } + +@media screen and (max-width: 1024px) and (orientation: landscape) { + body{ + #start{ + width: $Wi80-m; + #text_figli{ + #item_list{ + .card{ + width: $Wi33-m; + #item{ + width: calc(300% + (.5rem * 3)); + } + } + } + } + } + } +} diff --git a/user/themes/figureslibres/scss/theme/_sidebar.scss b/user/themes/figureslibres/scss/theme/_sidebar.scss index 98fc737..e50b0c7 100644 --- a/user/themes/figureslibres/scss/theme/_sidebar.scss +++ b/user/themes/figureslibres/scss/theme/_sidebar.scss @@ -1,6 +1,6 @@ .sidebar-right{ position: fixed; - width: 20%; + width: 15%; height: 100%; right: 0; top: 0; @@ -22,6 +22,33 @@ } a{ color: grey; + &::before{ + content: " "; + width: 5px; + height: 5px; + display: inline-block; + border-radius: 5px; + vertical-align: middle; + margin-right: 5px; + } + &#culturelle{ + &::before{ + background: $color_culturelle; + } + } + &#sociale{ + &::before{ + background: $color_sociale; + } + } + &#publique{ + &::before{ + background: $color_publique; + } + } + &.active{ + color: black; + } } } } diff --git a/user/themes/figureslibres/scss/theme/_start.scss b/user/themes/figureslibres/scss/theme/_start.scss index c526124..a15b85e 100644 --- a/user/themes/figureslibres/scss/theme/_start.scss +++ b/user/themes/figureslibres/scss/theme/_start.scss @@ -1,6 +1,7 @@ *{ box-sizing: border-box; } + html, body, header{ background: #f9f9f9fe; } @@ -12,7 +13,7 @@ body{ #text_figli{ p { a{ - position: relative; + // position: relative; &.marg{ margin-right: $marg; } @@ -20,20 +21,37 @@ body{ } #item_list{ width: 80vw; + margin-top: 0.5rem; .card{ width: calc((100% / 5) - ( #{$marg} * 2) ); - height: auto; - // overflow: hidden; display: inline-block; vertical-align: top; - line-height: 0; - margin-right: $marg; position: relative; - margin-top: $marg; + margin: 0 $marg $marg 0; + height: 100%; + & > .card-image{ + &.open{ + a{ + img{ + filter: grayscale(0); + transition: filter 0.3s ease; + } + } + } + } a{ display: block; + border-radius: 15px; + overflow: auto; + img{ + filter: grayscale(1); + transition: filter 0.3s ease; + } } .card-header{ + width: 100%; + padding: $marg; + flex-wrap: wrap; display: none; & > *{ font-size: 0.8rem; @@ -42,12 +60,15 @@ body{ } } &:hover{ + a{ + display: block; + img{ + filter: grayscale(0); + transition: filter 0.3s ease; + } + } .card-header{ - width: 100%; - background: rgba(255, 255, 255, 0.5); - padding: $marg; display: inline-flex; - flex-wrap: wrap; position: absolute; transform: translateY(-100%); } @@ -56,25 +77,69 @@ body{ vertical-align: baseline!important; } #item{ - width: calc( 307% - #{$marg} ); + width: calc( 500% + ( #{$marg} * 5) ); + padding: 2rem; + margin-top: $marg; + border-radius: 15px; .content-title{ - margin: 30px auto; + margin: 0 auto 0 auto; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; a{ display: block; } } .e-content{ - margin: 30px auto; + margin: 1rem auto 2rem auto; p{ font-size: 1rem; - line-height: 1rem; + line-height: 1.2rem; + } + } + &.publique{ + background: $color_publique; + + } + &.sociale{ + background: $color_sociale; + + } + &.culturelle{ + background: $color_culturelle; + } + } + } + &.publique{ + .card{ + &:hover{ + .card-header{ + background: $color_publique; + } + } + } + } + &.sociale{ + .card{ + &:hover{ + .card-header{ + background: $color_sociale; + } + } + } + } + &.culturelle{ + .card{ + &:hover{ + .card-header{ + background: $color_culturelle; } } } } } - - } + } } &:not(.home){ #start{ diff --git a/user/themes/figureslibres/scss/theme/_typography.scss b/user/themes/figureslibres/scss/theme/_typography.scss index df4e42c..8a09794 100644 --- a/user/themes/figureslibres/scss/theme/_typography.scss +++ b/user/themes/figureslibres/scss/theme/_typography.scss @@ -42,23 +42,52 @@ header{ font-family: $avara; } } + #item{ + .e-content{ + p{ + font-size: 1rem; + line-height: 1.2rem; + } + } + } } } #item{ - .content-title{ - display: flex; - margin: $marg 0; - & > * { - margin-right: $marg; - font-size: 1.15rem; - font-weight: bold; - } + h2{ + width: auto; + line-height: 4rem!important; + font-size: 4rem!important; + font-weight: normal!important; + } + span{ + font-size: 1rem; + } + &.publique{ + h2{ + font-family: $playfair; + } + span{ + font-family: $playfair; + font-weight: normal; + } + } + &.sociale{ + h2{ + font-family: $syne; + } + span{ + font-family: $syne; + font-weight: normal; + } + } + &.culturelle{ h2{ - margin: 0 $marg 0 0; - a{ - font-weight: bold; - } + font-family: $avara; + } + span{ + font-family: $avara; + font-weight: normal; } } } @@ -77,8 +106,8 @@ a.culturelle, span.culturelle{ .sidebar-right{ ul, a{ - font-size: 0.8rem; - line-height: 1.25rem; + font-size: 0.7rem; + line-height: 0.9rem; } } diff --git a/user/themes/figureslibres/templates/partials/base.html.twig b/user/themes/figureslibres/templates/partials/base.html.twig index a6038a4..f0f32d5 100644 --- a/user/themes/figureslibres/templates/partials/base.html.twig +++ b/user/themes/figureslibres/templates/partials/base.html.twig @@ -23,7 +23,6 @@ {% block javascripts %} {% do assets.addJs('jquery', 101) %} {% do assets.addJs('theme://js/site.js', {group:'bottom'}) %} - {% endblock %} {{ assets.js()|raw }} diff --git a/user/themes/figureslibres/templates/partials/blog-item.html.twig b/user/themes/figureslibres/templates/partials/blog-item.html.twig index 966933a..5734694 100644 --- a/user/themes/figureslibres/templates/partials/blog-item.html.twig +++ b/user/themes/figureslibres/templates/partials/blog-item.html.twig @@ -1,7 +1,10 @@
{% include 'partials/blog/title.html.twig' with {title_level: 'h2'} %} - {% include 'partials/blog/taxonomy.html.twig' %} - {% include 'partials/blog/date.html.twig' %} +
+ {% include 'partials/blog/taxonomy.html.twig' %} + {% include 'partials/blog/date.html.twig' %} +
+
diff --git a/user/themes/figureslibres/templates/partials/blog-list-item.html.twig b/user/themes/figureslibres/templates/partials/blog-list-item.html.twig index 09e64c6..e62a8d5 100644 --- a/user/themes/figureslibres/templates/partials/blog-list-item.html.twig +++ b/user/themes/figureslibres/templates/partials/blog-list-item.html.twig @@ -13,13 +13,12 @@ {% if page.header.title %} {% include 'partials/blog/title.html.twig' with {title_level: 'h2'} %} {% endif %} + {% if page.header.taxonomy.tag %} + {% include 'partials/blog/taxonomy.html.twig' %} + {% endif %} - {% if page.header.taxonomy.tag %} - {% include 'partials/blog/taxonomy.html.twig' %} - {% endif %} - - {% if page.header.date %} - {% include 'partials/blog/date.html.twig' %} - {% endif %} + {% if page.header.date %} + {% include 'partials/blog/date.html.twig' %} + {% endif %}
diff --git a/user/themes/figureslibres/templates/partials/blog/title.html.twig b/user/themes/figureslibres/templates/partials/blog/title.html.twig index aeed4c7..9bfe9eb 100644 --- a/user/themes/figureslibres/templates/partials/blog/title.html.twig +++ b/user/themes/figureslibres/templates/partials/blog/title.html.twig @@ -5,8 +5,8 @@ {% if page.header.continue_link is not same as(false) %} {% endif %} - {{ page.title }} + {{ page.title }} {% else %} - <{{ title_level }}>{{ page.title }} + <{{ title_level }}>{{ page.title }} {% endif %} diff --git a/user/themes/quark b/user/themes/quark index 78a9317..3a832dc 160000 --- a/user/themes/quark +++ b/user/themes/quark @@ -1 +1 @@ -Subproject commit 78a931745d7b9e6122ca41fe63a558f9e7465cf1 +Subproject commit 3a832dc0a7cca6e203aea5934de84b407e7d38bb