diff --git a/.gitignore b/.gitignore index 01b701cf8..755814004 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ user/plugins/* !user/plugins/.* user/themes/* !user/themes/.* +user/localhost/config/security.yaml # OS Generated .DS_Store* diff --git a/.htaccess b/.htaccess index 55bcd08a7..4017987a9 100644 --- a/.htaccess +++ b/.htaccess @@ -54,7 +54,7 @@ RewriteRule \.md$ error [F] # Block all direct access to files and folders beginning with a dot RewriteRule (^\.|/\.) - [F] # Block access to specific files in the root folder -RewriteRule ^(LICENSE|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess)$ error [F] +RewriteRule ^(LICENSE.txt|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess)$ error [F] ## End - Security diff --git a/CHANGELOG.md b/CHANGELOG.md index 764aff8f5..2fc9ca4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +# v1.0.0-rc.6 +## 12/01/2015 + +1. [](#new) + * Refactor Config classes for improved performance! + * Refactor Data classes to use `NestedArrayAccess` instead of `DataMutatorTrait` + * Added support for `classes` and `id` on medium objects to set CSS values + * Data objects: Allow function call chaining + * Data objects: Lazy load blueprints only if needed + * Automatically create unique security salt for each configuration + * Added Hungarian translation + * Added support for User groups +1. [](#improved) + * Improved robots.txt to disallow crawling of non-user folders + * Nonces only generated once per action and process + * Added IP into Nonce string calculation + * Nonces now use random string with random salt to improve performance + * Improved list form handling #475 + * Vendor library updates +1. [](#bugfix) + * Fixed help output for `bin/plugin` + * Fix for nested logic for lists and form parsing #273 + * Fix for array form fields and last entry not getting deleted + * Should not be able to set parent to self #308 + # v1.0.0-rc.5 ## 11/20/2015 @@ -6,7 +31,7 @@ * Implemented the ability for Plugins to provide their own CLI commands through `bin/plugin` * Added Croatian translation * Added missing `umask_fix` property to `system.yaml` - * Added current theme's config to global config. E.g. `config.theme.dropdown_enabled` + * Added current theme's config to global config. E.g. `config.theme.dropdown_enabled` * Added `append_url_extension` option to system config & page headers * Users have a new `state` property to allow disabling/banning * Added new `Page.relativePagePath()` helper method @@ -78,7 +103,7 @@ * German language improvements * Updated bundled composer 1. [](#bugfix) - * Accept variety of `true` values in `User.authorize()` method + * Accept variety of `true` values in `User.authorize()` method * Fix for `Validation` throwing an error if no label set # v1.0.0-rc.1 diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/bin/plugin b/bin/plugin index dc63f4a9d..ffe2dd81f 100755 --- a/bin/plugin +++ b/bin/plugin @@ -44,7 +44,7 @@ $grav['plugins']->init(); $grav['themes']->init(); $app = new Application('Grav Plugins Commands', GRAV_VERSION); -$pattern = '/([A-Z]\w+Command\.php)$/usm'; +$pattern = '([A-Z]\w+Command\.php)'; // get arguments and strip the application name if (null === $argv) { @@ -70,7 +70,7 @@ if (!$name) { $output->writeln(''); $output->writeln("Example:"); $output->writeln(" {$bin} error log -l 1 --trace"); - $list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '\/cli\/' . $pattern]); + $list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm']); if (count($list)) { $available = []; @@ -101,7 +101,7 @@ if ($plugin === null) { $path = 'plugins://' . $name . '/cli'; try { - $commands = Folder::all($path, ['compare' => 'Filename', 'pattern' => $pattern]); + $commands = Folder::all($path, ['compare' => 'Filename', 'pattern' => '/' . $pattern . '$/usm']); } catch (\RuntimeException $e) { $output->writeln("No Console Commands for '{$name}' where found in '{$path}'"); exit; diff --git a/composer.json b/composer.json index 724cff2f8..86116ed1f 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "mrclay/minify": "~2.2", "donatj/phpuseragentparser": "~0.3", "pimple/pimple": "~3.0", - "rockettheme/toolbox": "1.1.*", + "rockettheme/toolbox": "~1.2", "maximebf/debugbar": "~1.10" }, "autoload": { diff --git a/composer.lock b/composer.lock index c6c4fe6d9..d19e9a3f8 100644 --- a/composer.lock +++ b/composer.lock @@ -1,11 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "e1db721096772d41f16003b39b47c85a", - "content-hash": "294dd2282a332d96b19d163ad08e7ba7", + "hash": "b1323e540382de7390663756b3a87de7", "packages": [ { "name": "doctrine/cache", @@ -169,16 +168,16 @@ }, { "name": "erusev/parsedown-extra", - "version": "0.7.0", + "version": "0.7.1", "source": { "type": "git", "url": "https://github.com/erusev/parsedown-extra.git", - "reference": "11a44e076d02ffcc4021713398a60cd73f78b6f5" + "reference": "0db5cce7354e4b76f155d092ab5eb3981c21258c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/11a44e076d02ffcc4021713398a60cd73f78b6f5", - "reference": "11a44e076d02ffcc4021713398a60cd73f78b6f5", + "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/0db5cce7354e4b76f155d092ab5eb3981c21258c", + "reference": "0db5cce7354e4b76f155d092ab5eb3981c21258c", "shasum": "" }, "require": { @@ -209,7 +208,7 @@ "parsedown", "parser" ], - "time": "2015-01-25 14:52:34" + "time": "2015-11-01 10:19:22" }, { "name": "filp/whoops", @@ -217,16 +216,16 @@ "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "50a288b51058fa94cf5b37cfa4277535983cc9d5" + "reference": "9a393ceb80f7497b6513feb574638e87048fed55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/50a288b51058fa94cf5b37cfa4277535983cc9d5", - "reference": "50a288b51058fa94cf5b37cfa4277535983cc9d5", + "url": "https://api.github.com/repos/filp/whoops/zipball/9a393ceb80f7497b6513feb574638e87048fed55", + "reference": "9a393ceb80f7497b6513feb574638e87048fed55", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=5.3.3" }, "require-dev": { "mockery/mockery": "0.9.*" @@ -267,7 +266,7 @@ "whoops", "zf2" ], - "time": "2015-11-14 20:08:27" + "time": "2015-09-27 09:47:06" }, { "name": "gregwar/cache", @@ -666,16 +665,16 @@ }, { "name": "rockettheme/toolbox", - "version": "1.1.4", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/rockettheme/toolbox.git", - "reference": "ff677d8f66d1addd3590d0cb85bcbaff4174d9c9" + "reference": "0c7a3b4b6e4d73be8512e89f7acde6899334b7f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/ff677d8f66d1addd3590d0cb85bcbaff4174d9c9", - "reference": "ff677d8f66d1addd3590d0cb85bcbaff4174d9c9", + "url": "https://api.github.com/repos/rockettheme/toolbox/zipball/0c7a3b4b6e4d73be8512e89f7acde6899334b7f2", + "reference": "0c7a3b4b6e4d73be8512e89f7acde6899334b7f2", "shasum": "" }, "require": { @@ -711,20 +710,20 @@ "php", "rockettheme" ], - "time": "2015-10-15 23:27:40" + "time": "2015-11-24 17:04:24" }, { "name": "symfony/console", - "version": "v2.7.6", + "version": "v2.7.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5efd632294c8320ea52492db22292ff853a43766" + "reference": "16bb1cb86df43c90931df65f529e7ebd79636750" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5efd632294c8320ea52492db22292ff853a43766", - "reference": "5efd632294c8320ea52492db22292ff853a43766", + "url": "https://api.github.com/repos/symfony/console/zipball/16bb1cb86df43c90931df65f529e7ebd79636750", + "reference": "16bb1cb86df43c90931df65f529e7ebd79636750", "shasum": "" }, "require": { @@ -749,7 +748,10 @@ "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -767,20 +769,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2015-10-20 14:38:46" + "time": "2015-11-18 09:54:26" }, { "name": "symfony/event-dispatcher", - "version": "v2.7.6", + "version": "v2.7.7", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8" + "reference": "7e2f9c31645680026c2372edf66f863fc7757af5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87a5db5ea887763fa3a31a5471b512ff1596d9b8", - "reference": "87a5db5ea887763fa3a31a5471b512ff1596d9b8", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7e2f9c31645680026c2372edf66f863fc7757af5", + "reference": "7e2f9c31645680026c2372edf66f863fc7757af5", "shasum": "" }, "require": { @@ -806,7 +808,10 @@ "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -824,20 +829,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2015-10-11 09:39:48" + "time": "2015-10-30 20:10:21" }, { "name": "symfony/var-dumper", - "version": "v2.7.6", + "version": "v2.7.7", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "eb033050050916b6bfa51be71009ef67b16046c9" + "reference": "72bcb27411780eaee9469729aace73c0d46fb2b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/eb033050050916b6bfa51be71009ef67b16046c9", - "reference": "eb033050050916b6bfa51be71009ef67b16046c9", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/72bcb27411780eaee9469729aace73c0d46fb2b8", + "reference": "72bcb27411780eaee9469729aace73c0d46fb2b8", "shasum": "" }, "require": { @@ -858,7 +863,10 @@ ], "psr-4": { "Symfony\\Component\\VarDumper\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -880,20 +888,20 @@ "debug", "dump" ], - "time": "2015-10-25 17:17:38" + "time": "2015-11-18 13:41:01" }, { "name": "symfony/yaml", - "version": "v2.7.6", + "version": "v2.7.7", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "eca9019c88fbe250164affd107bc8057771f3f4d" + "reference": "4cfcd7a9fceba662b3c036b7d9a91f6197af046c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/eca9019c88fbe250164affd107bc8057771f3f4d", - "reference": "eca9019c88fbe250164affd107bc8057771f3f4d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4cfcd7a9fceba662b3c036b7d9a91f6197af046c", + "reference": "4cfcd7a9fceba662b3c036b7d9a91f6197af046c", "shasum": "" }, "require": { @@ -908,7 +916,10 @@ "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" - } + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -926,7 +937,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-10-11 09:39:48" + "time": "2015-11-18 13:41:01" }, { "name": "twig/twig", diff --git a/htaccess.txt b/htaccess.txt index 55bcd08a7..4017987a9 100644 --- a/htaccess.txt +++ b/htaccess.txt @@ -54,7 +54,7 @@ RewriteRule \.md$ error [F] # Block all direct access to files and folders beginning with a dot RewriteRule (^\.|/\.) - [F] # Block access to specific files in the root folder -RewriteRule ^(LICENSE|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess)$ error [F] +RewriteRule ^(LICENSE.txt|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess)$ error [F] ## End - Security diff --git a/lighttpd.conf b/lighttpd.conf index 69acbe484..8429b865f 100644 --- a/lighttpd.conf +++ b/lighttpd.conf @@ -27,7 +27,7 @@ url.rewrite-if-not-file = ( ) #IMPROVING SECURITY -$HTTP["url"] =~ "^/grav_path/(LICENSE|composer.json|composer.lock|nginx.conf|web.config)$" { +$HTTP["url"] =~ "^/grav_path/(LICENSE.txt|composer.json|composer.lock|nginx.conf|web.config)$" { url.access-deny = ("") } $HTTP["url"] =~ "^/grav_path/(.git|cache|bin|logs|backup)/(.*)" { diff --git a/nginx.conf b/nginx.conf index d325ff308..dbc581fee 100644 --- a/nginx.conf +++ b/nginx.conf @@ -38,7 +38,7 @@ server { # deny running scripts inside user folder location ~* /user/.*\.(txt|md|yaml|php|pl|py|cgi|twig|sh|bat)$ { return 403; } # deny access to specific files in the root folder - location ~ /(LICENSE|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess) { return 403; } + location ~ /(LICENSE.txt|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess) { return 403; } ## End - Security } diff --git a/robots.txt b/robots.txt index eb0536286..3b558d635 100644 --- a/robots.txt +++ b/robots.txt @@ -1,2 +1,11 @@ User-agent: * -Disallow: +Disallow: /backup/ +Disallow: /bin/ +Disallow: /cache/ +Disallow: /grav/ +Disallow: /logs/ +Disallow: /system/ +Disallow: /vendor/ +Disallow: /user/ +Allow: /user/pages/ +Allow: /user/themes/ diff --git a/system/blueprints/media/meta.yaml b/system/blueprints/media/meta.yaml new file mode 100644 index 000000000..7d242e0b5 --- /dev/null +++ b/system/blueprints/media/meta.yaml @@ -0,0 +1,7 @@ +form: + validation: loose + fields: + + alt_text: + type: string + label: Alt Text diff --git a/system/blueprints/media/move.yaml b/system/blueprints/media/move.yaml new file mode 100644 index 000000000..6287e0595 --- /dev/null +++ b/system/blueprints/media/move.yaml @@ -0,0 +1,8 @@ +form: + validation: loose + fields: + route: + type: select + label: PLUGIN_ADMIN.PAGE + classes: fancy + '@data-options': '\Grav\Common\Page\Pages::parents' diff --git a/system/blueprints/media/rename.yaml b/system/blueprints/media/rename.yaml new file mode 100644 index 000000000..529349cac --- /dev/null +++ b/system/blueprints/media/rename.yaml @@ -0,0 +1,8 @@ +form: + validation: loose + fields: + new_file_name: + type: text + label: PLUGIN_ADMIN_PRO.NEW_FILE_NAME + validate: + required: true diff --git a/system/blueprints/user/account.yaml b/system/blueprints/user/account.yaml index efe5bda74..83674c721 100644 --- a/system/blueprints/user/account.yaml +++ b/system/blueprints/user/account.yaml @@ -54,3 +54,26 @@ form: default: 'en' help: PLUGIN_ADMIN.LANGUAGE_HELP + groups: + type: selectize + size: large + label: PLUGIN_ADMIN.GROUPS + '@data-options': '\Grav\User\Groups::groups' + classes: fancy + help: PLUGIN_ADMIN.GROUPS_HELP + validate: + type: commalist + + access.admin: + type: array + label: PLUGIN_ADMIN.ADMIN_ACCESS + multiple: false + validate: + type: array + + access.site: + type: array + label: PLUGIN_ADMIN.SITE_ACCESS + multiple: false + validate: + type: array \ No newline at end of file diff --git a/system/blueprints/user/group.yaml b/system/blueprints/user/group.yaml new file mode 100644 index 000000000..e627a7c9e --- /dev/null +++ b/system/blueprints/user/group.yaml @@ -0,0 +1,44 @@ +title: Group +form: + validation: loose + + fields: + spacer: + type: spacer + text: '
' + + groupname: + type: text + size: large + label: PLUGIN_ADMIN.NAME + disabled: true + readonly: true + + readableName: + type: text + size: large + label: PLUGIN_ADMIN_PRO.READABLE_NAME + + description: + type: text + size: large + label: PLUGIN_ADMIN.DESCRIPTION + + icon: + type: text + size: small + label: PLUGIN_ADMIN_PRO.ICON + + access.admin: + type: array + label: PLUGIN_ADMIN.ADMIN_ACCESS + multiple: false + validate: + type: array + + access.site: + type: array + label: PLUGIN_ADMIN.SITE_ACCESS + multiple: false + validate: + type: array \ No newline at end of file diff --git a/system/blueprints/user/group_new.yaml b/system/blueprints/user/group_new.yaml new file mode 100644 index 000000000..dd816a954 --- /dev/null +++ b/system/blueprints/user/group_new.yaml @@ -0,0 +1,16 @@ +title: PLUGIN_ADMIN_PRO.ADD_GROUP + +form: + validation: loose + fields: + + content: + type: section + title: PLUGIN_ADMIN_PRO.ADD_GROUP + + groupname: + type: text + label: PLUGIN_ADMIN_PRO.GROUP_NAME + help: PLUGIN_ADMIN_PRO.GROUP_NAME_HELP + validate: + required: true diff --git a/system/config/system.yaml b/system/config/system.yaml index 8f09cf599..f64620cbf 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -108,6 +108,6 @@ media: session: enabled: true # Enable Session support timeout: 1800 # Timeout in seconds - name: grav-site # Name prefix of the session cookie + name: grav-site # Name prefix of the session cookie. Use alphanumeric, dashes or underscores only. Do not use dots in the session name diff --git a/system/defines.php b/system/defines.php index f3aacc286..2e0e01442 100644 --- a/system/defines.php +++ b/system/defines.php @@ -2,7 +2,7 @@ // Some standard defines define('GRAV', true); -define('GRAV_VERSION', '1.0.0-rc.5'); +define('GRAV_VERSION', '1.0.0-rc.6'); define('DS', '/'); // Directories and Paths diff --git a/system/languages/en.yaml b/system/languages/en.yaml index a1eab6f19..654c0d3e5 100644 --- a/system/languages/en.yaml +++ b/system/languages/en.yaml @@ -95,3 +95,4 @@ NICETIME: FORM: VALIDATION_FAIL: Validation failed: INVALID_INPUT: Invalid input in + MISSING_REQUIRED_FIELD: Missing required field: diff --git a/system/languages/es.yaml b/system/languages/es.yaml new file mode 100644 index 000000000..41df5d55b --- /dev/null +++ b/system/languages/es.yaml @@ -0,0 +1,42 @@ +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: dia + WEEK: semana + MONTH: mes + YEAR: año + DECADE: decada + SEC: seg + MIN: min + HR: hr + DAY: dia + WK: sem + MO: mes + YR: yr + 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: decadas + SEC_PLURAL: segs + MIN_PLURAL: mins + HR_PLURAL: hrs + DAY_PLURAL: dias + WK_PLURAL: sem + MO_PLURAL: mes + YR_PLURAL: años + DEC_PLURAL: decs +FORM: + VALIDATION_FAIL: Falló la validación. + INVALID_INPUT: "Dato inválido en: " + MISSING_REQUIRED_FIELD: "Falta el campo requerido: " + diff --git a/system/languages/fr.yaml b/system/languages/fr.yaml index bfc1c117f..4dcafc8a8 100644 --- a/system/languages/fr.yaml +++ b/system/languages/fr.yaml @@ -1,26 +1,60 @@ +FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Erreur : Frontmatter invalide\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" INFLECTOR_PLURALS: - '/$/': 's' - '/(bijou|caillou|chou|genou|hibou|joujou|pou|au|eu|eau)$/': '\1x' - '/(bleu|émeu|landau|lieu|pneu|sarrau)$/': '\1s' - '/(b|cor|ém|gemm|soupir|trav|vant|vitr)ail$/': '\1aux' - '/(s|x|z)$/': '\1' - '/ail$/': 'ails' - '/al$/': 'aux' + '/(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: - '/(bijou|caillou|chou|genou|hibou|joujou|pou|au|eu|eau)x$/': '\1' - '/(b|cor|ém|gemm|soupir|trav|vant|vitr)aux$/': '\1ail' - '/(journ|chev)aux$/': '\1al' - '/ails$/': 'ail' + '/(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: ['équipment', 'information', 'riz', 'argent', 'espèces', 'séries', 'poisson', 'mouton'] INFLECTOR_IRREGULAR: - 'madame': 'mesdames' - 'mademoiselle': 'mesdemoiselles' - 'monsieur': 'messieurs' + 'person': 'personnes' + 'man': 'Hommes' + 'child': 'enfants' + 'sex': 'sexes' + 'move': 'déplacemements' INFLECTOR_ORDINALS: 'default': 'ème' 'first': 'er' 'second': 'nd' + 'third': 'ème' NICETIME: NO_DATE_PROVIDED: Aucune date BAD_DATE: Date erronée @@ -48,7 +82,7 @@ NICETIME: DAY_PLURAL: jours WEEK_PLURAL: semaines MONTH_PLURAL: mois - YEAR_PLURAL: ans + YEAR_PLURAL: années DECADE_PLURAL: décennies SEC_PLURAL: s MIN_PLURAL: m @@ -58,3 +92,7 @@ NICETIME: MO_PLURAL: m YR_PLURAL: a DEC_PLURAL: d +FORM: + VALIDATION_FAIL: La validation a échoué : + INVALID_INPUT: Saisie non valide + MISSING_REQUIRED_FIELD: Champ obligatoire manquant : diff --git a/system/languages/hr.yaml b/system/languages/hr.yaml index 0d84281c5..3dbd80218 100644 --- a/system/languages/hr.yaml +++ b/system/languages/hr.yaml @@ -26,12 +26,18 @@ NICETIME: YR: g DEC: des SECOND_PLURAL: sekundi + SECOND_PLURAL_MORE_THAN_TWO: sekunde MINUTE_PLURAL: minuta + MINUTE_PLURAL_MORE_THAN_TWO: minute HOUR_PLURAL: sati + HOUR_PLURAL_MORE_THAN_TWO: sata DAY_PLURAL: dana WEEK_PLURAL: tjedana + WEEK_PLURAL_MORE_THAN_TWO: tjedna MONTH_PLURAL: mjeseci + MONTH_PLURAL_MORE_THAN_TWO: mjeseca YEAR_PLURAL: godina + YEAR_PLURAL_MORE_THAN_TWO: godine DECADE_PLURAL: desetljeća SEC_PLURAL: sek MIN_PLURAL: min diff --git a/system/languages/hu.yaml b/system/languages/hu.yaml new file mode 100644 index 000000000..71c0ee91e --- /dev/null +++ b/system/languages/hu.yaml @@ -0,0 +1,52 @@ +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_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: ó + DAY: nap + 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: ó + DAY_PLURAL: nap + 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: diff --git a/system/languages/it.yaml b/system/languages/it.yaml index 5f61ad3c1..c6a92ec9d 100644 --- a/system/languages/it.yaml +++ b/system/languages/it.yaml @@ -19,3 +19,7 @@ NICETIME: MONTH_PLURAL: mesi YEAR_PLURAL: anni DECADE_PLURAL: decadi +FORM: + VALIDATION_FAIL: Validazione fallita: + INVALID_INPUT: Input invalido in + MISSING_REQUIRED_FIELD: Campo richiesto mancante: diff --git a/system/src/Grav/Common/Config/Blueprints.php b/system/src/Grav/Common/Config/Blueprints.php deleted file mode 100644 index 1af61f47c..000000000 --- a/system/src/Grav/Common/Config/Blueprints.php +++ /dev/null @@ -1,207 +0,0 @@ -grav = $grav ?: Grav::instance(); - } - - public function init() - { - /** @var UniformResourceLocator $locator */ - $locator = $this->grav['locator']; - - $blueprints = $locator->findResources('blueprints://config'); - $plugins = $locator->findResources('plugins://'); - - $blueprintFiles = $this->getBlueprintFiles($blueprints, $plugins); - - $this->loadCompiledBlueprints($plugins + $blueprints, $blueprintFiles); - } - - protected function loadCompiledBlueprints($blueprints, $blueprintFiles) - { - $checksum = md5(serialize($blueprints)); - $filename = CACHE_DIR . 'compiled/blueprints/' . $checksum .'.php'; - $checksum .= ':'.md5(serialize($blueprintFiles)); - $class = get_class($this); - $file = PhpFile::instance($filename); - - if ($file->exists()) { - $cache = $file->exists() ? $file->content() : null; - } else { - $cache = null; - } - - - // Load real file if cache isn't up to date (or is invalid). - if ( - !is_array($cache) - || empty($cache['checksum']) - || empty($cache['$class']) - || $cache['checksum'] != $checksum - || $cache['@class'] != $class - ) { - // Attempt to lock the file for writing. - $file->lock(false); - - // Load blueprints. - $this->blueprints = new Blueprints(); - foreach ($blueprintFiles as $key => $files) { - $this->loadBlueprints($key); - } - - $cache = [ - '@class' => $class, - 'checksum' => $checksum, - 'files' => $blueprintFiles, - 'data' => $this->blueprints->toArray() - ]; - - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $file->save($cache); - $file->unlock(); - } - } else { - $this->blueprints = new Blueprints($cache['data']); - } - } - - /** - * Load global blueprints. - * - * @param string $key - * @param array $files - */ - public function loadBlueprints($key, array $files = null) - { - if (is_null($files)) { - $files = $this->files[$key]; - } - foreach ($files as $name => $item) { - $file = CompiledYamlFile::instance($item['file']); - $this->blueprints->embed($name, $file->content(), '/'); - } - } - - /** - * Get all blueprint files (including plugins). - * - * @param array $blueprints - * @param array $plugins - * @return array - */ - protected function getBlueprintFiles(array $blueprints, array $plugins) - { - $list = []; - foreach (array_reverse($plugins) as $folder) { - $list += $this->detectPlugins($folder, true); - } - foreach (array_reverse($blueprints) as $folder) { - $list += $this->detectConfig($folder, true); - } - return $list; - } - - /** - * Detects all plugins with a configuration file and returns last modification time. - * - * @param string $lookup Location to look up from. - * @param bool $blueprints - * @return array - * @internal - */ - protected function detectPlugins($lookup = SYSTEM_DIR, $blueprints = false) - { - $find = $blueprints ? 'blueprints.yaml' : '.yaml'; - $location = $blueprints ? 'blueprintFiles' : 'configFiles'; - $path = trim(Folder::getRelativePath($lookup), '/'); - if (isset($this->{$location}[$path])) { - return [$path => $this->{$location}[$path]]; - } - - $list = []; - - if (is_dir($lookup)) { - $iterator = new \DirectoryIterator($lookup); - - /** @var \DirectoryIterator $directory */ - foreach ($iterator as $directory) { - if (!$directory->isDir() || $directory->isDot()) { - continue; - } - - $name = $directory->getBasename(); - $filename = "{$path}/{$name}/" . ($find && $find[0] != '.' ? $find : $name . $find); - - if (is_file($filename)) { - $list["plugins/{$name}"] = ['file' => $filename, 'modified' => filemtime($filename)]; - } - } - } - - $this->{$location}[$path] = $list; - - return [$path => $list]; - } - - /** - * Detects all plugins with a configuration file and returns last modification time. - * - * @param string $lookup Location to look up from. - * @param bool $blueprints - * @return array - * @internal - */ - protected function detectConfig($lookup = SYSTEM_DIR, $blueprints = false) - { - $location = $blueprints ? 'blueprintFiles' : 'configFiles'; - $path = trim(Folder::getRelativePath($lookup), '/'); - if (isset($this->{$location}[$path])) { - return [$path => $this->{$location}[$path]]; - } - - if (is_dir($lookup)) { - // Find all system and user configuration files. - $options = [ - 'compare' => 'Filename', - 'pattern' => '|\.yaml$|', - 'filters' => [ - 'key' => '|\.yaml$|', - 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { - return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]; - }], - 'key' => 'SubPathname' - ]; - - $list = Folder::all($lookup, $options); - } else { - $list = []; - } - - $this->{$location}[$path] = $list; - - return [$path => $list]; - } -} diff --git a/system/src/Grav/Common/Config/CompiledBase.php b/system/src/Grav/Common/Config/CompiledBase.php new file mode 100644 index 000000000..f861cef34 --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledBase.php @@ -0,0 +1,236 @@ +cacheFolder = $cacheFolder; + $this->files = $files; + $this->path = $path ? rtrim($path, '\\/') . '/' : ''; + } + + /** + * Get filename for the compiled PHP file. + * + * @param string $name + * @return $this + */ + public function name($name = null) + { + if (!$this->name) { + $this->name = $name ?: md5(json_encode(array_keys($this->files))); + } + + return $this; + } + + /** + * Function gets called when cached configuration is saved. + */ + public function modified() {} + + /** + * Load the configuration. + * + * @return mixed + */ + public function load() + { + if ($this->object) { + return $this->object; + } + + $filename = $this->createFilename(); + if (!$this->loadCompiledFile($filename) && $this->loadFiles()) { + $this->saveCompiledFile($filename); + } + + return $this->object; + } + + /** + * Returns checksum from the configuration files. + * + * You can set $this->checksum = false to disable this check. + * + * @return bool|string + */ + public function checksum() + { + if (!isset($this->checksum)) { + $this->checksum = md5(json_encode($this->files) . $this->version); + } + + return $this->checksum; + } + + protected function createFilename() + { + return "{$this->cacheFolder}/{$this->name()->name}.php"; + } + + /** + * Create configuration object. + * + * @param array $data + */ + abstract protected function createObject(array $data = []); + + /** + * Finalize configuration object. + */ + abstract protected function finalizeObject(); + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + abstract protected function loadFile($name, $filename); + + /** + * Load and join all configuration files. + * + * @return bool + * @internal + */ + protected function loadFiles() + { + $this->createObject(); + + $list = array_reverse($this->files); + foreach ($list as $files) { + foreach ($files as $name => $item) { + $this->loadFile($name, $this->path . $item['file']); + } + } + + $this->finalizeObject(); + + return true; + } + + /** + * Load compiled file. + * + * @param string $filename + * @return bool + * @internal + */ + protected function loadCompiledFile($filename) + { + if (!file_exists($filename)) { + return false; + } + + $cache = include $filename; + if ( + !is_array($cache) + || !isset($cache['checksum']) + || !isset($cache['data']) + || !isset($cache['@class']) + || $cache['@class'] != get_class($this) + ) { + return false; + } + + // Load real file if cache isn't up to date (or is invalid). + if ($cache['checksum'] !== $this->checksum()) { + return false; + } + + $this->createObject($cache['data']); + + return true; + } + + /** + * Save compiled file. + * + * @param string $filename + * @throws \RuntimeException + * @internal + */ + protected function saveCompiledFile($filename) + { + $file = PhpFile::instance($filename); + + // Attempt to lock the file for writing. + try { + $file->lock(false); + } catch (\Exception $e) { + // Another process has locked the file; we will check this in a bit. + } + + if ($file->locked() === false) { + // File was already locked by another process. + return; + } + + $cache = [ + '@class' => get_class($this), + 'timestamp' => time(), + 'checksum' => $this->checksum(), + 'files' => $this->files, + 'data' => $this->object->toArray() + ]; + + $file->save($cache); + $file->unlock(); + $file->free(); + + $this->modified(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledBlueprints.php b/system/src/Grav/Common/Config/CompiledBlueprints.php new file mode 100644 index 000000000..d78a0e04e --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledBlueprints.php @@ -0,0 +1,49 @@ +object = new Blueprints($data); + } + + /** + * Finalize configuration object. + */ + protected function finalizeObject() {} + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + $this->object->embed($name, $file->content(), '/'); + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledConfig.php b/system/src/Grav/Common/Config/CompiledConfig.php new file mode 100644 index 000000000..cb9e59de2 --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledConfig.php @@ -0,0 +1,98 @@ +callable = $blueprints; + + return $this; + } + + /** + * @param bool $withDefaults + * @return mixed + */ + public function load($withDefaults = false) + { + $this->withDefaults = $withDefaults; + + return parent::load(); + } + + /** + * Create configuration object. + * + * @param array $data + */ + protected function createObject(array $data = []) + { + if ($this->withDefaults && empty($data) && is_callable($this->callable)) { + $blueprints = $this->callable; + $data = $blueprints()->getDefaults(); + } + + $this->object = new Config($data, $this->callable); + } + + /** + * Finalize configuration object. + */ + protected function finalizeObject() + { + $this->object->checksum($this->checksum()); + } + + /** + * Function gets called when cached configuration is saved. + */ + public function modified() + { + $this->object->modified(true); + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + $this->object->join($name, $file->content(), '/'); + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/CompiledLanguages.php b/system/src/Grav/Common/Config/CompiledLanguages.php new file mode 100644 index 000000000..ce4eb59dc --- /dev/null +++ b/system/src/Grav/Common/Config/CompiledLanguages.php @@ -0,0 +1,64 @@ +object = new Languages($data); + } + + /** + * Finalize configuration object. + */ + protected function finalizeObject() + { + $this->object->checksum($this->checksum()); + } + + + /** + * Function gets called when cached configuration is saved. + */ + public function modified() + { + $this->object->modified(true); + } + + /** + * Load single configuration file and append it to the correct position. + * + * @param string $name Name of the position. + * @param string $filename File to be loaded. + */ + protected function loadFile($name, $filename) + { + $file = CompiledYamlFile::instance($filename); + if (preg_match('|languages\.yaml$|', $filename)) { + $this->object->mergeRecursive($file->content()); + } else { + $this->object->join($name, $file->content(), '/'); + } + $file->free(); + } +} diff --git a/system/src/Grav/Common/Config/Config.php b/system/src/Grav/Common/Config/Config.php index ed13dbcbd..dc5169750 100644 --- a/system/src/Grav/Common/Config/Config.php +++ b/system/src/Grav/Common/Config/Config.php @@ -1,12 +1,10 @@ [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['system'], - ] - ], - 'user' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user'], - ] - ], - 'asset' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['assets'], - ] - ], - 'blueprints' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://blueprints', 'system/blueprints'], - ] - ], - 'config' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://config', 'system/config'], - ] - ], - 'plugins' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://plugins'], - ] - ], - 'plugin' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://plugins'], - ] - ], - 'themes' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://themes'], - ] - ], - 'languages' => [ - 'type' => 'ReadOnlyStream', - 'prefixes' => [ - '' => ['user://languages', 'system/languages'], - ] - ], - 'cache' => [ - 'type' => 'Stream', - 'prefixes' => [ - '' => ['cache'], - 'images' => ['images'] - ] - ], - 'log' => [ - 'type' => 'Stream', - 'prefixes' => [ - '' => ['logs'] - ] - ], - 'backup' => [ - 'type' => 'Stream', - 'prefixes' => [ - '' => ['backup'] - ] - ] - ]; - - protected $setup = []; - - protected $blueprintFiles = []; - protected $configFiles = []; - protected $languageFiles = []; protected $checksum; - protected $timestamp; - - protected $configLookup; - protected $blueprintLookup; - protected $pluginLookup; - protected $languagesLookup; - - protected $finder; - protected $environment; - protected $messages = []; - - protected $languages; - - public function __construct(array $setup = array(), Grav $grav = null, $environment = null) - { - $this->grav = $grav ?: Grav::instance(); - $this->finder = new ConfigFinder; - $this->environment = $environment ?: 'localhost'; - $this->messages[] = 'Environment Name: ' . $this->environment; - - // Make sure that - if (!isset($setup['streams']['schemes'])) { - $setup['streams']['schemes'] = []; - } - $setup['streams']['schemes'] += $this->streams; - - $setup = $this->autoDetectEnvironmentConfig($setup); - - $this->setup = $setup; - parent::__construct($setup); - - $this->check(); - } + protected $modified = false; public function key() { return $this->checksum(); } - public function reload() + public function checksum($checksum = null) { - $this->items = $this->setup; - $this->check(); - $this->init(); - $this->debug(); - - return $this; - } - - protected function check() - { - $streams = isset($this->items['streams']['schemes']) ? $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)); - if ($diff) { - throw new \InvalidArgumentException( - sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff)) - ); - } - } - - public function debug() - { - foreach ($this->messages as $message) { - $this->grav['debugger']->addMessage($message); - } - $this->messages = []; - } - - public function init() - { - /** @var UniformResourceLocator $locator */ - $locator = $this->grav['locator']; - - $this->configLookup = $locator->findResources('config://'); - $this->blueprintLookup = $locator->findResources('blueprints://config'); - $this->pluginLookup = $locator->findResources('plugins://'); - - - $this->loadCompiledBlueprints($this->blueprintLookup, $this->pluginLookup, 'master'); - $this->loadCompiledConfig($this->configLookup, $this->pluginLookup, 'master'); - - // process languages if supported - if ($this->get('system.languages.translations', true)) { - $this->languagesLookup = $locator->findResources('languages://'); - $this->loadCompiledLanguages($this->languagesLookup, $this->pluginLookup, 'master'); - } - - $this->initializeLocator($locator); - } - - public function checksum() - { - if (empty($this->checksum)) { - $checkBlueprints = $this->get('system.cache.check.blueprints', false); - $checkLanguages = $this->get('system.cache.check.languages', false); - $checkConfig = $this->get('system.cache.check.config', true); - $checkSystem = $this->get('system.cache.check.system', true); - - if (!$checkBlueprints && !$checkLanguages && !$checkConfig && !$checkSystem) { - $this->messages[] = 'Skip configuration timestamp check.'; - return false; - } - - // Generate checksum according to the configuration settings. - if (!$checkConfig) { - // Just check changes in system.yaml files and ignore all the other files. - $cc = $checkSystem ? $this->finder->locateConfigFile($this->configLookup, 'system') : []; - } else { - // Check changes in all configuration files. - $cc = $this->finder->locateConfigFiles($this->configLookup, $this->pluginLookup); - } - - if ($checkBlueprints) { - $cb = $this->finder->locateBlueprintFiles($this->blueprintLookup, $this->pluginLookup); - } else { - $cb = []; - } - - if ($checkLanguages) { - $cl = $this->finder->locateLanguageFiles($this->languagesLookup, $this->pluginLookup); - } else { - $cl = []; - } - - $this->checksum = md5(json_encode([$cc, $cb, $cl])); + if ($checksum !== null) { + $this->checksum = $checksum; } return $this->checksum; } - protected function autoDetectEnvironmentConfig($items) + public function modified($modified = null) { - $environment = $this->environment; - $env_stream = 'user://'.$environment.'/config'; - - if (file_exists(USER_DIR.$environment.'/config')) { - array_unshift($items['streams']['schemes']['config']['prefixes'][''], $env_stream); + if ($modified !== null) { + $this->modified = $modified; } - return $items; + return $this->modified; } - protected function loadCompiledBlueprints($blueprints, $plugins, $filename = null) - { - $checksum = md5(json_encode($blueprints)); - $filename = $filename - ? CACHE_DIR . 'compiled/blueprints/' . $filename . '-' . $this->environment . '.php' - : CACHE_DIR . 'compiled/blueprints/' . $checksum . '-' . $this->environment . '.php'; - $file = PhpFile::instance($filename); - $cache = $file->exists() ? $file->content() : null; - $blueprintFiles = $this->finder->locateBlueprintFiles($blueprints, $plugins); - $checksum .= ':'.md5(json_encode($blueprintFiles)); - $class = get_class($this); - - // Load real file if cache isn't up to date (or is invalid). - if ( - !is_array($cache) - || !isset($cache['checksum']) - || !isset($cache['@class']) - || $cache['checksum'] != $checksum - || $cache['@class'] != $class - ) { - // Attempt to lock the file for writing. - $file->lock(false); - - // Load blueprints. - $this->blueprints = new Blueprints; - foreach ($blueprintFiles as $files) { - $this->loadBlueprintFiles($files); - } - - $cache = [ - '@class' => $class, - 'checksum' => $checksum, - 'files' => $blueprintFiles, - 'data' => $this->blueprints->toArray() - ]; - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $this->messages[] = 'Saving compiled blueprints.'; - $file->save($cache); - $file->unlock(); - } - } else { - $this->blueprints = new Blueprints($cache['data']); - } - } - - protected function loadCompiledConfig($configs, $plugins, $filename = null) + public function reload() { - $checksum = md5(json_encode($configs)); - $filename = $filename - ? CACHE_DIR . 'compiled/config/' . $filename . '-' . $this->environment . '.php' - : CACHE_DIR . 'compiled/config/' . $checksum . '-' . $this->environment . '.php'; - $file = PhpFile::instance($filename); - $cache = $file->exists() ? $file->content() : null; - $class = get_class($this); - $checksum = $this->checksum(); - - if ( - !is_array($cache) - || !isset($cache['checksum']) - || !isset($cache['@class']) - || $cache['@class'] != $class - ) { - $this->messages[] = 'No cached configuration, compiling new configuration..'; - } else if ($cache['checksum'] !== $checksum) { - $this->messages[] = 'Configuration checksum mismatch, reloading configuration..'; - } else { - $this->messages[] = 'Configuration checksum matches, using cached version.'; - - $this->items = $cache['data']; - return; - } + $grav = Grav::instance(); - $configFiles = $this->finder->locateConfigFiles($configs, $plugins); + // Load new configuration. + $config = ConfigServiceProvider::load($grav); - // Attempt to lock the file for writing. - $file->lock(false); + /** @var Debugger $debugger */ + $debugger = $grav['debugger']; - // Load configuration. - foreach ($configFiles as $files) { - $this->loadConfigFiles($files); - } - $cache = [ - '@class' => $class, - 'timestamp' => time(), - 'checksum' => $checksum, - 'data' => $this->toArray() - ]; + if ($config->modified()) { + // Update current configuration. + $this->items = $config->toArray(); + $this->checksum($config->checksum()); + $this->modified(true); - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $this->messages[] = 'Saving compiled configuration.'; - $file->save($cache); - $file->unlock(); + $debugger->addMessage('Configuration was changed and saved.'); } - $this->items = $cache['data']; - } - - /** - * @param $languages - * @param $plugins - * @param null $filename - */ - protected function loadCompiledLanguages($languages, $plugins, $filename = null) - { - $checksum = md5(json_encode($languages)); - $filename = $filename - ? CACHE_DIR . 'compiled/languages/' . $filename . '-' . $this->environment . '.php' - : CACHE_DIR . 'compiled/languages/' . $checksum . '-' . $this->environment . '.php'; - $file = PhpFile::instance($filename); - $cache = $file->exists() ? $file->content() : null; - $languageFiles = $this->finder->locateLanguageFiles($languages, $plugins); - $checksum .= ':' . md5(json_encode($languageFiles)); - $class = get_class($this); - - // Load real file if cache isn't up to date (or is invalid). - if ( - !is_array($cache) - || !isset($cache['checksum']) - || !isset($cache['@class']) - || $cache['checksum'] != $checksum - || $cache['@class'] != $class - ) { - // Attempt to lock the file for writing. - $file->lock(false); - - // Load languages. - $this->languages = new Languages; - $pluginPaths = str_ireplace(GRAV_ROOT . '/', '', array_reverse($plugins)); - foreach ($pluginPaths as $path) { - if (isset($languageFiles[$path])) { - foreach ((array) $languageFiles[$path] as $plugin => $item) { - $lang_file = CompiledYamlFile::instance($item['file']); - $content = $lang_file->content(); - $this->languages->mergeRecursive($content); - } - unset($languageFiles[$path]); - } - } - - foreach ($languageFiles as $location) { - foreach ($location as $lang => $item) { - $lang_file = CompiledYamlFile::instance($item['file']); - $content = $lang_file->content(); - $this->languages->join($lang, $content, '/'); - } - } - - $cache = [ - '@class' => $class, - 'checksum' => $checksum, - 'files' => $languageFiles, - 'data' => $this->languages->toArray() - ]; - // If compiled file wasn't already locked by another process, save it. - if ($file->locked() !== false) { - $this->messages[] = 'Saving compiled languages.'; - $file->save($cache); - $file->unlock(); - } - } else { - $this->languages = new Languages($cache['data']); - } + return $this; } - /** - * Load blueprints. - * - * @param array $files - */ - public function loadBlueprintFiles(array $files) + public function debug() { - foreach ($files as $name => $item) { - $file = CompiledYamlFile::instance($item['file']); - $this->blueprints->embed($name, $file->content(), '/'); - } - } + /** @var Debugger $debugger */ + $debugger = Grav::instance()['debugger']; - /** - * Load configuration. - * - * @param array $files - */ - public function loadConfigFiles(array $files) - { - foreach ($files as $name => $item) { - $file = CompiledYamlFile::instance($item['file']); - $this->join($name, $file->content(), '/'); + $debugger->addMessage('Environment Name: ' . $this->environment); + if ($this->modified()) { + $debugger->addMessage('Configuration reloaded and cached.'); } } - /** - * Initialize resource locator by using the configuration. - * - * @param UniformResourceLocator $locator - */ - public function initializeLocator(UniformResourceLocator $locator) + public function init() { - $locator->reset(); - - $schemes = (array) $this->get('streams.schemes', []); - - foreach ($schemes as $scheme => $config) { - if (isset($config['paths'])) { - $locator->addPath($scheme, '', $config['paths']); - } - if (isset($config['prefixes'])) { - foreach ($config['prefixes'] as $prefix => $paths) { - $locator->addPath($scheme, $prefix, $paths); - } + $setup = Grav::instance()['setup']->toArray(); + foreach ($setup as $key => $value) { + if ($key === 'streams' || !is_array($value)) { + // Optimized as streams and simple values are fully defined in setup. + $this->items[$key] = $value; + } else { + $this->joinDefaults($key, $value); } } } /** - * Get available streams and their types from the configuration. - * - * @return array + * @return mixed + * @deprecated */ - public function getStreams() - { - $schemes = []; - foreach ((array) $this->get('streams.schemes') as $scheme => $config) { - $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream'; - if ($type[0] != '\\') { - $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type; - } - - $schemes[$scheme] = $type; - } - - return $schemes; - } - public function getLanguages() { - return $this->languages; + return Grav::instance()['languages']; } } diff --git a/system/src/Grav/Common/Config/ConfigFileFinder.php b/system/src/Grav/Common/Config/ConfigFileFinder.php new file mode 100644 index 000000000..887575a95 --- /dev/null +++ b/system/src/Grav/Common/Config/ConfigFileFinder.php @@ -0,0 +1,258 @@ +base = $base ? "{$base}/" : ''; + + return $this; + } + + /** + * Return all locations for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function locateFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $list += $this->detectRecursive($folder, $pattern, $levels); + } + return $list; + } + + /** + * Return all locations for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function getFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + + $files = $this->detectRecursive($folder, $pattern, $levels); + + $list += $files[trim($path, '/')]; + } + return $list; + } + + /** + * Return all paths for all the files with a timestamp. + * + * @param array $paths List of folders to look from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + */ + public function listFiles(array $paths, $pattern = '|\.yaml$|', $levels = -1) + { + $list = []; + foreach ($paths as $folder) { + $list = array_merge_recursive($list, $this->detectAll($folder, $pattern, $levels)); + } + return $list; + } + + /** + * Find filename from a list of folders. + * + * Note: Only finds the last override. + * + * @param string $filename + * @param array $folders + * @return array + */ + public function locateFileInFolder($filename, array $folders) + { + $list = []; + foreach ($folders as $folder) { + $list += $this->detectInFolder($folder, $filename); + } + return $list; + } + + /** + * Find filename from a list of folders. + * + * @param array $folders + * @param string $filename + * @return array + */ + public function locateInFolders(array $folders, $filename = null) + { + $list = []; + foreach ($folders as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + $list[$path] = $this->detectInFolder($folder, $filename); + } + return $list; + } + + /** + * Return all existing locations for a single file with a timestamp. + * + * @param array $paths Filesystem paths to look up from. + * @param string $name Configuration file to be located. + * @param string $ext File extension (optional, defaults to .yaml). + * @return array + */ + public function locateFile(array $paths, $name, $ext = '.yaml') + { + $filename = preg_replace('|[.\/]+|', '/', $name) . $ext; + + $list = []; + foreach ($paths as $folder) { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_file("{$folder}/{$filename}")) { + $modified = filemtime("{$folder}/{$filename}"); + } else { + $modified = 0; + } + $basename = $this->base . $name; + $list[$path] = [$basename => ['file' => "{$path}/{$filename}", 'modified' => $modified]]; + } + + return $list; + } + + /** + * Detects all directories with a configuration file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + * @internal + */ + protected function detectRecursive($folder, $pattern, $levels) + { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_dir($folder)) { + // Find all system and user configuration files. + $options = [ + 'levels' => $levels, + 'compare' => 'Filename', + 'pattern' => $pattern, + 'filters' => [ + 'pre-key' => $this->base, + 'key' => $pattern, + 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { + return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]; + } + ], + 'key' => 'SubPathname' + ]; + + $list = Folder::all($folder, $options); + + ksort($list); + } else { + $list = []; + } + + return [$path => $list]; + } + + /** + * Detects all directories with the lookup file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $lookup Filename to be located (defaults to directory name). + * @return array + * @internal + */ + protected function detectInFolder($folder, $lookup = null) + { + $folder = rtrim($folder, '/'); + $path = trim(Folder::getRelativePath($folder), '/'); + $base = $path === $folder ? '' : ($path ? substr($folder, 0, -strlen($path)) : $folder . '/'); + + $list = []; + + if (is_dir($folder)) { + $iterator = new \DirectoryIterator($folder); + + /** @var \DirectoryIterator $directory */ + foreach ($iterator as $directory) { + if (!$directory->isDir() || $directory->isDot()) { + continue; + } + + $name = $directory->getBasename(); + $find = ($lookup ?: $name) . '.yaml'; + $filename = "{$path}/{$name}/{$find}"; + + if (file_exists($base . $filename)) { + $basename = $this->base . $name; + $list[$basename] = ['file' => $filename, 'modified' => filemtime($base . $filename)]; + } + } + } + + return $list; + } + + /** + * Detects all plugins with a configuration file and returns them with last modification time. + * + * @param string $folder Location to look up from. + * @param string $pattern Pattern to match the file. Pattern will also be removed from the key. + * @param int $levels Maximum number of recursive directories. + * @return array + * @internal + */ + protected function detectAll($folder, $pattern, $levels) + { + $path = trim(Folder::getRelativePath($folder), '/'); + + if (is_dir($folder)) { + // Find all system and user configuration files. + $options = [ + 'levels' => $levels, + 'compare' => 'Filename', + 'pattern' => $pattern, + 'filters' => [ + 'pre-key' => $this->base, + 'key' => $pattern, + 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { + return ["{$path}/{$file->getSubPathname()}" => $file->getMTime()]; + } + ], + 'key' => 'SubPathname' + ]; + + $list = Folder::all($folder, $options); + + ksort($list); + } else { + $list = []; + } + + return $list; + } +} diff --git a/system/src/Grav/Common/Config/ConfigFinder.php b/system/src/Grav/Common/Config/ConfigFinder.php deleted file mode 100644 index 46069410d..000000000 --- a/system/src/Grav/Common/Config/ConfigFinder.php +++ /dev/null @@ -1,186 +0,0 @@ -detectInFolder($folder, 'blueprints'); - } - foreach (array_reverse($blueprints) as $folder) { - $list += $this->detectRecursive($folder); - } - return $list; - } - - /** - * Get all locations for configuration files (including plugins). - * - * @param array $configs - * @param array $plugins - * @return array - */ - public function locateConfigFiles(array $configs, array $plugins) - { - $list = []; - foreach (array_reverse($plugins) as $folder) { - $list += $this->detectInFolder($folder); - } - foreach (array_reverse($configs) as $folder) { - $list += $this->detectRecursive($folder); - } - return $list; - } - - public function locateLanguageFiles(array $languages, array $plugins) - { - $list = []; - foreach (array_reverse($plugins) as $folder) { - $list += $this->detectLanguagesInFolder($folder, 'languages'); - } - foreach (array_reverse($languages) as $folder) { - $list += $this->detectRecursive($folder); - } - return $list; - } - - /** - * Get all locations for a single configuration file. - * - * @param array $folders Locations to look up from. - * @param string $name Filename to be located. - * @return array - */ - public function locateConfigFile(array $folders, $name) - { - $filename = "{$name}.yaml"; - - $list = []; - foreach ($folders as $folder) { - $path = trim(Folder::getRelativePath($folder), '/'); - - if (is_file("{$folder}/{$filename}")) { - $modified = filemtime("{$folder}/{$filename}"); - } else { - $modified = 0; - } - $list[$path] = [$name => ['file' => "{$path}/{$filename}", 'modified' => $modified]]; - } - - return $list; - } - - /** - * Detects all plugins with a configuration file and returns them with last modification time. - * - * @param string $folder Location to look up from. - * @param string $lookup Filename to be located. - * @return array - * @internal - */ - protected function detectInFolder($folder, $lookup = null) - { - $path = trim(Folder::getRelativePath($folder), '/'); - - $list = []; - - if (is_dir($folder)) { - $iterator = new \FilesystemIterator($folder); - - /** @var \DirectoryIterator $directory */ - foreach ($iterator as $directory) { - if (!$directory->isDir()) { - continue; - } - - $name = $directory->getBasename(); - $find = ($lookup ?: $name) . '.yaml'; - $filename = "{$path}/{$name}/$find"; - - if (file_exists($filename)) { - $list["plugins/{$name}"] = ['file' => $filename, 'modified' => filemtime($filename)]; - } - } - } - - return [$path => $list]; - } - - protected function detectLanguagesInFolder($folder, $lookup = null) - { - $path = trim(Folder::getRelativePath($folder), '/'); - - $list = []; - - if (is_dir($folder)) { - $iterator = new \FilesystemIterator($folder); - - /** @var \DirectoryIterator $directory */ - foreach ($iterator as $directory) { - if (!$directory->isDir()) { - continue; - } - - $name = $directory->getBasename(); - $find = ($lookup ?: $name) . '.yaml'; - $filename = "{$path}/{$name}/$find"; - - if (file_exists($filename)) { - $list[$name] = ['file' => $filename, 'modified' => filemtime($filename)]; - } - } - } - - return [$path => $list]; - } - - /** - * Detects all plugins with a configuration file and returns them with last modification time. - * - * @param string $folder Location to look up from. - * @return array - * @internal - */ - protected function detectRecursive($folder) - { - $path = trim(Folder::getRelativePath($folder), '/'); - - if (is_dir($folder)) { - // Find all system and user configuration files. - $options = [ - 'compare' => 'Filename', - 'pattern' => '|\.yaml$|', - 'filters' => [ - 'key' => '|\.yaml$|', - 'value' => function (\RecursiveDirectoryIterator $file) use ($path) { - return ['file' => "{$path}/{$file->getSubPathname()}", 'modified' => $file->getMTime()]; - } - ], - 'key' => 'SubPathname' - ]; - - $list = Folder::all($folder, $options); - } else { - $list = []; - } - - return [$path => $list]; - } -} diff --git a/system/src/Grav/Common/Config/Languages.php b/system/src/Grav/Common/Config/Languages.php index c0141292e..4d92d87e9 100644 --- a/system/src/Grav/Common/Config/Languages.php +++ b/system/src/Grav/Common/Config/Languages.php @@ -11,6 +11,23 @@ */ class Languages extends Data { + public function checksum($checksum = null) + { + if ($checksum !== null) { + $this->checksum = $checksum; + } + + return $this->checksum; + } + + public function modified($modified = null) + { + if ($modified !== null) { + $this->modified = $modified; + } + + return $this->modified; + } public function reformat() { diff --git a/system/src/Grav/Common/Config/Setup.php b/system/src/Grav/Common/Config/Setup.php new file mode 100644 index 000000000..90178d6b2 --- /dev/null +++ b/system/src/Grav/Common/Config/Setup.php @@ -0,0 +1,249 @@ + [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['system'], + ] + ], + 'user' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user'], + ] + ], + 'environment' => [ + 'type' => 'ReadOnlyStream' + // If not defined, environment will be set up in the constructor. + ], + 'asset' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['assets'], + ] + ], + 'blueprints' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://blueprints', 'user://blueprints', 'system/blueprints'], + ] + ], + 'config' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://config', 'user://config', 'system/config'], + ] + ], + 'plugins' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://plugins'], + ] + ], + 'plugin' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://plugins'], + ] + ], + 'themes' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://themes'], + ] + ], + 'languages' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['environment://languages', 'user://languages', 'system/languages'], + ] + ], + 'cache' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['cache'], + 'images' => ['images'] + ] + ], + 'log' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['logs'] + ] + ], + 'backup' => [ + 'type' => 'Stream', + 'prefixes' => [ + '' => ['backup'] + ] + ], + 'image' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://images', 'system://images'] + ] + ], + 'page' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://pages'] + ] + ], + 'account' => [ + 'type' => 'ReadOnlyStream', + 'prefixes' => [ + '' => ['user://accounts'] + ] + ], + ]; + + public function __construct($environment = 'localhost') + { + // Pre-load setup.php which contains our initial configuration. + // Configuration may contain dynamic parts, which is why we need to always load it. + $file = GRAV_ROOT . '/setup.php'; + $setup = is_file($file) ? (array) include $file : []; + + // Add default streams defined in beginning of the class. + if (!isset($setup['streams']['schemes'])) { + $setup['streams']['schemes'] = []; + } + $setup['streams']['schemes'] += $this->streams; + + // Initialize class. + parent::__construct($setup); + + // Set up environment. + $this->def('environment', $environment); + $this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->environment}"]]); + } + + /** + * @return $this + */ + public function init() + { + $locator = new UniformResourceLocator(GRAV_ROOT); + $files = []; + + $guard = 5; + do { + $check = $files; + $this->initializeLocator($locator); + $files = $locator->findResources('config://streams.yaml'); + + if ($check === $files) { + break; + } + + // Update streams. + foreach ($files as $path) { + $file = CompiledYamlFile::instance($path); + $content = $file->content(); + if (!empty($content['schemes'])) { + $this->items['streams']['schemes'] = $content['schemes'] + $this->items['streams']['schemes']; + } + } + } while (--$guard); + + if (!$guard) { + throw new \RuntimeException('Setup: Configuration reload loop detected!'); + } + + // Make sure we have valid setup. + $this->check($locator); + + return $this; + } + + /** + * Initialize resource locator by using the configuration. + * + * @param UniformResourceLocator $locator + */ + public function initializeLocator(UniformResourceLocator $locator) + { + $locator->reset(); + + $schemes = (array) $this->get('streams.schemes', []); + + foreach ($schemes as $scheme => $config) { + if (isset($config['paths'])) { + $locator->addPath($scheme, '', $config['paths']); + } + if (isset($config['prefixes'])) { + foreach ($config['prefixes'] as $prefix => $paths) { + $locator->addPath($scheme, $prefix, $paths); + } + } + } + } + + /** + * Get available streams and their types from the configuration. + * + * @return array + */ + public function getStreams() + { + $schemes = []; + foreach ((array) $this->get('streams.schemes') as $scheme => $config) { + $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream'; + if ($type[0] != '\\') { + $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type; + } + + $schemes[$scheme] = $type; + } + + return $schemes; + } + + /** + * @param UniformResourceLocator $locator + * @throws \InvalidArgumentException + */ + protected function check(UniformResourceLocator $locator) + { + $streams = isset($this->items['streams']['schemes']) ? $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)); + if ($diff) { + throw new \InvalidArgumentException( + sprintf('Configuration is missing keys %s from streams.schemes!', implode(', ', $diff)) + ); + } + + if (!$locator->findResource('environment://config', true)) { + // If environment does not have its own directory, remove it from the lookup. + $this->set('streams.schemes.environment.prefixes', ['config' => []]); + $this->initializeLocator($locator); + } + + // 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(); + } + } +} diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 9bfc32303..8be6a8ea8 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -3,6 +3,8 @@ use Grav\Common\GravTrait; use RocketTheme\Toolbox\ArrayTraits\Export; +use RocketTheme\Toolbox\ArrayTraits\ExportInterface; +use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters; /** * Blueprint handles the inside logic of blueprints. @@ -10,9 +12,9 @@ * @author RocketTheme * @license MIT */ -class Blueprint +class Blueprint implements \ArrayAccess, ExportInterface { - use Export, DataMutatorTrait, GravTrait; + use Export, NestedArrayAccessWithGetters, GravTrait; public $name; @@ -240,9 +242,6 @@ protected function filterArray(array $data, array $rules) if ($rule) { // Item has been defined in blueprints. - if (is_array($field) && count($field) == 1 && reset($field) == '') { - continue; - } $field = Validation::filter($field, $rule); } elseif (is_array($field) && is_array($val)) { // Array has been defined in blueprints. @@ -312,91 +311,105 @@ protected function extraArray(array $data, array $rules, $prefix) } /** - * Gets all field definitions from the blueprints. - * - * @param array $fields - * @param array $params - * @param string $prefix - * @param array $current - * @internal - */ - protected function parseFormFields(array &$fields, $params, $prefix, array &$current) - { - // Go though all the fields in current level. - foreach ($fields as $key => &$field) { - $current[$key] = &$field; - // Set name from the array key. - $field['name'] = $prefix . $key; - $field += $params; - - if (isset($field['fields']) && (!isset($field['type']) || $field['type'] !== 'list')) { - // Recursively get all the nested fields. - $newParams = array_intersect_key($this->filter, $field); - $this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']); - } else if ($field['type'] !== 'ignore') { - // Add rule. + * Gets all field definitions from the blueprints. + * + * @param array $fields + * @param array $params + * @param string $prefix + * @param array $current + * @internal + */ + protected function parseFormFields(array &$fields, $params, $prefix, array &$current) + { + // Go though all the fields in current level. + foreach ($fields as $key => &$field) { + $current[$key] = &$field; + // Set name from the array key. + $field['name'] = $prefix . $key; + $field += $params; + + if (isset($field['fields']) && (!isset($field['type']) || $field['type'] !== 'list')) { + // Recursively get all the nested fields. + $newParams = array_intersect_key($this->filter, $field); + $this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']); + } else if ($field['type'] !== 'ignore') { $this->rules[$prefix . $key] = &$field; $this->addProperty($prefix . $key); - foreach ($field as $name => $value) { - // Support nested blueprints. - if ($this->context && $name == '@import') { - $values = (array) $value; - if (!isset($field['fields'])) { - $field['fields'] = array(); - } - foreach ($values as $bname) { - $b = $this->context->get($bname); - $field['fields'] = array_merge($field['fields'], $b->fields()); - } - } - - // Support for callable data values. - elseif (substr($name, 0, 6) == '@data-') { - $property = substr($name, 6); - if (is_array($value)) { - $func = array_shift($value); - } else { - $func = $value; - $value = array(); - } - list($o, $f) = preg_split('/::/', $func); - if (!$f && function_exists($o)) { - $data = call_user_func_array($o, $value); - } elseif ($f && method_exists($o, $f)) { - $data = call_user_func_array(array($o, $f), $value); - } - - // If function returns a value, - if (isset($data)) { - if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) { - // Combine field and @data-field together. - $field[$property] += $data; - } else { - // Or create/replace field with @data-field. - $field[$property] = $data; - } - } - } - - elseif (substr($name, 0, 8) == '@config-') { - $property = substr($name, 8); - $default = isset($field[$property]) ? $field[$property] : null; - $config = self::getGrav()['config']->get($value, $default); - - if (!is_null($config)) { - $field[$property] = $config; - } + if ($field['type'] === 'list') { + // we loop through list to get the actual field + foreach($field['fields'] as $subName => &$subField) { + $this->parseFormField($subField); } + } else { + $this->parseFormField($field); } - // Initialize predefined validation rule. if (isset($field['validate']['rule']) && $field['type'] !== 'ignore') { $field['validate'] += $this->getRule($field['validate']['rule']); } } - } - } + } + } + /** + * Parses individual field definition + * + * @param array $field + * @internal + */ + protected function parseFormField(&$field) { + foreach ($field as $name => $value) { + // Support nested blueprints. + if ($this->context && $name == '@import') { + $values = (array) $value; + if (!isset($field['fields'])) { + $field['fields'] = array(); + } + foreach ($values as $bname) { + $b = $this->context->get($bname); + $field['fields'] = array_merge($field['fields'], $b->fields()); + } + } + + // Support for callable data values. + elseif (substr($name, 0, 6) == '@data-') { + $property = substr($name, 6); + if (is_array($value)) { + $func = array_shift($value); + } else { + $func = $value; + $value = array(); + } + list($o, $f) = preg_split('/::/', $func); + if (!$f && function_exists($o)) { + $data = call_user_func_array($o, $value); + } elseif ($f && method_exists($o, $f)) { + $data = call_user_func_array(array($o, $f), $value); + } + + // If function returns a value, + if (isset($data)) { + if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) { + // Combine field and @data-field together. + $field[$property] += $data; + } else { + // Or create/replace field with @data-field. + $field[$property] = $data; + } + } + } + + elseif (substr($name, 0, 8) == '@config-') { + $property = substr($name, 8); + $default = isset($field[$property]) ? $field[$property] : null; + $config = self::getGrav()['config']->get($value, $default); + + if (!is_null($config)) { + $field[$property] = $config; + } + } + } + } /** * Add property to the definition. @@ -452,7 +465,9 @@ protected function checkRequired(array $data, array $fields) && $field['validate']['required'] === true && empty($data[$name])) { $value = isset($field['label']) ? $field['label'] : $field['name']; - throw new \RuntimeException("Missing required field: {$value}"); + $language = self::getGrav()['language']; + $message = sprintf($language->translate('FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $value); + throw new \RuntimeException($message); } } } diff --git a/system/src/Grav/Common/Data/Data.php b/system/src/Grav/Common/Data/Data.php index 6a35fbc9e..fe70efb1b 100644 --- a/system/src/Grav/Common/Data/Data.php +++ b/system/src/Grav/Common/Data/Data.php @@ -1,9 +1,11 @@ items = $items; - $this->blueprints = $blueprints; } @@ -57,126 +58,150 @@ public function value($name, $default = null, $separator = '.') } /** - * Set default value by using dot notation for nested arrays/objects. - * - * @example $data->def('this.is.my.nested.variable', 'default'); + * Join nested values together by using blueprints. * * @param string $name Dot separated path to the requested value. - * @param mixed $default Default value (or null). + * @param mixed $value Value to be joined. * @param string $separator Separator, defaults to '.' + * @return $this + * @throws \RuntimeException */ - public function def($name, $default = null, $separator = '.') + public function join($name, $value, $separator = '.') { - $this->set($name, $this->get($name, $default, $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->blueprints()->mergeData($old, $value, $name, $separator); + } + + $this->set($name, $value, $separator); + + return $this; } /** - * Join two values together by using blueprints if available. + * 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->blueprints()->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 join($name, $value, $separator = '.') + public function joinDefaults($name, $value, $separator = '.') { + if (is_object($value)) { + $value = (array) $value; + } $old = $this->get($name, null, $separator); - if ($old === null) { - // Variable does not exist yet: just use the incoming value. - } elseif ($this->blueprints) { - // Blueprints: join values by using blueprints. - $value = $this->blueprints->mergeData($old, $value, $name, $separator); - } else { - // No blueprints: replace existing top level variables with the new ones. - $value = array_merge($old, $value); + if ($old !== null) { + $value = $this->blueprints()->mergeData($value, $old, $name, $separator); } $this->set($name, $value, $separator); + + return $this; } /** - * Join two values together by using blueprints if available. + * Get value from the configuration and join it with given data. * * @param string $name Dot separated path to the requested value. - * @param mixed $value Value to be joined. + * @param array $value Value to be joined. * @param string $separator Separator, defaults to '.' + * @return array + * @throws \RuntimeException */ - public function joinDefaults($name, $value, $separator = '.') + public function getJoined($name, $value, $separator = '.') { + 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) { - // Variable does not exist yet: just use the incoming value. - } elseif ($this->blueprints) { - // Blueprints: join values by using blueprints. - $value = $this->blueprints->mergeData($value, $old, $name, $separator); - } else { - // No blueprints: replace existing top level variables with the new ones. - $value = array_merge($value, $old); + // No value set; no need to join data. + return $value; } - $this->set($name, $value, $separator); + if (!is_array($old)) { + throw new \RuntimeException('Value ' . $old); + } + + // Return joined data. + return $this->blueprints()->mergeData($old, $value, $name, $separator); } /** - * Merge two sets of data together. + * Merge two configurations together. * * @param array $data - * @return void + * @return $this */ public function merge(array $data) { - if ($this->blueprints) { - $this->items = $this->blueprints->mergeData($this->items, $data); - } else { - $this->items = array_merge($this->items, $data); - } + $this->items = $this->blueprints()->mergeData($this->items, $data); + + return $this; } /** - * Add default data to the set. + * Set default values to the configuration if variables were not set. * * @param array $data - * @return void + * @return $this */ public function setDefaults(array $data) { - if ($this->blueprints) { - $this->items = $this->blueprints->mergeData($data, $this->items); - } else { - $this->items = array_merge($data, $this->items); - } - } + $this->items = $this->blueprints()->mergeData($data, $this->items); - /** - * Return blueprints. - * - * @return Blueprint - */ - public function blueprints() - { - return $this->blueprints; + return $this; } /** * Validate by blueprints. * + * @return $this * @throws \Exception */ public function validate() { - if ($this->blueprints) { - $this->blueprints->validate($this->items); - } + $this->blueprints()->validate($this->items); + + return $this; } /** + * @return $this * Filter all items by using blueprints. */ public function filter() { - if ($this->blueprints) { - $this->items = $this->blueprints->filter($this->items); - } + $this->items = $this->blueprints()->filter($this->items); + + return $this; } /** @@ -186,7 +211,24 @@ public function filter() */ public function extra() { - return $this->blueprints ? $this->blueprints->extra($this->items) : array(); + return $this->blueprints()->extra($this->items); + } + + /** + * Return blueprints. + * + * @return Blueprints + */ + public function blueprints() + { + if (!$this->blueprints){ + $this->blueprints = new Blueprints; + } elseif (is_callable($this->blueprints)) { + // Lazy load blueprints. + $blueprints = $this->blueprints; + $this->blueprints = $blueprints(); + } + return $this->blueprints; } /** diff --git a/system/src/Grav/Common/Data/DataMutatorTrait.php b/system/src/Grav/Common/Data/DataMutatorTrait.php deleted file mode 100644 index 707b5078a..000000000 --- a/system/src/Grav/Common/Data/DataMutatorTrait.php +++ /dev/null @@ -1,68 +0,0 @@ -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 = '.') - { - $path = explode($separator, $name); - $current = $this->items; - foreach ($path as $field) { - if (is_object($current) && isset($current->{$field})) { - $current = $current->{$field}; - } elseif (is_array($current) && isset($current[$field])) { - $current = $current[$field]; - } else { - return $default; - } - } - - return $current; - } - - /** - * Set value by using dot notation for nested arrays/objects. - * - * @example $value = $data->set('this.is.my.nested.variable', true); - * - * @param string $name Dot separated path to the requested value. - * @param mixed $value New value. - * @param string $separator Separator, defaults to '.' - */ - public function set($name, $value, $separator = '.') - { - $path = explode($separator, $name); - $current = &$this->items; - foreach ($path as $field) { - if (is_object($current)) { - // Handle objects. - if (!isset($current->{$field})) { - $current->{$field} = array(); - } - $current = &$current->{$field}; - } else { - // Handle arrays and scalars. - if (!is_array($current)) { - $current = array($field => array()); - } elseif (!isset($current[$field])) { - $current[$field] = array(); - } - $current = &$current[$field]; - } - } - - $current = $value; - } - -} diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index c232467ba..9cebd0d76 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -31,6 +31,11 @@ public static function validate($value, array $field) return; } + // special case for files, value is never empty and errors with code 4 instead + if (empty($validate['required']) && $field['type'] == 'file' && (isset($value['error']) && ($value['error'] == UPLOAD_ERR_NO_FILE) || in_array(UPLOAD_ERR_NO_FILE, $value['error']))) { + return; + } + // Get language class $language = self::getGrav()['language']; @@ -78,6 +83,11 @@ public static function filter($value, array $field) return null; } + // special case for files, value is never empty and errors with code 4 instead + if (empty($validate['required']) && $field['type'] == 'file' && (isset($value['error']) && ($value['error'] == UPLOAD_ERR_NO_FILE) || in_array(UPLOAD_ERR_NO_FILE, $value['error']))) { + return null; + } + // if this is a YAML field, simply parse it and return the value if (isset($field['yaml']) && $field['yaml'] === true) { try { @@ -258,6 +268,24 @@ public static function typeToggle($value, array $params, array $field) return self::typeArray((array) $value, $params, $field); } + /** + * Custom input: file + * + * @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 typeFile($value, array $params, array $field) + { + return self::typeArray((array) $value, $params, $field); + } + + protected static function filterFile($value, array $params, array $field) + { + return (array) $value; + } + /** * HTML5 input: select * diff --git a/system/src/Grav/Common/File/CompiledFile.php b/system/src/Grav/Common/File/CompiledFile.php index 7120c9c30..6b6b76989 100644 --- a/system/src/Grav/Common/File/CompiledFile.php +++ b/system/src/Grav/Common/File/CompiledFile.php @@ -47,7 +47,11 @@ public function content($var = null) || $cache['filename'] != $this->filename ) { // Attempt to lock the file for writing. - $file->lock(false); + try { + $file->lock(false); + } catch (\Exception $e) { + // Another process has locked the file; we will check this in a bit. + } // Decode RAW file into compiled array. $data = (array) $this->decode($this->raw()); @@ -64,6 +68,7 @@ public function content($var = null) $file->unlock(); } } + $file->free(); $this->content = $cache['data']; } diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index af1b93352..62d610b65 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -19,12 +19,12 @@ public static function lastModifiedFolder($path) { $last_modified = 0; - $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); - $filterItr = new RecursiveFolderFilterIterator($dirItr); - $itr = new \RecursiveIteratorIterator($filterItr, \RecursiveIteratorIterator::SELF_FIRST); + $directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); + $filter = new RecursiveFolderFilterIterator($directory); + $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::SELF_FIRST); /** @var \RecursiveDirectoryIterator $file */ - foreach ($itr as $dir) { + foreach ($iterator as $dir) { $dir_modified = $dir->getMTime(); if ($dir_modified > $last_modified) { $last_modified = $dir_modified; @@ -46,12 +46,12 @@ public static function lastModifiedFile($path, $extensions = 'md|yaml') { $last_modified = 0; - $dirItr = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); - $itrItr = new \RecursiveIteratorIterator($dirItr, \RecursiveIteratorIterator::SELF_FIRST); - $itr = new \RegexIterator($itrItr, '/^.+\.'.$extensions.'$/i'); + $directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS); + $recursive = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST); + $iterator = new \RegexIterator($recursive, '/^.+\.'.$extensions.'$/i'); /** @var \RecursiveDirectoryIterator $file */ - foreach ($itr as $filepath => $file) { + foreach ($iterator as $filepath => $file) { $file_modified = $file->getMTime(); if ($file_modified > $last_modified) { $last_modified = $file_modified; @@ -64,8 +64,8 @@ public static function lastModifiedFile($path, $extensions = 'md|yaml') /** * Get relative path between target and base path. If path isn't relative, return full path. * - * @param string $path - * @param mixed|string $base + * @param string $path + * @param string $base * @return string */ public static function getRelativePath($path, $base = GRAV_ROOT) @@ -81,6 +81,43 @@ public static function getRelativePath($path, $base = GRAV_ROOT) return $path; } + /** + * Get relative path between target and base path. If path isn't relative, return full path. + * + * @param string $path + * @param string $base + * @return string + */ + public static function getRelativePathDotDot($path, $base) + { + $base = preg_replace('![\\\/]+!', '/', $base); + $path = preg_replace('![\\\/]+!', '/', $path); + + if ($path === $base) { + return ''; + } + + $baseParts = explode('/', isset($base[0]) && '/' === $base[0] ? substr($base, 1) : $base); + $pathParts = explode('/', isset($path[0]) && '/' === $path[0] ? substr($path, 1) : $path); + + array_pop($baseParts); + $lastPart = array_pop($pathParts); + foreach ($baseParts as $i => $directory) { + if (isset($pathParts[$i]) && $pathParts[$i] === $directory) { + unset($baseParts[$i], $pathParts[$i]); + } else { + break; + } + } + $pathParts[] = $lastPart; + $path = str_repeat('../', count($baseParts)) . implode('/', $pathParts); + + return '' === $path + || '/' === $path[0] + || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) + ? "./$path" : $path; + } + /** * Shift first directory out of the path. * @@ -96,8 +133,6 @@ public static function shift(&$path) return $result ?: null; } - - /** * Return recursive list of all files and directories under given path. * @@ -116,13 +151,17 @@ public static function all($path, array $params = array()) $pattern = isset($params['pattern']) ? $params['pattern'] : null; $filters = isset($params['filters']) ? $params['filters'] : null; $recursive = isset($params['recursive']) ? $params['recursive'] : true; + $levels = isset($params['levels']) ? $params['levels'] : -1; $key = isset($params['key']) ? 'get' . $params['key'] : null; $value = isset($params['value']) ? 'get' . $params['value'] : ($recursive ? 'getSubPathname' : 'getFilename'); + $folders = isset($params['folders']) ? $params['folders'] : true; + $files = isset($params['files']) ? $params['files'] : true; if ($recursive) { $directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS + \FilesystemIterator::CURRENT_AS_SELF); $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST); + $iterator->setMaxDepth(max($levels, -1)); } else { $iterator = new \FilesystemIterator($path); } @@ -131,6 +170,16 @@ public static function all($path, array $params = array()) /** @var \RecursiveDirectoryIterator $file */ foreach ($iterator as $file) { + // Ignore hidden files. + if ($file->getFilename()[0] == '.') { + continue; + } + if (!$folders && $file->isDir()) { + continue; + } + if (!$files && $file->isFile()) { + continue; + } if ($compare && $pattern && !preg_match($pattern, $file->{$compare}())) { continue; } @@ -138,7 +187,8 @@ public static function all($path, array $params = array()) $filePath = $file->{$value}(); if ($filters) { if (isset($filters['key'])) { - $fileKey = preg_replace($filters['key'], '', $fileKey); + $pre = !empty($filters['pre-key']) ? $filters['pre-key'] : ''; + $fileKey = $pre . preg_replace($filters['key'], '', $fileKey); } if (isset($filters['value'])) { $filter = $filters['value']; @@ -146,12 +196,12 @@ public static function all($path, array $params = array()) $filePath = call_user_func($filter, $file); } else { $filePath = preg_replace($filter, '', $filePath); + } } } - } if ($fileKey !== null) { - $results[$fileKey] = $filePath; + $results[$fileKey] = $filePath; } else { $results[] = $filePath; } @@ -163,11 +213,12 @@ public static function all($path, array $params = array()) /** * Recursively copy directory in filesystem. * - * @param string $source - * @param string $target + * @param string $source + * @param string $target + * @param string $ignore Ignore files matching pattern (regular expression). * @throws \RuntimeException */ - public static function copy($source, $target) + public static function copy($source, $target, $ignore = null) { $source = rtrim($source, '\\/'); $target = rtrim($target, '\\/'); @@ -177,19 +228,24 @@ public static function copy($source, $target) } // Make sure that path to the target exists before copying. - self::mkdir($target); + self::create($target); $success = true; // Go through all sub-directories and copy everything. $files = self::all($source); foreach ($files as $file) { + if ($ignore && preg_match($ignore, $file)) { + continue; + } $src = $source .'/'. $file; $dst = $target .'/'. $file; if (is_dir($src)) { - // Create current directory. - $success &= @mkdir($dst); + // Create current directory (if it doesn't exist). + if (!is_dir($dst)) { + $success &= @mkdir($dst, 0777, true); + } } else { // Or copy current file. $success &= @copy($src, $dst); @@ -208,8 +264,8 @@ public static function copy($source, $target) /** * Move directory in filesystem. * - * @param string $source - * @param string $target + * @param string $source + * @param string $target * @throws \RuntimeException */ public static function move($source, $target) @@ -219,7 +275,7 @@ public static function move($source, $target) } // Make sure that path to the target exists before moving. - self::mkdir(dirname($target)); + self::create(dirname($target)); // Just rename the directory. $success = @rename($source, $target); @@ -238,16 +294,16 @@ public static function move($source, $target) * Recursively delete directory from filesystem. * * @param string $target - * @throws \RuntimeException + * @param bool $include_target * @return bool */ - public static function delete($target) + public static function delete($target, $include_target = true) { if (!is_dir($target)) { - return; + return false; } - $success = self::doDelete($target); + $success = self::doDelete($target, $include_target); if (!$success) { $error = error_get_last(); @@ -255,16 +311,31 @@ public static function delete($target) } // Make sure that the change will be detected when caching. - @touch(dirname($target)); + if ($include_target) { + @touch(dirname($target)); + } else { + @touch($target); + } + return $success; } /** - * @param string $folder + * @param string $folder * @throws \RuntimeException * @internal */ public static function mkdir($folder) + { + self::create($folder); + } + + /** + * @param string $folder + * @throws \RuntimeException + * @internal + */ + public static function create($folder) { if (is_dir($folder)) { return; @@ -320,10 +391,11 @@ public static function rcopy($src, $dest) /** * @param string $folder + * @param bool $include_target * @return bool * @internal */ - protected static function doDelete($folder) + protected static function doDelete($folder, $include_target = true) { // Special case for symbolic links. if (is_link($folder)) { @@ -338,16 +410,16 @@ protected static function doDelete($folder) /** @var \DirectoryIterator $fileinfo */ foreach ($files as $fileinfo) { if ($fileinfo->isDir()) { - if (false === rmdir($fileinfo->getRealPath())) { + if (false === @rmdir($fileinfo->getRealPath())) { return false; } } else { - if (false === unlink($fileinfo->getRealPath())) { + if (false === @unlink($fileinfo->getRealPath())) { return false; } } } - return rmdir($folder); + return $include_target ? @rmdir($folder) : true; } } diff --git a/system/src/Grav/Common/GPM/Response.php b/system/src/Grav/Common/GPM/Response.php index f933f37f0..fcaed82b3 100644 --- a/system/src/Grav/Common/GPM/Response.php +++ b/system/src/Grav/Common/GPM/Response.php @@ -78,10 +78,12 @@ public static function get($uri = '', $options = [], $callback = null) throw new \RuntimeException('Could not start an HTTP request. `allow_url_open` is disabled and `cURL` is not available'); } - // disable time limit if possible to help with slow downloads - if (!Utils::isFunctionDisabled('set_time_limit') && !ini_get('safe_mode')) { - set_time_limit(0); - } + // check if this function is available, if so use it to stop any timeouts + try { + if (!Utils::isFunctionDisabled('set_time_limit') && !ini_get('safe_mode') && function_exists('set_time_limit')) { + set_time_limit(0); + } + } catch (\Exception $e) {} $options = array_replace_recursive(self::$defaults, $options); $method = 'get' . ucfirst(strtolower(self::$method)); diff --git a/system/src/Grav/Common/Language/LanguageCodes.php b/system/src/Grav/Common/Language/LanguageCodes.php index 2683c8b0a..86b130a90 100644 --- a/system/src/Grav/Common/Language/LanguageCodes.php +++ b/system/src/Grav/Common/Language/LanguageCodes.php @@ -193,7 +193,7 @@ class LanguageCodes ], "fr" => [ "name" => "French", - "nativeName" => "français" + "nativeName" => "Français" ], "ff" => [ "name" => "Fula", diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php index b5836bd45..cc1593c28 100644 --- a/system/src/Grav/Common/Page/Collection.php +++ b/system/src/Grav/Common/Page/Collection.php @@ -453,6 +453,55 @@ public function ofOneOfTheseTypes($types) return $this; } + /** + * Creates new collection with only pages of one of the specified access levels + * + * @return Collection The collection + */ + public function ofOneOfTheseAccessLevels($accessLevels) + { + $items = []; + + foreach ($this->items as $path => $slug) { + $page = $this->pages->get($path); + + if ($page !== null && isset($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)) { + foreach($accessLevel as $innerIndex => $innerAccessLevel) { + if (in_array($innerAccessLevel, $accessLevels)) { + $valid = true; + } + } + } else { + if (in_array($index, $accessLevels)) { + $valid = true; + } + } + } + if ($valid) { + $items[$path] = $slug; + } + } else { + //Single value for access + if (in_array($page->header()->access, $accessLevels)) { + $items[$path] = $slug; + } + } + + } + } + + $this->items = $items; + return $this; + } + + + } diff --git a/system/src/Grav/Common/Page/Medium/Medium.php b/system/src/Grav/Common/Page/Medium/Medium.php index b36864c27..a5e4001a7 100644 --- a/system/src/Grav/Common/Page/Medium/Medium.php +++ b/system/src/Grav/Common/Page/Medium/Medium.php @@ -214,8 +214,9 @@ public function parsedownElement($title = null, $alt = null, $class = null, $res else $style .= $key . ': ' . $value . ';'; } - if ($style) + if ($style) { $attributes['style'] = $style; + } if (empty($attributes['title'])) { if (!empty($title)) { @@ -392,6 +393,38 @@ public function lightbox($width = null, $height = null, $reset = true) return $this->link($reset, $attributes); } + /** + * Add a class to the element from Markdown or Twig + * Example: ![Example](myimg.png?classes=float-left) or ![Example](myimg.png?classes=myclass1,myclass2) + * + * @return $this + */ + public function classes() + { + $classes = func_get_args(); + if (!empty($classes)) { + $this->attributes['class'] = implode(',', (array)$classes); + } + + return $this; + } + + /** + * Add an id to the element from Markdown or Twig + * Example: ![Example](myimg.png?id=primary-img) + * + * @param $id + * @return $this + */ + public function id($id) + { + if (is_string($id)) { + $this->attributes['id'] = trim($id); + } + + return $this; + } + /** * Allows to add an inline style attribute from Markdown or Twig * Example: ![Example](myimg.png?style=float:left) diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index 00a0f756f..50b3fa83f 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -2002,16 +2002,16 @@ protected function evaluate($value) case 'children': $results = $this->children()->nonModular(); break; - + case 'all': + $results = $this->children(); + break; case 'parent': $collection = new Collection(); $results = $collection->addPage($this->parent()); break; - case 'siblings': $results = $this->parent()->children()->remove($this->path()); break; - case 'descendants': $results = $pages->all($this)->remove($this->path())->nonModular(); break; diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php index 53790bac6..efe7ef1c0 100644 --- a/system/src/Grav/Common/Page/Pages.php +++ b/system/src/Grav/Common/Page/Pages.php @@ -486,6 +486,36 @@ public static function pageTypes() return static::types(); } + /** + * Get access levels of the site pages + * + * @return array + */ + public function accessLevels() + { + $accessLevels = []; + foreach($this->all() as $page) { + if (isset($page->header()->access)) { + if (is_array($page->header()->access)) { + foreach($page->header()->access as $index => $accessLevel) { + if (is_array($accessLevel)) { + foreach($accessLevel as $innerIndex => $innerAccessLevel) { + array_push($accessLevels, $innerIndex); + } + } else { + array_push($accessLevels, $index); + } + } + } else { + + array_push($accessLevels, $page->header()->access); + } + } + } + + return array_unique($accessLevels); + } + /** * Get available parents. * @@ -498,7 +528,22 @@ public static function parents() /** @var Pages $pages */ $pages = $grav['pages']; - return $pages->getList(); + $parents = $pages->getList(); + + /** @var Admin $admin */ + $admin = $grav['admin']; + + // Remove current route from parents + if (isset($admin)) { + $page = $admin->getPage($admin->route); + $page_route = $page->route(); + if (isset($parents[$page_route])) { + unset($parents[$page_route]); + } + + } + + return $parents; } /** diff --git a/system/src/Grav/Common/Service/ConfigServiceProvider.php b/system/src/Grav/Common/Service/ConfigServiceProvider.php index 28a6ac4d9..febdcd6a7 100644 --- a/system/src/Grav/Common/Service/ConfigServiceProvider.php +++ b/system/src/Grav/Common/Service/ConfigServiceProvider.php @@ -1,10 +1,15 @@ init(); + }; - // Pre-load setup.php as it contains our initial configuration. - $file = GRAV_ROOT . '/setup.php'; - $this->setup = is_file($file) ? (array) include $file : []; - $this->environment = isset($this->setup['environment']) ? $this->setup['environment'] : null; + $container['blueprints'] = function ($c) { + return static::blueprints($c); + }; - $container['blueprints'] = function ($c) use ($self) { - return $self->loadMasterBlueprints($c); + $container['config'] = function ($c) { + return static::load($c); }; - $container['config'] = function ($c) use ($self) { - return $self->loadMasterConfig($c); + $container['languages'] = function ($c) { + return static::languages($c); }; } - public function loadMasterConfig(Container $container) + public static function setup(Container $container) { - $environment = $this->getEnvironment($container); + return new Setup($container['uri']->environment()); + } + + public static function blueprints(Container $container) + { + /** Setup $setup */ + $setup = $container['setup']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/blueprints', true, true); + + $files = []; + $paths = $locator->findResources('blueprints://config'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'blueprints'); - $config = new Config($this->setup, $container, $environment); + $blueprints = new CompiledBlueprints($cache, $files, GRAV_ROOT); - return $config; + return $blueprints->name("master-{$setup->environment}")->load(); } - public function loadMasterBlueprints(Container $container) + public static function load(Container $container) { - $environment = $this->getEnvironment($container); - $file = CACHE_DIR . 'compiled/blueprints/master-'.$environment.'.php'; - $data = is_file($file) ? (array) include $file : []; + /** Setup $setup */ + $setup = $container['setup']; - return new Blueprints($data, $container); + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/config', true, true); + + $files = []; + $paths = $locator->findResources('config://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths); + + $config = new CompiledConfig($cache, $files, GRAV_ROOT); + $config->setBlueprints(function() use ($container) { + return $container['blueprints']; + }); + + return $config->name("master-{$setup->environment}")->load(); } - public function getEnvironment(Container $container) + public static function languages(Container $container) { - if (!isset($this->environment)) { - $this->environment = $container['uri']->environment(); + /** Setup $setup */ + $setup = $container['setup']; + + /** @var Config $config */ + $config = $container['config']; + + /** @var UniformResourceLocator $locator */ + $locator = $container['locator']; + + $cache = $locator->findResource('cache://compiled/languages', true, true); + $files = []; + + // Process languages only if enabled in configuration. + if ($config->get('system.languages.translations', true)) { + $paths = $locator->findResources('languages://'); + $files += (new ConfigFileFinder)->locateFiles($paths); + $paths = $locator->findResources('plugins://'); + $files += (new ConfigFileFinder)->setBase('plugins')->locateInFolders($paths, 'languages'); } - return $this->environment; + $languages = new CompiledLanguages($cache, $files, GRAV_ROOT); + + return $languages->name("master-{$setup->environment}")->load(); } } diff --git a/system/src/Grav/Common/Service/StreamsServiceProvider.php b/system/src/Grav/Common/Service/StreamsServiceProvider.php index d8f7dffbb..ec7f9cd85 100644 --- a/system/src/Grav/Common/Service/StreamsServiceProvider.php +++ b/system/src/Grav/Common/Service/StreamsServiceProvider.php @@ -1,7 +1,7 @@ initializeLocator($locator); + /** @var Setup $setup */ + $setup = $c['setup']; + $setup->initializeLocator($locator); return $locator; }; $container['streams'] = function($c) { - /** @var Config $config */ - $config = $c['config']; + /** @var Setup $setup */ + $setup = $c['setup']; /** @var UniformResourceLocator $locator */ $locator = $c['locator']; @@ -34,7 +34,7 @@ public function register(Container $container) Stream::setLocator($locator); ReadOnlyStream::setLocator($locator); - return new StreamBuilder($config->getStreams($c)); + return new StreamBuilder($setup->getStreams($c)); }; } } diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index 4ea09e350..12a1cb633 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -355,6 +355,12 @@ public function nicetimeFilter($date, $long_strings = true) $periods[$j] .= '_PLURAL'; } + if ($this->grav['language']->getTranslation($this->grav['language']->getLanguage(), $periods[$j] . '_MORE_THAN_TWO')) { + if ($difference > 2) { + $periods[$j] .= '_MORE_THAN_TWO'; + } + } + $periods[$j] = $this->grav['language']->translate($periods[$j], null, true); return "$difference $periods[$j] {$tense}"; diff --git a/system/src/Grav/Common/User/Group.php b/system/src/Grav/Common/User/Group.php new file mode 100644 index 000000000..965fde5f6 --- /dev/null +++ b/system/src/Grav/Common/User/Group.php @@ -0,0 +1,127 @@ +get('groups'); + return $groups; + } + + /** + * Checks if a group exists + * + * @return object + */ + public static function group_exists($groupname) + { + return isset(self::groups()[$groupname]); + } + + /** + * Get a group by name + * + * @return object + */ + public static function load($groupname) + { + if (self::group_exists($groupname)) { + $content = self::groups()[$groupname]; + } else { + $content = []; + } + + $blueprints = new Blueprints('blueprints://'); + $blueprint = $blueprints->get('user/group'); + if (!isset($content['groupname'])) { + $content['groupname'] = $groupname; + } + $group = new Group($content, $blueprint); + + return $group; + } + + /** + * Save a group + */ + public function save() + { + $blueprints = new Blueprints('blueprints://'); + $blueprint = $blueprints->get('user/group'); + + $fields = $blueprint->fields(); + + self::getGrav()['config']->set("groups.$this->groupname", []); + + foreach($fields as $field) { + if ($field['type'] == 'text') { + $value = $field['name']; + if (isset($this->items[$value])) { + self::getGrav()['config']->set("groups.$this->groupname.$value", $this->items[$value]); + } + } + if ($field['type'] == 'array') { + $value = $field['name']; + $arrayValues = Utils::resolve($this->items, $field['name']); + + if ($arrayValues) foreach($arrayValues as $arrayIndex => $arrayValue) { + self::getGrav()['config']->set("groups.$this->groupname.$value.$arrayIndex", $arrayValue); + } + } + } + + $type = 'groups'; + $blueprints = $this->blueprints("config/{$type}"); + $obj = new Data(self::getGrav()['config']->get($type), $blueprints); + $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource("config://{$type}.yaml")); + $obj->file($file); + $obj->save(); + } + + /** + * Remove a group + * + * @param string $username + * @return bool True if the action was performed + */ + public static function remove($groupname) + { + $blueprints = new Blueprints('blueprints://'); + $blueprint = $blueprints->get('user/group'); + + $groups = self::getGrav()['config']->get("groups"); + unset($groups[$groupname]); + self::getGrav()['config']->set("groups", $groups); + + $type = 'groups'; + $obj = new Data(self::getGrav()['config']->get($type), $blueprint); + $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource("config://{$type}.yaml")); + $obj->file($file); + $obj->save(); + + return true; + } +} diff --git a/system/src/Grav/Common/User/User.php b/system/src/Grav/Common/User/User.php index 6282041a2..9caad3741 100644 --- a/system/src/Grav/Common/User/User.php +++ b/system/src/Grav/Common/User/User.php @@ -53,7 +53,7 @@ public static function load($username) * Remove user account. * * @param string $username - * @return bool True is the action was performed + * @return bool True if the action was performed */ public static function remove($username) { @@ -83,7 +83,10 @@ public function authenticate($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, self::getGrav()['config']->get('system.security.default_hash')); + Authentication::verify( + $password, + self::getGrav()['config']->get('system.security.default_hash', '$2y$10$kwsyMVwM8/7j0K/6LHT.g.Fs49xOCTp2b8hh/S5.dPJuJcJB6T.UK') + ); return false; } else { // Plain-text does match, we can update the hash and proceed @@ -146,7 +149,28 @@ public function authorize($action) return false; } - return Utils::isPositive($this->get("access.{$action}")); + $return = false; + + //Check group access level + $groups = $this->get('groups'); + if ($groups) foreach($groups as $group) { + $permission = self::getGrav()['config']->get("groups.{$group}.access.{$action}"); + if (Utils::isPositive($permission)) { + $return = true; + } + } + + //Check user access level + if (!$this->get('access')) { + return false; + } + + if (Utils::resolve($this->get('access'), $action) !== null) { + $permission = $this->get("access.{$action}"); + $return = Utils::isPositive($permission); + } + + return $return; } /** diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 515d05851..815959927 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -15,6 +15,8 @@ abstract class Utils { use GravTrait; + protected static $nonces = []; + /** * @param string $haystack * @param string $needle @@ -217,9 +219,11 @@ public static function download($file, $force_download = true) $filesize = filesize($file); // check if this function is available, if so use it to stop any timeouts - if (function_exists('set_time_limit')) { - set_time_limit(0); - } + try { + if (!Utils::isFunctionDisabled('set_time_limit') && !ini_get('safe_mode') && function_exists('set_time_limit')) { + set_time_limit(0); + } + } catch (\Exception $e) {} ignore_user_abort(false); @@ -409,6 +413,25 @@ public static function date2timestamp($date) } } + /** + * Get value of an array using dot notation + */ + public static function resolve(array $array, $path, $default = null) + { + $current = $array; + $p = strtok($path, '.'); + + while ($p !== false) { + if (!isset($current[$p])) { + return $default; + } + $current = $current[$p]; + $p = strtok('.'); + } + + return $current; + } + /** * Checks if a value is positive * @@ -421,7 +444,6 @@ public static function isPositive($value) return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true); } - /** * Generates a nonce string to be hashed. Called by self::getNonce() * @@ -435,11 +457,10 @@ private static function generateNonceString($action, $plusOneTick = false) if (isset(self::getGrav()['user'])) { $user = self::getGrav()['user']; $username = $user->username; + if (isset($_SERVER['REMOTE_ADDR'])) { + $username .= $_SERVER['REMOTE_ADDR']; + } } else { - $username = false; - } - - if (!$username) { $username = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; } @@ -450,7 +471,7 @@ private static function generateNonceString($action, $plusOneTick = false) $i++; } - return ( $i . '|' . $action . '|' . $username . '|' . $token ); + return ( $i . '|' . $action . '|' . $username . '|' . $token . '|' . self::getGrav()['config']->get('security.salt')); } /** @@ -467,19 +488,6 @@ private static function nonceTick() return (int)ceil(time() / ( $secondsInHalfADay )); } - /** - * Get hash of given string - * - * @param string $data string to hash - * - * @return string hashed value of $data, cut to 10 characters - */ - private static function hash($data) - { - $hash = password_hash($data, PASSWORD_DEFAULT); - return $hash; - } - /** * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given * action is the same for 12 hours. @@ -491,9 +499,14 @@ private static function hash($data) */ public static function getNonce($action, $plusOneTick = false) { - $nonce = self::hash(self::generateNonceString($action, $plusOneTick)); - $nonce = str_replace('/', 'SLASH', $nonce); - return $nonce; + // Don't regenerate this again if not needed + if (isset(static::$nonces[$action])) { + return static::$nonces[$action]; + } + $nonce = md5(self::generateNonceString($action, $plusOneTick)); + static::$nonces[$action] = $nonce; + + return static::$nonces[$action]; } /** @@ -506,21 +519,18 @@ public static function getNonce($action, $plusOneTick = false) */ public static function verifyNonce($nonce, $action) { - $nonce = str_replace('SLASH', '/', $nonce); - //Nonce generated 0-12 hours ago - if (password_verify(self::generateNonceString($action), $nonce)) { + if ($nonce == self::getNonce($action)) { return true; } //Nonce generated 12-24 hours ago $plusOneTick = true; - if (password_verify(self::generateNonceString($action, $plusOneTick), $nonce)) { + if ($nonce == self::getNonce($action, $plusOneTick)) { return true; } //Invalid nonce return false; } - } diff --git a/web.config b/web.config index c3dd31424..e09e94fb5 100644 --- a/web.config +++ b/web.config @@ -46,7 +46,7 @@ - +