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
+125 -9
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
## 04/23/2019
@@ -25,18 +141,18 @@
## 04/17/2019
1. [](#new)
* `FormInterface` now implements `RenderInterface`
* Added new `FormInterface::getTask()` method which reads the task from `form.task` in the blueprint
* `FormInterface` now implements `RenderInterface`
* Added new `FormInterface::getTask()` method which reads the task from `form.task` in the blueprint
1. [](#improved)
* Updated vendor libraries to latest
1. [](#bugfix)
* Rollback `redirect_default_route` logic as it has issues with multi-lang [#2459](https://github.com/getgrav/grav/issues/2459)
* Fix potential issue with `|contains` Twig filter on PHP 7.3
* Fix potential issue with `|contains` Twig filter on PHP 7.3
* Fixed bug in text field filtering: return empty string if value isn't a string or number [#2460](https://github.com/getgrav/grav/issues/2460)
* Force Asset `priority` to be an integer and not throw error if invalid string passed [#2461](https://github.com/getgrav/grav/issues/2461)
* Fixed bug in text field filtering: return empty string if value isn't a string or number
* Fixed `FlexForm` missing getter methods for defining form variables
# v1.6.5
## 04/15/2019
@@ -97,7 +213,7 @@
* Added `Grav\Framework\Object\ObjectIndex` class
* Added `Grav\Framework\Flex` classes
* Added support for hiding form fields in blueprints by using dynamic property like `security@: admin.foobar`, `scope@: object` or `scope-ignore@: object` to any field
* New experimental **FlexObjects** powered `Users` for increased performance and capability (**disabled** by default)
* New experimental **FlexObjects** powered `Users` for increased performance and capability (**disabled** by default)
* Added PSR-7 and PSR-15 classes
* Added `Grav\Framework\DI\Container` class
* Added `Grav\Framework\RequestHandler\RequestHandler` class
@@ -173,7 +289,7 @@
* Added ability to reset `Page::metadata` to allow rebuilding from automatically generated values
* Added back missing `page.types` field in system content configuration [admin#1612](https://github.com/getgrav/grav-plugin-admin/issues/1612)
* Console commands: add method for invalidating cache
* Updated languages
* Updated languages
* Improved `$page->forms()` call, added `$page->addForms()`
* Updated languages from crowdin
* Fixed `ImageMedium` constructor warning when file does not exist
@@ -194,7 +310,7 @@
* Added apcu autoloader optimization
* Additional helper methods in `Language`, `Languages`, and `LanguageCodes` classes
* Call `onFatalException` event also on internal PHP errors
* Built-in PHP Webserver: log requests before handling them
* Built-in PHP Webserver: log requests before handling them
* Added support for syslog and syslog facility logging (default: 'file')
* Improved usability of `System` configuration blueprint with side-tabs
1. [](#bugfix)
@@ -219,7 +335,7 @@
* Fixed failed login if user attempts to log in with upper case non-english letters
* Removed extra authenticated/authorized fields when saving existing user from a form
* Fixed `Grav\Framework\Route::__toString()` returning relative URL, not relative route
* Fixed handling of `append_url_extension` inside of `Page::templateFormat()` [#2264](https://github.com/getgrav/grav/issues/2264)
* Fixed handling of `append_url_extension` inside of `Page::templateFormat()` [#2264](https://github.com/getgrav/grav/issues/2264)
* Fixed a broken language string [#2261](https://github.com/getgrav/grav/issues/2261)
* Fixed clearing cache having no effect on Doctrine cache
* Fixed `Medium::relativePath()` for streams
@@ -272,7 +388,7 @@
* Updated vendor libraries
1. [](#bugfix)
* Support spaces with filenames in responsive images [#2300](https://github.com/getgrav/grav/pull/2300)
# v1.5.6
## 12/14/2018
+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:
* 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>
@@ -110,7 +110,8 @@ Good pull requests - patches, improvements, new features - are a fantastic
help. They should remain focused in scope and avoid containing unrelated
commits.
**Please ask first** in [Slack](https://getgrav.org/slack) or in the Forum before embarking on any significant pull request (e.g.
**Please ask first** in [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/)
before embarking on any significant pull request (e.g.
implementing features, refactoring code..),
otherwise you risk spending a lot of time working on something that the
project's developers might not want to merge into the project.
+12 -13
View File
@@ -79,19 +79,6 @@ $output = new ConsoleOutput();
$output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', 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) {
$output->writeln('');
$output->writeln('<red>Usage:</red>');
@@ -123,6 +110,18 @@ if (!$name) {
}
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) {
+8 -6
View File
@@ -24,14 +24,14 @@
"kodus/psr7-server": "*",
"nyholm/psr7": "^1.0",
"twig/twig": "~1.35",
"twig/twig": "~1.40",
"erusev/parsedown": "1.6.4",
"erusev/parsedown-extra": "~0.7",
"symfony/yaml": "~4.2",
"symfony/console": "~4.2",
"symfony/event-dispatcher": "~4.2",
"symfony/var-dumper": "~4.2",
"symfony/process": "~4.2",
"symfony/yaml": "~4.2.0",
"symfony/console": "~4.2.0",
"symfony/event-dispatcher": "~4.2.0",
"symfony/var-dumper": "~4.2.0",
"symfony/process": "~4.2.0",
"doctrine/cache": "^1.8",
"doctrine/collections": "^1.5",
"guzzlehttp/psr7": "^1.4",
@@ -89,6 +89,8 @@
"exclude": ["VERSION"]
},
"scripts": {
"api-16": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.16.md",
"api-15": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.md",
"post-create-project-cmd": "bin/grav install",
"phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M",
"phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M",
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:
type: text
size: x-small
size: small
append: PLUGIN_ADMIN.CHARACTERS
label: PLUGIN_ADMIN.SUMMARY_SIZE
help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP
+2
View File
@@ -12,5 +12,7 @@ form:
type: text
label: PLUGIN_ADMIN.USERNAME
help: PLUGIN_ADMIN.USERNAME_HELP
unset-disabled@: true
unset-readonly@: true
validate:
required: true
+10
View File
@@ -27,3 +27,13 @@ config:
title: Accounts
icon: fa-users
authorize: ['admin.users', 'admin.accounts', 'admin.super']
form:
fields:
username:
flex-disabled@: exists
disabled: false
flex-readonly@: exists
readonly: false
validate:
required: true
+1 -1
View File
@@ -8,7 +8,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.6.8');
define('GRAV_VERSION', '1.6.16');
define('GRAV_TESTING', false);
define('DS', '/');
+1 -1
View File
@@ -23,7 +23,7 @@ GRAV:
BAD_DATE: Fecha errónea
AGO: antes
FROM_NOW: desde ahora
JUST_NOW: justo ahora
JUST_NOW: hace un momento
SECOND: segundo
MINUTE: minuto
HOUR: hora
+23
View File
@@ -1,11 +1,30 @@
---
GRAV:
FRONTMATTER_ERROR_PAGE: "---\npealkiri: %1$s\n---\n\n# Viga: vigane Frontmatter'i\n\nasukoht: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
INFLECTOR_UNCOUNTABLE:
- 'equipment'
- 'informatsioon'
- 'rice'
- 'money'
- 'species'
- 'series'
- 'kala'
- 'lammas'
INFLECTOR_IRREGULAR:
'person': 'inimesed'
'man': 'mees'
'child': 'lapsed'
INFLECTOR_ORDINALS:
'default': '.'
'first': '.'
'second': '.'
'third': '.'
NICETIME:
NO_DATE_PROVIDED: Kuupäev määramata
BAD_DATE: Vigane kuupäev
AGO: tagasi
FROM_NOW: praegusest
JUST_NOW: just nüüd
SECOND: sekund
MINUTE: minut
HOUR: tundi
@@ -60,3 +79,7 @@ GRAV:
- 'reede'
- 'laupä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'
'/([ti])um$/i': '\1a'
'/(buffal|tomat)o$/i': '\1es'
'/(bu)s$/i': 'Bus'
'/(alias|status)/i': 'alias|status'
'/(ax|test)is$/i': '\1s'
'/s$/i': 's'
'/$/': 's'
+11
View File
@@ -11,6 +11,8 @@ GRAV:
- 'fish'
- 'sheep'
NICETIME:
NO_DATE_PROVIDED: Engin dagsetning gefin
BAD_DATE: Röng dagsetning
AGO: síðan
JUST_NOW: í þessu
SECOND: sekúndu
@@ -45,6 +47,7 @@ GRAV:
DEC_PLURAL: árat
FORM:
VALIDATION_FAIL: <b>Sannvottun mistókst:</b>
INVALID_INPUT: Ógilt inntak í
MISSING_REQUIRED_FIELD: 'Vantar nauðsynlegan reit:'
MONTHS_OF_THE_YEAR:
- 'janúar'
@@ -67,3 +70,11 @@ GRAV:
- 'Föstudagur'
- 'Laugardagur'
- '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'
- 'Novembro'
- 'Dezembro'
INFLECTOR_UNCOUNTABLE:
- 'equipment'
- 'information'
- 'arroz'
- 'money'
- 'species'
- 'series'
- 'fish'
- 'sheep'
+84
View File
@@ -1,10 +1,75 @@
---
GRAV:
FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Chyba: Chybný frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
INFLECTOR_PLURALS:
'/(quiz)$/i': '\1zes'
'/^(ox)$/i': '\1en'
'/([m|l])ouse$/i': '\1ice'
'/(matr|vert|ind)ix|ex$/i': '\1ices'
'/(x|ch|ss|sh)$/i': '\1es'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([^aeiouy]|qu)y$/i': '\1ies'
'/(hive)$/i': '\1s'
'/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
'/sis$/i': 'ses'
'/([ti])um$/i': '\1a'
'/(buffal|tomat)o$/i': '\1oes'
'/(bu)s$/i': '\1ses'
'/(alias|status)/i': '\1es'
'/(octop|vir)us$/i': '\1i'
'/(ax|test)is$/i': '\1es'
'/s$/i': 's'
'/$/': 's'
INFLECTOR_SINGULAR:
'/(quiz)zes$/i': '\1'
'/(matr)ices$/i': '\1ix'
'/(vert|ind)ices$/i': '\1ex'
'/^(ox)en/i': '\1'
'/(alias|status)es$/i': '\1'
'/([octop|vir])i$/i': '\1us'
'/(cris|ax|test)es$/i': '\1is'
'/(shoe)s$/i': '\1'
'/(o)es$/i': '\1'
'/(bus)es$/i': '\1'
'/([m|l])ice$/i': '\1ouse'
'/(x|ch|ss|sh)es$/i': '\1'
'/(m)ovies$/i': '\1ovie'
'/(s)eries$/i': '\1eries'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([lr])ves$/i': '\1f'
'/(tive)s$/i': '\1'
'/(hive)s$/i': '\1'
'/([^f])ves$/i': '\1fe'
'/(^analy)ses$/i': '\1sis'
'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
'/([ti])a$/i': '\1um'
'/(n)ews$/i': '\1ews'
INFLECTOR_UNCOUNTABLE:
- 'vybavenie'
- 'informácie'
- 'ryža'
- 'peniaze'
- 'druhy'
- 'séria'
- 'ryba'
- 'ovce'
INFLECTOR_IRREGULAR:
'person': 'ľudia'
'man': 'muži'
'child': 'deti'
'sex': 'pohlavia'
'move': 'pohyby'
INFLECTOR_ORDINALS:
'default': '.'
'first': '.'
'second': '.'
'third': '.'
NICETIME:
NO_DATE_PROVIDED: Neposkytnutý žiaden dátum
BAD_DATE: Nesprávny dátum
AGO: pred
FROM_NOW: odteraz
JUST_NOW: práve teraz
SECOND: sekunda
MINUTE: minúta
HOUR: hodina
@@ -14,10 +79,12 @@ GRAV:
YEAR: rok
DECADE: desaťročie
SEC: sek
MIN: min
HR: hod
WK: t
MO: m
YR: r
DEC: dec
SECOND_PLURAL: sekúnd
MINUTE_PLURAL: minút
HOUR_PLURAL: hodín
@@ -58,3 +125,20 @@ GRAV:
- 'Piatok'
- 'Sobota'
- 'Nedeľa'
CRON:
EVERY: každý
EVERY_HOUR: každú hodinu
EVERY_MINUTE: každú minútu
EVERY_DAY_OF_WEEK: každý deň v týždni
EVERY_DAY_OF_MONTH: každý deň v mesiaci
EVERY_MONTH: každý mesiac
TEXT_PERIOD: Každý <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:
FRONTMATTER_ERROR_PAGE: "--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```"
INFLECTOR_UNCOUNTABLE:
- 'utrustning'
- 'information'
- 'ris'
- 'pengar'
- 'arter'
- 'serier'
- 'fisk'
- 'får'
INFLECTOR_IRREGULAR:
'person': 'personer'
NICETIME:
NO_DATE_PROVIDED: Inget datum har angivits
BAD_DATE: Ogiltigt datum
+24 -4
View File
@@ -1,24 +1,44 @@
---
GRAV:
FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 錯誤: 不正確的 Frontmatter\n\n路徑: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
NICETIME:
NO_DATE_PROVIDED: 沒有提供日期
BAD_DATE: 錯誤日期
AGO: 之前
FROM_NOW: 之後
JUST_NOW: 剛剛
SECOND:
MINUTE:
HOUR: 小時
DAY:
WEEK:
MONTH:
YEAR:
DECADE: 十年
SEC:
MIN:
HR: 小時
WK:
MO:
YR:
DEC: 十年
SECOND_PLURAL:
MINUTE_PLURAL:
HOUR_PLURAL:
DAY_PLURAL:
WEEK_PLURAL:
HOUR_PLURAL:
DAY_PLURAL:
WEEK_PLURAL:
MONTH_PLURAL:
YEAR_PLURAL:
DECADE_PLURAL: 十年
SEC_PLURAL:
MIN_PLURAL:
HR_PLURAL:
WK_PLURAL:
WK_PLURAL:
MO_PLURAL:
YR_PLURAL:
DEC_PLURAL: 十年
FORM:
MISSING_REQUIRED_FIELD: 遺漏必填欄位:
MONTHS_OF_THE_YEAR:
- '一月'
- '二月'
+113 -32
View File
@@ -1,57 +1,122 @@
---
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:
NO_DATE_PROVIDED: 沒有提供日期
BAD_DATE: 錯誤日期
AGO:
FROM_NOW: 之後
JUST_NOW: 剛剛
NO_DATE_PROVIDED: 无日期信息
BAD_DATE: 无效日期
AGO:
FROM_NOW: 距今
JUST_NOW: 刚刚
SECOND:
MINUTE:
HOUR:
MINUTE:
HOUR:
DAY:
WEEK:
WEEK:
MONTH:
YEAR:
DECADE: 十年
SEC:
MIN:
HR:
WK:
MIN:
HR:
WK:
MO:
YR:
DEC:
DEC:
SECOND_PLURAL:
MINUTE_PLURAL:
HOUR_PLURAL:
HOUR_PLURAL:
DAY_PLURAL:
WEEK_PLURAL:
WEEK_PLURAL:
MONTH_PLURAL:
YEAR_PLURAL:
DECADE_PLURAL: 十年
SEC_PLURAL:
MIN_PLURAL:
HR_PLURAL:
WK_PLURAL:
HR_PLURAL:
WK_PLURAL:
MO_PLURAL:
YR_PLURAL:
DEC_PLURAL:
DEC_PLURAL:
FORM:
MISSING_REQUIRED_FIELD: 遺漏必填欄位:
VALIDATION_FAIL: <b>验证失败:</b>
INVALID_INPUT: 无效输入
MISSING_REQUIRED_FIELD: 必填字段缺失:
MONTHS_OF_THE_YEAR:
- '月'
- '月'
- '月'
- '月'
- '月'
- '月'
- '月'
- '月'
- '月'
- '月'
- '十一月'
- '十二月'
- '1月'
- '2月'
- '3月'
- '4月'
- '5月'
- '6月'
- '7月'
- '8月'
- '9月'
- '10月'
- '11月'
- '12月'
DAYS_OF_THE_WEEK:
- '星期一'
- '星期二'
@@ -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;
}
$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['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . $grav_basedir .DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['SCRIPT_NAME'] = $grav_basedir . 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);
require 'index.php';
require $grav_index;
@@ -24,19 +24,21 @@ trait LegacyAssetsTrait
// First argument is always the asset
array_shift($args);
if (\count($args) === 0) {
if (count($args) === 0) {
return [];
}
if (\count($args) === 1 && \is_array($args[0])) {
// New options array format
if (count($args) === 1 && is_array($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) {
case(Assets::INLINE_CSS_TYPE):
$defaults = ['priority' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
break;
case(Assets::JS_TYPE):
$defaults = ['priority' => null, 'pipeline' => true, 'loading' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
@@ -55,6 +57,11 @@ trait LegacyAssetsTrait
break;
case(Assets::INLINE_CSS_TYPE):
$defaults = ['priority' => null, 'group' => null];
$arguments = $this->createArgumentsFromLegacy($args, $defaults);
break;
default:
case(Assets::CSS_TYPE):
$defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null];
+5 -3
View File
@@ -437,6 +437,9 @@ class Cache extends Getters
case 'tmp-only':
$remove_paths = self::$tmp_remove;
break;
case 'invalidate':
$remove_paths = [];
break;
default:
if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) {
$remove_paths = self::$standard_remove;
@@ -528,7 +531,6 @@ class Cache extends Getters
}
/**
* Set the cache lifetime programmatically
*
@@ -540,7 +542,7 @@ class Cache extends Getters
return;
}
$interval = $future - $this->now;
$interval = (int)($future - $this->now);
if ($interval > 0 && $interval < $this->getLifetime()) {
$this->lifetime = $interval;
}
@@ -555,7 +557,7 @@ class Cache extends Getters
public function getLifetime()
{
if ($this->lifetime === null) {
$this->lifetime = $this->config->get('system.cache.lifetime') ?: 604800; // 1 week default
$this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default
}
return $this->lifetime;
+6 -8
View File
@@ -378,14 +378,12 @@ class Blueprint extends BlueprintForm
$grav = Grav::instance();
$actions = (array)$call['params'];
/** @var UserInterface $user */
if (isset($grav['user'])) {
$user = Grav::instance()['user'];
foreach ($actions as $action) {
if (!$user->authorize($action)) {
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
return;
}
/** @var UserInterface|null $user */
$user = $grav['user'] ?? null;
foreach ($actions as $action) {
if (!$user || !$user->authorize($action)) {
$this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
return;
}
}
}
+12 -2
View File
@@ -39,7 +39,8 @@ class Blueprints
public function get($type)
{
if (!isset($this->instances[$type])) {
$this->instances[$type] = $this->loadFile($type);
$blueprint = $this->loadFile($type);
$this->instances[$type] = $blueprint;
}
return $this->instances[$type];
@@ -99,6 +100,15 @@ class Blueprints
$blueprint->setContext($this->search);
}
return $blueprint->load()->init();
try {
$blueprint->load()->init();
} catch (\RuntimeException $e) {
$log = Grav::instance()['log'];
$log->error(sprintf('Blueprint %s cannot be loaded: %s', $name, $e->getMessage()));
throw $e;
}
return $blueprint;
}
}
+12 -1
View File
@@ -27,8 +27,9 @@ class Validation
if (!isset($field['type'])) {
$field['type'] = 'text';
}
$type = $validate['type'] ?? $field['type'];
$validate = (array)($field['validate'] ?? null);
$type = $validate['type'] ?? $field['type'];
$required = $validate['required'] ?? false;
// If value isn't required, we will stop validation if empty value is given.
@@ -165,6 +166,11 @@ class Validation
return (string) $value;
}
protected static function filterCheckbox($value, array $params, array $field)
{
return (bool) $value;
}
protected static function filterCommaList($value, array $params, array $field)
{
return \is_array($value) ? $value : preg_split('/\s*,\s*/', $value, -1, PREG_SPLIT_NO_EMPTY);
@@ -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'] ?? [];
$use = $field['use'] ?? 'values';
+10 -76
View File
@@ -10,54 +10,10 @@
namespace Grav\Common\Form;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Grav\Framework\Form\FormFlash as FrameworkFormFlash;
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
* @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
{
$tmp_dir = $this->getTmpDir();
if (!$this->uniqueId) {
return false;
}
$tmp_dir = $this->getTmpDir();
Folder::create($tmp_dir);
$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
{
$tmp_dir = $this->getTmpDir();
if (!$this->uniqueId) {
return false;
}
$tmp_dir = $this->getTmpDir();
Folder::create($tmp_dir);
$tmp_file = $upload['file']['tmp_name'];
@@ -136,32 +98,4 @@ class FormFlash extends \Grav\Framework\Form\FormFlash
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;
class CachedCollection extends Iterator {
class CachedCollection extends Iterator
{
protected static $cache;
public function __construct($items)
{
parent::__construct();
$method = static::class . __METHOD__;
// local cache to speed things up
if (!isset(self::$cache[get_called_class() . __METHOD__])) {
self::$cache[get_called_class() . __METHOD__] = $items;
if (!isset(self::$cache[$method])) {
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]);
}
}
@@ -11,8 +11,8 @@ namespace Grav\Common\GPM\Common;
use Grav\Common\Data\Data;
class Package {
class Package
{
/**
* @var Data
*/
+13 -12
View File
@@ -770,7 +770,7 @@ class GPM extends Iterator
* @param array $ignore_packages_list
*
* @return bool
* @throws \Exception
* @throws \RuntimeException
*/
public function checkNoOtherPackageNeedsThisDependencyInALowerVersion(
$slug,
@@ -793,8 +793,8 @@ class GPM extends Iterator
$compatible = $this->checkNextSignificantReleasesAreCompatible($version,
$other_dependency_version);
if (!$compatible) {
if (!in_array($dependent_package, $ignore_packages_list)) {
throw new \Exception("Package <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>.",
if (!in_array($dependent_package, $ignore_packages_list, true)) {
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);
}
}
@@ -850,10 +850,10 @@ class GPM extends Iterator
) {
//Needs a Grav update first
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.
@@ -863,10 +863,10 @@ class GPM extends Iterator
) {
//Needs a Grav update first
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)) {
@@ -1092,6 +1092,7 @@ class GPM extends Iterator
if ($this->versionFormatIsEqualOrHigher($version)) {
return trim(substr($version, 2));
}
return $version;
}
@@ -1104,7 +1105,7 @@ class GPM extends Iterator
*
* @return bool
*/
public function versionFormatIsNextSignificantRelease($version)
public function versionFormatIsNextSignificantRelease($version): bool
{
return strpos($version, '~') === 0;
}
@@ -1118,7 +1119,7 @@ class GPM extends Iterator
*
* @return bool
*/
public function versionFormatIsEqualOrHigher($version)
public function versionFormatIsEqualOrHigher($version): bool
{
return strpos($version, '>=') === 0;
}
@@ -1136,7 +1137,7 @@ class GPM extends Iterator
*
* @return bool
*/
public function checkNextSignificantReleasesAreCompatible($version1, $version2)
public function checkNextSignificantReleasesAreCompatible($version1, $version2): bool
{
$version1array = explode('.', $version1);
$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
*/
@@ -16,6 +16,7 @@ abstract class AbstractPackageCollection extends BaseCollection
public function __construct($items)
{
parent::__construct();
foreach ($items as $name => $data) {
$data->set('slug', $name);
$this->items[$name] = new Package($data, $this->type);
@@ -25,6 +25,7 @@ class Plugins extends AbstractPackageCollection
{
/** @var \Grav\Common\Plugins $plugins */
$plugins = Grav::instance()['plugins'];
parent::__construct($plugins->all());
}
}
@@ -43,7 +43,7 @@ class AbstractPackageCollection extends BaseCollection
{
parent::__construct();
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');
@@ -37,9 +37,9 @@ class GravCore extends AbstractPackageCollection
$this->fetch($refresh, $callback);
$this->data = json_decode($this->raw, true);
$this->version = isset($this->data['version']) ? $this->data['version'] : '-';
$this->date = isset($this->data['date']) ? $this->data['date'] : '-';
$this->min_php = isset($this->data['min_php']) ? $this->data['min_php'] : null;
$this->version = $this->data['version'] ?? '-';
$this->date = $this->data['date'] ?? '-';
$this->min_php = $this->data['min_php'] ?? null;
if (isset($this->data['assets'])) {
foreach ((array)$this->data['assets'] as $slug => $data) {
+17 -240
View File
@@ -9,24 +9,20 @@
namespace Grav\Common\Helpers;
use Grav\Common\Grav;
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\Utils;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Excerpts
{
/**
* Process Grav image media URL from HTML tag
*
* @param string $html HTML tag e.g. `<img src="image.jpg" />`
* @param PageInterface $page The current page object
* @return string Returns final HTML string
* @param string $html HTML tag e.g. `<img src="image.jpg" />`
* @param PageInterface|null $page Page, defaults to the current page object
* @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');
@@ -112,157 +108,29 @@ class Excerpts
* Process a Link excerpt
*
* @param array $excerpt
* @param PageInterface $page
* @param PageInterface|null $page Page, defaults to the current page object
* @param string $type
* @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);
// If there is a query, then parse it and build action calls.
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
$carry[$parts[0]] = $value;
return $carry;
}, []);
// Valid attributes supported.
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
unset($actions['noprocess']);
} else {
// Loop through actions for the image and call them.
foreach ($actions as $attrib => $value) {
$key = $attrib;
if (in_array($attrib, $valid_attributes, true)) {
// support both class and classes.
if ($attrib === 'classes') {
$attrib = 'class';
}
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
unset($actions[$key]);
}
}
}
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
}
// If no query elements left, unset query.
if (empty($url_parts['query'])) {
unset ($url_parts['query']);
}
// Set path to / if not set.
if (empty($url_parts['path'])) {
$url_parts['path'] = '';
}
// If scheme isn't http(s)..
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
// Handle custom streams.
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
$url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
unset($url_parts['stream'], $url_parts['scheme']);
}
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
// Handle paths and such.
$url_parts = Uri::convertUrl($page, $url_parts, $type);
// Build the URL from the component parts and set it on the element.
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
return $excerpts->processLinkExcerpt($excerpt, $type);
}
/**
* Process an image excerpt
*
* @param array $excerpt
* @param PageInterface $page
* @param PageInterface|null $page Page, defaults to the current page object
* @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']));
$url_parts = static::parseUrl($url);
$excerpts = new ExcerptsObject($page);
$media = null;
$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;
return $excerpts->processImageExcerpt($excerpt);
}
/**
@@ -270,104 +138,13 @@ class Excerpts
*
* @param Medium $medium
* @param string|array $url
* @param PageInterface|null $page Page, defaults to the current page object
* @return Medium
*/
public static function processMediaActions($medium, $url)
public static function processMediaActions($medium, $url, PageInterface $page = null)
{
if (!is_array($url)) {
$url_parts = parse_url($url);
} else {
$url_parts = $url;
}
$excerpts = new ExcerptsObject($page);
$actions = [];
// if there is a query, then parse it and build action calls
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = $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;
return $excerpts->processMediaActions($medium, $url);
}
}
+1 -1
View File
@@ -234,7 +234,7 @@ class Truncator {
}
/**
* @inheritDoc
*
*/
public function truncate(
$text,
+14 -1
View File
@@ -20,7 +20,8 @@ class YamlLinter
{
$errors = static::lintConfig();
$errors = $errors + static::lintPages();
$errors = $errors + static::lintBlueprints();
return $errors;
}
@@ -34,6 +35,18 @@ class YamlLinter
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')
{
$lint_errors = [];
+6 -1
View File
@@ -104,6 +104,11 @@ class Language
public function getAvailable()
{
$languagesArray = $this->languages; //Make local copy
$languagesArray = array_map(function($value) {
return preg_quote($value);
}, $languagesArray);
sort($languagesArray);
return implode('|', array_reverse($languagesArray));
@@ -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
* @return string
+13 -3
View File
@@ -10,6 +10,7 @@
namespace Grav\Common\Markdown;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts;
class Parsedown extends \Parsedown
{
@@ -18,12 +19,21 @@ class Parsedown extends \Parsedown
/**
* Parsedown constructor.
*
* @param PageInterface $page
* @param Excerpts|null $excerpts
* @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;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Markdown\Excerpts;
class ParsedownExtra extends \ParsedownExtra
{
@@ -18,14 +19,23 @@ class ParsedownExtra extends \ParsedownExtra
/**
* ParsedownExtra constructor.
*
* @param PageInterface $page
* @param Excerpts|null $excerpts
* @param array|null $defaults
* @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();
$this->init($page, $defaults);
$this->init($excerpts, $defaults);
}
}
@@ -9,15 +9,13 @@
namespace Grav\Common\Markdown;
use Grav\Common\Grav;
use Grav\Common\Helpers\Excerpts;
use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Page\Interfaces\PageInterface;
use RocketTheme\Toolbox\Event\Event;
trait ParsedownGravTrait
{
/** @var PageInterface $page */
protected $page;
/** @var Excerpts */
protected $excerpts;
protected $special_chars;
protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
@@ -28,28 +26,49 @@ trait ParsedownGravTrait
/**
* Initialization function to setup key variables needed by the MarkdownGravLinkTrait
*
* @param PageInterface $page
* @param PageInterface|Excerpts|null $excerpts
* @param array|null $defaults
*/
protected function init($page, $defaults)
protected function init($excerpts = null, $defaults = null)
{
$grav = Grav::instance();
$this->page = $page;
$this->BlockTypes['{'] [] = 'TwigTag';
$this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
if ($defaults === null) {
$defaults = (array)Grav::instance()['config']->get('system.pages.markdown');
if (!$excerpts || $excerpts instanceof PageInterface) {
// Deprecated in Grav 1.6.10
if ($defaults) {
$defaults = ['markdown' => $defaults];
}
$this->excerpts = new Excerpts($excerpts, $defaults);
user_error(__CLASS__ . '::' . __FUNCTION__ . '($page, $defaults) is deprecated since Grav 1.6.10, use ->init(new ' . Excerpts::class . '($page, [\'markdown\' => $defaults])) instead.', E_USER_DEPRECATED);
} else {
$this->excerpts = $excerpts;
}
$this->setBreaksEnabled($defaults['auto_line_breaks']);
$this->setUrlsLinked($defaults['auto_url_links']);
$this->setMarkupEscaped($defaults['escape_markup']);
$this->setSpecialChars($defaults['special_chars']);
$this->BlockTypes['{'][] = 'TwigTag';
$this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
$grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $this, '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)
{
$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;
}
@@ -128,7 +148,8 @@ trait ParsedownGravTrait
*/
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;
}
@@ -210,7 +231,7 @@ trait ParsedownGravTrait
// if this is an image process it
if (isset($excerpt['element']['attributes']['src'])) {
$excerpt = Excerpts::processImageExcerpt($excerpt, $this->page);
$excerpt = $this->excerpts->processImageExcerpt($excerpt);
}
return $excerpt;
@@ -218,11 +239,7 @@ trait ParsedownGravTrait
protected function inlineLink($excerpt)
{
if (isset($excerpt['type'])) {
$type = $excerpt['type'];
} else {
$type = 'link';
}
$type = $excerpt['type'] ?? 'link';
// do some trickery to get around Parsedown requirement for valid URL if its Twig in there
if (preg_match($this->twig_link_regex, $excerpt['text'], $matches)) {
@@ -238,13 +255,15 @@ trait ParsedownGravTrait
// if this is a link
if (isset($excerpt['element']['attributes']['href'])) {
$excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type);
$excerpt = $this->excerpts->processLinkExcerpt($excerpt, $type);
}
return $excerpt;
}
// For extending this class via plugins
/**
* For extending this class via plugins
*/
public function __call($method, $args)
{
if (isset($this->{$method}) === true) {
@@ -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)
{
if (!$file) {
return;
}
$this->offsetSet($name, $file);
switch ($file->type) {
case 'image':
@@ -170,8 +170,7 @@ class ImageMedium extends Medium
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$image_path = $locator->findResource('cache://images', true);
$image_dir = $locator->findResource('cache://images', false);
$image_path = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
$saved_image_path = $this->saveImage();
$output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
@@ -181,6 +180,7 @@ class ImageMedium extends Medium
}
if (Utils::startsWith($output, $image_path)) {
$image_dir = $locator->findResource('cache://images', false);
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
}
@@ -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
*/
@@ -10,6 +10,7 @@
namespace Grav\Common\Page\Medium;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Page\Markdown\Excerpts;
trait ParsedownHtmlTrait
{
@@ -33,7 +34,7 @@ trait ParsedownHtmlTrait
$element = $this->parsedownElement($title, $alt, $class, $id, $reset);
if (!$this->parsedown) {
$this->parsedown = new Parsedown(null, null);
$this->parsedown = new Parsedown(new Excerpts());
}
return $this->parsedown->elementToHtml($element);
+22 -14
View File
@@ -19,6 +19,7 @@ use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Taxonomy;
use Grav\Common\Uri;
use Grav\Common\Utils;
@@ -27,7 +28,6 @@ use Negotiation\Accept;
use Negotiation\Negotiator;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile;
use Symfony\Component\Yaml\Exception\ParseException;
define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
@@ -819,23 +819,31 @@ class Page implements PageInterface
/** @var Config $config */
$config = Grav::instance()['config'];
$defaults = (array)$config->get('system.pages.markdown');
$markdownDefaults = (array)$config->get('system.pages.markdown');
if (isset($this->header()->markdown)) {
$defaults = array_merge($defaults, $this->header()->markdown);
$markdownDefaults = array_merge($markdownDefaults, $this->header()->markdown);
}
// pages.markdown_extra is deprecated, but still check it...
if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) {
if (!isset($markdownDefaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) {
user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED);
$defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra');
$markdownDefaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra');
}
$extra = $markdownDefaults['extra'] ?? false;
$defaults = [
'markdown' => $markdownDefaults,
'images' => $config->get('system.images', [])
];
$excerpts = new Excerpts($this, $defaults);
// Initialize the preferred variant of Parsedown
if ($defaults['extra']) {
$parsedown = new ParsedownExtra($this, $defaults);
if ($extra) {
$parsedown = new ParsedownExtra($excerpts);
} else {
$parsedown = new Parsedown($this, $defaults);
$parsedown = new Parsedown($excerpts);
}
$this->content = $parsedown->text($this->content);
@@ -1397,12 +1405,12 @@ class Page implements PageInterface
return $this->template_format;
}
// Use content negotitation via the `accept:` header
$http_accept = $_SERVER['HTTP_ACCEPT'] ?? false;
// Use content negotiation via the `accept:` header
$http_accept = $_SERVER['HTTP_ACCEPT'] ?? null;
if (is_string($http_accept)) {
$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);
$media_type = $negotiator->getBest($http_accept, $priorities);
@@ -2847,9 +2855,9 @@ class Page implements PageInterface
$result = [];
foreach ((array)$value as $key => $val) {
if (is_int($key)) {
$result = $result + $this->evaluate($val)->toArray();
$result = $result + $this->evaluate($val, $only_published)->toArray();
} else {
$result = $result + $this->evaluate([$key => $val])->toArray();
$result = $result + $this->evaluate([$key => $val], $only_published)->toArray();
}
}
@@ -2930,7 +2938,7 @@ class Page implements PageInterface
case 'page':
case 'self':
$results = new Collection();
$results = $results->addPage($page)->nonModular();
$results = $results->addPage($page);
break;
case 'descendants':
+21 -3
View File
@@ -88,6 +88,13 @@ class Pages
*/
protected $ignore_hidden;
/** @var string */
protected $check_method;
protected $pages_cache_id;
protected $initialized = false;
/**
* @var Types
*/
@@ -98,8 +105,6 @@ class Pages
*/
static protected $home_route;
protected $pages_cache_id;
/**
* Constructor
*
@@ -226,11 +231,20 @@ class Pages
return $this->baseUrl($lang, $absolute) . Uri::filterPath($route);
}
public function setCheckMethod($method)
{
$this->check_method = strtolower($method);
}
/**
* Class initialization. Must be called before using this class.
*/
public function init()
{
if ($this->initialized) {
return;
}
$config = $this->grav['config'];
$this->ignore_files = $config->get('system.pages.ignore_files');
$this->ignore_folders = $config->get('system.pages.ignore_folders');
@@ -240,6 +254,10 @@ class Pages
$this->children = [];
$this->routes = [];
if (!$this->check_method) {
$this->setCheckMethod($config->get('system.cache.check.method', 'file'));
}
$this->buildPages();
}
@@ -947,7 +965,7 @@ class Pages
$taxonomy = $this->grav['taxonomy'];
// how should we check for last modified? Default is by file
switch (strtolower($config->get('system.cache.check.method', 'file'))) {
switch ($this->check_method) {
case 'none':
case 'off':
$hash = 0;
+19 -2
View File
@@ -151,15 +151,32 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
if (\is_string($params)) {
$dispatcher->addListener($eventName, [$this, $params]);
} 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 {
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
*/
+34 -14
View File
@@ -133,12 +133,25 @@ class Plugins extends Iterator
*/
public static function all()
{
$plugins = Grav::instance()['plugins'];
$grav = Grav::instance();
$plugins = $grav['plugins'];
$list = [];
foreach ($plugins as $instance) {
$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) {
$list[$name] = $result;
@@ -185,24 +198,31 @@ class Plugins extends Iterator
$grav = Grav::instance();
$locator = $grav['locator'];
$filePath = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
if (!is_file($filePath)) {
$file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
if (is_file($file)) {
// Local variables available in the file: $grav, $config, $name, $file
$class = include_once $file;
$pluginClassFormat = [
'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
'Grav\\Plugin\\' . Inflector::camelize($name) . 'Plugin'
];
foreach ($pluginClassFormat as $pluginClass) {
if (class_exists($pluginClass)) {
$class = new $pluginClass($name, $grav);
break;
}
}
} else {
$grav['log']->addWarning(
sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clear-cache`", $name)
);
return null;
}
require_once $filePath;
$pluginClassName = 'Grav\\Plugin\\' . ucfirst($name) . 'Plugin';
if (!class_exists($pluginClassName)) {
$pluginClassName = 'Grav\\Plugin\\' . $grav['inflector']->camelize($name) . 'Plugin';
if (!class_exists($pluginClassName)) {
throw new \RuntimeException(sprintf("Plugin '%s' class not found! Try reinstalling this plugin.", $name));
}
}
return new $pluginClassName($name, $grav);
return $class;
}
}
@@ -30,10 +30,13 @@ class RequestProcessor extends ProcessorBase
$request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true));
}
$uri = $request->getUri();
$ext = mb_strtolower(pathinfo($uri->getPath(), PATHINFO_EXTENSION));
$request = $request
->withAttribute('grav', $this->container)
->withAttribute('time', $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME)
->withAttribute('route', Uri::getCurrentRoute())
->withAttribute('route', Uri::getCurrentRoute()->withExtension($ext))
->withAttribute('referrer', $this->container['uri']->referrer());
$event = new RequestHandlerEvent(['request' => $request, 'handler' => $handler]);
@@ -27,9 +27,10 @@ class AccountsServiceProvider implements ServiceProviderInterface
public function register(Container $container)
{
$container['accounts'] = function (Container $container) {
/** @var Debugger $debugger */
$debugger = $container['debugger'];
if ($container['config']->get('system.accounts.type') === 'flex') {
$type = strtolower(defined('GRAV_USER_INSTANCE') ? GRAV_USER_INSTANCE : $container['config']->get('system.accounts.type', 'data'));
if ($type === 'flex') {
/** @var Debugger $debugger */
$debugger = $container['debugger'];
$debugger->addMessage('User Accounts: Flex Directory');
return $this->flexAccounts($container);
}
@@ -46,7 +47,9 @@ class AccountsServiceProvider implements ServiceProviderInterface
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.
return new DataUser\UserCollection(User::class);
@@ -54,7 +57,9 @@ class AccountsServiceProvider implements ServiceProviderInterface
protected function flexAccounts(Container $container)
{
define('GRAV_USER_INSTANCE', 'FLEX');
if (!defined('GRAV_USER_INSTANCE')) {
define('GRAV_USER_INSTANCE', 'FLEX');
}
/** @var Config $config */
$config = $container['config'];
@@ -31,8 +31,8 @@ class RequestServiceProvider implements ServiceProviderInterface
return $creator->fromGlobals();
};
$container['route'] = function() {
return Uri::getCurrentRoute();
};
$container['route'] = $container->factory(function() {
return clone Uri::getCurrentRoute();
});
}
}
@@ -13,6 +13,7 @@ use Grav\Common\Config\Config;
use Grav\Common\Debugger;
use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\Session\Message;
@@ -49,14 +50,17 @@ class SessionServiceProvider implements ServiceProviderInterface
// Activate admin if we're inside the admin path.
$is_admin = false;
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.
$current_route = str_replace(Uri::filterPath($uri->rootUrl(false)), '', parse_url($uri->url(true), PHP_URL_PATH));
// Test to see if path starts with a supported language + admin base
$lang = Utils::pathPrefixedByLangCode($current_route);
$lang_admin_base = '/' . $lang . $admin_base;
// Check no language, simple language prefix (en) and region specific language prefix (en-US).
$pos = strpos($current_route, $base);
if ($pos === 0 || $pos === 3 || $pos === 6) {
if (Utils::startsWith($current_route, $admin_base) || Utils::startsWith($current_route, $lang_admin_base)) {
$cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800);
$enabled = $is_admin = true;
}
+14 -3
View File
@@ -100,7 +100,19 @@ class Themes extends Iterator
}
$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) {
$list[$theme] = $result;
@@ -196,8 +208,7 @@ class Themes extends Iterator
foreach ($themeClassFormat as $themeClass) {
if (class_exists($themeClass)) {
$themeClassName = $themeClass;
$class = new $themeClassName($grav, $config, $name);
$class = new $themeClass($grav, $config, $name);
break;
}
}
@@ -41,6 +41,6 @@ class TwigNodeMarkdown extends Node implements NodeOutputInterface
->write('$lines = explode("\n", $content);' . PHP_EOL)
->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL)
->write('$content = join("\n", $content);' . PHP_EOL)
->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($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('ksort', [$this, 'ksortFilter']),
new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['is_safe' => ['html']]),
new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]),
new \Twig_SimpleFilter('md5', [$this, 'md5Filter']),
new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']),
new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']),
@@ -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
* @return string
@@ -613,12 +613,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
/**
* @param string $string
*
* @param array $context
* @param bool $block Block or Line processing
* @return mixed|string
*/
public function markdownFunction($string, $block = true)
public function markdownFunction($context, $string, $block = true)
{
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)
{
/** @var UserInterface $user */
$user = $this->grav['user'];
/** @var UserInterface|null $user */
$user = $this->grav['user'] ?? null;
if (!$user->authenticated || (isset($user->authorized) && !$user->authorized)) {
if (!$user || !$user->authenticated || (isset($user->authorized) && !$user->authorized)) {
return false;
}
@@ -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 bool $raw
@@ -1154,7 +1156,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
$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);
+3 -3
View File
@@ -1154,7 +1154,7 @@ class Uri
$this->scheme = $env['X-FORWARDED-PROTO'];
} elseif (isset($env['HTTP_CLOUDFRONT_FORWARDED_PROTO'])) {
$this->scheme = $env['HTTP_CLOUDFRONT_FORWARDED_PROTO'];
} elseif (isset($env['REQUEST_SCHEME'])) {
} elseif (isset($env['REQUEST_SCHEME']) && empty($env['HTTPS'])) {
$this->scheme = $env['REQUEST_SCHEME'];
} else {
$https = $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
*
* @param string $element
@@ -1345,7 +1345,7 @@ class Uri
*/
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
if (preg_match('/(' . $valid_page_types . ')/', $extension)) {
@@ -118,4 +118,13 @@ class UserCollection implements UserCollectionInterface
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;
use Grav\Common\Data\Blueprint;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Media;
@@ -21,6 +22,7 @@ use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Storage\FileStorage;
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
use Grav\Framework\Flex\Traits\FlexMediaTrait;
use Grav\Framework\Form\FormFlashFile;
@@ -381,6 +383,31 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
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.
*
@@ -420,6 +447,15 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
*/
public function save()
{
// TODO: We may want to handle this in the storage layer in the future.
$key = $this->getStorageKey();
if (!$key || strpos($key, '@@')) {
$storage = $this->getFlexDirectory()->getStorage();
if ($storage instanceof FileStorage) {
$this->setStorageKey($this->getKey());
}
}
$password = $this->getProperty('password');
if (null !== $password) {
$this->unsetProperty('password');
@@ -431,6 +467,20 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
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
*/
@@ -9,7 +9,7 @@
namespace Grav\Common\User\Interfaces;
interface UserCollectionInterface
interface UserCollectionInterface extends \Countable
{
/**
* Load user account.
@@ -148,12 +148,13 @@ trait UserTrait
// Try looking for provider.
$provider = $this->get('provider');
if (\is_array($provider)) {
if (isset($provider['avatar_url']) && \is_string($provider['avatar_url'])) {
return $provider['avatar_url'];
$provider_options = $this->get($provider);
if (\is_array($provider_options)) {
if (isset($provider_options['avatar_url']) && \is_string($provider_options['avatar_url'])) {
return $provider_options['avatar_url'];
}
if (isset($provider['avatar']) && \is_string($provider['avatar'])) {
return $provider['avatar'];
if (isset($provider_options['avatar']) && \is_string($provider_options['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\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Markdown\Excerpts;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -27,55 +28,103 @@ abstract class Utils
/**
* Simple helper method to make getting a Grav URL easier
*
* @param string $input
* @param string|object $input
* @param bool $domain
* @param bool $fail_gracefully
* @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)) {
$input = '/';
if ((!is_string($input) && !method_exists($input, '__toString')) || !trim($input)) {
if ($fail_gracefully) {
$input = '/';
} else {
return false;
}
}
if (Grav::instance()['config']->get('system.absolute_urls', false)) {
$domain = true;
}
$input = (string)$input;
if (Grav::instance()['uri']->isExternal($input)) {
if (Uri::isExternal($input)) {
return $input;
}
$grav = Grav::instance();
/** @var Uri $uri */
$uri = Grav::instance()['uri'];
$uri = $grav['uri'];
$root = $uri->rootUrl();
$input = Utils::replaceFirstOccurrence($root, '', $input);
$input = ltrim((string)$input, '/');
if (Utils::contains((string)$input, '://')) {
if (static::contains((string)$input, '://')) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$locator = $grav['locator'];
$parts = Uri::parseUrl($input);
if ($parts) {
$resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false);
if (is_array($parts)) {
// Make sure we always have scheme, host, port and path.
$scheme = $parts['scheme'] ?? '';
$host = $parts['host'] ?? '';
$port = $parts['port'] ?? '';
$path = $parts['path'] ?? '';
if (isset($parts['query'])) {
$resource = $resource . '?' . $parts['query'];
if ($scheme && !$port) {
// If URL has a scheme, we need to check if it's one of Grav streams.
if (!$locator->schemeExists($scheme)) {
// If scheme does not exists as a stream, assume it's external.
return str_replace(' ', '%20', $input);
}
// Attempt to find the resource (because of parse_url() we need to put host back to path).
$resource = $locator->findResource("{$scheme}://{$host}{$path}", false);
if ($resource === false) {
if (!$fail_gracefully) {
return false;
}
// Return location where the file would be if it was saved.
$resource = $locator->findResource("{$scheme}://{$host}{$path}", false, true);
}
} elseif ($host || $port) {
// If URL doesn't have scheme but has host or port, it is external.
return str_replace(' ', '%20', $input);
}
if (!empty($resource)) {
// Add query string back.
if (isset($parts['query'])) {
$resource .= '?' . $parts['query'];
}
// Add fragment back.
if (isset($parts['fragment'])) {
$resource .= '#' . $parts['fragment'];
}
}
} else {
// Not a valid URL (can still be a stream).
$resource = $locator->findResource($input, false);
}
} else {
$root = $uri->rootUrl();
if (static::startsWith($input, $root)) {
$input = static::replaceFirstOccurrence($root, '', $input);
}
$input = ltrim($input, '/');
$resource = $input;
}
if (!$fail_gracefully && $resource === false) {
return false;
}
$domain = $domain ?: $grav['config']->get('system.absolute_urls', false);
return rtrim($uri->rootUrl($domain), '/') . '/' . ($resource ?? '');
}
@@ -274,6 +323,35 @@ abstract class Utils
return (object)array_merge((array)$obj1, (array)$obj2);
}
/**
* Lowercase an entire array. Useful when combined with `in_array()`
*
* @param array $a
* @return array|false
*/
public static function arrayLower(Array $a)
{
return array_map('mb_strtolower', $a);
}
/**
* Simple function to remove item/s in an array by value
*
* @param $search array
* @param $value string|array
* @return array
*/
public static function arrayRemoveValue(Array $search, $value)
{
foreach ((array) $value as $val) {
$key = array_search($val, $search);
if ($key !== false) {
unset($search[$key]);
}
}
return $search;
}
/**
* Recursive Merge with uniqueness
*
@@ -974,17 +1052,19 @@ abstract class Utils
*
* @param string $string The path
*
* @return bool
* @return bool|string Either false or the language
*
*/
public static function pathPrefixedByLangCode($string)
{
if (strlen($string) <= 3) {
return false;
$languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
$parts = explode('/', trim($string, '/'));
if (count($parts) > 0 && in_array($parts[0], $languages_enabled)) {
return $parts[0];
}
$languages_enabled = Grav::instance()['config']->get('system.languages.supported', []);
return $string[0] === '/' && $string[3] === '/' && \in_array(substr($string, 1, 2), $languages_enabled, true);
return false;
}
/**
@@ -1054,12 +1134,9 @@ abstract class Utils
*/
private static function generateNonceString($action, $previousTick = false)
{
$username = '';
if (isset(Grav::instance()['user'])) {
$user = Grav::instance()['user'];
$username = $user->username;
}
$grav = Grav::instance();
$username = isset($grav['user']) ? $grav['user']->username : '';
$token = session_id();
$i = self::nonceTick();
@@ -1067,7 +1144,7 @@ abstract class Utils
$i--;
}
return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . $grav['config']->get('security.salt'));
}
/**
@@ -1281,7 +1358,7 @@ abstract class Utils
}
/**
* Get's path based on a token
* Get path based on a token
*
* @param string $path
* @param PageInterface|null $page
@@ -1341,6 +1418,8 @@ abstract class Utils
$post_max_size = static::parseSize(ini_get('post_max_size'));
if ($post_max_size > 0) {
$max_size = $post_max_size;
} else {
$max_size = 0;
}
$upload_max = static::parseSize(ini_get('upload_max_filesize'));
@@ -1388,7 +1467,7 @@ abstract class Utils
$pow = min($pow, count($units) - 1);
// Uncomment one of the following alternatives
$bytes /= pow(1024, $pow);
$bytes /= 1024 ** $pow;
// $bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
@@ -1404,11 +1483,12 @@ abstract class Utils
{
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
$size = preg_replace('/[^0-9\.]/', '', $size);
if ($unit) {
return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
} else {
return round($size);
$size = $size * pow(1024, stripos('bkmgtpezy', $unit[0]));
}
return (int) abs(round($size));
}
/**
@@ -1446,19 +1526,28 @@ abstract class Utils
*
* @param string $string
*
* @param bool $block Block or Line processing
* @param bool $block Block or Line processing
* @param null $page
* @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;
$defaults = Grav::instance()['config']->get('system.pages.markdown');
$grav = Grav::instance();
$page = $page ?? $grav['page'] ?? null;
$defaults = [
'markdown' => $grav['config']->get('system.pages.markdown', []),
'images' => $grav['config']->get('system.images', [])
];
$extra = $defaults['markdown']['extra'] ?? false;
$excerpts = new Excerpts($page, $defaults);
// Initialize the preferred variant of Parsedown
if ($defaults['extra']) {
$parsedown = new ParsedownExtra($page, $defaults);
if ($extra) {
$parsedown = new ParsedownExtra($excerpts);
} else {
$parsedown = new Parsedown($page, $defaults);
$parsedown = new Parsedown($excerpts);
}
if ($block) {
@@ -1477,12 +1566,11 @@ abstract class Utils
* @param int $prefix
*
* @return string
* @throws \InvalidArgumentException if provided an invalid IP
*/
public static function getSubnet($ip, $prefix = 64)
{
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException('Invalid IP: ' . $ip);
return $ip;
}
// Packed representation of IP
@@ -1509,4 +1597,23 @@ abstract class Utils
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')
->setAliases(['clearcache', 'cache-clear'])
->setDescription('Clears Grav cache')
->addOption('invalidate', null, InputOption::VALUE_NONE, 'Invalidate cache, but do not remove any files')
->addOption('purge', null, InputOption::VALUE_NONE, 'If set purge old caches')
->addOption('all', null, InputOption::VALUE_NONE, 'If set will remove all including compiled, twig, doctrine caches')
->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*')
@@ -64,6 +65,8 @@ class ClearCacheCommand extends ConsoleCommand
$remove = 'cache-only';
} elseif ($this->input->getOption('tmp-only')) {
$remove = 'tmp-only';
} elseif ($this->input->getOption('invalidate')) {
$remove = 'invalidate';
} else {
$remove = 'standard';
}
@@ -61,6 +61,15 @@ class YamlLinterCommand extends ConsoleCommand
$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)
+1 -1
View File
@@ -114,7 +114,7 @@ class InfoCommand extends ConsoleCommand
if ($info === 'date') {
$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);
@@ -372,7 +372,7 @@ class InstallCommand extends ConsoleCommand
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
$answer = $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" '- <red>Skipped!</red> ");
@@ -437,7 +437,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
}
/**
* Implementes JsonSerializable interface.
* Implements JsonSerializable interface.
*
* @return array
*/
@@ -84,7 +84,7 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
}
/**
* Implementes JsonSerializable interface.
* Implements JsonSerializable interface.
*
* @return array
*/
@@ -53,11 +53,11 @@ class CsvFormatter extends AbstractFormatter
$header = array_keys(reset($data));
// Encode the field names
$string = implode($delimiter, $header). "\n";
$string = $this->encodeLine($header, $delimiter);
// Encode the data
foreach ($data as $row) {
$string .= implode($delimiter, $row). "\n";
$string .= $this->encodeLine($row, $delimiter);
}
return $string;
@@ -87,4 +87,23 @@ class CsvFormatter extends AbstractFormatter
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);
if ($decoded !== false) {
return $decoded;
return (array) $decoded;
}
}
try {
return YamlParser::parse($data);
return (array) YamlParser::parse($data);
} catch (ParseException $e) {
if ($this->useCompatibleDecoder()) {
return (array) FallbackYamlParser::parse($data);
+1 -1
View File
@@ -91,7 +91,7 @@ class Flex implements \Countable
// Return the directories in the given order.
$directories = [];
foreach ($types as $type) {
$directories = $this->types[$type] ?? null;
$directories[$type] = $this->types[$type] ?? null;
}
return $keepMissing ? $directories : array_filter($directories);
@@ -9,6 +9,7 @@
namespace Grav\Framework\Flex;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Grav\Common\Debugger;
use Grav\Common\Grav;
@@ -125,7 +126,7 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
/**
* @param array $filters
* @return FlexCollectionInterface
* @return FlexCollectionInterface|Collection
*/
public function filterBy(array $filters)
{
@@ -490,7 +491,12 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
$twig = $grav['twig'];
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) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
@@ -315,15 +315,16 @@ class FlexDirectory implements FlexAuthorizeInterface
$gravCache = $grav['cache'];
$config = $this->getConfig('cache.' . $namespace);
if (empty($config['enabled'])) {
throw new \RuntimeException(sprintf('Flex: %s %s cache not enabled', $this->type, $namespace));
}
$timeout = $config['timeout'] ?? 60;
$cache = new MemoryCache('flex-objects-' . $this->getFlexType());
} else {
$timeout = $config['timeout'] ?? 60;
$key = $gravCache->getKey();
if (Utils::isAdminPlugin()) {
$key = substr($key, 0, -1);
$key = $gravCache->getKey();
if (Utils::isAdminPlugin()) {
$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) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
@@ -519,7 +520,9 @@ class FlexDirectory implements FlexAuthorizeInterface
// Store updated rows to the cache.
if ($updated) {
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);
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
@@ -640,7 +643,10 @@ class FlexDirectory implements FlexAuthorizeInterface
/** @var string|FlexIndexInterface $className */
$className = $this->getIndexClass();
$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 {
$cache->set('__keys', $keys);
} catch (InvalidArgumentException $e) {
+4 -3
View File
@@ -20,6 +20,7 @@ use Grav\Framework\Form\Traits\FormTrait;
use Grav\Framework\Route\Route;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\Template;
use Twig\TemplateWrapper;
/**
@@ -82,7 +83,7 @@ class FlexForm implements FlexFormInterface
}
/**
* @return Data|FlexObjectInterface
* @return Data|FlexObjectInterface|object
*/
public function getData()
{
@@ -103,7 +104,7 @@ class FlexForm implements FlexFormInterface
$value = $this->data ? $this->data[$name] : null;
// 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)
@@ -277,7 +278,7 @@ class FlexForm implements FlexFormInterface
/**
* @param string $layout
* @return TemplateWrapper
* @return Template|TemplateWrapper
* @throws LoaderError
* @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.
$previous = null;
foreach (array_reverse($orderings) as $field => $ordering) {
$field = (string)$field;
if ($this->getKeyField() === $field) {
$keys = $this->getKeys();
$search = array_combine($keys, $keys);
$search = array_combine($keys, $keys) ?: [];
} elseif ($field === 'flex_key') {
$search = $this->getFlexKeys();
} else {
@@ -462,7 +463,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
$first = reset($entries);
if ($first) {
$keys = array_keys($first);
$keys = array_combine($keys, $keys);
$keys = array_combine($keys, $keys) ?: [];
} else {
$keys = [];
}
+32 -10
View File
@@ -31,6 +31,7 @@ use Psr\SimpleCache\InvalidArgumentException;
use RocketTheme\Toolbox\Event\Event;
use Twig\Error\LoaderError;
use Twig\Error\SyntaxError;
use Twig\Template;
use Twig\TemplateWrapper;
/**
@@ -162,6 +163,13 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$properties = $properties ?? $this->getFlexDirectory()->getConfig('data.search.fields', []);
if (!$properties) {
foreach ($this->getFlexDirectory()->getConfig('admin.list.fields', []) as $property => $value) {
if (!empty($value['link'])) {
$properties[] = $property;
}
}
}
$weight = 0;
foreach ((array)$properties as $property) {
@@ -273,7 +281,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return (float)$options['ends_with'];
}
if ((!$tested || !empty($options['contains'])) && Utils::contains($value, $search, $options['case_sensitive'] ?? false)) {
return (float)$options['contains'];
return (float)($options['contains'] ?? 1);
}
return 0;
@@ -384,7 +392,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
}
try {
$data = $cache ? $cache->get($key) : null;
$data = $cache && $key ? $cache->get($key) : null;
$block = $data ? HtmlBlock::fromArray($data) : null;
} catch (InvalidArgumentException $e) {
@@ -403,7 +411,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
}
if (!$block) {
$block = HtmlBlock::create($key);
$block = HtmlBlock::create($key ?: null);
$block->setChecksum($checksum);
if ($key === false) {
$block->disableCache();
@@ -427,7 +435,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$block->setContent($output);
try {
$cache && $block->isCached() && $cache->set($key, $block->toArray());
$cache && $key && $block->isCached() && $cache->set($key, $block->toArray());
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
}
@@ -534,7 +542,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]);
$value = reset($result);
$storageKey = key($result);
$storageKey = (string)key($result);
if ($value && $storageKey) {
$this->setStorageKey($storageKey);
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')) {
$this->saveUpdatedMedia();
}
@@ -551,7 +568,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache();
}
} catch (InvalidArgumentException $e) {
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
@@ -579,7 +596,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache();
}
} catch (InvalidArgumentException $e) {
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e);
@@ -729,7 +746,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
}
$grav = Grav::instance();
/** @var Flex $flex */
/** @var Flex|null $flex */
$flex = $grav['flex_objects'] ?? null;
$directory = $flex ? $flex->getDirectory($type) : null;
if (!$directory) {
@@ -800,7 +817,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
/**
* @param string $layout
* @return TemplateWrapper
* @return Template|TemplateWrapper
* @throws LoaderError
* @throws SyntaxError
*/
@@ -812,7 +829,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$twig = $grav['twig'];
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) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
@@ -128,7 +128,7 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface
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
{
$extension = $this->dataFormatter->getDefaultFileExtension();
/** @var string $pattern */
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
$this->dataFolder = $options['folder'];
@@ -192,6 +192,9 @@ class SimpleStorage extends AbstractFilesystemStorage
$list = [];
foreach ($rows as $key => $row) {
if (strpos($key, '@@')) {
$key = $this->getNewKey();
}
$this->data[$key] = $list[$key] = $row;
}
@@ -224,7 +227,12 @@ class SimpleStorage extends AbstractFilesystemStorage
$keys = array_keys($this->data);
$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;
}
@@ -13,6 +13,7 @@ namespace Grav\Framework\Flex\Traits;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
/**
@@ -25,10 +26,11 @@ trait FlexAuthorizeTrait
public function isAuthorized(string $action, string $scope = null, UserInterface $user = null) : bool
{
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
@@ -44,7 +46,11 @@ trait FlexAuthorizeTrait
$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
@@ -49,7 +49,7 @@ trait FlexMediaTrait
*/
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()
{
return $this->getFlexDirectory()->getMediaFolder($this->getStorageKey());
return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : '';
}
/**
@@ -153,6 +153,12 @@ trait FlexMediaTrait
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$path = $media->getPath();
if (!$path) {
$language = $grav['language'];
throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
}
if ($locator->isStream($path)) {
$path = $locator->findResource($path, true, true);
$locator->clearCache($path);
@@ -202,12 +208,16 @@ trait FlexMediaTrait
}
$media = $this->getMedia();
$path = $media->getPath();
if (!$path) {
return;
}
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$targetPath = $media->getPath() . '/' . $dirname;
$targetFile = $media->getPath() . '/' . $filename;
$targetPath = $path . '/' . $dirname;
$targetFile = $path . '/' . $filename;
if ($locator->isStream($targetFile)) {
$targetPath = $locator->findResource($targetPath, true, true);
$targetFile = $locator->findResource($targetFile, true, true);
@@ -229,7 +239,6 @@ trait FlexMediaTrait
}
// Remove Extra Files
foreach (scandir($targetPath, SCANDIR_SORT_NONE) as $file) {
$preg_name = preg_quote($fileParts['filename'], '`');
$preg_ext =preg_quote($fileParts['extension'], '`');
+197 -110
View File
@@ -10,12 +10,18 @@
namespace Grav\Framework\Form;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Form\Interfaces\FormFlashInterface;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class FormFlash implements \JsonSerializable
class FormFlash implements FormFlashInterface
{
/** @var bool */
protected $exists;
/** @var string */
protected $sessionId;
/** @var string */
@@ -26,6 +32,10 @@ class FormFlash implements \JsonSerializable
protected $url;
/** @var array */
protected $user;
/** @var int */
protected $createdTimestamp;
/** @var int */
protected $updatedTimestamp;
/** @var array */
protected $data;
/** @var array */
@@ -34,29 +44,35 @@ class FormFlash implements \JsonSerializable
protected $uploadedFiles;
/** @var string[] */
protected $uploadObjects;
/** @var bool */
protected $exists;
/** @var string */
protected $folder;
/**
* @param string $sessionId
* @return string
* @inheritDoc
*/
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);
/**
* FormFlashObject constructor.
* @param string $sessionId
* @param string $uniqueId
* @param string|null $formName
*/
public function __construct(string $sessionId, string $uniqueId, string $formName = null)
{
$this->sessionId = $sessionId;
$this->uniqueId = $uniqueId;
$args = func_get_args();
$config = [
'session_id' => $args[0],
'unique_id' => $args[1] ?? null,
'form_name' => $args[2] ?? null,
];
}
$this->sessionId = $config['session_id'] ?? 'no-session';
$this->uniqueId = $config['unique_id'] ?? '';
$folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : '');
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder;
$file = $this->getTmpIndex();
$this->exists = $file->exists();
@@ -66,65 +82,58 @@ class FormFlash implements \JsonSerializable
} catch (\Exception $e) {
$data = [];
}
$this->formName = null !== $formName ? $content['form'] ?? '' : '';
$this->formName = $content['form'] ?? $config['form_name'] ?? '';
$this->url = $data['url'] ?? '';
$this->user = $data['user'] ?? null;
$this->updatedTimestamp = $data['timestamps']['updated'] ?? time();
$this->createdTimestamp = $data['timestamps']['created'] ?? $this->updatedTimestamp;
$this->data = $data['data'] ?? null;
$this->files = $data['files'] ?? [];
} else {
$this->formName = $formName;
$this->formName = $config['form_name'] ?? '';
$this->url = '';
$this->createdTimestamp = $this->updatedTimestamp = time();
$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
{
return $this->formName;
}
/**
* @return string
*/
public function getUniqieId(): string
{
return $this->uniqueId;
}
/**
* @return bool
*/
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
* @inheritDoc
*/
public function getUrl(): string
{
@@ -132,18 +141,7 @@ class FormFlash implements \JsonSerializable
}
/**
* @param string $url
* @return $this
*/
public function setUrl(string $url): self
{
$this->url = $url;
return $this;
}
/**
* @return string
* @inheritDoc
*/
public function getUsername(): string
{
@@ -151,7 +149,7 @@ class FormFlash implements \JsonSerializable
}
/**
* @return string
* @inheritDoc
*/
public function getUserEmail(): string
{
@@ -159,40 +157,84 @@ class FormFlash implements \JsonSerializable
}
/**
* @param string|null $username
* @return $this
* @inheritDoc
*/
public function setUserName(string $username = null): self
public function getCreatedTimestamp(): int
{
$this->user['username'] = $username;
return $this;
return $this->createdTimestamp;
}
/**
* @param string|null $email
* @return $this
* @inheritDoc
*/
public function setUserEmail(string $email = null): self
public function getUpdatedTimestamp(): int
{
$this->user['email'] = $email;
return $this;
return $this->updatedTimestamp;
}
/**
* @inheritDoc
*/
public function getData(): ?array
{
return $this->data;
}
/**
* @inheritDoc
*/
public function setData(?array $data): void
{
$this->data = $data;
}
/**
* @param string $field
* @return array
* @inheritDoc
*/
public function exists(): bool
{
return $this->exists;
}
/**
* @inheritDoc
*/
public function save(): self
{
if (!($this->folder && $this->uniqueId)) {
return $this;
}
if ($this->data || $this->files) {
// Only save if there is data or files to be saved.
$file = $this->getTmpIndex();
$file->save($this->jsonSerialize());
$this->exists = true;
} elseif ($this->exists) {
// Delete empty form flash if it exists (it carries no information).
return $this->delete();
}
return $this;
}
/**
* @inheritDoc
*/
public function delete(): self
{
if ($this->folder && $this->uniqueId) {
$this->removeTmpDir();
$this->files = [];
$this->exists = false;
}
return $this;
}
/**
* @inheritDoc
*/
public function getFilesByField(string $field): array
{
@@ -208,8 +250,7 @@ class FormFlash implements \JsonSerializable
}
/**
* @param bool $includeOriginal
* @return array
* @inheritDoc
*/
public function getFilesByFields($includeOriginal = false): array
{
@@ -225,12 +266,7 @@ class FormFlash implements \JsonSerializable
}
/**
* 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
* @inheritDoc
*/
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.
*
* @param string $filename
* @param string $field
* @param array $crop
* @return bool
* @inheritDoc
*/
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.
*
* @param string $name
* @param string $field
* @return bool
* @inheritDoc
*/
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()
{
@@ -332,7 +359,7 @@ class FormFlash implements \JsonSerializable
}
/**
* @return array
* @inheritDoc
*/
public function jsonSerialize(): array
{
@@ -341,17 +368,72 @@ class FormFlash implements \JsonSerializable
'unique_id' => $this->uniqueId,
'url' => $this->url,
'user' => $this->user,
'timestamps' => [
'created' => $this->createdTimestamp,
'updated' => time(),
],
'data' => $this->data,
'files' => $this->files
];
}
/**
* @param string $url
* @return $this
*/
public function setUrl(string $url): self
{
$this->url = $url;
return $this;
}
/**
* @param UserInterface|null $user
* @return $this
*/
public function setUser(UserInterface $user = null)
{
if ($user && $user->username) {
$this->user = [
'username' => $user->username,
'email' => $user->email ?? ''
];
} else {
$this->user = null;
}
return $this;
}
/**
* @param string|null $username
* @return $this
*/
public function setUserName(string $username = null): self
{
$this->user['username'] = $username;
return $this;
}
/**
* @param string|null $email
* @return $this
*/
public function setUserEmail(string $email = null): self
{
$this->user['email'] = $email;
return $this;
}
/**
* @return string
*/
public function getTmpDir(): string
{
return 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
{
$filename = $this->getTmpDir() . '/' . $name;
if ($name && is_file($filename)) {
$tmpDir = $this->getTmpDir();
$filename = $tmpDir ? $tmpDir . '/' . $name : '';
if ($name && $filename && is_file($filename)) {
unlink($filename);
}
}
@@ -377,7 +460,7 @@ class FormFlash implements \JsonSerializable
protected function removeTmpDir(): void
{
$tmpDir = $this->getTmpDir();
if (file_exists($tmpDir)) {
if ($tmpDir && file_exists($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
{
if (!($this->folder && $this->uniqueId)) {
throw new \RuntimeException('Cannot upload files: form flash folder not defined');
}
$field = $field ?: 'undefined';
if (!isset($this->files[$field])) {
$this->files[$field] = [];
@@ -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\Grav;
use Grav\Common\Twig\Twig;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\ContentBlock\HtmlBlock;
use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Session\SessionInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Twig\Error\LoaderError;
@@ -296,12 +298,12 @@ trait FormTrait
public function getButtons(): array
{
return $this->getBlueprint()['form']['buttons'] ?? [];
return $this->getBlueprint()->get('form/buttons') ?? [];
}
public function getTasks(): array
{
return $this->getBlueprint()['form']['tasks'] ?? [];
return $this->getBlueprint()->get('form/tasks') ?? [];
}
abstract public function getBlueprint(): Blueprint;
@@ -336,35 +338,56 @@ trait FormTrait
public function getFlash(): FormFlash
{
if (null === $this->flash) {
/** @var Grav $grav */
$grav = Grav::instance();
$id = null;
$user = $grav['user'] ?? null;
if (isset($user)) {
$rememberState = $this->getBlueprint()->get('form/remember_state');
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();
}
$config = [
'session_id' => $this->getSessionId(),
'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(),
'folder' => $this->getFlashFolder()
];
$this->flash = new FormFlash($id, $this->getUniqueId(), $this->getName());
$this->flash->setUrl($grav['uri']->url)->setUser($user);
}
$this->flash = new FormFlash($config);
$this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
}
return $this->flash;
}
/**
* Get all available form flash objects for this form.
*
* @return FormFlash[]
*/
public function getAllFlashes(): array
{
$folder = $this->getFlashFolder();
if (!$folder || !is_dir($folder)) {
return [];
}
$name = $this->getName();
$list = [];
/** @var \SplFileInfo $file */
foreach (new \FilesystemIterator($folder) as $file) {
$uniqueId = $file->getFilename();
$config = [
'session_id' => $this->getSessionId(),
'unique_id' => $uniqueId,
'form_name' => $name,
'folder' => $this->getFlashFolder()
];
$flash = new FormFlash($config);
if ($flash->exists() && $flash->getFormName() === $name) {
$list[] = $flash;
}
}
return $list;
}
/**
* {@inheritdoc}
* @see FormInterface::render()
@@ -389,11 +412,51 @@ trait FormTrait
return $block;
}
protected function getSessionId(): string
{
/** @var Grav $grav */
$grav = Grav::instance();
/** @var SessionInterface $session */
$session = $grav['session'] ?? null;
return $session ? ($session->getId() ?? '') : '';
}
protected function unsetFlash(): void
{
$this->flash = null;
}
protected function getFlashFolder(): ?string
{
$grav = Grav::instance();
/** @var UserInterface $user */
$user = $grav['user'] ?? null;
$userExists = $user && $user->exists();
$username = $userExists ? $user->username : null;
$mediaFolder = $userExists ? $user->getMediaFolder() : null;
$session = $grav['session'] ?? null;
$sessionId = $session ? $session->getId() : null;
// Fill template token keys/value pairs.
$dataMap = [
'[FORM_NAME]' => $this->getName(),
'[SESSIONID]' => $sessionId ?? '!!',
'[USERNAME]' => $username ?? '!!',
'[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!',
'[ACCOUNT]' => $mediaFolder ?? '!!'
];
$flashFolder = $this->getBlueprint()->get('form/flash_folder', 'tmp://forms/[SESSIONID]');
$path = str_replace(array_keys($dataMap), array_values($dataMap), $flashFolder);
// Make sure we only return valid paths.
return strpos($path, '!!') === false ? rtrim($path, '/') : null;
}
/**
* Set a single error.
*
@@ -577,7 +640,7 @@ trait FormTrait
foreach ($data as $key => &$value) {
if (\is_array($value)) {
$value = $this->jsonDecode($value);
} elseif ($value === '') {
} elseif (trim($value) === '') {
unset($data[$key]);
} else {
$value = json_decode($value, true);
@@ -29,7 +29,7 @@ interface RenderInterface
* @example {% render object layout 'custom' with { variable: 'value' } %}
*
* @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.
* @api
+38 -14
View File
@@ -154,16 +154,11 @@ class Route
* If the parameter exists in both, return Grav parameter.
*
* @param string $param
* @return string|null
* @return string|array|null
*/
public function getParam($param)
{
$value = $this->getGravParam($param);
if ($value === null) {
$value = $this->getQueryParam($param);
}
return $value;
return $this->getGravParam($param) ?? $this->getQueryParam($param);
}
/**
@@ -177,7 +172,7 @@ class Route
/**
* @param string $param
* @return string|null
* @return string|array|null
*/
public function getQueryParam($param)
{
@@ -193,6 +188,7 @@ class Route
public function withRoute($route)
{
$this->route = $route;
return $this;
}
@@ -205,6 +201,7 @@ class Route
public function withRoot($root)
{
$this->root = $root;
return $this;
}
@@ -250,6 +247,25 @@ class Route
return $this->withParam('queryParams', $param, $value);
}
public function withoutParams()
{
return $this->withoutGravParams()->withoutQueryParams();
}
public function withoutGravParams()
{
$this->gravParams = [];
return $this;
}
public function withoutQueryParams()
{
$this->queryParams = [];
return $this;
}
/**
* @return \Grav\Framework\Uri\Uri
*/
@@ -292,22 +308,30 @@ class Route
*/
protected function withParam($type, $param, $value)
{
$oldValue = $this->{$type}[$param] ?? null;
$values = $this->{$type} ?? [];
$oldValue = $values[$param] ?? null;
if ($oldValue === $value) {
return $this;
}
$new = clone $this;
$new = $this->copy();
if ($value === null) {
unset($new->{$type}[$param]);
unset($values[$param]);
} else {
$new->{$type}[$param] = $value;
$values[$param] = $value;
}
$new->{$type} = $values;
return $new;
}
protected function copy()
{
return clone $this;
}
/**
* @param bool $includeRoot
* @return string
@@ -350,8 +374,8 @@ class Route
$this->language = $gravParts['language'];
$this->route = $gravParts['route'];
$this->extension = $gravParts['extension'] ?? '';
$this->gravParams = $gravParts['params'];
$this->queryParams = $parts['query_params'];
$this->gravParams = $gravParts['params'] ?: [];
$this->queryParams = $parts['query_params'] ?: [];
} else {
$this->root = RouteFactory::getRoot();
@@ -84,11 +84,11 @@ class UriPartsFilter
*/
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;
}
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