modif order

This commit is contained in:
2019-10-09 21:07:23 +02:00
parent 130ebff543
commit c5b29bfdbe
276 changed files with 4413 additions and 1918 deletions
+8
View File
@@ -0,0 +1,8 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: grav
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: # Replace with a single custom sponsorship URL
+116
View File
@@ -1,3 +1,119 @@
# v1.6.16
## 09/19/2019
1. [](#bugfix)
* Fixed Flex user creation if file storage is being used [#2444](https://github.com/getgrav/grav/issues/2444)
* Fixed `Badly encoded JSON data` warning when uploading files [#2663](https://github.com/getgrav/grav/issues/2663)
# v1.6.15
## 08/20/2019
1. [](#improved)
* Improved robots.txt [#2632](https://github.com/getgrav/grav/issues/2632)
1. [](#bugfix)
* Fixed broken markdown Twig tag [#2635](https://github.com/getgrav/grav/issues/2635)
* Force Symfony 4.2 in Grav 1.6 to remove a bunch of deprecated messages
# v1.6.14
## 08/18/2019
1. [](#bugfix)
* Actually include fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627)
# v1.6.13
## 08/16/2019
1. [](#bugfix)
* Regression fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627)
# v1.6.12
## 08/14/2019
1. [](#new)
* Added support for custom `FormFlash` save locations
* Added a new `Utils::arrayLower()` method for lowercasing arrays
* Support new GRAV_BASEDIR environment variable [#2541](https://github.com/getgrav/grav/pull/2541)
* Allow users to override plugin handler priorities [#2165](https://github.com/getgrav/grav/pull/2165)
1. [](#improved)
* Use new `Utils::getSupportedPageTypes()` to enforce `html,htm` at the front of the list [#2531](https://github.com/getgrav/grav/issues/2531)
* Updated vendor libraries
* Markdown filter is now page-aware so that it works with modular references [admin#1731](https://github.com/getgrav/grav-plugin-admin/issues/1731)
* Check of `GRAV_USER_INSTANCE` constant is already defined [#2621](https://github.com/getgrav/grav/pull/2621)
1. [](#bugfix)
* Fixed some potential issues when `$grav['user']` is not set
* Fixed error when calling `Media::add($name, null)`
* Fixed `url()` returning wrong path if using stream with grav root path in it, eg: `user-data://shop` when Grav is in `/shop`
* Fixed `url()` not returning a path to non-existing file (`user-data://shop` => `/user/data/shop`) if it is set to fail gracefully
* Fixed `url()` returning false on unknown streams, such as `ftp://domain.com`, they should be treated as external URL
* Fixed Flex User to have permissions to save and delete his own user
* Fixed new Flex User creation not being possible because of username could not be given
* Fixed fatal error 'Expiration date must be an integer, a DateInterval or null, "double" given' [#2529](https://github.com/getgrav/grav/issues/2529)
* Fixed non-existing Flex object having a bad media folder
* Fixed collections using `page@.self:` should allow modular pages if requested
* Fixed an error when trying to delete a file from non-existing Flex Object
* Fixed `FlexObject::exists()` failing sometimes just after the object has been saved
* Fixed CSV formatter not encoding strings with `"` and `,` properly
* Fixed var order in `Validation.php` [#2610](https://github.com/getgrav/grav/issues/2610)
# v1.6.11
## 06/21/2019
1. [](#new)
* Added `FormTrait::getAllFlashes()` method to get all the available form flash objects for the form
* Added creation and update timestamps to `FormFlash` objects
1. [](#improved)
* Added `FormFlashInterface`, changed constructor to take `$config` array
1. [](#bugfix)
* Fixed error in `ImageMedium::url()` if the image cache folder does not exist
* Fixed empty form flash name after file upload or form state update
* Fixed a bug in `Route::withParam()` method
* Fixed issue with `FormFlash` objects when there is no session initialized
# v1.6.10
## 06/14/2019
1. [](#improved)
* Added **page blueprints** to `YamlLinter` CLI and Admin reports
* Removed `Gitter` and `Slack` [#2502](https://github.com/getgrav/grav/issues/2502)
* Optimizations for Plugin/Theme loading
* Generalized markdown classes so they can be used outside of `Page` scope with a custom `Excerpts` class instance
* Change minimal port number to 0 (unix socket) [#2452](https://github.com/getgrav/grav/issues/2452)
1. [](#bugfix)
* Force question to install demo content in theme update [#2493](https://github.com/getgrav/grav/issues/2493)
* Fixed GPM errors from blueprints not being logged [#2505](https://github.com/getgrav/grav/issues/2505)
* Don't error when IP is invalid [#2507](https://github.com/getgrav/grav/issues/2507)
* Fixed regression with `bin/plugin` not listing the plugins available (1c725c0)
* Fixed bitwise operator in `TwigExtension::exifFunc()` [#2518](https://github.com/getgrav/grav/issues/2518)
* Fixed issue with lang prefix incorrectly identifying as admin [#2511](https://github.com/getgrav/grav/issues/2511)
* Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511)
* Fixed regresssion issue of `Utils::Url()` not returning `false` on failure. Added new optional `fail_gracefully` 3rd attribute to return string that caused failure [#2524](https://github.com/getgrav/grav/issues/2524)
# v1.6.9
## 05/09/2019
1. [](#new)
* Added `Route::withoutParams()` methods
* Added `Pages::setCheckMethod()` method to override page configuration in Admin Plugin
* Added `Cache::clearCache('invalidate')` parameter for just invalidating the cache without deleting any cached files
* Made `UserCollectionInderface` to extend `Countable` to get the count of existing users
1. [](#improved)
* Flex admin: added default search options for flex objects
* Flex collection and object now fall back to the default template if template file doesn't exist
* Updated Vendor libraries including Twig 1.40.1
* Updated language files from `https://crowdin.com/project/grav-core`
1. [](#bugfix)
* Fixed `$grav['route']` from being modified when the route instance gets modified
* Fixed Assets options array mixed with standalone priority [#2477](https://github.com/getgrav/grav/issues/2477)
* Fix for `avatar_url` provided by 3rd party providers
* Fixed non standard `lang` code lengths in `Utils` and `Session` detection
* Fixed saving a new object in Flex `SimpleStorage`
* Fixed exception in `Flex::getDirectories()` if the first parameter is set
* Output correct "Last Updated" in `bin/gpm info` command
* Checkbox getting interpreted as string, so created new `Validation::filterCheckbox()`
* Fixed backwards compatibility to `select` field with `selectize.create` set to true [git-sync#141](https://github.com/trilbymedia/grav-plugin-git-sync/issues/141)
* Fixed `YamlFormatter::decode()` to always return array [#2494](https://github.com/getgrav/grav/pull/2494)
* Fixed empty `$grav['request']->getAttribute('route')->getExtension()`
# v1.6.8 # v1.6.8
## 04/23/2019 ## 04/23/2019
+3 -2
View File
@@ -29,7 +29,7 @@ The issue tracker is the preferred channel for [bug reports](#bugs),
requests](#pull-requests), but please respect the following restrictions: requests](#pull-requests), but please respect the following restrictions:
* Please **do not** use the issue tracker for support requests. Use * Please **do not** use the issue tracker for support requests. Use
[the Forum](http://getgrav.org/forum) or [the Gitter chat](https://gitter.im/getgrav/grav). [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/).
<a name="bugs"></a> <a name="bugs"></a>
@@ -110,7 +110,8 @@ Good pull requests - patches, improvements, new features - are a fantastic
help. They should remain focused in scope and avoid containing unrelated help. They should remain focused in scope and avoid containing unrelated
commits. commits.
**Please ask first** in [Slack](https://getgrav.org/slack) or in the Forum before embarking on any significant pull request (e.g. **Please ask first** in [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/)
before embarking on any significant pull request (e.g.
implementing features, refactoring code..), implementing features, refactoring code..),
otherwise you risk spending a lot of time working on something that the otherwise you risk spending a lot of time working on something that the
project's developers might not want to merge into the project. project's developers might not want to merge into the project.
+12 -13
View File
@@ -79,19 +79,6 @@ $output = new ConsoleOutput();
$output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold'))); $output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold')));
$output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold'))); $output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold')));
if (is_null($plugin)) {
$output->writeln('');
$output->writeln("<red>$name plugin not found</red>");
die;
}
if (!$plugin->enabled) {
$output->writeln('');
$output->writeln("<red>$name not enabled</red>");
die;
}
if (!$name) { if (!$name) {
$output->writeln(''); $output->writeln('');
$output->writeln('<red>Usage:</red>'); $output->writeln('<red>Usage:</red>');
@@ -123,6 +110,18 @@ if (!$name) {
} }
exit; exit;
} else {
if (is_null($plugin)) {
$output->writeln('');
$output->writeln("<red>$name plugin not found</red>");
die;
}
if (!$plugin->enabled) {
$output->writeln('');
$output->writeln("<red>$name not enabled</red>");
die;
}
} }
if ($plugin === null) { if ($plugin === null) {
+8 -6
View File
@@ -24,14 +24,14 @@
"kodus/psr7-server": "*", "kodus/psr7-server": "*",
"nyholm/psr7": "^1.0", "nyholm/psr7": "^1.0",
"twig/twig": "~1.35", "twig/twig": "~1.40",
"erusev/parsedown": "1.6.4", "erusev/parsedown": "1.6.4",
"erusev/parsedown-extra": "~0.7", "erusev/parsedown-extra": "~0.7",
"symfony/yaml": "~4.2", "symfony/yaml": "~4.2.0",
"symfony/console": "~4.2", "symfony/console": "~4.2.0",
"symfony/event-dispatcher": "~4.2", "symfony/event-dispatcher": "~4.2.0",
"symfony/var-dumper": "~4.2", "symfony/var-dumper": "~4.2.0",
"symfony/process": "~4.2", "symfony/process": "~4.2.0",
"doctrine/cache": "^1.8", "doctrine/cache": "^1.8",
"doctrine/collections": "^1.5", "doctrine/collections": "^1.5",
"guzzlehttp/psr7": "^1.4", "guzzlehttp/psr7": "^1.4",
@@ -89,6 +89,8 @@
"exclude": ["VERSION"] "exclude": ["VERSION"]
}, },
"scripts": { "scripts": {
"api-16": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.16.md",
"api-15": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.md",
"post-create-project-cmd": "bin/grav install", "post-create-project-cmd": "bin/grav install",
"phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M", "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M",
"phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M", "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M",
Generated
+391 -317
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
{
"version": 2,
"builds": [{ "src": "*.php", "use": "@now/php" }]
}
+1 -1
View File
@@ -65,7 +65,7 @@ form:
summary.size: summary.size:
type: text type: text
size: x-small size: small
append: PLUGIN_ADMIN.CHARACTERS append: PLUGIN_ADMIN.CHARACTERS
label: PLUGIN_ADMIN.SUMMARY_SIZE label: PLUGIN_ADMIN.SUMMARY_SIZE
help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP
+2
View File
@@ -12,5 +12,7 @@ form:
type: text type: text
label: PLUGIN_ADMIN.USERNAME label: PLUGIN_ADMIN.USERNAME
help: PLUGIN_ADMIN.USERNAME_HELP help: PLUGIN_ADMIN.USERNAME_HELP
unset-disabled@: true
unset-readonly@: true
validate: validate:
required: true required: true
+10
View File
@@ -27,3 +27,13 @@ config:
title: Accounts title: Accounts
icon: fa-users icon: fa-users
authorize: ['admin.users', 'admin.accounts', 'admin.super'] authorize: ['admin.users', 'admin.accounts', 'admin.super']
form:
fields:
username:
flex-disabled@: exists
disabled: false
flex-readonly@: exists
readonly: false
validate:
required: true
+1 -1
View File
@@ -8,7 +8,7 @@
// Some standard defines // Some standard defines
define('GRAV', true); define('GRAV', true);
define('GRAV_VERSION', '1.6.8'); define('GRAV_VERSION', '1.6.16');
define('GRAV_TESTING', false); define('GRAV_TESTING', false);
define('DS', '/'); define('DS', '/');
+1 -1
View File
@@ -23,7 +23,7 @@ GRAV:
BAD_DATE: Fecha errónea BAD_DATE: Fecha errónea
AGO: antes AGO: antes
FROM_NOW: desde ahora FROM_NOW: desde ahora
JUST_NOW: justo ahora JUST_NOW: hace un momento
SECOND: segundo SECOND: segundo
MINUTE: minuto MINUTE: minuto
HOUR: hora HOUR: hora
+23
View File
@@ -1,11 +1,30 @@
--- ---
GRAV: GRAV:
FRONTMATTER_ERROR_PAGE: "---\npealkiri: %1$s\n---\n\n# Viga: vigane Frontmatter'i\n\nasukoht: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" FRONTMATTER_ERROR_PAGE: "---\npealkiri: %1$s\n---\n\n# Viga: vigane Frontmatter'i\n\nasukoht: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
INFLECTOR_UNCOUNTABLE:
- 'equipment'
- 'informatsioon'
- 'rice'
- 'money'
- 'species'
- 'series'
- 'kala'
- 'lammas'
INFLECTOR_IRREGULAR:
'person': 'inimesed'
'man': 'mees'
'child': 'lapsed'
INFLECTOR_ORDINALS:
'default': '.'
'first': '.'
'second': '.'
'third': '.'
NICETIME: NICETIME:
NO_DATE_PROVIDED: Kuupäev määramata NO_DATE_PROVIDED: Kuupäev määramata
BAD_DATE: Vigane kuupäev BAD_DATE: Vigane kuupäev
AGO: tagasi AGO: tagasi
FROM_NOW: praegusest FROM_NOW: praegusest
JUST_NOW: just nüüd
SECOND: sekund SECOND: sekund
MINUTE: minut MINUTE: minut
HOUR: tundi HOUR: tundi
@@ -60,3 +79,7 @@ GRAV:
- 'reede' - 'reede'
- 'laupäev' - 'laupäev'
- 'pühapäev' - 'pühapäev'
CRON:
EVERY: iga
EVERY_MONTH: iga kuu
TEXT_PERIOD: Iga <b />
+2
View File
@@ -14,6 +14,8 @@ GRAV:
'/sis$/i': 'ses' '/sis$/i': 'ses'
'/([ti])um$/i': '\1a' '/([ti])um$/i': '\1a'
'/(buffal|tomat)o$/i': '\1es' '/(buffal|tomat)o$/i': '\1es'
'/(bu)s$/i': 'Bus'
'/(alias|status)/i': 'alias|status'
'/(ax|test)is$/i': '\1s' '/(ax|test)is$/i': '\1s'
'/s$/i': 's' '/s$/i': 's'
'/$/': 's' '/$/': 's'
+11
View File
@@ -11,6 +11,8 @@ GRAV:
- 'fish' - 'fish'
- 'sheep' - 'sheep'
NICETIME: NICETIME:
NO_DATE_PROVIDED: Engin dagsetning gefin
BAD_DATE: Röng dagsetning
AGO: síðan AGO: síðan
JUST_NOW: í þessu JUST_NOW: í þessu
SECOND: sekúndu SECOND: sekúndu
@@ -45,6 +47,7 @@ GRAV:
DEC_PLURAL: árat DEC_PLURAL: árat
FORM: FORM:
VALIDATION_FAIL: <b>Sannvottun mistókst:</b> VALIDATION_FAIL: <b>Sannvottun mistókst:</b>
INVALID_INPUT: Ógilt inntak í
MISSING_REQUIRED_FIELD: 'Vantar nauðsynlegan reit:' MISSING_REQUIRED_FIELD: 'Vantar nauðsynlegan reit:'
MONTHS_OF_THE_YEAR: MONTHS_OF_THE_YEAR:
- 'janúar' - 'janúar'
@@ -67,3 +70,11 @@ GRAV:
- 'Föstudagur' - 'Föstudagur'
- 'Laugardagur' - 'Laugardagur'
- 'Sunnudagur' - 'Sunnudagur'
CRON:
TEXT_TIME: ' á <b />:<b />'
TEXT_DOW: ' á <b />'
TEXT_MONTH: ' af <b />'
TEXT_DOM: ' á <b />'
ERROR1: Merkið %s er ekki stutt!
ERROR3: Það ætti að setja jquery_element inn í stillingar jqCron
ERROR4: Óþekkt segð
+9
View File
@@ -35,3 +35,12 @@ GRAV:
- 'Outubro' - 'Outubro'
- 'Novembro' - 'Novembro'
- 'Dezembro' - 'Dezembro'
INFLECTOR_UNCOUNTABLE:
- 'equipment'
- 'information'
- 'arroz'
- 'money'
- 'species'
- 'series'
- 'fish'
- 'sheep'
+84
View File
@@ -1,10 +1,75 @@
--- ---
GRAV: GRAV:
FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Chyba: Chybný frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
INFLECTOR_PLURALS:
'/(quiz)$/i': '\1zes'
'/^(ox)$/i': '\1en'
'/([m|l])ouse$/i': '\1ice'
'/(matr|vert|ind)ix|ex$/i': '\1ices'
'/(x|ch|ss|sh)$/i': '\1es'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([^aeiouy]|qu)y$/i': '\1ies'
'/(hive)$/i': '\1s'
'/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
'/sis$/i': 'ses'
'/([ti])um$/i': '\1a'
'/(buffal|tomat)o$/i': '\1oes'
'/(bu)s$/i': '\1ses'
'/(alias|status)/i': '\1es'
'/(octop|vir)us$/i': '\1i'
'/(ax|test)is$/i': '\1es'
'/s$/i': 's'
'/$/': 's'
INFLECTOR_SINGULAR:
'/(quiz)zes$/i': '\1'
'/(matr)ices$/i': '\1ix'
'/(vert|ind)ices$/i': '\1ex'
'/^(ox)en/i': '\1'
'/(alias|status)es$/i': '\1'
'/([octop|vir])i$/i': '\1us'
'/(cris|ax|test)es$/i': '\1is'
'/(shoe)s$/i': '\1'
'/(o)es$/i': '\1'
'/(bus)es$/i': '\1'
'/([m|l])ice$/i': '\1ouse'
'/(x|ch|ss|sh)es$/i': '\1'
'/(m)ovies$/i': '\1ovie'
'/(s)eries$/i': '\1eries'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([lr])ves$/i': '\1f'
'/(tive)s$/i': '\1'
'/(hive)s$/i': '\1'
'/([^f])ves$/i': '\1fe'
'/(^analy)ses$/i': '\1sis'
'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
'/([ti])a$/i': '\1um'
'/(n)ews$/i': '\1ews'
INFLECTOR_UNCOUNTABLE:
- 'vybavenie'
- 'informácie'
- 'ryža'
- 'peniaze'
- 'druhy'
- 'séria'
- 'ryba'
- 'ovce'
INFLECTOR_IRREGULAR:
'person': 'ľudia'
'man': 'muži'
'child': 'deti'
'sex': 'pohlavia'
'move': 'pohyby'
INFLECTOR_ORDINALS:
'default': '.'
'first': '.'
'second': '.'
'third': '.'
NICETIME: NICETIME:
NO_DATE_PROVIDED: Neposkytnutý žiaden dátum NO_DATE_PROVIDED: Neposkytnutý žiaden dátum
BAD_DATE: Nesprávny dátum BAD_DATE: Nesprávny dátum
AGO: pred AGO: pred
FROM_NOW: odteraz FROM_NOW: odteraz
JUST_NOW: práve teraz
SECOND: sekunda SECOND: sekunda
MINUTE: minúta MINUTE: minúta
HOUR: hodina HOUR: hodina
@@ -14,10 +79,12 @@ GRAV:
YEAR: rok YEAR: rok
DECADE: desaťročie DECADE: desaťročie
SEC: sek SEC: sek
MIN: min
HR: hod HR: hod
WK: t WK: t
MO: m MO: m
YR: r YR: r
DEC: dec
SECOND_PLURAL: sekúnd SECOND_PLURAL: sekúnd
MINUTE_PLURAL: minút MINUTE_PLURAL: minút
HOUR_PLURAL: hodín HOUR_PLURAL: hodín
@@ -58,3 +125,20 @@ GRAV:
- 'Piatok' - 'Piatok'
- 'Sobota' - 'Sobota'
- 'Nedeľa' - 'Nedeľa'
CRON:
EVERY: každý
EVERY_HOUR: každú hodinu
EVERY_MINUTE: každú minútu
EVERY_DAY_OF_WEEK: každý deň v týždni
EVERY_DAY_OF_MONTH: každý deň v mesiaci
EVERY_MONTH: každý mesiac
TEXT_PERIOD: Každý <b />
TEXT_MINS: ' at <b /> minute(s) past the hour'
TEXT_TIME: ' at <b />:<b />'
TEXT_DOW: ' on <b />'
TEXT_MONTH: ' of <b />'
TEXT_DOM: ' on <b />'
ERROR1: Tag %s nieje podporovaný!
ERROR2: Chybný počet položiek
ERROR3: jquery_element musí byť nastavený v nastaveniach pre jqCron
ERROR4: Neznámy výraz
+11
View File
@@ -1,6 +1,17 @@
--- ---
GRAV: GRAV:
FRONTMATTER_ERROR_PAGE: "--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```" FRONTMATTER_ERROR_PAGE: "--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```"
INFLECTOR_UNCOUNTABLE:
- 'utrustning'
- 'information'
- 'ris'
- 'pengar'
- 'arter'
- 'serier'
- 'fisk'
- 'får'
INFLECTOR_IRREGULAR:
'person': 'personer'
NICETIME: NICETIME:
NO_DATE_PROVIDED: Inget datum har angivits NO_DATE_PROVIDED: Inget datum har angivits
BAD_DATE: Ogiltigt datum BAD_DATE: Ogiltigt datum
+24 -4
View File
@@ -1,24 +1,44 @@
--- ---
GRAV: GRAV:
FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 錯誤: 不正確的 Frontmatter\n\n路徑: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
NICETIME: NICETIME:
NO_DATE_PROVIDED: 沒有提供日期
BAD_DATE: 錯誤日期
AGO: 之前
FROM_NOW: 之後
JUST_NOW: 剛剛
SECOND: 秒 SECOND: 秒
MINUTE: 分 MINUTE: 分
HOUR: 小時 HOUR: 小時
DAY: 天
WEEK: 週
MONTH: 月
YEAR: 年
DECADE: 十年
SEC: 秒
MIN: 分
HR: 小時
WK: 週
MO: 月
YR: 年
DEC: 十年
SECOND_PLURAL: 秒 SECOND_PLURAL: 秒
MINUTE_PLURAL: 分 MINUTE_PLURAL: 分
HOUR_PLURAL: 時 HOUR_PLURAL: 小時
DAY_PLURAL: 日 DAY_PLURAL: 天
WEEK_PLURAL: 周 WEEK_PLURAL: 週
MONTH_PLURAL: 月 MONTH_PLURAL: 月
YEAR_PLURAL: 年 YEAR_PLURAL: 年
DECADE_PLURAL: 十年 DECADE_PLURAL: 十年
SEC_PLURAL: 秒 SEC_PLURAL: 秒
MIN_PLURAL: 分 MIN_PLURAL: 分
HR_PLURAL: 時 HR_PLURAL: 時
WK_PLURAL: 周 WK_PLURAL: 週
MO_PLURAL: 月 MO_PLURAL: 月
YR_PLURAL: 年 YR_PLURAL: 年
DEC_PLURAL: 十年 DEC_PLURAL: 十年
FORM:
MISSING_REQUIRED_FIELD: 遺漏必填欄位:
MONTHS_OF_THE_YEAR: MONTHS_OF_THE_YEAR:
- '一月' - '一月'
- '二月' - '二月'
+113 -32
View File
@@ -1,57 +1,122 @@
--- ---
GRAV: GRAV:
FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 錯誤: 不正確的 Frontmatter\n\n路徑: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```" FRONTMATTER_ERROR_PAGE: "---\n标题: %1$s\n---\n\n# 错误:无效参数\n\n位置: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
INFLECTOR_PLURALS:
'/(quiz)$/i': '\1zes'
'/^(ox)$/i': '\1en'
'/([m|l])ouse$/i': '\1ice'
'/(matr|vert|ind)ix|ex$/i': '\1ices'
'/(x|ch|ss|sh)$/i': '\1es'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([^aeiouy]|qu)y$/i': '\1ies'
'/(hive)$/i': '\1s'
'/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
'/sis$/i': 'ses'
'/([ti])um$/i': '\1a'
'/(buffal|tomat)o$/i': '\1oes'
'/(bu)s$/i': '\1ses'
'/(alias|status)/i': '\1es'
'/(octop|vir)us$/i': '\1i'
'/(ax|test)is$/i': '\1es'
'/s$/i': 's'
'/$/': 's'
INFLECTOR_SINGULAR:
'/(quiz)zes$/i': '\1'
'/(matr)ices$/i': '\1ix'
'/(vert|ind)ices$/i': '\1ex'
'/^(ox)en/i': '\1'
'/(alias|status)es$/i': '\1'
'/([octop|vir])i$/i': '\1us'
'/(cris|ax|test)es$/i': '\1is'
'/(shoe)s$/i': '\1'
'/(o)es$/i': '\1'
'/(bus)es$/i': '\1'
'/([m|l])ice$/i': '\1ouse'
'/(x|ch|ss|sh)es$/i': '\1'
'/(m)ovies$/i': '\1ovie'
'/(s)eries$/i': '\1eries'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([lr])ves$/i': '\1f'
'/(tive)s$/i': '\1'
'/(hive)s$/i': '\1'
'/([^f])ves$/i': '\1fe'
'/(^analy)ses$/i': '\1sis'
'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
'/([ti])a$/i': '\1um'
'/(n)ews$/i': '\1ews'
INFLECTOR_UNCOUNTABLE:
- '装备'
- '信息'
- '大米'
- '钱'
- '物种'
- '系列'
- '鱼'
- '羊'
INFLECTOR_IRREGULAR:
'person': '人员'
'man': '男人'
'child': '儿童'
'sex': '性别'
'move': '移动'
INFLECTOR_ORDINALS:
'default': 'th'
'first': 'st'
'second': 'md'
'third': 'rd'
NICETIME: NICETIME:
NO_DATE_PROVIDED: 沒有提供日期 NO_DATE_PROVIDED: 无日期信息
BAD_DATE: 錯誤日期 BAD_DATE: 无效日期
AGO: 之前 AGO: 前
FROM_NOW: 之後 FROM_NOW: 距今
JUST_NOW: 剛剛 JUST_NOW: 刚刚
SECOND: 秒 SECOND: 秒
MINUTE: 分 MINUTE: 分钟
HOUR: 小時 HOUR: 小时
DAY: 天 DAY: 天
WEEK: 週 WEEK: 周
MONTH: 月 MONTH: 月
YEAR: 年 YEAR: 年
DECADE: 十年 DECADE: 十年
SEC: 秒 SEC: 秒
MIN: 分 MIN: 分钟
HR: 小時 HR: 小时
WK: 週 WK: 周
MO: 月 MO: 月
YR: 年 YR: 年
DEC: 十年 DEC: 年代
SECOND_PLURAL: 秒 SECOND_PLURAL: 秒
MINUTE_PLURAL: 分 MINUTE_PLURAL: 分
HOUR_PLURAL: 小時 HOUR_PLURAL: 小时
DAY_PLURAL: 天 DAY_PLURAL: 天
WEEK_PLURAL: 週 WEEK_PLURAL: 周
MONTH_PLURAL: 月 MONTH_PLURAL: 月
YEAR_PLURAL: 年 YEAR_PLURAL: 年
DECADE_PLURAL: 十年 DECADE_PLURAL: 十年
SEC_PLURAL: 秒 SEC_PLURAL: 秒
MIN_PLURAL: 分 MIN_PLURAL: 分
HR_PLURAL: 時 HR_PLURAL: 时
WK_PLURAL: 週 WK_PLURAL: 周
MO_PLURAL: 月 MO_PLURAL: 月
YR_PLURAL: 年 YR_PLURAL: 年
DEC_PLURAL: 十年 DEC_PLURAL: 年代
FORM: FORM:
MISSING_REQUIRED_FIELD: 遺漏必填欄位: VALIDATION_FAIL: <b>验证失败:</b>
INVALID_INPUT: 无效输入
MISSING_REQUIRED_FIELD: 必填字段缺失:
MONTHS_OF_THE_YEAR: MONTHS_OF_THE_YEAR:
- '一月' - '1月'
- '二月' - '2月'
- '三月' - '3月'
- '四月' - '4月'
- '五月' - '5月'
- '六月' - '6月'
- '七月' - '7月'
- '八月' - '8月'
- '九月' - '9月'
- '十月' - '10月'
- '十一月' - '11月'
- '十二月' - '12月'
DAYS_OF_THE_WEEK: DAYS_OF_THE_WEEK:
- '星期一' - '星期一'
- '星期二' - '星期二'
@@ -60,4 +125,20 @@ GRAV:
- '星期五' - '星期五'
- '星期六' - '星期六'
- '星期日' - '星期日'
CRON:
EVERY: 每隔
EVERY_HOUR: 每小时
EVERY_MINUTE: 每分钟
EVERY_DAY_OF_WEEK: 一周中的每一天
EVERY_DAY_OF_MONTH: 月份中的每一天
EVERY_MONTH: 每月
TEXT_PERIOD: 所有 <b />
TEXT_MINS: ' 在 <b /> 小时过后的分钟'
TEXT_TIME: ' 在 <b />:<b />'
TEXT_DOW: ' on <b />'
TEXT_MONTH: ' of <b />'
TEXT_DOM: ' on <b />'
ERROR1: 不支持分享类型 %s
ERROR2: 无效数字
ERROR3: 请在 jqCron 设置中设定 jquery_element
ERROR4: 无法识别表达式
+14 -4
View File
@@ -17,11 +17,21 @@ if (is_file($_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $_SERVER['SCRIPT_N
return false; return false;
} }
$grav_index = 'index.php';
/* Check the GRAV_BASEDIR environment variable and use if set */
$grav_basedir = getenv('GRAV_BASEDIR') ?: '';
if ($grav_basedir) {
$grav_index = ltrim($grav_basedir, '/') . DIRECTORY_SEPARATOR . $grav_index;
$grav_basedir = DIRECTORY_SEPARATOR . trim($grav_basedir, DIRECTORY_SEPARATOR);
define('GRAV_ROOT', str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . $grav_basedir);
}
$_SERVER = array_merge($_SERVER, $_ENV); $_SERVER = array_merge($_SERVER, $_ENV);
$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'index.php'; $_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . $grav_basedir .DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR . 'index.php'; $_SERVER['SCRIPT_NAME'] = $grav_basedir . DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR . 'index.php'; $_SERVER['PHP_SELF'] = $grav_basedir . DIRECTORY_SEPARATOR . 'index.php';
error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4); error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4);
require 'index.php'; require $grav_index;
@@ -24,19 +24,21 @@ trait LegacyAssetsTrait
// First argument is always the asset // First argument is always the asset
array_shift($args); array_shift($args);
if (\count($args) === 0) { if (count($args) === 0) {
return []; return [];
} }
if (\count($args) === 1 && \is_array($args[0])) { // New options array format
if (count($args) === 1 && is_array($args[0])) {
return $args[0]; return $args[0];
} }
// Handle obscure case where options array is mixed with a priority
if (count($args) === 2 && is_array($args[0]) && is_int($args[1])) {
$arguments = $args[0];
$arguments['priority'] = $args[1];
return $arguments;
}
switch ($type) { switch ($type) {
case(Assets::INLINE_CSS_TYPE):
$defaults = ['priority' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
break;
case(Assets::JS_TYPE): case(Assets::JS_TYPE):
$defaults = ['priority' => null, 'pipeline' => true, 'loading' => null, 'group' => null]; $defaults = ['priority' => null, 'pipeline' => true, 'loading' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults); $arguments = $this->createArgumentsFromLegacy($args, $defaults);
@@ -55,6 +57,11 @@ trait LegacyAssetsTrait
break; break;
case(Assets::INLINE_CSS_TYPE):
$defaults = ['priority' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
break;
default: default:
case(Assets::CSS_TYPE): case(Assets::CSS_TYPE):
$defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null]; $defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null];
+5 -3
View File
@@ -437,6 +437,9 @@ class Cache extends Getters
case 'tmp-only': case 'tmp-only':
$remove_paths = self::$tmp_remove; $remove_paths = self::$tmp_remove;
break; break;
case 'invalidate':
$remove_paths = [];
break;
default: default:
if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) { if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) {
$remove_paths = self::$standard_remove; $remove_paths = self::$standard_remove;
@@ -528,7 +531,6 @@ class Cache extends Getters
} }
/** /**
* Set the cache lifetime programmatically * Set the cache lifetime programmatically
* *
@@ -540,7 +542,7 @@ class Cache extends Getters
return; return;
} }
$interval = $future - $this->now; $interval = (int)($future - $this->now);
if ($interval > 0 && $interval < $this->getLifetime()) { if ($interval > 0 && $interval < $this->getLifetime()) {
$this->lifetime = $interval; $this->lifetime = $interval;
} }
@@ -555,7 +557,7 @@ class Cache extends Getters
public function getLifetime() public function getLifetime()
{ {
if ($this->lifetime === null) { if ($this->lifetime === null) {
$this->lifetime = $this->config->get('system.cache.lifetime') ?: 604800; // 1 week default $this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default
} }
return $this->lifetime; return $this->lifetime;
+6 -8
View File
@@ -378,14 +378,12 @@ class Blueprint extends BlueprintForm
$grav = Grav::instance(); $grav = Grav::instance();
$actions = (array)$call['params']; $actions = (array)$call['params'];
/** @var UserInterface $user */ /** @var UserInterface|null $user */
if (isset($grav['user'])) { $user = $grav['user'] ?? null;
$user = Grav::instance()['user']; foreach ($actions as $action) {
foreach ($actions as $action) { if (!$user || !$user->authorize($action)) {
if (!$user->authorize($action)) { $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]); return;
return;
}
} }
} }
} }
+12 -2
View File
@@ -39,7 +39,8 @@ class Blueprints
public function get($type) public function get($type)
{ {
if (!isset($this->instances[$type])) { if (!isset($this->instances[$type])) {
$this->instances[$type] = $this->loadFile($type); $blueprint = $this->loadFile($type);
$this->instances[$type] = $blueprint;
} }
return $this->instances[$type]; return $this->instances[$type];
@@ -99,6 +100,15 @@ class Blueprints
$blueprint->setContext($this->search); $blueprint->setContext($this->search);
} }
return $blueprint->load()->init(); try {
$blueprint->load()->init();
} catch (\RuntimeException $e) {
$log = Grav::instance()['log'];
$log->error(sprintf('Blueprint %s cannot be loaded: %s', $name, $e->getMessage()));
throw $e;
}
return $blueprint;
} }
} }
+12 -1
View File
@@ -27,8 +27,9 @@ class Validation
if (!isset($field['type'])) { if (!isset($field['type'])) {
$field['type'] = 'text'; $field['type'] = 'text';
} }
$type = $validate['type'] ?? $field['type'];
$validate = (array)($field['validate'] ?? null); $validate = (array)($field['validate'] ?? null);
$type = $validate['type'] ?? $field['type'];
$required = $validate['required'] ?? false; $required = $validate['required'] ?? false;
// If value isn't required, we will stop validation if empty value is given. // If value isn't required, we will stop validation if empty value is given.
@@ -165,6 +166,11 @@ class Validation
return (string) $value; return (string) $value;
} }
protected static function filterCheckbox($value, array $params, array $field)
{
return (bool) $value;
}
protected static function filterCommaList($value, array $params, array $field) protected static function filterCommaList($value, array $params, array $field)
{ {
return \is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY); return \is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
@@ -571,6 +577,11 @@ class Validation
} }
} }
// If creating new values is allowed, no further checks are needed.
if (!empty($field['selectize']['create'])) {
return true;
}
$options = $field['options'] ?? []; $options = $field['options'] ?? [];
$use = $field['use'] ?? 'values'; $use = $field['use'] ?? 'values';
+10 -76
View File
@@ -10,54 +10,10 @@
namespace Grav\Common\Form; namespace Grav\Common\Form;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav; use Grav\Framework\Form\FormFlash as FrameworkFormFlash;
use Grav\Common\User\Interfaces\UserInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class FormFlash extends \Grav\Framework\Form\FormFlash class FormFlash extends FrameworkFormFlash
{ {
/**
* @param string $sessionId
*/
public static function clearSession(string $sessionId): void
{
$folder = static::getSessionTmpDir($sessionId);
if (is_dir($folder)) {
Folder::delete($folder);
}
}
/**
* @param string $sessionId
* @return string
*/
public static function getSessionTmpDir(string $sessionId): string
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
return $locator->findResource("tmp://forms/{$sessionId}", true, true);
}
/**
* @param UserInterface|null $user
* @return $this
*/
public function setUser(UserInterface $user = null)
{
if ($user && $user->username) {
$this->user = [
'username' => $user->username,
'email' => $user->email ?? ''
];
} else {
$this->user = null;
}
return $this;
}
/** /**
* @return array * @return array
* @deprecated 1.6 For backwards compatibility only, do not use * @deprecated 1.6 For backwards compatibility only, do not use
@@ -89,8 +45,11 @@ class FormFlash extends \Grav\Framework\Form\FormFlash
*/ */
public function uploadFile(string $field, string $filename, array $upload): bool public function uploadFile(string $field, string $filename, array $upload): bool
{ {
$tmp_dir = $this->getTmpDir(); if (!$this->uniqueId) {
return false;
}
$tmp_dir = $this->getTmpDir();
Folder::create($tmp_dir); Folder::create($tmp_dir);
$tmp_file = $upload['file']['tmp_name']; $tmp_file = $upload['file']['tmp_name'];
@@ -118,8 +77,11 @@ class FormFlash extends \Grav\Framework\Form\FormFlash
*/ */
public function cropFile(string $field, string $filename, array $upload, array $crop): bool public function cropFile(string $field, string $filename, array $upload, array $crop): bool
{ {
$tmp_dir = $this->getTmpDir(); if (!$this->uniqueId) {
return false;
}
$tmp_dir = $this->getTmpDir();
Folder::create($tmp_dir); Folder::create($tmp_dir);
$tmp_file = $upload['file']['tmp_name']; $tmp_file = $upload['file']['tmp_name'];
@@ -136,32 +98,4 @@ class FormFlash extends \Grav\Framework\Form\FormFlash
return true; return true;
} }
/**
* @return YamlFile
*/
protected function getTmpIndex(): YamlFile
{
// Do not use CompiledYamlFile as the file can change multiple times per second.
return YamlFile::instance($this->getTmpDir() . '/index.yaml');
}
/**
* @param string $name
*/
protected function removeTmpFile(string $name): void
{
$filename = $this->getTmpDir() . '/' . $name;
if ($name && is_file($filename)) {
unlink($filename);
}
}
protected function removeTmpDir(): void
{
$tmpDir = $this->getTmpDir();
if (file_exists($tmpDir)) {
Folder::delete($tmpDir);
}
}
} }
@@ -11,19 +11,22 @@ namespace Grav\Common\GPM\Common;
use Grav\Common\Iterator; use Grav\Common\Iterator;
class CachedCollection extends Iterator { class CachedCollection extends Iterator
{
protected static $cache; protected static $cache;
public function __construct($items) public function __construct($items)
{ {
parent::__construct(); parent::__construct();
$method = static::class . __METHOD__;
// local cache to speed things up // local cache to speed things up
if (!isset(self::$cache[get_called_class() . __METHOD__])) { if (!isset(self::$cache[$method])) {
self::$cache[get_called_class() . __METHOD__] = $items; self::$cache[$method] = $items;
} }
foreach (self::$cache[get_called_class() . __METHOD__] as $name => $item) { foreach (self::$cache[$method] as $name => $item) {
$this->append([$name => $item]); $this->append([$name => $item]);
} }
} }
@@ -11,8 +11,8 @@ namespace Grav\Common\GPM\Common;
use Grav\Common\Data\Data; use Grav\Common\Data\Data;
class Package { class Package
{
/** /**
* @var Data * @var Data
*/ */
+13 -12
View File
@@ -770,7 +770,7 @@ class GPM extends Iterator
* @param array $ignore_packages_list * @param array $ignore_packages_list
* *
* @return bool * @return bool
* @throws \Exception * @throws \RuntimeException
*/ */
public function checkNoOtherPackageNeedsThisDependencyInALowerVersion( public function checkNoOtherPackageNeedsThisDependencyInALowerVersion(
$slug, $slug,
@@ -793,8 +793,8 @@ class GPM extends Iterator
$compatible = $this->checkNextSignificantReleasesAreCompatible($version, $compatible = $this->checkNextSignificantReleasesAreCompatible($version,
$other_dependency_version); $other_dependency_version);
if (!$compatible) { if (!$compatible) {
if (!in_array($dependent_package, $ignore_packages_list)) { if (!in_array($dependent_package, $ignore_packages_list, true)) {
throw new \Exception("Package <cyan>$slug</cyan> is required in an older version by package <cyan>$dependent_package</cyan>. This package needs a newer version, and because of this it cannot be installed. The <cyan>$dependent_package</cyan> package must be updated to use a newer release of <cyan>$slug</cyan>.", throw new \RuntimeException("Package <cyan>$slug</cyan> is required in an older version by package <cyan>$dependent_package</cyan>. This package needs a newer version, and because of this it cannot be installed. The <cyan>$dependent_package</cyan> package must be updated to use a newer release of <cyan>$slug</cyan>.",
2); 2);
} }
} }
@@ -850,10 +850,10 @@ class GPM extends Iterator
) { ) {
//Needs a Grav update first //Needs a Grav update first
throw new \RuntimeException("<red>One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this"); throw new \RuntimeException("<red>One of the packages require PHP {$dependencies['php']}. Please update PHP to resolve this");
} else {
unset($dependencies[$dependency_slug]);
continue;
} }
unset($dependencies[$dependency_slug]);
continue;
} }
//First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell. //First, check for Grav dependency. If a dependency requires Grav > the current version, abort and tell.
@@ -863,10 +863,10 @@ class GPM extends Iterator
) { ) {
//Needs a Grav update first //Needs a Grav update first
throw new \RuntimeException("<red>One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release."); throw new \RuntimeException("<red>One of the packages require Grav {$dependencies['grav']}. Please update Grav to the latest release.");
} else {
unset($dependencies[$dependency_slug]);
continue;
} }
unset($dependencies[$dependency_slug]);
continue;
} }
if ($this->isPluginInstalled($dependency_slug)) { if ($this->isPluginInstalled($dependency_slug)) {
@@ -1092,6 +1092,7 @@ class GPM extends Iterator
if ($this->versionFormatIsEqualOrHigher($version)) { if ($this->versionFormatIsEqualOrHigher($version)) {
return trim(substr($version, 2)); return trim(substr($version, 2));
} }
return $version; return $version;
} }
@@ -1104,7 +1105,7 @@ class GPM extends Iterator
* *
* @return bool * @return bool
*/ */
public function versionFormatIsNextSignificantRelease($version) public function versionFormatIsNextSignificantRelease($version): bool
{ {
return strpos($version, '~') === 0; return strpos($version, '~') === 0;
} }
@@ -1118,7 +1119,7 @@ class GPM extends Iterator
* *
* @return bool * @return bool
*/ */
public function versionFormatIsEqualOrHigher($version) public function versionFormatIsEqualOrHigher($version): bool
{ {
return strpos($version, '>=') === 0; return strpos($version, '>=') === 0;
} }
@@ -1136,7 +1137,7 @@ class GPM extends Iterator
* *
* @return bool * @return bool
*/ */
public function checkNextSignificantReleasesAreCompatible($version1, $version2) public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool
{ {
$version1array = explode('.', $version1); $version1array = explode('.', $version1);
$version2array = explode('.', $version2); $version2array = explode('.', $version2);
+1 -1
View File
@@ -103,7 +103,7 @@ class Licenses
} }
/** /**
* Get's the License File object * Get the License File object
* *
* @return \RocketTheme\Toolbox\File\FileInterface * @return \RocketTheme\Toolbox\File\FileInterface
*/ */
@@ -16,6 +16,7 @@ abstract class AbstractPackageCollection extends BaseCollection
public function __construct($items) public function __construct($items)
{ {
parent::__construct(); parent::__construct();
foreach ($items as $name => $data) { foreach ($items as $name => $data) {
$data->set('slug', $name); $data->set('slug', $name);
$this->items[$name] = new Package($data, $this->type); $this->items[$name] = new Package($data, $this->type);
@@ -25,6 +25,7 @@ class Plugins extends AbstractPackageCollection
{ {
/** @var \Grav\Common\Plugins $plugins */ /** @var \Grav\Common\Plugins $plugins */
$plugins = Grav::instance()['plugins']; $plugins = Grav::instance()['plugins'];
parent::__construct($plugins->all()); parent::__construct($plugins->all());
} }
} }
@@ -43,7 +43,7 @@ class AbstractPackageCollection extends BaseCollection
{ {
parent::__construct(); parent::__construct();
if ($repository === null) { if ($repository === null) {
throw new \RuntimeException("A repository is required to indicate the origin of the remote collection"); throw new \RuntimeException('A repository is required to indicate the origin of the remote collection');
} }
$channel = Grav::instance()['config']->get('system.gpm.releases', 'stable'); $channel = Grav::instance()['config']->get('system.gpm.releases', 'stable');
@@ -37,9 +37,9 @@ class GravCore extends AbstractPackageCollection
$this->fetch($refresh, $callback); $this->fetch($refresh, $callback);
$this->data = json_decode($this->raw, true); $this->data = json_decode($this->raw, true);
$this->version = isset($this->data['version']) ? $this->data['version'] : '-'; $this->version = $this->data['version'] ?? '-';
$this->date = isset($this->data['date']) ? $this->data['date'] : '-'; $this->date = $this->data['date'] ?? '-';
$this->min_php = isset($this->data['min_php']) ? $this->data['min_php'] : null; $this->min_php = $this->data['min_php'] ?? null;
if (isset($this->data['assets'])) { if (isset($this->data['assets'])) {
foreach ((array)$this->data['assets'] as $slug => $data) { foreach ((array)$this->data['assets'] as $slug => $data) {
+17 -240
View File
@@ -9,24 +9,20 @@
namespace Grav\Common\Helpers; namespace Grav\Common\Helpers;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Uri; use Grav\Common\Page\Markdown\Excerpts as ExcerptsObject;
use Grav\Common\Page\Medium\Medium; use Grav\Common\Page\Medium\Medium;
use Grav\Common\Utils;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Excerpts class Excerpts
{ {
/** /**
* Process Grav image media URL from HTML tag * Process Grav image media URL from HTML tag
* *
* @param string $html HTML tag e.g. `<img src="image.jpg" />` * @param string $html HTML tag e.g. `<img src="image.jpg" />`
* @param PageInterface $page The current page object * @param PageInterface|null $page Page, defaults to the current page object
* @return string Returns final HTML string * @return string Returns final HTML string
*/ */
public static function processImageHtml($html, PageInterface $page) public static function processImageHtml($html, PageInterface $page = null)
{ {
$excerpt = static::getExcerptFromHtml($html, 'img'); $excerpt = static::getExcerptFromHtml($html, 'img');
@@ -112,157 +108,29 @@ class Excerpts
* Process a Link excerpt * Process a Link excerpt
* *
* @param array $excerpt * @param array $excerpt
* @param PageInterface $page * @param PageInterface|null $page Page, defaults to the current page object
* @param string $type * @param string $type
* @return mixed * @return mixed
*/ */
public static function processLinkExcerpt($excerpt, PageInterface $page, $type = 'link') public static function processLinkExcerpt($excerpt, PageInterface $page = null, $type = 'link')
{ {
$url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href'])); $excerpts = new ExcerptsObject($page);
$url_parts = static::parseUrl($url); return $excerpts->processLinkExcerpt($excerpt, $type);
// If there is a query, then parse it and build action calls.
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
$carry[$parts[0]] = $value;
return $carry;
}, []);
// Valid attributes supported.
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
unset($actions['noprocess']);
} else {
// Loop through actions for the image and call them.
foreach ($actions as $attrib => $value) {
$key = $attrib;
if (in_array($attrib, $valid_attributes, true)) {
// support both class and classes.
if ($attrib === 'classes') {
$attrib = 'class';
}
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
unset($actions[$key]);
}
}
}
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
}
// If no query elements left, unset query.
if (empty($url_parts['query'])) {
unset ($url_parts['query']);
}
// Set path to / if not set.
if (empty($url_parts['path'])) {
$url_parts['path'] = '';
}
// If scheme isn't http(s)..
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
// Handle custom streams.
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
$url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
unset($url_parts['stream'], $url_parts['scheme']);
}
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
// Handle paths and such.
$url_parts = Uri::convertUrl($page, $url_parts, $type);
// Build the URL from the component parts and set it on the element.
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
} }
/** /**
* Process an image excerpt * Process an image excerpt
* *
* @param array $excerpt * @param array $excerpt
* @param PageInterface $page * @param PageInterface|null $page Page, defaults to the current page object
* @return array * @return array
*/ */
public static function processImageExcerpt(array $excerpt, PageInterface $page) public static function processImageExcerpt(array $excerpt, PageInterface $page = null)
{ {
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src'])); $excerpts = new ExcerptsObject($page);
$url_parts = static::parseUrl($url);
$media = null; return $excerpts->processImageExcerpt($excerpt);
$filename = null;
if (!empty($url_parts['stream'])) {
$filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? '');
$media = $page->getMedia();
} else {
$grav = Grav::instance();
// File is also local if scheme is http(s) and host matches.
$local_file = isset($url_parts['path'])
&& (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
&& (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host());
if ($local_file) {
$filename = basename($url_parts['path']);
$folder = dirname($url_parts['path']);
// Get the local path to page media if possible.
if ($folder === $page->url(false, false, false)) {
// Get the media objects for this page.
$media = $page->getMedia();
} else {
// see if this is an external page to this one
$base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
/** @var PageInterface $ext_page */
$ext_page = $grav['pages']->dispatch($page_route, true);
if ($ext_page) {
$media = $ext_page->getMedia();
} else {
$grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
}
}
}
}
// If there is a media file that matches the path referenced..
if ($media && $filename && isset($media[$filename])) {
// Get the medium object.
/** @var Medium $medium */
$medium = $media[$filename];
// Process operations
$medium = static::processMediaActions($medium, $url_parts);
$element_excerpt = $excerpt['element']['attributes'];
$alt = $element_excerpt['alt'] ?? '';
$title = $element_excerpt['title'] ?? '';
$class = $element_excerpt['class'] ?? '';
$id = $element_excerpt['id'] ?? '';
$excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
} else {
// Not a current page media file, see if it needs converting to relative.
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
}
return $excerpt;
} }
/** /**
@@ -270,104 +138,13 @@ class Excerpts
* *
* @param Medium $medium * @param Medium $medium
* @param string|array $url * @param string|array $url
* @param PageInterface|null $page Page, defaults to the current page object
* @return Medium * @return Medium
*/ */
public static function processMediaActions($medium, $url) public static function processMediaActions($medium, $url, PageInterface $page = null)
{ {
if (!is_array($url)) { $excerpts = new ExcerptsObject($page);
$url_parts = parse_url($url);
} else {
$url_parts = $url;
}
$actions = []; return $excerpts->processMediaActions($medium, $url);
// if there is a query, then parse it and build action calls
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = $parts[1] ?? null;
$carry[] = ['method' => $parts[0], 'params' => $value];
return $carry;
}, []);
}
if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) {
$actions[] = ['method' => 'fixOrientation', 'params' => ''];
}
$defaults = Grav::instance()['config']->get('system.images.defaults');
if (is_array($defaults) && count($defaults)) {
foreach ($defaults as $method => $params) {
$actions[] = [
'method' => $method,
'params' => $params,
];
}
}
// loop through actions for the image and call them
foreach ($actions as $action) {
$matches = [];
if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
$args = [explode(',', $matches[1])];
} else {
$args = explode(',', $action['params']);
}
$medium = call_user_func_array([$medium, $action['method']], $args);
}
if (isset($url_parts['fragment'])) {
$medium->urlHash($url_parts['fragment']);
}
return $medium;
}
/**
* Variation of parse_url() which works also with local streams.
*
* @param string $url
* @return array|bool
*/
protected static function parseUrl($url)
{
$url_parts = Utils::multibyteParseUrl($url);
if (isset($url_parts['scheme'])) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Special handling for the streams.
if ($locator->schemeExists($url_parts['scheme'])) {
if (isset($url_parts['host'])) {
// Merge host and path into a path.
$url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
unset($url_parts['host']);
}
$url_parts['stream'] = true;
}
}
return $url_parts;
}
/**
* @param string $url
* @return bool|string
*/
protected static function resolveStream($url)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($url)) {
return $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
}
return $url;
} }
} }
+1 -1
View File
@@ -234,7 +234,7 @@ class Truncator {
} }
/** /**
* @inheritDoc *
*/ */
public function truncate( public function truncate(
$text, $text,
@@ -20,6 +20,7 @@ class YamlLinter
{ {
$errors = static::lintConfig(); $errors = static::lintConfig();
$errors = $errors + static::lintPages(); $errors = $errors + static::lintPages();
$errors = $errors + static::lintBlueprints();
return $errors; return $errors;
} }
@@ -34,6 +35,18 @@ class YamlLinter
return static::recurseFolder('config://'); return static::recurseFolder('config://');
} }
public static function lintBlueprints()
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$current_theme = Grav::instance()['config']->get('system.pages.theme');
$theme_path = 'themes://' . $current_theme . '/blueprints';
$locator->addPath('blueprints', '', [$theme_path]);
return static::recurseFolder('blueprints://');
}
public static function recurseFolder($path, $extensions = 'md|yaml') public static function recurseFolder($path, $extensions = 'md|yaml')
{ {
$lint_errors = []; $lint_errors = [];
+6 -1
View File
@@ -104,6 +104,11 @@ class Language
public function getAvailable() public function getAvailable()
{ {
$languagesArray = $this->languages; //Make local copy $languagesArray = $this->languages; //Make local copy
$languagesArray = array_map(function($value) {
return preg_quote($value);
}, $languagesArray);
sort($languagesArray); sort($languagesArray);
return implode('|', array_reverse($languagesArray)); return implode('|', array_reverse($languagesArray));
@@ -229,7 +234,7 @@ class Language
} }
/** /**
* Get's a URL prefix based on configuration * Get a URL prefix based on configuration
* *
* @param string|null $lang * @param string|null $lang
* @return string * @return string
+13 -3
View File
@@ -10,6 +10,7 @@
namespace Grav\Common\Markdown; namespace Grav\Common\Markdown;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts;
class Parsedown extends \Parsedown class Parsedown extends \Parsedown
{ {
@@ -18,12 +19,21 @@ class Parsedown extends \Parsedown
/** /**
* Parsedown constructor. * Parsedown constructor.
* *
* @param PageInterface $page * @param Excerpts|null $excerpts
* @param array|null $defaults * @param array|null $defaults
*/ */
public function __construct($page, $defaults) public function __construct($excerpts = null, $defaults = null)
{ {
$this->init($page, $defaults); if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) {
// Deprecated in Grav 1.6.10
if ($defaults) {
$defaults = ['markdown' => $defaults];
}
$excerpts = new Excerpts($excerpts, $defaults);
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
}
$this->init($excerpts, $defaults);
} }
} }
@@ -10,6 +10,7 @@
namespace Grav\Common\Markdown; namespace Grav\Common\Markdown;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts;
class ParsedownExtra extends \ParsedownExtra class ParsedownExtra extends \ParsedownExtra
{ {
@@ -18,14 +19,23 @@ class ParsedownExtra extends \ParsedownExtra
/** /**
* ParsedownExtra constructor. * ParsedownExtra constructor.
* *
* @param PageInterface $page * @param Excerpts|null $excerpts
* @param array|null $defaults * @param array|null $defaults
* @throws \Exception * @throws \Exception
*/ */
public function __construct($page, $defaults) public function __construct($excerpts = null, $defaults = null)
{ {
if (!$excerpts || $excerpts instanceof PageInterface || null !== $defaults) {
// Deprecated in Grav 1.6.10
if ($defaults) {
$defaults = ['markdown' => $defaults];
}
$excerpts = new Excerpts($excerpts, $defaults);
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use new ' . __CLASS__ . '(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
}
parent::__construct(); parent::__construct();
$this->init($page, $defaults); $this->init($excerpts, $defaults);
} }
} }
@@ -9,15 +9,13 @@
namespace Grav\Common\Markdown; namespace Grav\Common\Markdown;
use Grav\Common\Grav; use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Helpers\Excerpts;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use RocketTheme\Toolbox\Event\Event;
trait ParsedownGravTrait trait ParsedownGravTrait
{ {
/** @var PageInterface $page */ /** @var Excerpts */
protected $page; protected $excerpts;
protected $special_chars; protected $special_chars;
protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/'; protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
@@ -28,28 +26,49 @@ trait ParsedownGravTrait
/** /**
* Initialization function to setup key variables needed by the MarkdownGravLinkTrait * Initialization function to setup key variables needed by the MarkdownGravLinkTrait
* *
* @param PageInterface $page * @param PageInterface|Excerpts|null $excerpts
* @param array|null $defaults * @param array|null $defaults
*/ */
protected function init($page, $defaults) protected function init($excerpts = null, $defaults = null)
{ {
$grav = Grav::instance(); if (!$excerpts || $excerpts instanceof PageInterface) {
// Deprecated in Grav 1.6.10
$this->page = $page; if ($defaults) {
$this->BlockTypes['{'] [] = 'TwigTag'; $defaults = ['markdown' => $defaults];
$this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot']; }
$this->excerpts = new Excerpts($excerpts, $defaults);
if ($defaults === null) { user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
$defaults = (array)Grav::instance()['config']->get('system.pages.markdown'); } else {
$this->excerpts = $excerpts;
} }
$this->setBreaksEnabled($defaults['auto_line_breaks']); $this->BlockTypes['{'][] = 'TwigTag';
$this->setUrlsLinked($defaults['auto_url_links']); $this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
$this->setMarkupEscaped($defaults['escape_markup']);
$this->setSpecialChars($defaults['special_chars']);
$grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this, 'page' => $page])); $defaults = $this->excerpts->getConfig();
if (isset($defaults['markdown']['auto_line_breaks'])) {
$this->setBreaksEnabled($defaults['markdown']['auto_line_breaks']);
}
if (isset($defaults['markdown']['auto_url_links'])) {
$this->setUrlsLinked($defaults['markdown']['auto_url_links']);
}
if (isset($defaults['markdown']['escape_markup'])) {
$this->setMarkupEscaped($defaults['markdown']['escape_markup']);
}
if (isset($defaults['markdown']['special_chars'])) {
$this->setSpecialChars($defaults['markdown']['special_chars']);
}
$this->excerpts->fireInitializedEvent($this);
}
/**
* @return Excerpts
*/
public function getExcerpts()
{
return $this->excerpts;
} }
/** /**
@@ -114,7 +133,8 @@ trait ParsedownGravTrait
*/ */
protected function isBlockContinuable($Type) protected function isBlockContinuable($Type)
{ {
$continuable = \in_array($Type, $this->continuable_blocks) || method_exists($this, 'block' . $Type . 'Continue'); $continuable = \in_array($Type, $this->continuable_blocks, true)
|| method_exists($this, 'block' . $Type . 'Continue');
return $continuable; return $continuable;
} }
@@ -128,7 +148,8 @@ trait ParsedownGravTrait
*/ */
protected function isBlockCompletable($Type) protected function isBlockCompletable($Type)
{ {
$completable = \in_array($Type, $this->completable_blocks) || method_exists($this, 'block' . $Type . 'Complete'); $completable = \in_array($Type, $this->completable_blocks, true)
|| method_exists($this, 'block' . $Type . 'Complete');
return $completable; return $completable;
} }
@@ -210,7 +231,7 @@ trait ParsedownGravTrait
// if this is an image process it // if this is an image process it
if (isset($excerpt['element']['attributes']['src'])) { if (isset($excerpt['element']['attributes']['src'])) {
$excerpt = Excerpts::processImageExcerpt($excerpt, $this->page); $excerpt = $this->excerpts->processImageExcerpt($excerpt);
} }
return $excerpt; return $excerpt;
@@ -218,11 +239,7 @@ trait ParsedownGravTrait
protected function inlineLink($excerpt) protected function inlineLink($excerpt)
{ {
if (isset($excerpt['type'])) { $type = $excerpt['type'] ?? 'link';
$type = $excerpt['type'];
} else {
$type = 'link';
}
// do some trickery to get around Parsedown requirement for valid URL if its Twig in there // do some trickery to get around Parsedown requirement for valid URL if its Twig in there
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) { if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
@@ -238,13 +255,15 @@ trait ParsedownGravTrait
// if this is a link // if this is a link
if (isset($excerpt['element']['attributes']['href'])) { if (isset($excerpt['element']['attributes']['href'])) {
$excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type); $excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type);
} }
return $excerpt; return $excerpt;
} }
// For extending this class via plugins /**
* For extending this class via plugins
*/
public function __call($method, $args) public function __call($method, $args)
{ {
if (isset($this->{$method}) === true) { if (isset($this->{$method}) === true) {
@@ -0,0 +1,329 @@
<?php
/**
* @package Grav\Common\Page
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Markdown;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Medium\Link;
use Grav\Common\Uri;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Utils;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Excerpts
{
/** @var PageInterface */
protected $page;
/** @var array */
protected $config;
public function __construct(PageInterface $page = null, array $config = null)
{
$this->page = $page ?? Grav::instance()['page'] ?? null;
// Add defaults to the configuration.
if (null === $config || !isset($config['markdown'], $config['images'])) {
$c = Grav::instance()['config'];
$config = $config ?? [];
$config += [
'markdown' => $c->get('system.pages.markdown', []),
'images' => $c->get('system.images', [])
];
}
$this->config = $config;
}
public function getPage(): PageInterface
{
return $this->page;
}
public function getConfig(): array
{
return $this->config;
}
public function fireInitializedEvent($markdown): void
{
$grav = Grav::instance();
$grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page]));
}
/**
* Process a Link excerpt
*
* @param array $excerpt
* @param string $type
* @return array
*/
public function processLinkExcerpt(array $excerpt, string $type = 'link'): array
{
$url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
$url_parts = $this->parseUrl($url);
// If there is a query, then parse it and build action calls.
if (isset($url_parts['query'])) {
$actions = array_reduce(
explode('&', $url_parts['query']),
static function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
$carry[$parts[0]] = $value;
return $carry;
},
[]
);
// Valid attributes supported.
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
unset($actions['noprocess']);
} else {
// Loop through actions for the image and call them.
foreach ($actions as $attrib => $value) {
$key = $attrib;
if (in_array($attrib, $valid_attributes, true)) {
// support both class and classes.
if ($attrib === 'classes') {
$attrib = 'class';
}
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
unset($actions[$key]);
}
}
}
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
}
// If no query elements left, unset query.
if (empty($url_parts['query'])) {
unset ($url_parts['query']);
}
// Set path to / if not set.
if (empty($url_parts['path'])) {
$url_parts['path'] = '';
}
// If scheme isn't http(s)..
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
// Handle custom streams.
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
$grav = Grav::instance();
$url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
unset($url_parts['stream'], $url_parts['scheme']);
}
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
// Handle paths and such.
$url_parts = Uri::convertUrl($this->page, $url_parts, $type);
// Build the URL from the component parts and set it on the element.
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
/**
* Process an image excerpt
*
* @param array $excerpt
* @return array
*/
public function processImageExcerpt(array $excerpt): array
{
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
$url_parts = $this->parseUrl($url);
$media = null;
$filename = null;
if (!empty($url_parts['stream'])) {
$filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? '');
$media = $this->page->getMedia();
} else {
$grav = Grav::instance();
// File is also local if scheme is http(s) and host matches.
$local_file = isset($url_parts['path'])
&& (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
&& (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host());
if ($local_file) {
$filename = basename($url_parts['path']);
$folder = dirname($url_parts['path']);
// Get the local path to page media if possible.
if ($this->page && $folder === $this->page->url(false, false, false)) {
// Get the media objects for this page.
$media = $this->page->getMedia();
} else {
// see if this is an external page to this one
$base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
/** @var PageInterface $ext_page */
$ext_page = $grav['pages']->dispatch($page_route, true);
if ($ext_page) {
$media = $ext_page->getMedia();
} else {
$grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
}
}
}
}
// If there is a media file that matches the path referenced..
if ($media && $filename && isset($media[$filename])) {
// Get the medium object.
/** @var Medium $medium */
$medium = $media[$filename];
// Process operations
$medium = $this->processMediaActions($medium, $url_parts);
$element_excerpt = $excerpt['element']['attributes'];
$alt = $element_excerpt['alt'] ?? '';
$title = $element_excerpt['title'] ?? '';
$class = $element_excerpt['class'] ?? '';
$id = $element_excerpt['id'] ?? '';
$excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
} else {
// Not a current page media file, see if it needs converting to relative.
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
}
return $excerpt;
}
/**
* Process media actions
*
* @param Medium $medium
* @param string|array $url
* @return Medium|Link
*/
public function processMediaActions($medium, $url)
{
$url_parts = is_string($url) ? $this->parseUrl($url) : $url;
$actions = [];
// if there is a query, then parse it and build action calls
if (isset($url_parts['query'])) {
$actions = array_reduce(
explode('&', $url_parts['query']),
static function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = $parts[1] ?? null;
$carry[] = ['method' => $parts[0], 'params' => $value];
return $carry;
},
[]
);
}
$config = $this->getConfig();
if (!empty($config['images']['auto_fix_orientation'])) {
$actions[] = ['method' => 'fixOrientation', 'params' => ''];
}
$defaults = $config['images']['defaults'] ?? [];
if (count($defaults)) {
foreach ($defaults as $method => $params) {
$actions[] = [
'method' => $method,
'params' => $params,
];
}
}
// loop through actions for the image and call them
foreach ($actions as $action) {
$matches = [];
if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
$args = [explode(',', $matches[1])];
} else {
$args = explode(',', $action['params']);
}
$medium = call_user_func_array([$medium, $action['method']], $args);
}
if (isset($url_parts['fragment'])) {
$medium->urlHash($url_parts['fragment']);
}
return $medium;
}
/**
* Variation of parse_url() which works also with local streams.
*
* @param string $url
* @return array|bool
*/
protected function parseUrl(string $url)
{
$url_parts = Utils::multibyteParseUrl($url);
if (isset($url_parts['scheme'])) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Special handling for the streams.
if ($locator->schemeExists($url_parts['scheme'])) {
if (isset($url_parts['host'])) {
// Merge host and path into a path.
$url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
unset($url_parts['host']);
}
$url_parts['stream'] = true;
}
}
return $url_parts;
}
/**
* @param string $url
* @return bool|string
*/
protected function resolveStream(string $url)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($url)) {
return $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
}
return $url;
}
}
@@ -154,6 +154,9 @@ abstract class AbstractMedia implements ExportInterface, MediaCollectionInterfac
*/ */
public function add($name, $file) public function add($name, $file)
{ {
if (!$file) {
return;
}
$this->offsetSet($name, $file); $this->offsetSet($name, $file);
switch ($file->type) { switch ($file->type) {
case 'image': case 'image':
@@ -170,8 +170,7 @@ class ImageMedium extends Medium
{ {
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator']; $locator = Grav::instance()['locator'];
$image_path = $locator->findResource('cache://images', true); $image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
$image_dir = $locator->findResource('cache://images', false);
$saved_image_path = $this->saveImage(); $saved_image_path = $this->saveImage();
$output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path); $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
@@ -181,6 +180,7 @@ class ImageMedium extends Medium
} }
if (Utils::startsWith($output, $image_path)) { if (Utils::startsWith($output, $image_path)) {
$image_dir = $locator->findResource('cache://images', false);
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output); $output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
} }
@@ -232,7 +232,7 @@ class ImageMedium extends Medium
} }
/** /**
* Allows the ability to override the Inmage's Pretty name stored in cache * Allows the ability to override the image's pretty name stored in cache
* *
* @param string $name * @param string $name
*/ */
@@ -10,6 +10,7 @@
namespace Grav\Common\Page\Medium; namespace Grav\Common\Page\Medium;
use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\Parsedown;
use Grav\Common\Page\Markdown\Excerpts;
trait ParsedownHtmlTrait trait ParsedownHtmlTrait
{ {
@@ -33,7 +34,7 @@ trait ParsedownHtmlTrait
$element = $this->parsedownElement($title, $alt, $class, $id, $reset); $element = $this->parsedownElement($title, $alt, $class, $id, $reset);
if (!$this->parsedown) { if (!$this->parsedown) {
$this->parsedown = new Parsedown(null, null); $this->parsedown = new Parsedown(new Excerpts());
} }
return $this->parsedown->elementToHtml($element); return $this->parsedown->elementToHtml($element);
+22 -14
View File
@@ -19,6 +19,7 @@ use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra; use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Media\Traits\MediaTrait; use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Taxonomy; use Grav\Common\Taxonomy;
use Grav\Common\Uri; use Grav\Common\Uri;
use Grav\Common\Utils; use Grav\Common\Utils;
@@ -27,7 +28,6 @@ use Negotiation\Accept;
use Negotiation\Negotiator; use Negotiation\Negotiator;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile; use RocketTheme\Toolbox\File\MarkdownFile;
use Symfony\Component\Yaml\Exception\ParseException;
define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u'); define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
@@ -819,23 +819,31 @@ class Page implements PageInterface
/** @var Config $config */ /** @var Config $config */
$config = Grav::instance()['config']; $config = Grav::instance()['config'];
$defaults = (array)$config->get('system.pages.markdown'); $markdownDefaults = (array)$config->get('system.pages.markdown');
if (isset($this->header()->markdown)) { if (isset($this->header()->markdown)) {
$defaults = array_merge($defaults, $this->header()->markdown); $markdownDefaults = array_merge($markdownDefaults, $this->header()->markdown);
} }
// pages.markdown_extra is deprecated, but still check it... // pages.markdown_extra is deprecated, but still check it...
if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) { if (!isset($markdownDefaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) {
user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED); user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED);
$defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra'); $markdownDefaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra');
} }
$extra = $markdownDefaults['extra'] ?? false;
$defaults = [
'markdown' => $markdownDefaults,
'images' => $config->get('system.images', [])
];
$excerpts = new Excerpts($this, $defaults);
// Initialize the preferred variant of Parsedown // Initialize the preferred variant of Parsedown
if ($defaults['extra']) { if ($extra) {
$parsedown = new ParsedownExtra($this, $defaults); $parsedown = new ParsedownExtra($excerpts);
} else { } else {
$parsedown = new Parsedown($this, $defaults); $parsedown = new Parsedown($excerpts);
} }
$this->content = $parsedown->text($this->content); $this->content = $parsedown->text($this->content);
@@ -1397,12 +1405,12 @@ class Page implements PageInterface
return $this->template_format; return $this->template_format;
} }
// Use content negotitation via the `accept:` header // Use content negotiation via the `accept:` header
$http_accept = $_SERVER['HTTP_ACCEPT'] ?? false; $http_accept = $_SERVER['HTTP_ACCEPT'] ?? null;
if (is_string($http_accept)) { if (is_string($http_accept)) {
$negotiator = new Negotiator(); $negotiator = new Negotiator();
$supported_types = Grav::instance()['config']->get('system.pages.types', ['html', 'json']); $supported_types = Utils::getSupportPageTypes(['html', 'json']);
$priorities = Utils::getMimeTypes($supported_types); $priorities = Utils::getMimeTypes($supported_types);
$media_type = $negotiator->getBest($http_accept, $priorities); $media_type = $negotiator->getBest($http_accept, $priorities);
@@ -2847,9 +2855,9 @@ class Page implements PageInterface
$result = []; $result = [];
foreach ((array)$value as $key => $val) { foreach ((array)$value as $key => $val) {
if (is_int($key)) { if (is_int($key)) {
$result = $result + $this->evaluate($val)->toArray(); $result = $result + $this->evaluate($val, $only_published)->toArray();
} else { } else {
$result = $result + $this->evaluate([$key => $val])->toArray(); $result = $result + $this->evaluate([$key => $val], $only_published)->toArray();
} }
} }
@@ -2930,7 +2938,7 @@ class Page implements PageInterface
case 'page': case 'page':
case 'self': case 'self':
$results = new Collection(); $results = new Collection();
$results = $results->addPage($page)->nonModular(); $results = $results->addPage($page);
break; break;
case 'descendants': case 'descendants':
+21 -3
View File
@@ -88,6 +88,13 @@ class Pages
*/ */
protected $ignore_hidden; protected $ignore_hidden;
/** @var string */
protected $check_method;
protected $pages_cache_id;
protected $initialized = false;
/** /**
* @var Types * @var Types
*/ */
@@ -98,8 +105,6 @@ class Pages
*/ */
static protected $home_route; static protected $home_route;
protected $pages_cache_id;
/** /**
* Constructor * Constructor
* *
@@ -226,11 +231,20 @@ class Pages
return $this->baseUrl($lang, $absolute) . Uri::filterPath($route); return $this->baseUrl($lang, $absolute) . Uri::filterPath($route);
} }
public function setCheckMethod($method)
{
$this->check_method = strtolower($method);
}
/** /**
* Class initialization. Must be called before using this class. * Class initialization. Must be called before using this class.
*/ */
public function init() public function init()
{ {
if ($this->initialized) {
return;
}
$config = $this->grav['config']; $config = $this->grav['config'];
$this->ignore_files = $config->get('system.pages.ignore_files'); $this->ignore_files = $config->get('system.pages.ignore_files');
$this->ignore_folders = $config->get('system.pages.ignore_folders'); $this->ignore_folders = $config->get('system.pages.ignore_folders');
@@ -240,6 +254,10 @@ class Pages
$this->children = []; $this->children = [];
$this->routes = []; $this->routes = [];
if (!$this->check_method) {
$this->setCheckMethod($config->get('system.cache.check.method', 'file'));
}
$this->buildPages(); $this->buildPages();
} }
@@ -947,7 +965,7 @@ class Pages
$taxonomy = $this->grav['taxonomy']; $taxonomy = $this->grav['taxonomy'];
// how should we check for last modified? Default is by file // how should we check for last modified? Default is by file
switch (strtolower($config->get('system.cache.check.method', 'file'))) { switch ($this->check_method) {
case 'none': case 'none':
case 'off': case 'off':
$hash = 0; $hash = 0;
+19 -2
View File
@@ -151,15 +151,32 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
if (\is_string($params)) { if (\is_string($params)) {
$dispatcher->addListener($eventName, [$this, $params]); $dispatcher->addListener($eventName, [$this, $params]);
} elseif (\is_string($params[0])) { } elseif (\is_string($params[0])) {
$dispatcher->addListener($eventName, [$this, $params[0]], $params[1] ?? 0); $dispatcher->addListener($eventName, [$this, $params[0]], $this->getPriority($params, $eventName));
} else { } else {
foreach ($params as $listener) { foreach ($params as $listener) {
$dispatcher->addListener($eventName, [$this, $listener[0]], $listener[1] ?? 0); $dispatcher->addListener($eventName, [$this, $listener[0]], $this->getPriority($listener, $eventName));
} }
} }
} }
} }
/**
* @param array $params
* @param string $eventName
*/
private function getPriority($params, $eventName)
{
$grav = Grav::instance();
$override = implode('.', ["priorities", $this->name, $eventName, $params[0]]);
if ($grav['config']->get($override) !== null)
{
return $grav['config']->get($override);
} elseif (isset($params[1])) {
return $params[1];
}
return 0;
}
/** /**
* @param array $events * @param array $events
*/ */
+34 -14
View File
@@ -133,12 +133,25 @@ class Plugins extends Iterator
*/ */
public static function all() public static function all()
{ {
$plugins = Grav::instance()['plugins']; $grav = Grav::instance();
$plugins = $grav['plugins'];
$list = []; $list = [];
foreach ($plugins as $instance) { foreach ($plugins as $instance) {
$name = $instance->name; $name = $instance->name;
$result = self::get($name);
try {
$result = self::get($name);
} catch (\Exception $e) {
$exception = new \RuntimeException(sprintf('Plugin %s: %s', $name, $e->getMessage()), $e->getCode(), $e);
/** @var Debugger $debugger */
$debugger = $grav['debugger'];
$debugger->addMessage("Plugin {$name} cannot be loaded, please check Exceptions tab", 'error');
$debugger->addException($exception);
continue;
}
if ($result) { if ($result) {
$list[$name] = $result; $list[$name] = $result;
@@ -185,24 +198,31 @@ class Plugins extends Iterator
$grav = Grav::instance(); $grav = Grav::instance();
$locator = $grav['locator']; $locator = $grav['locator'];
$filePath = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT); $file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
if (!is_file($filePath)) {
if (is_file($file)) {
// Local variables available in the file: $grav, $config, $name, $file
$class = include_once $file;
$pluginClassFormat = [
'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
'Grav\\Plugin\\' . Inflector::camelize($name) . 'Plugin'
];
foreach ($pluginClassFormat as $pluginClass) {
if (class_exists($pluginClass)) {
$class = new $pluginClass($name, $grav);
break;
}
}
} else {
$grav['log']->addWarning( $grav['log']->addWarning(
sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name) sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name)
); );
return null; return null;
} }
require_once $filePath; return $class;
$pluginClassName = 'Grav\\Plugin\\' . ucfirst($name) . 'Plugin';
if (!class_exists($pluginClassName)) {
$pluginClassName = 'Grav\\Plugin\\' . $grav['inflector']->camelize($name) . 'Plugin';
if (!class_exists($pluginClassName)) {
throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.", $name));
}
}
return new $pluginClassName($name, $grav);
} }
} }
@@ -30,10 +30,13 @@ class RequestProcessor extends ProcessorBase
$request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true)); $request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true));
} }
$uri = $request->getUri();
$ext = mb_strtolower(pathinfo($uri->getPath(), PATHINFO_EXTENSION));
$request = $request $request = $request
->withAttribute('grav', $this->container) ->withAttribute('grav', $this->container)
->withAttribute('time', $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME) ->withAttribute('time', $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME)
->withAttribute('route', Uri::getCurrentRoute()) ->withAttribute('route', Uri::getCurrentRoute()->withExtension($ext))
->withAttribute('referrer', $this->container['uri']->referrer()); ->withAttribute('referrer', $this->container['uri']->referrer());
$event = new RequestHandlerEvent(['request' => $request, 'handler' => $handler]); $event = new RequestHandlerEvent(['request' => $request, 'handler' => $handler]);
@@ -27,9 +27,10 @@ class AccountsServiceProvider implements ServiceProviderInterface
public function register(Container $container) public function register(Container $container)
{ {
$container['accounts'] = function (Container $container) { $container['accounts'] = function (Container $container) {
/** @var Debugger $debugger */ $type = strtolower(defined('GRAV_USER_INSTANCE') ? GRAV_USER_INSTANCE : $container['config']->get('system.accounts.type', 'data'));
$debugger = $container['debugger']; if ($type === 'flex') {
if ($container['config']->get('system.accounts.type') === 'flex') { /** @var Debugger $debugger */
$debugger = $container['debugger'];
$debugger->addMessage('User Accounts: Flex Directory'); $debugger->addMessage('User Accounts: Flex Directory');
return $this->flexAccounts($container); return $this->flexAccounts($container);
} }
@@ -46,7 +47,9 @@ class AccountsServiceProvider implements ServiceProviderInterface
protected function dataAccounts(Container $container) protected function dataAccounts(Container $container)
{ {
define('GRAV_USER_INSTANCE', 'DATA'); if (!defined('GRAV_USER_INSTANCE')) {
define('GRAV_USER_INSTANCE', 'DATA');
}
// Use User class for backwards compatibility. // Use User class for backwards compatibility.
return new DataUser\UserCollection(User::class); return new DataUser\UserCollection(User::class);
@@ -54,7 +57,9 @@ class AccountsServiceProvider implements ServiceProviderInterface
protected function flexAccounts(Container $container) protected function flexAccounts(Container $container)
{ {
define('GRAV_USER_INSTANCE', 'FLEX'); if (!defined('GRAV_USER_INSTANCE')) {
define('GRAV_USER_INSTANCE', 'FLEX');
}
/** @var Config $config */ /** @var Config $config */
$config = $container['config']; $config = $container['config'];
@@ -31,8 +31,8 @@ class RequestServiceProvider implements ServiceProviderInterface
return $creator->fromGlobals(); return $creator->fromGlobals();
}; };
$container['route'] = function() { $container['route'] = $container->factory(function() {
return Uri::getCurrentRoute(); return clone Uri::getCurrentRoute();
}; });
} }
} }
@@ -13,6 +13,7 @@ use Grav\Common\Config\Config;
use Grav\Common\Debugger; use Grav\Common\Debugger;
use Grav\Common\Session; use Grav\Common\Session;
use Grav\Common\Uri; use Grav\Common\Uri;
use Grav\Common\Utils;
use Pimple\Container; use Pimple\Container;
use Pimple\ServiceProviderInterface; use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\Session\Message; use RocketTheme\Toolbox\Session\Message;
@@ -49,14 +50,17 @@ class SessionServiceProvider implements ServiceProviderInterface
// Activate admin if we're inside the admin path. // Activate admin if we're inside the admin path.
$is_admin = false; $is_admin = false;
if ($config->get('plugins.admin.enabled')) { if ($config->get('plugins.admin.enabled')) {
$base = '/' . trim($config->get('plugins.admin.route'), '/'); $admin_base = '/' . trim($config->get('plugins.admin.route'), '/');
// Uri::route() is not processed yet, let's quickly get what we need. // Uri::route() is not processed yet, let's quickly get what we need.
$current_route = str_replace(Uri::filterPath($uri->rootUrl(false)), '', parse_url($uri->url(true), PHP_URL_PATH)); $current_route = str_replace(Uri::filterPath($uri->rootUrl(false)), '', parse_url($uri->url(true), PHP_URL_PATH));
// Test to see if path starts with a supported language + admin base
$lang = Utils::pathPrefixedByLangCode($current_route);
$lang_admin_base = '/' . $lang . $admin_base;
// Check no language, simple language prefix (en) and region specific language prefix (en-US). // Check no language, simple language prefix (en) and region specific language prefix (en-US).
$pos = strpos($current_route, $base); if (Utils::startsWith($current_route, $admin_base) || Utils::startsWith($current_route, $lang_admin_base)) {
if ($pos === 0 || $pos === 3 || $pos === 6) {
$cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800); $cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800);
$enabled = $is_admin = true; $enabled = $is_admin = true;
} }
+14 -3
View File
@@ -100,7 +100,19 @@ class Themes extends Iterator
} }
$theme = $directory->getFilename(); $theme = $directory->getFilename();
$result = $this->get($theme);
try {
$result = $this->get($theme);
} catch (\Exception $e) {
$exception = new \RuntimeException(sprintf('Theme %s: %s', $theme, $e->getMessage()), $e->getCode(), $e);
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage("Theme {$theme} cannot be loaded, please check Exceptions tab", 'error');
$debugger->addException($exception);
continue;
}
if ($result) { if ($result) {
$list[$theme] = $result; $list[$theme] = $result;
@@ -196,8 +208,7 @@ class Themes extends Iterator
foreach ($themeClassFormat as $themeClass) { foreach ($themeClassFormat as $themeClass) {
if (class_exists($themeClass)) { if (class_exists($themeClass)) {
$themeClassName = $themeClass; $class = new $themeClass($grav, $config, $name);
$class = new $themeClassName($grav, $config, $name);
break; break;
} }
} }
@@ -41,6 +41,6 @@ class TwigNodeMarkdown extends Node implements NodeOutputInterface
->write('$lines = explode("\n", $content);' . PHP_EOL) ->write('$lines = explode("\n", $content);' . PHP_EOL)
->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL) ->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL)
->write('$content = join("\n", $content);' . PHP_EOL) ->write('$content = join("\n", $content);' . PHP_EOL)
->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($content);' . PHP_EOL); ->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($context, $content);' . PHP_EOL);
} }
} }
+11 -9
View File
@@ -84,7 +84,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']), new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']),
new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']), new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']),
new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']), new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['is_safe' => ['html']]), new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]),
new \Twig_SimpleFilter('md5', [$this, 'md5Filter']), new \Twig_SimpleFilter('md5', [$this, 'md5Filter']),
new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']), new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']),
new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']), new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']),
@@ -455,7 +455,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
} }
/** /**
* Gets a human readable output for cron sytnax * Gets a human readable output for cron syntax
* *
* @param $at * @param $at
* @return string * @return string
@@ -613,12 +613,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
/** /**
* @param string $string * @param string $string
* *
* @param array $context
* @param bool $block Block or Line processing * @param bool $block Block or Line processing
* @return mixed|string * @return mixed|string
*/ */
public function markdownFunction($string, $block = true) public function markdownFunction($context, $string, $block = true)
{ {
return Utils::processMarkdown($string, $block); $page = $context['page'] ?? null;
return Utils::processMarkdown($string, $block, $page);
} }
/** /**
@@ -1004,10 +1006,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*/ */
public function authorize($action) public function authorize($action)
{ {
/** @var UserInterface $user */ /** @var UserInterface|null $user */
$user = $this->grav['user']; $user = $this->grav['user'] ?? null;
if (!$user->authenticated || (isset($user->authorized) && !$user->authorized)) { if (!$user || !$user->authenticated || (isset($user->authorized) && !$user->authorized)) {
return false; return false;
} }
@@ -1136,7 +1138,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
} }
/** /**
* Get's the Exif data for a file * Get the Exif data for a file
* *
* @param string $image * @param string $image
* @param bool $raw * @param bool $raw
@@ -1154,7 +1156,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
$exif_reader = $this->grav['exif']->getReader(); $exif_reader = $this->grav['exif']->getReader();
if ($image & file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) { if ($image && file_exists($image) && $this->config->get('system.media.auto_metadata_exif') && $exif_reader) {
$exif_data = $exif_reader->read($image); $exif_data = $exif_reader->read($image);
+3 -3
View File
@@ -1154,7 +1154,7 @@ class Uri
$this->scheme = $env['X-FORWARDED-PROTO']; $this->scheme = $env['X-FORWARDED-PROTO'];
} elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) { } elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) {
$this->scheme = $env['HTTP_CLOUDFRONT_FORWARDED_PROTO']; $this->scheme = $env['HTTP_CLOUDFRONT_FORWARDED_PROTO'];
} elseif (isset($env['REQUEST_SCHEME'])) { } elseif (isset($env['REQUEST_SCHEME']) && empty($env['HTTPS'])) {
$this->scheme = $env['REQUEST_SCHEME']; $this->scheme = $env['REQUEST_SCHEME'];
} else { } else {
$https = $env['HTTPS'] ?? ''; $https = $env['HTTPS'] ?? '';
@@ -1286,7 +1286,7 @@ class Uri
} }
/** /**
* Get's post from either $_POST or JSON response object * Get post from either $_POST or JSON response object
* By default returns all data, or can return a single item * By default returns all data, or can return a single item
* *
* @param string $element * @param string $element
@@ -1345,7 +1345,7 @@ class Uri
*/ */
public function isValidExtension($extension) public function isValidExtension($extension)
{ {
$valid_page_types = implode('|', Grav::instance()['config']->get('system.pages.types')); $valid_page_types = implode('|', Utils::getSupportPageTypes());
// Strip the file extension for valid page types // Strip the file extension for valid page types
if (preg_match('/(' . $valid_page_types . ')/', $extension)) { if (preg_match('/(' . $valid_page_types . ')/', $extension)) {
@@ -118,4 +118,13 @@ class UserCollection implements UserCollectionInterface
return $file_path && unlink($file_path); return $file_path && unlink($file_path);
} }
public function count(): int
{
// check for existence of a user account
$account_dir = $file_path = Grav::instance()['locator']->findResource('account://');
$accounts = glob($account_dir . '/*.yaml') ?: [];
return count($accounts);
}
} }
@@ -9,6 +9,7 @@
namespace Grav\Common\User\FlexUser; namespace Grav\Common\User\FlexUser;
use Grav\Common\Data\Blueprint;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface; use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Media; use Grav\Common\Page\Media;
@@ -21,6 +22,7 @@ use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter; use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Flex\FlexDirectory; use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexObject; use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Storage\FileStorage;
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait; use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
use Grav\Framework\Flex\Traits\FlexMediaTrait; use Grav\Framework\Flex\Traits\FlexMediaTrait;
use Grav\Framework\Form\FormFlashFile; use Grav\Framework\Form\FormFlashFile;
@@ -381,6 +383,31 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
return $this->getBlueprint()->extra($this->toArray()); return $this->getBlueprint()->extra($this->toArray());
} }
/**
* @param string $name
* @return Blueprint
*/
public function getBlueprint(string $name = '')
{
$blueprint = clone parent::getBlueprint($name);
$blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
$params = (array)$call['params'];
$method = array_shift($params);
if (method_exists($this, $method)) {
$value = $this->{$method}(...$params);
if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
} else {
$field[$property] = $value;
}
}
});
return $blueprint->init();
}
/** /**
* Return unmodified data as raw string. * Return unmodified data as raw string.
* *
@@ -420,6 +447,15 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
*/ */
public function save() public function save()
{ {
// TODO: We may want to handle this in the storage layer in the future.
$key = $this->getStorageKey();
if (!$key || strpos($key, '@@')) {
$storage = $this->getFlexDirectory()->getStorage();
if ($storage instanceof FileStorage) {
$this->setStorageKey($this->getKey());
}
}
$password = $this->getProperty('password'); $password = $this->getProperty('password');
if (null !== $password) { if (null !== $password) {
$this->unsetProperty('password'); $this->unsetProperty('password');
@@ -431,6 +467,20 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
return parent::save(); return parent::save();
} }
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): bool
{
if (null === $user) {
/** @var UserInterface $user */
$user = Grav::instance()['user'] ?? null;
}
if ($user instanceof User && $user->getStorageKey() === $this->getStorageKey()) {
return true;
}
return parent::isAuthorized($action, $scope, $user);
}
/** /**
* @return array * @return array
*/ */
@@ -9,7 +9,7 @@
namespace Grav\Common\User\Interfaces; namespace Grav\Common\User\Interfaces;
interface UserCollectionInterface interface UserCollectionInterface extends \Countable
{ {
/** /**
* Load user account. * Load user account.
@@ -148,12 +148,13 @@ trait UserTrait
// Try looking for provider. // Try looking for provider.
$provider = $this->get('provider'); $provider = $this->get('provider');
if (\is_array($provider)) { $provider_options = $this->get($provider);
if (isset($provider['avatar_url']) && \is_string($provider['avatar_url'])) { if (\is_array($provider_options)) {
return $provider['avatar_url']; if (isset($provider_options['avatar_url']) && \is_string($provider_options['avatar_url'])) {
return $provider_options['avatar_url'];
} }
if (isset($provider['avatar']) && \is_string($provider['avatar'])) { if (isset($provider_options['avatar']) && \is_string($provider_options['avatar'])) {
return $provider['avatar']; return $provider_options['avatar'];
} }
} }
+154 -47
View File
@@ -13,6 +13,7 @@ use Grav\Common\Helpers\Truncator;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra; use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Markdown\Excerpts;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -27,55 +28,103 @@ abstract class Utils
/** /**
* Simple helper method to make getting a Grav URL easier * Simple helper method to make getting a Grav URL easier
* *
* @param string $input * @param string|object $input
* @param bool $domain * @param bool $domain
* @param bool $fail_gracefully
* @return bool|null|string * @return bool|null|string
*/ */
public static function url($input, $domain = false) public static function url($input, $domain = false, $fail_gracefully = false)
{ {
if (!trim((string)$input)) { if ((!is_string($input) && !method_exists($input, '__toString')) || !trim($input)) {
$input = '/'; if ($fail_gracefully) {
$input = '/';
} else {
return false;
}
} }
if (Grav::instance()['config']->get('system.absolute_urls', false)) { $input = (string)$input;
$domain = true;
}
if (Grav::instance()['uri']->isExternal($input)) { if (Uri::isExternal($input)) {
return $input; return $input;
} }
$grav = Grav::instance();
/** @var Uri $uri */ /** @var Uri $uri */
$uri = Grav::instance()['uri']; $uri = $grav['uri'];
$root = $uri->rootUrl(); if (static::contains((string)$input, '://')) {
$input = Utils::replaceFirstOccurrence($root, '', $input);
$input = ltrim((string)$input, '/');
if (Utils::contains((string)$input, '://')) {
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator']; $locator = $grav['locator'];
$parts = Uri::parseUrl($input); $parts = Uri::parseUrl($input);
if ($parts) { if (is_array($parts)) {
$resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false); // Make sure we always have scheme, host, port and path.
$scheme = $parts['scheme'] ?? '';
$host = $parts['host'] ?? '';
$port = $parts['port'] ?? '';
$path = $parts['path'] ?? '';
if (isset($parts['query'])) { if ($scheme && !$port) {
$resource = $resource . '?' . $parts['query']; // If URL has a scheme, we need to check if it's one of Grav streams.
if (!$locator->schemeExists($scheme)) {
// If scheme does not exists as a stream, assume it's external.
return str_replace(' ', '%20', $input);
}
// Attempt to find the resource (because of parse_url() we need to put host back to path).
$resource = $locator->findResource("{$scheme}://{$host}{$path}", false);
if ($resource === false) {
if (!$fail_gracefully) {
return false;
}
// Return location where the file would be if it was saved.
$resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true);
}
} elseif ($host || $port) {
// If URL doesn't have scheme but has host or port, it is external.
return str_replace(' ', '%20', $input);
} }
if (!empty($resource)) {
// Add query string back.
if (isset($parts['query'])) {
$resource .= '?' . $parts['query'];
}
// Add fragment back.
if (isset($parts['fragment'])) {
$resource .= '#' . $parts['fragment'];
}
}
} else { } else {
// Not a valid URL (can still be a stream). // Not a valid URL (can still be a stream).
$resource = $locator->findResource($input, false); $resource = $locator->findResource($input, false);
} }
} else { } else {
$root = $uri->rootUrl();
if (static::startsWith($input, $root)) {
$input = static::replaceFirstOccurrence($root, '', $input);
}
$input = ltrim($input, '/');
$resource = $input; $resource = $input;
} }
if (!$fail_gracefully && $resource === false) {
return false;
}
$domain = $domain ?: $grav['config']->get('system.absolute_urls', false);
return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? ''); return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? '');
} }
@@ -274,6 +323,35 @@ abstract class Utils
return (object)array_merge((array)$obj1, (array)$obj2); return (object)array_merge((array)$obj1, (array)$obj2);
} }
/**
* Lowercase an entire array. Useful when combined with `in_array()`
*
* @param array $a
* @return array|false
*/
public static function arrayLower(Array $a)
{
return array_map('mb_strtolower', $a);
}
/**
* Simple function to remove item/s in an array by value
*
* @param $search array
* @param $value string|array
* @return array
*/
public static function arrayRemoveValue(Array $search, $value)
{
foreach ((array) $value as $val) {
$key = array_search($val, $search);
if ($key !== false) {
unset($search[$key]);
}
}
return $search;
}
/** /**
* Recursive Merge with uniqueness * Recursive Merge with uniqueness
* *
@@ -974,17 +1052,19 @@ abstract class Utils
* *
* @param string $string The path * @param string $string The path
* *
* @return bool * @return bool|string Either false or the language
*
*/ */
public static function pathPrefixedByLangCode($string) public static function pathPrefixedByLangCode($string)
{ {
if (strlen($string) <= 3) { $languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
return false; $parts = explode('/', trim($string, '/'));
if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) {
return $parts[0];
} }
$languages_enabled = Grav::instance()['config']->get('system.languages.supported', []); return false;
return $string[0] === '/' && $string[3] === '/' && \in_array(substr($string, 1, 2), $languages_enabled, true);
} }
/** /**
@@ -1054,12 +1134,9 @@ abstract class Utils
*/ */
private static function generateNonceString($action, $previousTick = false) private static function generateNonceString($action, $previousTick = false)
{ {
$username = ''; $grav = Grav::instance();
if (isset(Grav::instance()['user'])) {
$user = Grav::instance()['user'];
$username = $user->username;
}
$username = isset($grav['user']) ? $grav['user']->username : '';
$token = session_id(); $token = session_id();
$i = self::nonceTick(); $i = self::nonceTick();
@@ -1067,7 +1144,7 @@ abstract class Utils
$i--; $i--;
} }
return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt')); return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . $grav['config']->get('security.salt'));
} }
/** /**
@@ -1281,7 +1358,7 @@ abstract class Utils
} }
/** /**
* Get's path based on a token * Get path based on a token
* *
* @param string $path * @param string $path
* @param PageInterface|null $page * @param PageInterface|null $page
@@ -1341,6 +1418,8 @@ abstract class Utils
$post_max_size = static::parseSize(ini_get('post_max_size')); $post_max_size = static::parseSize(ini_get('post_max_size'));
if ($post_max_size > 0) { if ($post_max_size > 0) {
$max_size = $post_max_size; $max_size = $post_max_size;
} else {
$max_size = 0;
} }
$upload_max = static::parseSize(ini_get('upload_max_filesize')); $upload_max = static::parseSize(ini_get('upload_max_filesize'));
@@ -1388,7 +1467,7 @@ abstract class Utils
$pow = min($pow, count($units) - 1); $pow = min($pow, count($units) - 1);
// Uncomment one of the following alternatives // Uncomment one of the following alternatives
$bytes /= pow(1024, $pow); $bytes /= 1024 ** $pow;
// $bytes /= (1 << (10 * $pow)); // $bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow]; return round($bytes, $precision) . ' ' . $units[$pow];
@@ -1404,11 +1483,12 @@ abstract class Utils
{ {
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size); $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
$size = preg_replace('/[^0-9\.]/', '', $size); $size = preg_replace('/[^0-9\.]/', '', $size);
if ($unit) { if ($unit) {
return round($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); $size = $size * pow(1024, stripos('bkmgtpezy', $unit[0]));
} else {
return round($size);
} }
return (int) abs(round($size));
} }
/** /**
@@ -1446,19 +1526,28 @@ abstract class Utils
* *
* @param string $string * @param string $string
* *
* @param bool $block Block or Line processing * @param bool $block Block or Line processing
* @param null $page
* @return string * @return string
* @throws \Exception
*/ */
public static function processMarkdown($string, $block = true) public static function processMarkdown($string, $block = true, $page = null)
{ {
$page = Grav::instance()['page'] ?? null; $grav = Grav::instance();
$defaults = Grav::instance()['config']->get('system.pages.markdown'); $page = $page ?? $grav['page'] ?? null;
$defaults = [
'markdown' => $grav['config']->get('system.pages.markdown', []),
'images' => $grav['config']->get('system.images', [])
];
$extra = $defaults['markdown']['extra'] ?? false;
$excerpts = new Excerpts($page, $defaults);
// Initialize the preferred variant of Parsedown // Initialize the preferred variant of Parsedown
if ($defaults['extra']) { if ($extra) {
$parsedown = new ParsedownExtra($page, $defaults); $parsedown = new ParsedownExtra($excerpts);
} else { } else {
$parsedown = new Parsedown($page, $defaults); $parsedown = new Parsedown($excerpts);
} }
if ($block) { if ($block) {
@@ -1477,12 +1566,11 @@ abstract class Utils
* @param int $prefix * @param int $prefix
* *
* @return string * @return string
* @throws \InvalidArgumentException if provided an invalid IP
*/ */
public static function getSubnet($ip, $prefix = 64) public static function getSubnet($ip, $prefix = 64)
{ {
if (!filter_var($ip, FILTER_VALIDATE_IP)) { if (!filter_var($ip, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException('Invalid IP: ' . $ip); return $ip;
} }
// Packed representation of IP // Packed representation of IP
@@ -1509,4 +1597,23 @@ abstract class Utils
return $subnet; return $subnet;
} }
/**
* Wrapper to ensure html, htm in the front of the supported page types
*
* @param array|null $defaults
* @return array|mixed
*/
public static function getSupportPageTypes(array $defaults = null)
{
$types = Grav::instance()['config']->get('system.pages.types', $defaults);
// remove html/htm
$types = static::arrayRemoveValue($types, ['html', 'htm']);
// put them back at the front
$types = array_merge(['html', 'htm'], $types);
return $types;
}
} }
@@ -21,6 +21,7 @@ class ClearCacheCommand extends ConsoleCommand
->setName('cache') ->setName('cache')
->setAliases(['clearcache', 'cache-clear']) ->setAliases(['clearcache', 'cache-clear'])
->setDescription('Clears Grav cache') ->setDescription('Clears Grav cache')
->addOption('invalidate', null, InputOption::VALUE_NONE, 'Invalidate cache, but do not remove any files')
->addOption('purge', null, InputOption::VALUE_NONE, 'If set purge old caches') ->addOption('purge', null, InputOption::VALUE_NONE, 'If set purge old caches')
->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches') ->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches')
->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*') ->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*')
@@ -64,6 +65,8 @@ class ClearCacheCommand extends ConsoleCommand
$remove = 'cache-only'; $remove = 'cache-only';
} elseif ($this->input->getOption('tmp-only')) { } elseif ($this->input->getOption('tmp-only')) {
$remove = 'tmp-only'; $remove = 'tmp-only';
} elseif ($this->input->getOption('invalidate')) {
$remove = 'invalidate';
} else { } else {
$remove = 'standard'; $remove = 'standard';
} }
@@ -61,6 +61,15 @@ class YamlLinterCommand extends ConsoleCommand
$this->displayErrors($errors, $io); $this->displayErrors($errors, $io);
} }
$io->section('Page Blueprints');
$errors = YamlLinter::lintBlueprints();
if (empty($errors)) {
$io->success('No YAML Linting issues with blueprints');
} else {
$this->displayErrors($errors, $io);
}
} }
protected function displayErrors($errors, $io) protected function displayErrors($errors, $io)
+1 -1
View File
@@ -114,7 +114,7 @@ class InfoCommand extends ConsoleCommand
if ($info === 'date') { if ($info === 'date') {
$name = 'Last Update'; $name = 'Last Update';
$data = date('D, j M Y, H:i:s, P ', strtotime('2014-09-16T00:07:16Z')); $data = date('D, j M Y, H:i:s, P ', strtotime($data));
} }
$name = str_pad($name, 12); $name = str_pad($name, 12);
@@ -372,7 +372,7 @@ class InstallCommand extends ConsoleCommand
$helper = $this->getHelper('question'); $helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false); $question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question); $answer = $helper->ask($this->input, $this->output, $question);
if (!$answer) { if (!$answer) {
$this->output->writeln(" '- <red>Skipped!</red> "); $this->output->writeln(" '- <red>Skipped!</red> ");
@@ -437,7 +437,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
} }
/** /**
* Implementes JsonSerializable interface. * Implements JsonSerializable interface.
* *
* @return array * @return array
*/ */
@@ -84,7 +84,7 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
} }
/** /**
* Implementes JsonSerializable interface. * Implements JsonSerializable interface.
* *
* @return array * @return array
*/ */
@@ -53,11 +53,11 @@ class CsvFormatter extends AbstractFormatter
$header = array_keys(reset($data)); $header = array_keys(reset($data));
// Encode the field names // Encode the field names
$string = implode($delimiter, $header). "\n"; $string = $this->encodeLine($header, $delimiter);
// Encode the data // Encode the data
foreach ($data as $row) { foreach ($data as $row) {
$string .= implode($delimiter, $row). "\n"; $string .= $this->encodeLine($row, $delimiter);
} }
return $string; return $string;
@@ -87,4 +87,23 @@ class CsvFormatter extends AbstractFormatter
return $list; return $list;
} }
protected function encodeLine(array $line, $delimiter = null): string
{
foreach ($line as $key => &$value) {
$value = $this->escape((string)$value);
}
unset($value);
return implode($delimiter, $line). "\n";
}
protected function escape(string $value)
{
if (preg_match('/[,"\r\n]/u', $value)) {
$value = '"' . preg_replace('/"/', '""', $value) . '"';
}
return $value;
}
} }
@@ -97,12 +97,12 @@ class YamlFormatter extends AbstractFormatter
@ini_set('yaml.decode_php', $saved); @ini_set('yaml.decode_php', $saved);
if ($decoded !== false) { if ($decoded !== false) {
return $decoded; return (array) $decoded;
} }
} }
try { try {
return YamlParser::parse($data); return (array) YamlParser::parse($data);
} catch (ParseException $e) { } catch (ParseException $e) {
if ($this->useCompatibleDecoder()) { if ($this->useCompatibleDecoder()) {
return (array) FallbackYamlParser::parse($data); return (array) FallbackYamlParser::parse($data);
+1 -1
View File
@@ -91,7 +91,7 @@ class Flex implements \Countable
// Return the directories in the given order. // Return the directories in the given order.
$directories = []; $directories = [];
foreach ($types as $type) { foreach ($types as $type) {
$directories = $this->types[$type] ?? null; $directories[$type] = $this->types[$type] ?? null;
} }
return $keepMissing ? $directories : array_filter($directories); return $keepMissing ? $directories : array_filter($directories);
@@ -9,6 +9,7 @@
namespace Grav\Framework\Flex; namespace Grav\Framework\Flex;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Grav\Common\Debugger; use Grav\Common\Debugger;
use Grav\Common\Grav; use Grav\Common\Grav;
@@ -125,7 +126,7 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
/** /**
* @param array $filters * @param array $filters
* @return FlexCollectionInterface * @return FlexCollectionInterface|Collection
*/ */
public function filterBy(array $filters) public function filterBy(array $filters)
{ {
@@ -490,7 +491,12 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
$twig = $grav['twig']; $twig = $grav['twig'];
try { try {
return $twig->twig()->resolveTemplate(["flex-objects/layouts/{$this->getFlexType()}/collection/{$layout}.html.twig"]); return $twig->twig()->resolveTemplate(
[
"flex-objects/layouts/{$this->getFlexType()}/collection/{$layout}.html.twig",
"flex-objects/layouts/_default/collection/{$layout}.html.twig"
]
);
} catch (LoaderError $e) { } catch (LoaderError $e) {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = Grav::instance()['debugger']; $debugger = Grav::instance()['debugger'];
@@ -315,15 +315,16 @@ class FlexDirectory implements FlexAuthorizeInterface
$gravCache = $grav['cache']; $gravCache = $grav['cache'];
$config = $this->getConfig('cache.' . $namespace); $config = $this->getConfig('cache.' . $namespace);
if (empty($config['enabled'])) { if (empty($config['enabled'])) {
throw new \RuntimeException(sprintf('Flex: %s %s cache not enabled', $this->type, $namespace)); $cache = new MemoryCache('flex-objects-' . $this->getFlexType());
} } else {
$timeout = $config['timeout'] ?? 60; $timeout = $config['timeout'] ?? 60;
$key = $gravCache->getKey(); $key = $gravCache->getKey();
if (Utils::isAdminPlugin()) { if (Utils::isAdminPlugin()) {
$key = substr($key, 0, -1); $key = substr($key, 0, -1);
}
$cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout);
} }
$cache = new DoctrineCache($gravCache->getCacheDriver(), 'flex-objects-' . $this->getFlexType() . $key, $timeout);
} catch (\Exception $e) { } catch (\Exception $e) {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = Grav::instance()['debugger']; $debugger = Grav::instance()['debugger'];
@@ -519,7 +520,9 @@ class FlexDirectory implements FlexAuthorizeInterface
// Store updated rows to the cache. // Store updated rows to the cache.
if ($updated) { if ($updated) {
try { try {
$debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug'); if (!$cache instanceof MemoryCache) {
$debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug');
}
$cache->setMultiple($updated); $cache->setMultiple($updated);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$debugger->addException($e); $debugger->addException($e);
@@ -640,7 +643,10 @@ class FlexDirectory implements FlexAuthorizeInterface
/** @var string|FlexIndexInterface $className */ /** @var string|FlexIndexInterface $className */
$className = $this->getIndexClass(); $className = $this->getIndexClass();
$keys = $className::loadEntriesFromStorage($storage); $keys = $className::loadEntriesFromStorage($storage);
$debugger->addMessage(sprintf('Flex: Caching %s index of %d objects', $this->type, \count($keys)), 'debug'); if (!$cache instanceof MemoryCache) {
$debugger->addMessage(sprintf('Flex: Caching %s index of %d objects', $this->type, \count($keys)),
'debug');
}
try { try {
$cache->set('__keys', $keys); $cache->set('__keys', $keys);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
+4 -3
View File
@@ -20,6 +20,7 @@ use Grav\Framework\Form\Traits\FormTrait;
use Grav\Framework\Route\Route; use Grav\Framework\Route\Route;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
use Twig\Template;
use Twig\TemplateWrapper; use Twig\TemplateWrapper;
/** /**
@@ -82,7 +83,7 @@ class FlexForm implements FlexFormInterface
} }
/** /**
* @return Data|FlexObjectInterface * @return Data|FlexObjectInterface|object
*/ */
public function getData() public function getData()
{ {
@@ -103,7 +104,7 @@ class FlexForm implements FlexFormInterface
$value = $this->data ? $this->data[$name] : null; $value = $this->data ? $this->data[$name] : null;
// Return the form data or fall back to the object property. // Return the form data or fall back to the object property.
return $value ?? $this->getObject()->value($name); return $value ?? $this->getObject()->getFormValue($name);
} }
public function getDefaultValue(string $name) public function getDefaultValue(string $name)
@@ -277,7 +278,7 @@ class FlexForm implements FlexFormInterface
/** /**
* @param string $layout * @param string $layout
* @return TemplateWrapper * @return Template|TemplateWrapper
* @throws LoaderError * @throws LoaderError
* @throws SyntaxError * @throws SyntaxError
*/ */
+3 -2
View File
@@ -312,9 +312,10 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
// Ordering can be done by using index only. // Ordering can be done by using index only.
$previous = null; $previous = null;
foreach (array_reverse($orderings) as $field => $ordering) { foreach (array_reverse($orderings) as $field => $ordering) {
$field = (string)$field;
if ($this->getKeyField() === $field) { if ($this->getKeyField() === $field) {
$keys = $this->getKeys(); $keys = $this->getKeys();
$search = array_combine($keys, $keys); $search = array_combine($keys, $keys) ?: [];
} elseif ($field === 'flex_key') { } elseif ($field === 'flex_key') {
$search = $this->getFlexKeys(); $search = $this->getFlexKeys();
} else { } else {
@@ -462,7 +463,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
$first = reset($entries); $first = reset($entries);
if ($first) { if ($first) {
$keys = array_keys($first); $keys = array_keys($first);
$keys = array_combine($keys, $keys); $keys = array_combine($keys, $keys) ?: [];
} else { } else {
$keys = []; $keys = [];
} }
+32 -10
View File
@@ -31,6 +31,7 @@ use Psr\SimpleCache\InvalidArgumentException;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
use Twig\Template;
use Twig\TemplateWrapper; use Twig\TemplateWrapper;
/** /**
@@ -162,6 +163,13 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
{ {
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$properties = $properties ?? $this->getFlexDirectory()->getConfig('data.search.fields', []); $properties = $properties ?? $this->getFlexDirectory()->getConfig('data.search.fields', []);
if (!$properties) {
foreach ($this->getFlexDirectory()->getConfig('admin.list.fields', []) as $property => $value) {
if (!empty($value['link'])) {
$properties[] = $property;
}
}
}
$weight = 0; $weight = 0;
foreach ((array)$properties as $property) { foreach ((array)$properties as $property) {
@@ -273,7 +281,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return (float)$options['ends_with']; return (float)$options['ends_with'];
} }
if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $options['case_sensitive'] ?? false)) { if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $options['case_sensitive'] ?? false)) {
return (float)$options['contains']; return (float)($options['contains'] ?? 1);
} }
return 0; return 0;
@@ -384,7 +392,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
} }
try { try {
$data = $cache ? $cache->get($key) : null; $data = $cache && $key ? $cache->get($key) : null;
$block = $data ? HtmlBlock::fromArray($data) : null; $block = $data ? HtmlBlock::fromArray($data) : null;
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
@@ -403,7 +411,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
} }
if (!$block) { if (!$block) {
$block = HtmlBlock::create($key); $block = HtmlBlock::create($key ?: null);
$block->setChecksum($checksum); $block->setChecksum($checksum);
if ($key === false) { if ($key === false) {
$block->disableCache(); $block->disableCache();
@@ -427,7 +435,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$block->setContent($output); $block->setContent($output);
try { try {
$cache && $block->isCached() && $cache->set($key, $block->toArray()); $cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$debugger->addException($e); $debugger->addException($e);
} }
@@ -534,7 +542,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]); $result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]);
$value = reset($result); $value = reset($result);
$storageKey = key($result); $storageKey = (string)key($result);
if ($value && $storageKey) { if ($value && $storageKey) {
$this->setStorageKey($storageKey); $this->setStorageKey($storageKey);
if (!$this->hasKey()) { if (!$this->hasKey()) {
@@ -542,6 +550,15 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
} }
} }
// FIXME: For some reason locator caching isn't cleared for the file, investigate!
$locator = Grav::instance()['locator'];
$locator->clearCache();
// Make sure that the object exists before continuing (just in case).
if (!$this->exists()) {
throw new \RuntimeException('Saving failed: Object does not exist!');
}
if (method_exists($this, 'saveUpdatedMedia')) { if (method_exists($this, 'saveUpdatedMedia')) {
$this->saveUpdatedMedia(); $this->saveUpdatedMedia();
} }
@@ -551,7 +568,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
if (method_exists($this, 'clearMediaCache')) { if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache(); $this->clearMediaCache();
} }
} catch (InvalidArgumentException $e) { } catch (\Exception $e) {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = Grav::instance()['debugger']; $debugger = Grav::instance()['debugger'];
$debugger->addException($e); $debugger->addException($e);
@@ -579,7 +596,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
if (method_exists($this, 'clearMediaCache')) { if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache(); $this->clearMediaCache();
} }
} catch (InvalidArgumentException $e) { } catch (\Exception $e) {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = Grav::instance()['debugger']; $debugger = Grav::instance()['debugger'];
$debugger->addException($e); $debugger->addException($e);
@@ -729,7 +746,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
} }
$grav = Grav::instance(); $grav = Grav::instance();
/** @var Flex $flex */ /** @var Flex|null $flex */
$flex = $grav['flex_objects'] ?? null; $flex = $grav['flex_objects'] ?? null;
$directory = $flex ? $flex->getDirectory($type) : null; $directory = $flex ? $flex->getDirectory($type) : null;
if (!$directory) { if (!$directory) {
@@ -800,7 +817,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
/** /**
* @param string $layout * @param string $layout
* @return TemplateWrapper * @return Template|TemplateWrapper
* @throws LoaderError * @throws LoaderError
* @throws SyntaxError * @throws SyntaxError
*/ */
@@ -812,7 +829,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$twig = $grav['twig']; $twig = $grav['twig'];
try { try {
return $twig->twig()->resolveTemplate(["flex-objects/layouts/{$this->getFlexType()}/object/{$layout}.html.twig"]); return $twig->twig()->resolveTemplate(
[
"flex-objects/layouts/{$this->getFlexType()}/object/{$layout}.html.twig",
"flex-objects/layouts/_default/object/{$layout}.html.twig"
]
);
} catch (LoaderError $e) { } catch (LoaderError $e) {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = Grav::instance()['debugger']; $debugger = Grav::instance()['debugger'];
@@ -128,7 +128,7 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface
return $path; return $path;
} }
return (string) $locator->findResource($path) ?: $locator->findResource($path, true, true); return (string)($locator->findResource($path) ?: $locator->findResource($path, true, true));
} }
/** /**
@@ -455,6 +455,8 @@ class FolderStorage extends AbstractFilesystemStorage
protected function initOptions(array $options): void protected function initOptions(array $options): void
{ {
$extension = $this->dataFormatter->getDefaultFileExtension(); $extension = $this->dataFormatter->getDefaultFileExtension();
/** @var string $pattern */
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern; $pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
$this->dataFolder = $options['folder']; $this->dataFolder = $options['folder'];
@@ -192,6 +192,9 @@ class SimpleStorage extends AbstractFilesystemStorage
$list = []; $list = [];
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
if (strpos($key, '@@')) {
$key = $this->getNewKey();
}
$this->data[$key] = $list[$key] = $row; $this->data[$key] = $list[$key] = $row;
} }
@@ -224,7 +227,12 @@ class SimpleStorage extends AbstractFilesystemStorage
$keys = array_keys($this->data); $keys = array_keys($this->data);
$keys[array_search($src, $keys, true)] = $dst; $keys[array_search($src, $keys, true)] = $dst;
$this->data = array_combine($keys, $this->data); $data = array_combine($keys, $this->data);
if (false === $data) {
throw new \LogicException('Bad data');
}
$this->data = $data;
return true; return true;
} }
@@ -13,6 +13,7 @@ namespace Grav\Framework\Flex\Traits;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface; use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface; use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
/** /**
@@ -25,10 +26,11 @@ trait FlexAuthorizeTrait
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) : bool public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) : bool
{ {
if (null === $user) { if (null === $user) {
$user = Grav::instance()['user']; /** @var UserInterface $user */
$user = Grav::instance()['user'] ?? null;
} }
return $this->isAuthorizedAction($user, $action, $scope) || $this->isAuthorizedSuperAdmin($user); return $user && ($this->isAuthorizedAction($user, $action, $scope) || $this->isAuthorizedSuperAdmin($user));
} }
protected function isAuthorizedSuperAdmin(UserInterface $user): bool protected function isAuthorizedSuperAdmin(UserInterface $user): bool
@@ -44,7 +46,11 @@ trait FlexAuthorizeTrait
$action = $this->exists() ? 'update' : 'create'; $action = $this->exists() ? 'update' : 'create';
} }
return $user->authorize(sprintf($this->_authorize, $scope, $action)); $directory = $this instanceof FlexDirectory ? $this : $this->getFlexDirectory();
$config = $directory->getConfig();
$allowed = $config->get("{$scope}.actions.{$action}") ?? $config->get("actions.{$action}") ?? true;
return $allowed && $user->authorize(sprintf($this->_authorize, $scope, $action));
} }
protected function setAuthorizeRule(string $authorize) : void protected function setAuthorizeRule(string $authorize) : void
@@ -49,7 +49,7 @@ trait FlexMediaTrait
*/ */
public function getStorageFolder() public function getStorageFolder()
{ {
return $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()); return $this->exists() ? $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()) : '';
} }
/** /**
@@ -57,7 +57,7 @@ trait FlexMediaTrait
*/ */
public function getMediaFolder() public function getMediaFolder()
{ {
return $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()); return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : '';
} }
/** /**
@@ -153,6 +153,12 @@ trait FlexMediaTrait
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = $grav['locator']; $locator = $grav['locator'];
$path = $media->getPath(); $path = $media->getPath();
if (!$path) {
$language = $grav['language'];
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
}
if ($locator->isStream($path)) { if ($locator->isStream($path)) {
$path = $locator->findResource($path, true, true); $path = $locator->findResource($path, true, true);
$locator->clearCache($path); $locator->clearCache($path);
@@ -202,12 +208,16 @@ trait FlexMediaTrait
} }
$media = $this->getMedia(); $media = $this->getMedia();
$path = $media->getPath();
if (!$path) {
return;
}
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = $grav['locator']; $locator = $grav['locator'];
$targetPath = $media->getPath() . '/' . $dirname; $targetPath = $path . '/' . $dirname;
$targetFile = $media->getPath() . '/' . $filename; $targetFile = $path . '/' . $filename;
if ($locator->isStream($targetFile)) { if ($locator->isStream($targetFile)) {
$targetPath = $locator->findResource($targetPath, true, true); $targetPath = $locator->findResource($targetPath, true, true);
$targetFile = $locator->findResource($targetFile, true, true); $targetFile = $locator->findResource($targetFile, true, true);
@@ -229,7 +239,6 @@ trait FlexMediaTrait
} }
// Remove Extra Files // Remove Extra Files
foreach (scandir($targetPath, SCANDIR_SORT_NONE) as $file) { foreach (scandir($targetPath, SCANDIR_SORT_NONE) as $file) {
$preg_name = preg_quote($fileParts['filename'], '`'); $preg_name = preg_quote($fileParts['filename'], '`');
$preg_ext =preg_quote($fileParts['extension'], '`'); $preg_ext =preg_quote($fileParts['extension'], '`');
+197 -110
View File
@@ -10,12 +10,18 @@
namespace Grav\Framework\Form; namespace Grav\Framework\Form;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\File\YamlFile; use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class FormFlash implements \JsonSerializable class FormFlash implements FormFlashInterface
{ {
/** @var bool */
protected $exists;
/** @var string */ /** @var string */
protected $sessionId; protected $sessionId;
/** @var string */ /** @var string */
@@ -26,6 +32,10 @@ class FormFlash implements \JsonSerializable
protected $url; protected $url;
/** @var array */ /** @var array */
protected $user; protected $user;
/** @var int */
protected $createdTimestamp;
/** @var int */
protected $updatedTimestamp;
/** @var array */ /** @var array */
protected $data; protected $data;
/** @var array */ /** @var array */
@@ -34,29 +44,35 @@ class FormFlash implements \JsonSerializable
protected $uploadedFiles; protected $uploadedFiles;
/** @var string[] */ /** @var string[] */
protected $uploadObjects; protected $uploadObjects;
/** @var bool */ /** @var string */
protected $exists; protected $folder;
/** /**
* @param string $sessionId * @inheritDoc
* @return string
*/ */
public static function getSessionTmpDir(string $sessionId): string public function __construct($config)
{ {
return "tmp://forms/{$sessionId}"; // Backwards compatibility with Grav 1.6 plugins.
} if (!is_array($config)) {
user_error(__CLASS__ . '::' . __FUNCTION__ . '($sessionId, $uniqueId, $formName) is deprecated since Grav 1.6.11, use $config parameter instead', E_USER_DEPRECATED);
/** $args = func_get_args();
* FormFlashObject constructor. $config = [
* @param string $sessionId 'session_id' => $args[0],
* @param string $uniqueId 'unique_id' => $args[1] ?? null,
* @param string|null $formName 'form_name' => $args[2] ?? null,
*/ ];
public function __construct(string $sessionId, string $uniqueId, string $formName = null) }
{
$this->sessionId = $sessionId;
$this->uniqueId = $uniqueId;
$this->sessionId = $config['session_id'] ?? 'no-session';
$this->uniqueId = $config['unique_id'] ?? '';
$folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : '');
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder;
$file = $this->getTmpIndex(); $file = $this->getTmpIndex();
$this->exists = $file->exists(); $this->exists = $file->exists();
@@ -66,65 +82,58 @@ class FormFlash implements \JsonSerializable
} catch (\Exception $e) { } catch (\Exception $e) {
$data = []; $data = [];
} }
$this->formName = null !== $formName ? $content['form'] ?? '' : ''; $this->formName = $content['form'] ?? $config['form_name'] ?? '';
$this->url = $data['url'] ?? ''; $this->url = $data['url'] ?? '';
$this->user = $data['user'] ?? null; $this->user = $data['user'] ?? null;
$this->updatedTimestamp = $data['timestamps']['updated'] ?? time();
$this->createdTimestamp = $data['timestamps']['created'] ?? $this->updatedTimestamp;
$this->data = $data['data'] ?? null; $this->data = $data['data'] ?? null;
$this->files = $data['files'] ?? []; $this->files = $data['files'] ?? [];
} else { } else {
$this->formName = $formName; $this->formName = $config['form_name'] ?? '';
$this->url = ''; $this->url = '';
$this->createdTimestamp = $this->updatedTimestamp = time();
$this->files = []; $this->files = [];
} }
} }
/** /**
* @return string * @inheritDoc
*/
public function getSessionId(): string
{
return $this->sessionId;
}
/**
* @inheritDoc
*/
public function getUniqueId(): string
{
return $this->uniqueId;
}
/**
* @deprecated 1.6.11 Use '->getUniqueId()' method instead.
*/
public function getUniqieId(): string
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6.11, use ->getUniqueId() method instead', E_USER_DEPRECATED);
return $this->getUniqueId();
}
/**
* @inheritDoc
*/ */
public function getFormName(): string public function getFormName(): string
{ {
return $this->formName; return $this->formName;
} }
/**
* @return string
*/
public function getUniqieId(): string
{
return $this->uniqueId;
}
/** /**
* @return bool * @inheritDoc
*/
public function exists(): bool
{
return $this->exists;
}
/**
* @return $this
*/
public function save(): self
{
$file = $this->getTmpIndex();
$file->save($this->jsonSerialize());
$this->exists = true;
return $this;
}
public function delete(): self
{
$this->removeTmpDir();
$this->files = [];
$this->exists = false;
return $this;
}
/**
* @return string
*/ */
public function getUrl(): string public function getUrl(): string
{ {
@@ -132,18 +141,7 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* @param string $url * @inheritDoc
* @return $this
*/
public function setUrl(string $url): self
{
$this->url = $url;
return $this;
}
/**
* @return string
*/ */
public function getUsername(): string public function getUsername(): string
{ {
@@ -151,7 +149,7 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* @return string * @inheritDoc
*/ */
public function getUserEmail(): string public function getUserEmail(): string
{ {
@@ -159,40 +157,84 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* @param string|null $username * @inheritDoc
* @return $this
*/ */
public function setUserName(string $username = null): self public function getCreatedTimestamp(): int
{ {
$this->user['username'] = $username; return $this->createdTimestamp;
return $this;
} }
/** /**
* @param string|null $email * @inheritDoc
* @return $this
*/ */
public function setUserEmail(string $email = null): self public function getUpdatedTimestamp(): int
{ {
$this->user['email'] = $email; return $this->updatedTimestamp;
return $this;
} }
/**
* @inheritDoc
*/
public function getData(): ?array public function getData(): ?array
{ {
return $this->data; return $this->data;
} }
/**
* @inheritDoc
*/
public function setData(?array $data): void public function setData(?array $data): void
{ {
$this->data = $data; $this->data = $data;
} }
/** /**
* @param string $field * @inheritDoc
* @return array */
public function exists(): bool
{
return $this->exists;
}
/**
* @inheritDoc
*/
public function save(): self
{
if (!($this->folder && $this->uniqueId)) {
return $this;
}
if ($this->data || $this->files) {
// Only save if there is data or files to be saved.
$file = $this->getTmpIndex();
$file->save($this->jsonSerialize());
$this->exists = true;
} elseif ($this->exists) {
// Delete empty form flash if it exists (it carries no information).
return $this->delete();
}
return $this;
}
/**
* @inheritDoc
*/
public function delete(): self
{
if ($this->folder && $this->uniqueId) {
$this->removeTmpDir();
$this->files = [];
$this->exists = false;
}
return $this;
}
/**
* @inheritDoc
*/ */
public function getFilesByField(string $field): array public function getFilesByField(string $field): array
{ {
@@ -208,8 +250,7 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* @param bool $includeOriginal * @inheritDoc
* @return array
*/ */
public function getFilesByFields($includeOriginal = false): array public function getFilesByFields($includeOriginal = false): array
{ {
@@ -225,12 +266,7 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* Add uploaded file to the form flash. * @inheritDoc
*
* @param UploadedFileInterface $upload
* @param string|null $field
* @param array|null $crop
* @return string Return name of the file
*/ */
public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string
{ {
@@ -256,12 +292,7 @@ class FormFlash implements \JsonSerializable
/** /**
* Add existing file to the form flash. * @inheritDoc
*
* @param string $filename
* @param string $field
* @param array $crop
* @return bool
*/ */
public function addFile(string $filename, string $field, array $crop = null): bool public function addFile(string $filename, string $field, array $crop = null): bool
{ {
@@ -282,11 +313,7 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* Remove any file from form flash. * @inheritDoc
*
* @param string $name
* @param string $field
* @return bool
*/ */
public function removeFile(string $name, string $field = null): bool public function removeFile(string $name, string $field = null): bool
{ {
@@ -318,7 +345,7 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* Clear form flash from all uploaded files. * @inheritDoc
*/ */
public function clearFiles() public function clearFiles()
{ {
@@ -332,7 +359,7 @@ class FormFlash implements \JsonSerializable
} }
/** /**
* @return array * @inheritDoc
*/ */
public function jsonSerialize(): array public function jsonSerialize(): array
{ {
@@ -341,17 +368,72 @@ class FormFlash implements \JsonSerializable
'unique_id' => $this->uniqueId, 'unique_id' => $this->uniqueId,
'url' => $this->url, 'url' => $this->url,
'user' => $this->user, 'user' => $this->user,
'timestamps' => [
'created' => $this->createdTimestamp,
'updated' => time(),
],
'data' => $this->data, 'data' => $this->data,
'files' => $this->files 'files' => $this->files
]; ];
} }
/**
* @param string $url
* @return $this
*/
public function setUrl(string $url): self
{
$this->url = $url;
return $this;
}
/**
* @param UserInterface|null $user
* @return $this
*/
public function setUser(UserInterface $user = null)
{
if ($user && $user->username) {
$this->user = [
'username' => $user->username,
'email' => $user->email ?? ''
];
} else {
$this->user = null;
}
return $this;
}
/**
* @param string|null $username
* @return $this
*/
public function setUserName(string $username = null): self
{
$this->user['username'] = $username;
return $this;
}
/**
* @param string|null $email
* @return $this
*/
public function setUserEmail(string $email = null): self
{
$this->user['email'] = $email;
return $this;
}
/** /**
* @return string * @return string
*/ */
public function getTmpDir(): string public function getTmpDir(): string
{ {
return static::getSessionTmpDir($this->sessionId) . '/' . $this->uniqueId; return $this->folder && $this->uniqueId ? "{$this->folder}/{$this->uniqueId}" : '';
} }
/** /**
@@ -368,8 +450,9 @@ class FormFlash implements \JsonSerializable
*/ */
protected function removeTmpFile(string $name): void protected function removeTmpFile(string $name): void
{ {
$filename = $this->getTmpDir() . '/' . $name; $tmpDir = $this->getTmpDir();
if ($name && is_file($filename)) { $filename = $tmpDir ? $tmpDir . '/' . $name : '';
if ($name && $filename && is_file($filename)) {
unlink($filename); unlink($filename);
} }
} }
@@ -377,7 +460,7 @@ class FormFlash implements \JsonSerializable
protected function removeTmpDir(): void protected function removeTmpDir(): void
{ {
$tmpDir = $this->getTmpDir(); $tmpDir = $this->getTmpDir();
if (file_exists($tmpDir)) { if ($tmpDir && file_exists($tmpDir)) {
Folder::delete($tmpDir); Folder::delete($tmpDir);
} }
} }
@@ -390,6 +473,10 @@ class FormFlash implements \JsonSerializable
*/ */
protected function addFileInternal(?string $field, string $name, array $data, array $crop = null): void protected function addFileInternal(?string $field, string $name, array $data, array $crop = null): void
{ {
if (!($this->folder && $this->uniqueId)) {
throw new \RuntimeException('Cannot upload files: form flash folder not defined');
}
$field = $field ?: 'undefined'; $field = $field ?: 'undefined';
if (!isset($this->files[$field])) { if (!isset($this->files[$field])) {
$this->files[$field] = []; $this->files[$field] = [];
@@ -0,0 +1,165 @@
<?php
/**
* @package Grav\Framework\Form
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Form\Interfaces;
use Psr\Http\Message\UploadedFileInterface;
interface FormFlashInterface extends \JsonSerializable
{
/**
* @param array $config Available configuration keys: session_id, unique_id, form_name
*/
public function __construct($config);
/**
* Get session Id associated to this form instance.
*
* @return string
*/
public function getSessionId(): string;
/**
* Get unique identifier associated to this form instance.
*
* @return string
*/
public function getUniqueId(): string;
/**
* Get form name associated to this form instance.
*
* @return string
*/
public function getFormName(): string;
/**
* Get URL associated to this form instance.
*
* @return string
*/
public function getUrl(): string;
/**
* Get username from the user who was associated to this form instance.
*
* @return string
*/
public function getUsername(): string;
/**
* Get email from the user who was associated to this form instance.
*
* @return string
*/
public function getUserEmail(): string;
/**
* Get creation timestamp for this form flash.
*
* @return int
*/
public function getCreatedTimestamp(): int;
/**
* Get last updated timestamp for this form flash.
*
* @return int
*/
public function getUpdatedTimestamp(): int;
/**
* Get raw form data.
*
* @return array|null
*/
public function getData(): ?array;
/**
* Set raw form data.
*
* @param array|null $data
*/
public function setData(?array $data): void;
/**
* Check if this form flash exists.
*
* @return bool
*/
public function exists(): bool;
/**
* Save this form flash.
*
* @return $this
*/
public function save();
/**
* Delete this form flash.
*/
public function delete();
/**
* Get all files associated to a form field.
*
* @param string $field
* @return array
*/
public function getFilesByField(string $field): array;
/**
* Get all files grouped by the associated form fields.
*
* @param bool $includeOriginal
* @return array
*/
public function getFilesByFields($includeOriginal = false): array;
/**
* Add uploaded file to the form flash.
*
* @param UploadedFileInterface $upload
* @param string|null $field
* @param array|null $crop
* @return string Return name of the file
*/
public function addUploadedFile(UploadedFileInterface $upload, string $field = null, array $crop = null): string;
/**
* Add existing file to the form flash.
*
* @param string $filename
* @param string $field
* @param array $crop
* @return bool
*/
public function addFile(string $filename, string $field, array $crop = null): bool;
/**
* Remove any file from form flash.
*
* @param string $name
* @param string $field
* @return bool
*/
public function removeFile(string $name, string $field = null): bool;
/**
* Clear form flash from all uploaded files.
*/
public function clearFiles();
/**
* @return array
*/
public function jsonSerialize(): array;
}
@@ -15,9 +15,11 @@ use Grav\Common\Data\ValidationException;
use Grav\Common\Form\FormFlash; use Grav\Common\Form\FormFlash;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Twig\Twig; use Grav\Common\Twig\Twig;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Framework\ContentBlock\HtmlBlock; use Grav\Framework\ContentBlock\HtmlBlock;
use Grav\Framework\Form\Interfaces\FormInterface; use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Session\SessionInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
@@ -296,12 +298,12 @@ trait FormTrait
public function getButtons(): array public function getButtons(): array
{ {
return $this->getBlueprint()['form']['buttons'] ?? []; return $this->getBlueprint()->get('form/buttons') ?? [];
} }
public function getTasks(): array public function getTasks(): array
{ {
return $this->getBlueprint()['form']['tasks'] ?? []; return $this->getBlueprint()->get('form/tasks') ?? [];
} }
abstract public function getBlueprint(): Blueprint; abstract public function getBlueprint(): Blueprint;
@@ -336,35 +338,56 @@ trait FormTrait
public function getFlash(): FormFlash public function getFlash(): FormFlash
{ {
if (null === $this->flash) { if (null === $this->flash) {
/** @var Grav $grav */
$grav = Grav::instance(); $grav = Grav::instance();
$id = null; $config = [
'session_id' => $this->getSessionId(),
$user = $grav['user'] ?? null; 'unique_id' => $this->getUniqueId(),
if (isset($user)) { 'form_name' => $this->getName(),
$rememberState = $this->getBlueprint()->get('form/remember_state'); 'folder' => $this->getFlashFolder()
if ($rememberState === 'user') { ];
$id = $user->username;
}
}
// Session Required for flash form
$session = $grav['session'] ?? null;
if (isset($session)) {
// By default store flash by the session id.
if (null === $id) {
$id = $session->getId();
}
$this->flash = new FormFlash($id, $this->getUniqueId(), $this->getName()); $this->flash = new FormFlash($config);
$this->flash->setUrl($grav['uri']->url)->setUser($user); $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
}
} }
return $this->flash; return $this->flash;
} }
/**
* Get all available form flash objects for this form.
*
* @return FormFlash[]
*/
public function getAllFlashes(): array
{
$folder = $this->getFlashFolder();
if (!$folder || !is_dir($folder)) {
return [];
}
$name = $this->getName();
$list = [];
/** @var \SplFileInfo $file */
foreach (new \FilesystemIterator($folder) as $file) {
$uniqueId = $file->getFilename();
$config = [
'session_id' => $this->getSessionId(),
'unique_id' => $uniqueId,
'form_name' => $name,
'folder' => $this->getFlashFolder()
];
$flash = new FormFlash($config);
if ($flash->exists() && $flash->getFormName() === $name) {
$list[] = $flash;
}
}
return $list;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FormInterface::render() * @see FormInterface::render()
@@ -389,11 +412,51 @@ trait FormTrait
return $block; return $block;
} }
protected function getSessionId(): string
{
/** @var Grav $grav */
$grav = Grav::instance();
/** @var SessionInterface $session */
$session = $grav['session'] ?? null;
return $session ? ($session->getId() ?? '') : '';
}
protected function unsetFlash(): void protected function unsetFlash(): void
{ {
$this->flash = null; $this->flash = null;
} }
protected function getFlashFolder(): ?string
{
$grav = Grav::instance();
/** @var UserInterface $user */
$user = $grav['user'] ?? null;
$userExists = $user && $user->exists();
$username = $userExists ? $user->username : null;
$mediaFolder = $userExists ? $user->getMediaFolder() : null;
$session = $grav['session'] ?? null;
$sessionId = $session ? $session->getId() : null;
// Fill template token keys/value pairs.
$dataMap = [
'[FORM_NAME]' => $this->getName(),
'[SESSIONID]' => $sessionId ?? '!!',
'[USERNAME]' => $username ?? '!!',
'[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!',
'[ACCOUNT]' => $mediaFolder ?? '!!'
];
$flashFolder = $this->getBlueprint()->get('form/flash_folder', 'tmp://forms/[SESSIONID]');
$path = str_replace(array_keys($dataMap), array_values($dataMap), $flashFolder);
// Make sure we only return valid paths.
return strpos($path, '!!') === false ? rtrim($path, '/') : null;
}
/** /**
* Set a single error. * Set a single error.
* *
@@ -577,7 +640,7 @@ trait FormTrait
foreach ($data as $key => &$value) { foreach ($data as $key => &$value) {
if (\is_array($value)) { if (\is_array($value)) {
$value = $this->jsonDecode($value); $value = $this->jsonDecode($value);
} elseif ($value === '') { } elseif (trim($value) === '') {
unset($data[$key]); unset($data[$key]);
} else { } else {
$value = json_decode($value, true); $value = json_decode($value, true);
@@ -29,7 +29,7 @@ interface RenderInterface
* @example {% render object layout 'custom' with { variable: 'value' } %} * @example {% render object layout 'custom' with { variable: 'value' } %}
* *
* @param string|null $layout Layout to be used. * @param string|null $layout Layout to be used.
* @param array|null $context Extra context given to the renderer. * @param array $context Extra context given to the renderer.
* *
* @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output. * @return ContentBlockInterface|HtmlBlock Returns `HtmlBlock` containing the rendered output.
* @api * @api
+38 -14
View File
@@ -154,16 +154,11 @@ class Route
* If the parameter exists in both, return Grav parameter. * If the parameter exists in both, return Grav parameter.
* *
* @param string $param * @param string $param
* @return string|null * @return string|array|null
*/ */
public function getParam($param) public function getParam($param)
{ {
$value = $this->getGravParam($param); return $this->getGravParam($param) ?? $this->getQueryParam($param);
if ($value === null) {
$value = $this->getQueryParam($param);
}
return $value;
} }
/** /**
@@ -177,7 +172,7 @@ class Route
/** /**
* @param string $param * @param string $param
* @return string|null * @return string|array|null
*/ */
public function getQueryParam($param) public function getQueryParam($param)
{ {
@@ -193,6 +188,7 @@ class Route
public function withRoute($route) public function withRoute($route)
{ {
$this->route = $route; $this->route = $route;
return $this; return $this;
} }
@@ -205,6 +201,7 @@ class Route
public function withRoot($root) public function withRoot($root)
{ {
$this->root = $root; $this->root = $root;
return $this; return $this;
} }
@@ -250,6 +247,25 @@ class Route
return $this->withParam('queryParams', $param, $value); return $this->withParam('queryParams', $param, $value);
} }
public function withoutParams()
{
return $this->withoutGravParams()->withoutQueryParams();
}
public function withoutGravParams()
{
$this->gravParams = [];
return $this;
}
public function withoutQueryParams()
{
$this->queryParams = [];
return $this;
}
/** /**
* @return \Grav\Framework\Uri\Uri * @return \Grav\Framework\Uri\Uri
*/ */
@@ -292,22 +308,30 @@ class Route
*/ */
protected function withParam($type, $param, $value) protected function withParam($type, $param, $value)
{ {
$oldValue = $this->{$type}[$param] ?? null; $values = $this->{$type} ?? [];
$oldValue = $values[$param] ?? null;
if ($oldValue === $value) { if ($oldValue === $value) {
return $this; return $this;
} }
$new = clone $this; $new = $this->copy();
if ($value === null) { if ($value === null) {
unset($new->{$type}[$param]); unset($values[$param]);
} else { } else {
$new->{$type}[$param] = $value; $values[$param] = $value;
} }
$new->{$type} = $values;
return $new; return $new;
} }
protected function copy()
{
return clone $this;
}
/** /**
* @param bool $includeRoot * @param bool $includeRoot
* @return string * @return string
@@ -350,8 +374,8 @@ class Route
$this->language = $gravParts['language']; $this->language = $gravParts['language'];
$this->route = $gravParts['route']; $this->route = $gravParts['route'];
$this->extension = $gravParts['extension'] ?? ''; $this->extension = $gravParts['extension'] ?? '';
$this->gravParams = $gravParts['params']; $this->gravParams = $gravParts['params'] ?: [];
$this->queryParams = $parts['query_params']; $this->queryParams = $parts['query_params'] ?: [];
} else { } else {
$this->root = RouteFactory::getRoot(); $this->root = RouteFactory::getRoot();
@@ -84,11 +84,11 @@ class UriPartsFilter
*/ */
public static function filterPort($port = null) public static function filterPort($port = null)
{ {
if (null === $port || (\is_int($port) && ($port >= 1 && $port <= 65535))) { if (null === $port || (\is_int($port) && ($port >= 0 && $port <= 65535))) {
return $port; return $port;
} }
throw new \InvalidArgumentException('Uri port must be null or an integer between 1 and 65535'); throw new \InvalidArgumentException('Uri port must be null or an integer between 0 and 65535');
} }
/** /**
Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Some files were not shown because too many files have changed in this diff Show More