Compare commits

...

246 Commits

Author SHA1 Message Date
Andy Miller
e2ed3098a3 prepare for beta.7 release 2019-08-30 12:10:53 -06:00
Matias Griese
4e9ca82a0f FlexForm: Fixed some compatibility issues with Form plugin 2019-08-30 15:42:00 +03:00
Matias Griese
f032f310b5 Improved language support 2019-08-30 10:38:27 +03:00
Andy Miller
6b665b112c prepare for b.6 release 2019-08-29 11:48:57 -06:00
Matias Griese
d9dbe5520d Added version support to Flex index file 2019-08-29 15:04:45 +03:00
Matias Griese
5b8674122a Fixed blueprint values for system.accounts.storage 2019-08-29 14:22:14 +03:00
Matias Griese
2e245cd36f Flex improvements 2019-08-29 12:25:06 +03:00
Matias Griese
e36a2ea1b0 FlexFolderStorage: fixed error on deleting file 2019-08-28 18:09:33 +03:00
Matias Griese
53b7c95b0d Flex: Improve copy logic 2019-08-28 14:12:02 +03:00
Matias Griese
46975cca22 Keep interfaces backwards compatible with Grav 1.6 2019-08-28 10:04:08 +03:00
Matias Griese
a418acc32b Fixed unpublished blog posts being displayed on the front-end [#2650] 2019-08-27 11:52:42 +03:00
Matias Griese
eeb35fc521 Prioritize Accounts menu item in the admin menu 2019-08-27 10:58:11 +03:00
Matias Griese
90ca9f9d49 Fixed/improved Flex caching 2019-08-26 22:51:59 +03:00
Matias Griese
ba267a389e Flex: Implemented copy 2019-08-26 18:27:04 +03:00
Matias Griese
0ab99806db Fixed compatibility regression 2019-08-26 10:51:34 +03:00
Matias Griese
c300a3b8f8 JSON: Display trace information only in debug mode 2019-08-26 10:14:01 +03:00
Matias Griese
8480fb68ac Added custom data and flex handlers to all flex objects 2019-08-26 10:07:39 +03:00
Matias Griese
8e82056afa ControllerResponseTrait: Better error response 2019-08-24 12:31:04 +03:00
Matias Griese
3e34d54b9a Added type parameter to Pages::PageTypes() 2019-08-24 12:29:43 +03:00
Matias Griese
9ad7b208ba Blueprint: Added object support for custom handlers 2019-08-24 12:28:31 +03:00
Matias Griese
1fa62d2bdc FlexIndex: Added option to return non-existing entries in the index 2019-08-23 15:13:39 +03:00
Matias Griese
b659c56aec Added ControllerResponseTrait class 2019-08-23 12:53:18 +03:00
Matias Griese
3bd02b95fe Put back cache clear on flex save 2019-08-22 22:04:43 +03:00
Matias Griese
9e5ad84a48 Fixed another issue with flex keys 2019-08-22 20:28:18 +03:00
Matias Griese
40a6c4bf72 Greatly improve Flex Pages performance, memory use 2019-08-22 19:00:55 +03:00
Matias Griese
22acffac5c Update Pages unit tests 2019-08-22 15:16:27 +03:00
Matias Griese
ede749821d Flex Pages: Added missing page events 2019-08-22 10:36:00 +03:00
Matias Griese
f26e518c03 Work around cache clear (needs better solution) 2019-08-21 17:06:12 +03:00
Matias Griese
0f6a517589 Flex: Clear only index cache on save 2019-08-21 16:08:01 +03:00
Matias Griese
3530f4fdef Further improved speed of loading objects in Flex collections, now with cache turned on 2019-08-21 15:52:20 +03:00
Matias Griese
b92476d40d Greatly improved speed of loading Flex collections multiple times 2019-08-21 14:18:35 +03:00
Matias Griese
0d0bb2c229 Better follow protected Object Property interfaces in traits 2019-08-21 12:28:05 +03:00
Matias Griese
de59bad0f8 Keep storage key and timestamp inside FlexObject (allows diff check against stored object) 2019-08-21 12:25:06 +03:00
Matias Griese
72ad49610c Added FlectIndex::loadIndex() with metadata (including timestamp) 2019-08-21 12:22:59 +03:00
Matias Griese
dcd1f3b10d Added object meta caching in Flex FolderStorage 2019-08-21 12:21:34 +03:00
Matias Griese
52cf554ea2 Added support for not instantiating pages, useful to speed up tasks 2019-08-21 10:47:55 +03:00
Andy Miller
f30f6485ba Merge branch 'develop' into 1.7
# Conflicts:
#	composer.json
#	composer.lock
#	system/defines.php
2019-08-20 17:23:04 -06:00
Andy Miller
dd8b503aa0 Merge tag '1.6.15' into develop
Release v1.6.15
2019-08-20 17:22:26 -06:00
Andy Miller
dab30673e0 Merge branch 'release/1.6.15' 2019-08-20 17:22:25 -06:00
Andy Miller
13689c2065 prepare for release 2019-08-20 17:22:16 -06:00
Andy Miller
6e23627f26 update changelog 2019-08-20 17:21:32 -06:00
Andy Miller
7db85cc79c Force Symfony 4.2 2019-08-20 17:06:19 -06:00
Matias Griese
9b6f218f33 Make Flex Pages to work with Header 2019-08-19 20:58:46 +03:00
Matias Griese
829da9ee3a Improved bin/grav yamllinter CLI command by adding an option to find YAML Linting issues from the whole site or custom folder 2019-08-19 13:18:39 +03:00
Matias Griese
033b54104e Merge branch 'develop' of github.com:getgrav/grav into 1.7
# Conflicts:
#	CHANGELOG.md
2019-08-19 10:53:27 +03:00
Matias Griese
e5cedd074b Fixed broken markdown Twig tag [#2635] 2019-08-19 10:46:03 +03:00
Matias Griese
a6741cb761 Fixed broken taxonomies [#2633] 2019-08-19 10:21:24 +03:00
Matias Griese
8cbc2a27cd Fixed enabling PHP Debug Bar causes fatal error in Gantry [#2634] 2019-08-19 10:04:59 +03:00
Matias Griese
5f1639dc63 Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-08-19 09:53:55 +03:00
Daithí Seán Ó Foghlú
ed87faad92 Update robots.txt (#2632)
I have found that Bing/Yahoo/DuckDuckGo, Yandex and Google report crawl errors when using the default robots.txt. Specifically their bots will not crawl the the path '/' or any sub-paths. I agree that the current robots.txt should work and properly implements the specification. However it still does not work.

In my experience explicitly permitting the path '/' by adding the directive Allow: / resolves the issue.

More details can be found in a blog post about the issue here: https://www.dfoley.ie/blog/starting-with-the-indieweb
2019-08-18 11:22:33 -06:00
Andy Miller
8d8b803e66 updated with composer 1.9.0 2019-08-18 10:00:37 -06:00
Andy Miller
e4ed00d84a Merge branch 'develop' into 1.7
# Conflicts:
#	CHANGELOG.md
#	system/defines.php
#	system/router.php
2019-08-18 09:54:50 -06:00
Andy Miller
239f34d40c Merge branch 'release/1.6.14' 2019-08-18 09:52:55 -06:00
Andy Miller
20b9ca56fa Merge tag '1.6.14' into develop
Release v1.6.14
2019-08-18 09:52:55 -06:00
Andy Miller
647ae0fda3 prepare for release 2019-08-18 09:52:45 -06:00
Andy Miller
806dbd9ee5 refactor 2019-08-18 09:51:10 -06:00
Andy Miller
1ab8442630 add fix 2019-08-18 09:50:56 -06:00
Andy Miller
040c34d693 Merge branch 'develop' into 1.7
# Conflicts:
#	system/defines.php
2019-08-16 09:53:44 -06:00
Andy Miller
a2ea6faf4d Merge tag '1.6.13' into develop
Release v1.6.13
2019-08-16 09:53:21 -06:00
Andy Miller
8942aa8afc Merge branch 'develop' into 1.7
# Conflicts:
#	CHANGELOG.md
2019-08-16 07:46:32 -06:00
Matias Griese
256cbe3f12 Added experimental support for Flex Pages (**Flex Objects** plugin required) 2019-08-16 15:53:18 +03:00
Matias Griese
8b31ee173e Added configuration option for Flex Page, enabled experimental options in Admin Plugin 2019-08-16 15:49:58 +03:00
Andy Miller
182970eb78 Fix for bad check on $grav_basedir 2019-08-15 14:24:40 -06:00
Matias Griese
9ed3da3df2 Fixed $page->summary() always striping HTML tags if the summary was set by $page->setSummary() 2019-08-15 19:37:19 +03:00
Matias Griese
14eaa4d00a Moved setSummary() from PageLegacyInterface to PageContentInterface 2019-08-15 19:23:05 +03:00
Matias Griese
e134e3dbd9 Fixed some docblocks 2019-08-15 13:49:30 +03:00
Andy Miller
5bfb168cd7 Don’t override the system config setting 2019-08-14 16:28:06 -06:00
Andy Miller
5aef09a410 prepare for beta release 2019-08-14 16:21:07 -06:00
Andy Miller
76732ab671 Merge branch 'develop' into 1.7
# Conflicts:
#	system/defines.php
2019-08-14 16:20:27 -06:00
Matias Griese
f54d9af758 Fixed regression in non-cached pages showing 404 2019-08-14 23:02:06 +03:00
Matias Griese
f883191d99 FormTrait: better debug messages on what went wrong on form submit 2019-08-14 21:40:47 +03:00
Matias Griese
de5ead78d1 Greatly simplify and generalize Pages::evaluate() method 2019-08-14 14:03:41 +03:00
Matias Griese
44bbdf7e39 Moved collection() and evaluate() logic from Page class into Pages class 2019-08-14 13:22:30 +03:00
Matias Griese
4b4eedf467 FormTrait: better debug messages on what went wrong on form submit 2019-08-14 13:16:33 +03:00
Matias Griese
bb477fd3b1 Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-08-14 11:01:46 +03:00
Matias Griese
758e316a65 Merge remote-tracking branch 'origin/1.7' into 1.7 2019-08-14 11:01:17 +03:00
Matias Griese
2c38e24d00 Make FlexForm::setName() more robust 2019-08-14 11:01:07 +03:00
Andy Miller
ca24e63d22 Merge branch 'develop' into 1.7 2019-08-12 16:06:27 -06:00
Matias Griese
f1909d80db Added $grav->close() method to properly terminate the request with a response 2019-08-12 17:11:39 +03:00
Matias Griese
7718dd7e98 Improve Flex forms for better configurability 2019-08-12 11:50:16 +03:00
Matias Griese
cc66070e85 Move Page::templateFormat() content negotiation logic into Utils::getPageFormat() 2019-08-12 11:40:22 +03:00
Matias Griese
bbdc54b406 Create PageFormInterface and PageFormTrait 2019-08-12 11:37:31 +03:00
Matias Griese
c013f63b26 Create PageCollectionInterface and use it 2019-08-12 11:34:42 +03:00
Andy Miller
aa007badb5 Merge branch 'develop' into 1.7
# Conflicts:
#	system/src/Grav/Common/Twig/TwigExtension.php
2019-08-09 18:29:22 -06:00
Andy Miller
bb2e7a720b Merge branch 'develop' into 1.7 2019-08-09 17:16:41 -06:00
Andy Miller
c36e6abd66 Add language codes method 2019-08-09 16:34:08 -06:00
Andy Miller
c2b1142b7a Merge branch 'develop' into 1.7
# Conflicts:
#	composer.lock
2019-08-09 13:35:39 -06:00
Andy Miller
e03fb200a6 update vendor libs 2019-08-09 13:35:25 -06:00
Matias Griese
a964a34a6f Minor fix to make SimpleStorage to update modified time on save (now for real) 2019-07-30 13:59:23 +03:00
Matias Griese
1c28fd4c4c Minor fix to make SimpleStorage to update modified time on save 2019-07-30 13:54:38 +03:00
Matias Griese
e8529e7d0b Make flex storage classes to return storage metadata along with the data 2019-07-30 13:51:15 +03:00
Matias Griese
a6032af594 Add support for {EXT} in Flex FolderStorage pattern 2019-07-30 13:11:33 +03:00
Matias Griese
ea49415e14 Make FlexDirectory::loadObjects() more robust against missing values 2019-07-26 10:15:11 +03:00
Matias Griese
7b26022f9f Added Language::getPageExtensions() to get full list of supported page language extensions 2019-07-26 10:13:24 +03:00
Matias Griese
443fecfeb6 Added Language::getPageExtensions() to get full list of supported page language extensions 2019-07-25 14:40:38 +03:00
Matias Griese
c3324e3702 Added FlexStorage::getMetaData() to get updated object meta information for listed keys, storage improvements 2019-07-25 14:39:06 +03:00
Matias Griese
9e60408769 Flex form: Undo unique id dependency to form name 2019-07-19 16:11:52 +03:00
Matias Griese
3737bc9371 Flex form: Allow custom form layouts in admin, too (page raw mode) 2019-07-19 12:44:51 +03:00
Matias Griese
3d2360c995 Fix bad check in CSV escaping 2019-07-18 20:44:04 +03:00
Matias Griese
08b8505b6d Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-07-18 16:12:18 +03:00
Andy Miller
9607a99a7d Updated clockwork to 4.0.4 2019-07-16 13:04:40 -06:00
Matias Griese
7e63935001 Deprecated FlexDirectory::update() and FlexDirectory::remove() 2019-07-16 17:40:33 +03:00
Matias Griese
3d767a4d25 Flex objects no longer return temporary key if they do not have one; empty key is returned instead 2019-07-16 17:39:58 +03:00
Matias Griese
ee53e1be6e Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-07-16 10:11:47 +03:00
Andy Miller
676924cce5 Merge branch 'develop' into 1.7 2019-07-12 12:02:46 -06:00
Matias Griese
36a3a95ed9 Added getFlexFeatures() method to return all features that FlexObject or FlexCollection implements 2019-07-12 13:45:02 +03:00
Matias Griese
95c58c8361 FlexDirectory::getObject() can now be called without any parameters to create a new object 2019-07-12 12:40:23 +03:00
Matias Griese
66c17a8f53 Added hasFlexFeature() method to test if FlexObject or FlexCollection implements a given feature 2019-07-12 12:39:05 +03:00
Andy Miller
7172da8ed6 fix 2019-07-11 08:49:54 -06:00
Andy Miller
c55ea919ef Merge branch 'develop' into 1.7 2019-07-11 08:40:33 -06:00
Matias Griese
53216631a6 * Fixed FlexForm to allow multiple form instances with non-existing objects 2019-07-11 15:24:35 +03:00
Matias Griese
69d6b52a0e Fixed Form not to use deleted flash object until the end of the request fixing issues with reset 2019-07-11 15:23:50 +03:00
Andy Miller
ea1e0a76c1 vendor updates 2019-07-09 22:00:43 -06:00
Andy Miller
833fe8b729 Added a new bin/grav server CLI command 2019-07-09 15:54:39 -06:00
Matias Griese
23d508b390 Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-07-09 14:01:45 +03:00
Andy Miller
e8b24479b9 Option to show/hide sensitive data 2019-07-08 15:53:25 -06:00
Matias Griese
1485c23aba Make Route objects immutable 2019-07-04 13:18:43 +03:00
Matias Griese
d43357f366 Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-07-04 11:50:03 +03:00
Matias Griese
c8e5aa05f9 Merge remote-tracking branch 'origin/1.7' into 1.7 2019-07-02 22:01:07 +03:00
Matias Griese
04ccce1f67 Workaround bug in flex forms 2019-07-02 22:00:56 +03:00
Andy Miller
5826821895 Merge branch '1.7' of github.com:getgrav/grav into 1.7 2019-07-02 11:32:15 -06:00
Andy Miller
3ed341304b updated readme 2019-07-02 11:32:10 -06:00
Matias Griese
025e73affd Merge remote-tracking branch 'origin/1.7' into 1.7 2019-07-02 20:29:04 +03:00
Matias Griese
5635ba2bb7 Merge branch 'develop' of github.com:getgrav/grav into 1.7
# Conflicts:
#	CHANGELOG.md
2019-07-02 20:28:55 +03:00
Andy Miller
95637a243c Better support for Symfony local server symfony server:start 2019-07-02 11:23:32 -06:00
Andy Miller
cd417a1509 prepare for release 2019-07-01 14:54:49 -06:00
Andy Miller
631ae3d3d5 Merge branch '1.7' of github.com:getgrav/grav into 1.7
# Conflicts:
#	CHANGELOG.md
2019-07-01 13:45:42 -06:00
Andy Miller
7c34224304 Merge branch 'develop' into 1.7
# Conflicts:
#	CHANGELOG.md
2019-07-01 13:45:03 -06:00
Matias Griese
6062e47377 Merge branch 'develop' of github.com:getgrav/grav into 1.7
# Conflicts:
#	CHANGELOG.md
2019-07-01 22:26:31 +03:00
Matias Griese
a94abb4fb2 Added configuration option to set fallback content languages individually for every language 2019-07-01 14:46:10 +03:00
Matias Griese
8ab317b49a Merge remote-tracking branch 'origin/1.7' into 1.7 2019-06-29 14:09:57 +03:00
Matias Griese
44dda3d607 Fixed Language::getFallbackPageExtensions() logic to what it should have been 2019-06-29 14:09:46 +03:00
Andy Miller
75ed986437 Updated vendor libs 2019-06-28 14:48:12 -06:00
Andy Miller
3ac785b9ce Fix for changed location of phpdebug CSS 2019-06-28 14:47:13 -06:00
Matias Griese
de367e1558 FIx wrong nesting level in system.yamlL file 2019-06-28 20:52:15 +03:00
Matias Griese
4fead303d1 Changelog update 2019-06-28 15:39:44 +03:00
Matias Griese
41898af46f Merge branch 'feature/multilang' into 1.7
# Conflicts:
#	CHANGELOG.md
#	system/blueprints/config/system.yaml
2019-06-28 15:30:41 +03:00
Matias Griese
fe8833876c Changelog update 2019-06-28 15:27:27 +03:00
Matias Griese
e116998914 Added new system.debugger.censored configuration option to hide potentially sensitive information 2019-06-28 15:26:31 +03:00
Matias Griese
053f96dec1 Make Debugger::addException() to accept \Throwable 2019-06-28 13:39:04 +03:00
Matias Griese
94494c3c96 Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-06-28 13:37:39 +03:00
Matias Griese
5afae3c3f2 Merge branch '1.7' of github.com:getgrav/grav into 1.7 2019-06-27 18:11:58 +03:00
Matias Griese
eed3d84a10 Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-06-27 18:11:24 +03:00
Matias Griese
e7b996104f Merge branches 'develop' and 'feature/multilang' of github.com:getgrav/grav into feature/multilang
# Conflicts:
#	CHANGELOG.md
2019-06-27 18:10:36 +03:00
Matias Griese
3f176c1924 Changelog update 2019-06-27 17:59:05 +03:00
Matias Griese
91d20d8840 Changelog update 2019-06-27 14:47:05 +03:00
Matias Griese
6998505e3c Merge branch 'develop' of github.com:getgrav/grav into feature/multilang
# Conflicts:
#	CHANGELOG.md
2019-06-27 14:46:28 +03:00
Matias Griese
ffa2e0a6f6 Added new configuration option system.languages.include_default_lang_file_extension to keep default language in .md files if set to false 2019-06-27 14:44:35 +03:00
Matias Griese
b54e4fb71b Better form value handling for page name 2019-06-27 13:49:31 +03:00
Matias Griese
8f391d327a Fixed another bug in Page::untranslatedLanguages() 2019-06-27 13:01:11 +03:00
Matias Griese
e0e29f468b Fixed Language::getFallbackPageExtensions() to append .md file after the default language extension 2019-06-27 12:06:07 +03:00
Andy Miller
89b9cc5367 Merge branch 'develop' into 1.7 2019-06-26 09:06:19 -06:00
Matias Griese
f6c30cbeae Fixed .md page to be assigned to the default language and to be listed in translated/untranslated page list 2019-06-26 10:18:14 +03:00
Matias Griese
cca7b6b1d4 Minor cleanup in InitializeProcessor 2019-06-25 10:45:59 +03:00
Andy Miller
c765787102 vendor updates 2019-06-24 17:43:26 -06:00
Andy Miller
dff3872b43 prepare for release 2019-06-24 17:41:20 -06:00
Andy Miller
3c91cea232 Updated changelog 2019-06-24 16:59:03 -06:00
Andy Miller
a2e9c013ad Fixed Clockwork on Windows machines 2019-06-24 16:58:46 -06:00
Andy Miller
dd82ab45bc updated changelog 2019-06-24 16:28:33 -06:00
Andy Miller
46b710b435 Fix for clockwork in subdomains - Don’t rely on base_uri 2019-06-24 16:26:37 -06:00
Matias Griese
a6d3e1ee8e Merge branches '1.7' and 'develop' of github.com:getgrav/grav into 1.7
# Conflicts:
#	CHANGELOG.md
2019-06-24 20:41:28 +03:00
Andy Miller
2b29b17044 Prepare for release 2019-06-21 15:20:27 -06:00
Andy Miller
5316f0f28c Merge branch 'develop' into 1.7
# Conflicts:
#	system/defines.php
2019-06-21 15:19:50 -06:00
Matias Griese
ac4d6cc8d0 Merge branches '1.7' and 'develop' of github.com:getgrav/grav into 1.7 2019-06-21 23:00:14 +03:00
Andy Miller
565947e074 Tweak clockwork message #2558 2019-06-20 21:07:04 -06:00
Andy Miller
25d1767e6c updated changelog 2019-06-20 19:48:04 -06:00
Andy Miller
c079f9b95b Merge branch 'develop' into 1.7 2019-06-20 19:46:26 -06:00
Matias Griese
20cfb45c14 Fixed error if user has no form flashes 2019-06-20 21:18:44 +03:00
Matias Griese
5314558a8e Merge branches '1.7' and 'develop' of github.com:getgrav/grav into 1.7 2019-06-20 21:12:31 +03:00
Matias Griese
02b93d510a Merge branch 'develop' of github.com:getgrav/grav into 1.7
# Conflicts:
#	system/src/Grav/Common/Page/Pages.php
2019-06-20 20:26:49 +03:00
Matias Griese
8dfb0ca993 Merge branch 'develop' of github.com:getgrav/grav into 1.7 2019-06-20 01:05:07 +03:00
Matias Griese
574df5cf80 Merge branches '1.7' and 'develop' of github.com:getgrav/grav into 1.7
# Conflicts:
#	CHANGELOG.md
2019-06-19 21:52:28 +03:00
Andy Miller
34dcd8c346 prepare for beta release 2019-06-14 14:45:29 -06:00
Andy Miller
5079077e1d Merge branch 'develop' into 1.7
# Conflicts:
#	CHANGELOG.md
#	system/defines.php
2019-06-14 14:43:00 -06:00
Andy Miller
1f120a0127 set to beta.1 2019-06-14 13:43:35 -06:00
Andy Miller
fe05c9b87b clockwork debugger by default 2019-06-14 13:43:24 -06:00
Andy Miller
f795a634b5 minor vendor updates 2019-06-14 13:43:13 -06:00
Andy Miller
888b93926c updated changelog 2019-06-14 13:42:47 -06:00
Matias Griese
687f29f912 Profiler: Remove even more noise 2019-06-14 15:17:15 +03:00
Matias Griese
273bc9d970 Profiler: remove some noise 2019-06-14 15:12:48 +03:00
Matias Griese
d593d5a392 Clockwork: Order profiler calls by the time used, some extra filtering 2019-06-14 15:00:17 +03:00
Matias Griese
18fb688cde Clockwork: Added xhprof profiler tab 2019-06-14 14:43:27 +03:00
Matias Griese
d8fccb0edd Clockwork: Added call traces to deprecated messages 2019-06-14 13:48:45 +03:00
Andy Miller
5843347c46 tidy up 2019-06-13 10:04:08 -06:00
Andy Miller
fedb0625b8 z-index fix 2019-06-06 12:09:04 -06:00
Andy Miller
72d012e401 minified 2 2019-06-04 20:11:41 -06:00
Andy Miller
8d77b50055 optimized clockwork css 2019-06-04 20:09:59 -06:00
Andy Miller
42eefbd34c Reworked debugger assets a little 2019-06-04 17:24:56 -06:00
Andy Miller
e5e5bf1bd8 minor twig update 2019-06-04 14:14:16 -06:00
Andy Miller
01ec334127 Merge branch '1.7' into feature/clockwork-twig 2019-06-04 14:07:43 -06:00
Matias Griese
916469a903 Fixed missing timer titles in clockwork 2019-06-04 10:09:57 +03:00
Matias Griese
55f205c801 Composer update 2019-06-04 10:03:54 +03:00
Matias Griese
8a3fc57cd8 Merge branch '1.7' into feature/clockwork-twig
# Conflicts:
#	composer.lock
2019-06-04 10:01:47 +03:00
Andy Miller
6cd1cca4fc Accidentally commited Twig 2 2019-06-03 15:32:28 -06:00
Andy Miller
1c9926ff38 Roll back to Symfony 4.2 until we have some deprecated errors sorted 2019-06-03 15:31:15 -06:00
Andy Miller
638477b06d Roll back to Symfony 4.2 until we have some deprecated errors sorted 2019-06-03 15:31:04 -06:00
Andy Miller
7e3ca73b0e removed unused use 2019-06-03 10:49:25 -06:00
Andy Miller
57a3a20868 Merge branch '1.7' into feature/clockwork-twig
# Conflicts:
#	system/src/Grav/Common/Debugger.php
2019-06-03 10:49:10 -06:00
Matias Griese
4e45c5be95 Properly handle termination in twig redirect and if theme does not exist 2019-06-03 13:05:15 +03:00
Matias Griese
ca89156c4f Added $grav->exit() method to properly terminate the request with a response 2019-06-03 12:10:43 +03:00
Matias Griese
dc5390b3dc Some code cleanup (debugger) 2019-06-03 11:14:22 +03:00
Matias Griese
0e8c7eae99 Added PHP profiling support for redirect responses 2019-06-03 11:08:47 +03:00
Matias Griese
115bdb7e10 Processor cleanup, moved logRequest() and debuggerRequest() into Debugger class 2019-06-03 10:44:10 +03:00
Matias Griese
9738c55633 Clockwork: Added request logging for Grav redirects 2019-06-03 10:29:59 +03:00
Matias Griese
23921c1a35 Some phpstan level 3 fixes 2019-06-03 08:53:25 +03:00
Andy Miller
1f3547b15b Merge branch '1.7' into feature/clockwork-twig 2019-06-02 14:23:40 -06:00
Matias Griese
6974a24669 Some phpstan fixes 2019-06-02 20:30:56 +03:00
Matias Griese
2cf35ec2c7 Composer update, fixed deprecated Twig (1.40 compatible) 2019-06-02 15:19:21 +03:00
Andy Miller
d21bb6b8c7 Merge branch '1.7' into feature/clockwork-twig 2019-05-31 10:59:12 -06:00
Matias Griese
9b8b480c8c Added basic overridable support for the fields 2019-05-31 13:39:23 +03:00
Matias Griese
ac654d56d0 Improved debugger message support 2019-05-31 13:24:06 +03:00
Matias Griese
db9e1a197e Minor fix to support toggleable on required form fields 2019-05-31 13:22:49 +03:00
Matias Griese
19bb9a6966 Regression: Fixed missing timeline name in debugbar 2019-05-30 14:44:02 +03:00
Matias Griese
b7a1c7b72a Added support for Tideways XHProf PHP Extension for profiling requests 2019-05-30 11:59:26 +03:00
Andy Miller
042486bc73 Merge branch 'feature/clockwork-twig' of github.com:getgrav/grav into feature/clockwork-twig 2019-05-30 05:43:48 +02:00
Andy Miller
42cc1c6b01 tweaked arrows 2019-05-30 00:06:45 +02:00
Matias Griese
407d2c8323 Merge branch 'feature/clockwork-twig' of github.com:getgrav/grav into feature/clockwork-twig 2019-05-29 13:54:18 +03:00
Matias Griese
8ab14c0639 Merge branch '1.7' of github.com:getgrav/grav into feature/clockwork-twig 2019-05-29 13:54:07 +03:00
Matias Griese
48c281024f Fixed Twig 2.10 errors 2019-05-29 13:53:29 +03:00
Andy Miller
f7e1bec0cf Very rought first working profiling 2019-05-29 12:33:24 +02:00
Matias Griese
41c7973fc7 Merge branches '1.7' and 'feature/clockwork-twig' of github.com:getgrav/grav into feature/clockwork-twig 2019-05-29 12:07:49 +03:00
Andy Miller
bc93e70d11 Force Twig 2.0 for now (testing and compatibility) 2019-05-29 10:17:59 +02:00
Andy Miller
320ab41435 Initial twig profiler integration..no output yet 2019-05-29 10:16:33 +02:00
Andy Miller
7213c393fd Force Twig 2.0 for now (testing and compatibility) 2019-05-29 10:16:03 +02:00
Andy Miller
6caadc8396 Merge branch '1.7' into feature/clockwork-twig 2019-05-29 09:48:32 +02:00
Andy Miller
e0d1385061 Added default to Utils::param() 2019-05-28 19:52:08 +02:00
Andy Miller
e1eed973a2 field sizes 2019-05-27 19:33:18 +03:00
Andy Miller
f4645fc77e Debugbar/Clockwork Toggle. 2019-05-27 19:20:21 +03:00
Andy Miller
136ec07450 1.7 version in defines 2019-05-27 14:54:16 +03:00
Andy Miller
e01605de55 Merge branch 'feature/clockwork' into feature/clockwork-twig 2019-05-27 13:45:16 +03:00
Andy Miller
5cc48c4e01 initial stuff 2019-05-27 13:45:00 +03:00
Matias Griese
5e2545c606 Improve debugger to have less impact when it's not enabled 2019-05-27 13:42:43 +03:00
Andy Miller
0fd2627cea Merge branch 'feature/clockwork' of github.com:getgrav/grav into feature/clockwork 2019-05-27 11:58:37 +03:00
Andy Miller
cfcd955cc2 Fixed regresssion issue of Utils::Url() not returning false on failure #2524 2019-05-27 11:58:31 +03:00
Matias Griese
7d59b69709 Debugger/Clockwork: add configuration, plugins and streams to the log 2019-05-27 11:26:34 +03:00
Matias Griese
e8eb1d9a44 Debugger: Fixed error if xdebug isn't installed 2019-05-26 17:25:59 +03:00
Andy Miller
2117748f18 updated composer.lock 2019-05-26 17:05:12 +03:00
Matias Griese
e76733e8c3 Optimization: Initialize debugbar only after the configuration has been loaded
Optimization: Combine some early Grav processors into a single one
2019-05-26 16:55:24 +03:00
Matias Griese
2da5f90e9c Merge branch 'develop' of github.com:getgrav/grav into feature/clockwork
# Conflicts:
#	CHANGELOG.md
2019-05-26 16:48:15 +03:00
Matias Griese
81b100cd3b Merge remote-tracking branch 'origin/feature/clockwork' into feature/clockwork 2019-05-26 16:42:56 +03:00
Matias Griese
19be8f5104 Fixed some phpstan level 2 issues 2019-05-26 16:42:36 +03:00
Matias Griese
ad64a9d857 Fixed reutrn type of processMediaActions 2019-05-26 16:24:58 +03:00
Andy Miller
331f416e36 Fixed bitwise operator in TwigExtension::exifFunc() #2518 2019-05-26 11:37:18 +03:00
Matias Griese
3a48c79b21 Merge branch 'develop' of github.com:getgrav/grav into feature/clockwork 2019-05-24 08:39:35 +03:00
Matias Griese
0c66de25c4 Improve clockwork debug messages, make it easier to disable debugbar or clockwork 2019-05-23 23:12:55 +03:00
Matias Griese
0bd227c22d Add basic clockwork support 2019-05-23 15:39:44 +03:00
115 changed files with 5987 additions and 3212 deletions

View File

@@ -1,11 +1,118 @@
# v1.7.0-beta.7
## 08/30/2019
1. [](#improved)
* Improved language support
1. [](#bugfix)
* `FlexForm`: Fixed some compatibility issues with Form plugin
# v1.7.0-beta.6
## 08/29/2019
1. [](#new)
* Added experimental support for `Flex Pages` (**Flex Objects** plugin required)
1. [](#improved)
* Improved `bin/grav yamllinter` CLI command by adding an option to find YAML Linting issues from the whole site or custom folder
* Added support for not instantiating pages, useful to speed up tasks
* Greatly improved speed of loading Flex collections
1. [](#bugfix)
* Fixed `$page->summary()` always striping HTML tags if the summary was set by `$page->setSummary()`
* Fixed `Flex->getObject()` when using Flex Key
* Grav 1.7: Fixed enabling PHP Debug Bar causes fatal error in Gantry [#2634](https://github.com/getgrav/grav/issues/2634)
* Grav 1.7: Fixed broken taxonomies [#2633](https://github.com/getgrav/grav/issues/2633)
* Grav 1.7: Fixed unpublished blog posts being displayed on the front-end [#2650](https://github.com/getgrav/grav/issues/2650)
# v1.7.0-beta.5
## 08/11/2019
1. [](#new)
* Added a new `bin/grav server` CLI command to easily run Symfony or PHP built-in webservers
* Added `hasFlexFeature()` method to test if `FlexObject` or `FlexCollection` implements a given feature
* Added `getFlexFeatures()` method to return all features that `FlexObject` or `FlexCollection` implements
* Deprecated `FlexDirectory::update()` and `FlexDirectory::remove()`
* Added `FlexStorage::getMetaData()` to get updated object meta information for listed keys
* Added `Language::getPageExtensions()` to get full list of supported page language extensions
* Added `$grav->close()` method to properly terminate the request with a response
* Added `Pages::getCollection()` method
1. [](#improved)
* Better support for Symfony local server `symfony server:start`
* Make `Route` objects immutable
* `FlexDirectory::getObject()` can now be called without any parameters to create a new object
* Flex objects no longer return temporary key if they do not have one; empty key is returned instead
* Updated vendor libraries
* Moved `collection()` and `evaluate()` logic from `Page` class into `Pages` class
1. [](#bugfix)
* Fixed `Form` not to use deleted flash object until the end of the request fixing issues with reset
* Fixed `FlexForm` to allow multiple form instances with non-existing objects
* Fixed `FlexObject` search by using `key`
* Grav 1.7: Fixed clockwork messages with arrays and objects
# v1.7.0-beta.4
## 07/01/2019
1. [](#new)
* Updated with Grav 1.6.12 features, improvements & fixes
* Added new configuration option `system.debugger.censored` to hide potentially sensitive information
* Added new configuration option `system.languages.include_default_lang_file_extension` to keep default language in `.md` files if set to `false`
* Added configuration option to set fallback content languages individually for every language
1. [](#improved)
* Updated Vendor libraries
1. [](#bugfix)
* Fixed `.md` page to be assigned to the default language and to be listed in translated/untranslated page list
* Fixed `Language::getFallbackPageExtensions()` to fall back only to default language instead of going through all languages
* Fixed `Language::getFallbackPageExtensions()` returning wrong file extensions when passing custom page extension
# v1.7.0-beta.3
## 06/24/2019
1. [](#bugfix)
* Fixed Clockwork on Windows machines
* Fixed parent field issues on Windows machines
* Fixed unreliable Clockwork calls in sub-folders
# v1.7.0-beta.2
## 06/21/2019
1. [](#new)
* Updated with Grav 1.6.11 fixes
1. [](#improved)
* Updated the Clockwork text
# v1.7.0-beta.1
## 06/14/2019
1. [](#new)
* Added support for [Clockwork](https://underground.works/clockwork) developer tools (now default debugger)
* Added support for [Tideways XHProf](https://github.com/tideways/php-xhprof-extension) PHP Extension for profiling method calls
* Added Twig profiling for Clockwork debugger
* Updated Symfony Components to 4.3
* Added support for Twig 2.11 (compatible with Twig 1.40+)
* Optimization: Initialize debugbar only after the configuration has been loaded
* Optimization: Combine some early Grav processors into a single one
# 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 # v1.6.13
## 08/12/2019 ## 08/16/2019
1. [](#bugfix) 1. [](#bugfix)
* Regression fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627) * Regression fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627)
# v1.6.12 # v1.6.12
## 08/11/2019 ## 08/14/2019
1. [](#new) 1. [](#new)
* Added support for custom `FormFlash` save locations * Added support for custom `FormFlash` save locations

View File

@@ -63,5 +63,6 @@ $app->addCommands(array(
new \Grav\Console\Cli\SecurityCommand(), new \Grav\Console\Cli\SecurityCommand(),
new \Grav\Console\Cli\LogViewerCommand(), new \Grav\Console\Cli\LogViewerCommand(),
new \Grav\Console\Cli\YamlLinterCommand(), new \Grav\Console\Cli\YamlLinterCommand(),
new \Grav\Console\Cli\ServerCommand(),
)); ));
$app->run(); $app->run();

View File

@@ -24,14 +24,14 @@
"kodus/psr7-server": "*", "kodus/psr7-server": "*",
"nyholm/psr7": "^1.0", "nyholm/psr7": "^1.0",
"twig/twig": "~1.40", "twig/twig": "~1.0",
"erusev/parsedown": "1.6.4", "erusev/parsedown": "1.6.4",
"erusev/parsedown-extra": "~0.7", "erusev/parsedown-extra": "~0.7",
"symfony/yaml": "~4.2", "symfony/yaml": "~4.2",
"symfony/console": "~4.2", "symfony/console": "~4.2.0",
"symfony/event-dispatcher": "~4.2", "symfony/event-dispatcher": "~4.2.0",
"symfony/var-dumper": "~4.2", "symfony/var-dumper": "~4.2.0",
"symfony/process": "~4.2", "symfony/process": "~4.2.0",
"doctrine/cache": "^1.8", "doctrine/cache": "^1.8",
"doctrine/collections": "^1.5", "doctrine/collections": "^1.5",
"guzzlehttp/psr7": "^1.4", "guzzlehttp/psr7": "^1.4",
@@ -50,7 +50,8 @@
"composer/ca-bundle": "^1.0", "composer/ca-bundle": "^1.0",
"dragonmantank/cron-expression": "^1.2", "dragonmantank/cron-expression": "^1.2",
"phive/twig-extensions-deferred": "^1.0", "phive/twig-extensions-deferred": "^1.0",
"willdurand/negotiation": "^2.3" "willdurand/negotiation": "^2.3",
"itsgoingd/clockwork": "~4.0"
}, },
"require-dev": { "require-dev": {
"codeception/codeception": "^2.4", "codeception/codeception": "^2.4",
@@ -77,6 +78,10 @@
{ {
"type": "vcs", "type": "vcs",
"url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator" "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator"
},
{
"type": "vcs",
"url": "https://github.com/itsgoingd/clockwork"
} }
], ],
"autoload": { "autoload": {

282
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f9429e7cd2e75a232f968b01a1024983", "content-hash": "fe6ec382807a811a027202be339f8384",
"packages": [ "packages": [
{ {
"name": "antoligy/dom-string-iterators", "name": "antoligy/dom-string-iterators",
@@ -286,9 +286,9 @@
"authors": [ "authors": [
{ {
"name": "Jesse G. Donat", "name": "Jesse G. Donat",
"role": "Developer",
"email": "donatj@gmail.com", "email": "donatj@gmail.com",
"homepage": "http://donatstudios.com", "homepage": "http://donatstudios.com"
"role": "Developer"
} }
], ],
"description": "Lightning fast, minimalist PHP UserAgent string parser.", "description": "Lightning fast, minimalist PHP UserAgent string parser.",
@@ -662,6 +662,67 @@
], ],
"time": "2019-07-01T23:21:34+00:00" "time": "2019-07-01T23:21:34+00:00"
}, },
{
"name": "itsgoingd/clockwork",
"version": "v4.0.7",
"source": {
"type": "git",
"url": "https://github.com/itsgoingd/clockwork.git",
"reference": "aa4c909b74fc5eb69367266bfad5a592bcc98b63"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/aa4c909b74fc5eb69367266bfad5a592bcc98b63",
"reference": "aa4c909b74fc5eb69367266bfad5a592bcc98b63",
"shasum": ""
},
"require": {
"php": ">=5.5",
"psr/log": "1.*"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Clockwork\\Support\\Laravel\\ClockworkServiceProvider"
],
"aliases": {
"Clockwork": "Clockwork\\Support\\Laravel\\Facade"
}
}
},
"autoload": {
"psr-4": {
"Clockwork\\": "Clockwork/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "itsgoingd",
"email": "itsgoingd@luzer.sk",
"homepage": "https://twitter.com/itsgoingd"
}
],
"description": "php dev tools integrated to your browser",
"homepage": "https://underground.works/clockwork",
"keywords": [
"debugging",
"devtools",
"laravel",
"logging",
"lumen",
"profiling",
"slim"
],
"support": {
"source": "https://github.com/itsgoingd/clockwork/tree/v4.0.7",
"issues": "https://github.com/itsgoingd/clockwork/issues"
},
"time": "2019-08-07T12:40:49+00:00"
},
{ {
"name": "kodus/psr7-server", "name": "kodus/psr7-server",
"version": "1.0.1", "version": "1.0.1",
@@ -757,15 +818,15 @@
"authors": [ "authors": [
{ {
"name": "Craig Duncan", "name": "Craig Duncan",
"role": "Developer",
"email": "git@duncanc.co.uk", "email": "git@duncanc.co.uk",
"homepage": "https://github.com/duncan3dc", "homepage": "https://github.com/duncan3dc"
"role": "Developer"
}, },
{ {
"name": "Joe Tannenbaum", "name": "Joe Tannenbaum",
"role": "Developer",
"email": "hey@joe.codes", "email": "hey@joe.codes",
"homepage": "http://joe.codes/", "homepage": "http://joe.codes/"
"role": "Developer"
} }
], ],
"description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.", "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.",
@@ -822,9 +883,9 @@
"authors": [ "authors": [
{ {
"name": "Matthias Mullie", "name": "Matthias Mullie",
"role": "Developer",
"email": "minify@mullie.eu", "email": "minify@mullie.eu",
"homepage": "http://www.mullie.eu", "homepage": "http://www.mullie.eu"
"role": "Developer"
} }
], ],
"description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.", "description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.",
@@ -872,9 +933,9 @@
"authors": [ "authors": [
{ {
"name": "Matthias Mullie", "name": "Matthias Mullie",
"role": "Developer",
"email": "pathconverter@mullie.eu", "email": "pathconverter@mullie.eu",
"homepage": "http://www.mullie.eu", "homepage": "http://www.mullie.eu"
"role": "Developer"
} }
], ],
"description": "Relative path converter", "description": "Relative path converter",
@@ -990,8 +1051,8 @@
"authors": [ "authors": [
{ {
"name": "Tom Van Herreweghe", "name": "Tom Van Herreweghe",
"homepage": "http://theanalogguy.be", "role": "Developer",
"role": "Developer" "homepage": "http://theanalogguy.be"
} }
], ],
"description": "Object-Oriented EXIF parsing", "description": "Object-Oriented EXIF parsing",
@@ -1825,27 +1886,25 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v4.3.3", "version": "v4.2.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "8b0ae5742ce9aaa8b0075665862c1ca397d1c1d9" "reference": "fc2e274aade6567a750551942094b2145ade9b6c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/8b0ae5742ce9aaa8b0075665862c1ca397d1c1d9", "url": "https://api.github.com/repos/symfony/console/zipball/fc2e274aade6567a750551942094b2145ade9b6c",
"reference": "8b0ae5742ce9aaa8b0075665862c1ca397d1c1d9", "reference": "fc2e274aade6567a750551942094b2145ade9b6c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"symfony/polyfill-mbstring": "~1.0", "symfony/contracts": "^1.0",
"symfony/polyfill-php73": "^1.8", "symfony/polyfill-mbstring": "~1.0"
"symfony/service-contracts": "^1.1"
}, },
"conflict": { "conflict": {
"symfony/dependency-injection": "<3.4", "symfony/dependency-injection": "<3.4",
"symfony/event-dispatcher": "<4.3",
"symfony/process": "<3.3" "symfony/process": "<3.3"
}, },
"provide": { "provide": {
@@ -1855,10 +1914,9 @@
"psr/log": "~1.0", "psr/log": "~1.0",
"symfony/config": "~3.4|~4.0", "symfony/config": "~3.4|~4.0",
"symfony/dependency-injection": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0",
"symfony/event-dispatcher": "^4.3", "symfony/event-dispatcher": "~3.4|~4.0",
"symfony/lock": "~3.4|~4.0", "symfony/lock": "~3.4|~4.0",
"symfony/process": "~3.4|~4.0", "symfony/process": "~3.4|~4.0"
"symfony/var-dumper": "^4.3"
}, },
"suggest": { "suggest": {
"psr/log": "For using the console logger", "psr/log": "For using the console logger",
@@ -1869,7 +1927,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "4.3-dev" "dev-master": "4.2-dev"
} }
}, },
"autoload": { "autoload": {
@@ -1896,7 +1954,7 @@
], ],
"description": "Symfony Console Component", "description": "Symfony Console Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-07-24T17:13:59+00:00" "time": "2019-07-24T17:13:20+00:00"
}, },
{ {
"name": "symfony/contracts", "name": "symfony/contracts",
@@ -1977,36 +2035,30 @@
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v4.3.3", "version": "v4.2.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/event-dispatcher.git", "url": "https://github.com/symfony/event-dispatcher.git",
"reference": "212b020949331b6531250584531363844b34a94e" "reference": "852548c7c704f14d2f6700c8d872a05bd2028732"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/212b020949331b6531250584531363844b34a94e", "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/852548c7c704f14d2f6700c8d872a05bd2028732",
"reference": "212b020949331b6531250584531363844b34a94e", "reference": "852548c7c704f14d2f6700c8d872a05bd2028732",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"symfony/event-dispatcher-contracts": "^1.1" "symfony/contracts": "^1.0"
}, },
"conflict": { "conflict": {
"symfony/dependency-injection": "<3.4" "symfony/dependency-injection": "<3.4"
}, },
"provide": {
"psr/event-dispatcher-implementation": "1.0",
"symfony/event-dispatcher-implementation": "1.1"
},
"require-dev": { "require-dev": {
"psr/log": "~1.0", "psr/log": "~1.0",
"symfony/config": "~3.4|~4.0", "symfony/config": "~3.4|~4.0",
"symfony/dependency-injection": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0",
"symfony/expression-language": "~3.4|~4.0", "symfony/expression-language": "~3.4|~4.0",
"symfony/http-foundation": "^3.4|^4.0",
"symfony/service-contracts": "^1.1",
"symfony/stopwatch": "~3.4|~4.0" "symfony/stopwatch": "~3.4|~4.0"
}, },
"suggest": { "suggest": {
@@ -2016,7 +2068,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "4.3-dev" "dev-master": "4.2-dev"
} }
}, },
"autoload": { "autoload": {
@@ -2043,7 +2095,7 @@
], ],
"description": "Symfony EventDispatcher Component", "description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-06-27T06:42:14+00:00" "time": "2019-06-26T06:46:55+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
@@ -2336,16 +2388,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v4.3.3", "version": "v4.2.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c" "reference": "808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/856d35814cf287480465bb7a6c413bb7f5f5e69c", "url": "https://api.github.com/repos/symfony/process/zipball/808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2",
"reference": "856d35814cf287480465bb7a6c413bb7f5f5e69c", "reference": "808a4be7e0dd7fcb6a2b1ed2ba22dd581402c5e2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2354,7 +2406,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "4.3-dev" "dev-master": "4.2-dev"
} }
}, },
"autoload": { "autoload": {
@@ -2381,20 +2433,20 @@
], ],
"description": "Symfony Process Component", "description": "Symfony Process Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-05-30T16:10:05+00:00" "time": "2019-05-30T16:06:08+00:00"
}, },
{ {
"name": "symfony/var-dumper", "name": "symfony/var-dumper",
"version": "v4.3.3", "version": "v4.2.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-dumper.git", "url": "https://github.com/symfony/var-dumper.git",
"reference": "e4110b992d2cbe198d7d3b244d079c1c58761d07" "reference": "4e18e041a477edbb8c54e053f179672f9413816c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/e4110b992d2cbe198d7d3b244d079c1c58761d07", "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4e18e041a477edbb8c54e053f179672f9413816c",
"reference": "e4110b992d2cbe198d7d3b244d079c1c58761d07", "reference": "4e18e041a477edbb8c54e053f179672f9413816c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2423,7 +2475,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "4.3-dev" "dev-master": "4.2-dev"
} }
}, },
"autoload": { "autoload": {
@@ -2457,7 +2509,7 @@
"debug", "debug",
"dump" "dump"
], ],
"time": "2019-07-27T06:42:46+00:00" "time": "2019-07-27T06:42:33+00:00"
}, },
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
@@ -2562,19 +2614,19 @@
"authors": [ "authors": [
{ {
"name": "Fabien Potencier", "name": "Fabien Potencier",
"role": "Lead Developer",
"email": "fabien@symfony.com", "email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org", "homepage": "http://fabien.potencier.org"
"role": "Lead Developer"
}, },
{ {
"name": "Armin Ronacher", "name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com", "role": "Project Founder",
"role": "Project Founder" "email": "armin.ronacher@active-4.com"
}, },
{ {
"name": "Twig Team", "name": "Twig Team",
"homepage": "https://twig.symfony.com/contributors", "role": "Contributors",
"role": "Contributors" "homepage": "https://twig.symfony.com/contributors"
} }
], ],
"description": "Twig, the flexible, fast, and secure template language for PHP", "description": "Twig, the flexible, fast, and secure template language for PHP",
@@ -3241,16 +3293,16 @@
}, },
{ {
"name": "myclabs/deep-copy", "name": "myclabs/deep-copy",
"version": "1.9.1", "version": "1.9.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/myclabs/DeepCopy.git", "url": "https://github.com/myclabs/DeepCopy.git",
"reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72" "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
"reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72", "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3285,7 +3337,7 @@
"object", "object",
"object graph" "object graph"
], ],
"time": "2019-04-07T13:18:21+00:00" "time": "2019-08-09T12:45:53+00:00"
}, },
{ {
"name": "nette/bootstrap", "name": "nette/bootstrap",
@@ -3812,16 +3864,16 @@
}, },
{ {
"name": "nikic/php-parser", "name": "nikic/php-parser",
"version": "v4.2.2", "version": "v4.2.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nikic/PHP-Parser.git", "url": "https://github.com/nikic/PHP-Parser.git",
"reference": "1bd73cc04c3843ad8d6b0bfc0956026a151fc420" "reference": "e612609022e935f3d0337c1295176505b41188c8"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bd73cc04c3843ad8d6b0bfc0956026a151fc420", "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/e612609022e935f3d0337c1295176505b41188c8",
"reference": "1bd73cc04c3843ad8d6b0bfc0956026a151fc420", "reference": "e612609022e935f3d0337c1295176505b41188c8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -3829,7 +3881,7 @@
"php": ">=7.0" "php": ">=7.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^6.5 || ^7.0" "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0"
}, },
"bin": [ "bin": [
"bin/php-parse" "bin/php-parse"
@@ -3859,7 +3911,7 @@
"parser", "parser",
"php" "php"
], ],
"time": "2019-05-25T20:07:01+00:00" "time": "2019-08-12T20:17:41+00:00"
}, },
{ {
"name": "ocramius/package-versions", "name": "ocramius/package-versions",
@@ -3949,18 +4001,18 @@
"authors": [ "authors": [
{ {
"name": "Arne Blankerts", "name": "Arne Blankerts",
"email": "arne@blankerts.de", "role": "Developer",
"role": "Developer" "email": "arne@blankerts.de"
}, },
{ {
"name": "Sebastian Heuer", "name": "Sebastian Heuer",
"email": "sebastian@phpeople.de", "role": "Developer",
"role": "Developer" "email": "sebastian@phpeople.de"
}, },
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "Developer",
"role": "Developer" "email": "sebastian@phpunit.de"
} }
], ],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
@@ -3996,18 +4048,18 @@
"authors": [ "authors": [
{ {
"name": "Arne Blankerts", "name": "Arne Blankerts",
"email": "arne@blankerts.de", "role": "Developer",
"role": "Developer" "email": "arne@blankerts.de"
}, },
{ {
"name": "Sebastian Heuer", "name": "Sebastian Heuer",
"email": "sebastian@phpeople.de", "role": "Developer",
"role": "Developer" "email": "sebastian@phpeople.de"
}, },
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "Developer",
"role": "Developer" "email": "sebastian@phpunit.de"
} }
], ],
"description": "Library for handling version information and constraints", "description": "Library for handling version information and constraints",
@@ -4277,16 +4329,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "0.11.12", "version": "0.11.14",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "56b3eb2a371b60537fd20794e24af9e7e8ed4e30" "reference": "b343bbf83d0dd08db917408d8efd99b140ce04c6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/56b3eb2a371b60537fd20794e24af9e7e8ed4e30", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b343bbf83d0dd08db917408d8efd99b140ce04c6",
"reference": "56b3eb2a371b60537fd20794e24af9e7e8ed4e30", "reference": "b343bbf83d0dd08db917408d8efd99b140ce04c6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -4297,7 +4349,7 @@
"nette/robot-loader": "^3.0.1", "nette/robot-loader": "^3.0.1",
"nette/schema": "^1.0", "nette/schema": "^1.0",
"nette/utils": "^2.4.5 || ^3.0", "nette/utils": "^2.4.5 || ^3.0",
"nikic/php-parser": "^4.0.2", "nikic/php-parser": "^4.2.3",
"php": "~7.1", "php": "~7.1",
"phpstan/phpdoc-parser": "^0.3.5", "phpstan/phpdoc-parser": "^0.3.5",
"symfony/console": "~3.2 || ~4.0", "symfony/console": "~3.2 || ~4.0",
@@ -4307,7 +4359,7 @@
"symfony/console": "3.4.16 || 4.1.5" "symfony/console": "3.4.16 || 4.1.5"
}, },
"require-dev": { "require-dev": {
"brianium/paratest": "^2.0", "brianium/paratest": "^2.0 || ^3.0",
"consistence/coding-standard": "^3.5", "consistence/coding-standard": "^3.5",
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4",
"ext-intl": "*", "ext-intl": "*",
@@ -4322,7 +4374,7 @@
"phpstan/phpstan-php-parser": "^0.11", "phpstan/phpstan-php-parser": "^0.11",
"phpstan/phpstan-phpunit": "^0.11", "phpstan/phpstan-phpunit": "^0.11",
"phpstan/phpstan-strict-rules": "^0.11", "phpstan/phpstan-strict-rules": "^0.11",
"phpunit/phpunit": "^7.0", "phpunit/phpunit": "^7.5.14 || ^8.0",
"slevomat/coding-standard": "^4.7.2", "slevomat/coding-standard": "^4.7.2",
"squizlabs/php_codesniffer": "^3.3.2" "squizlabs/php_codesniffer": "^3.3.2"
}, },
@@ -4348,7 +4400,7 @@
"MIT" "MIT"
], ],
"description": "PHPStan - PHP Static Analysis Tool", "description": "PHPStan - PHP Static Analysis Tool",
"time": "2019-07-08T06:55:18+00:00" "time": "2019-08-17T12:49:22+00:00"
}, },
{ {
"name": "phpstan/phpstan-deprecation-rules", "name": "phpstan/phpstan-deprecation-rules",
@@ -4451,8 +4503,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "lead",
"role": "lead" "email": "sebastian@phpunit.de"
} }
], ],
"description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
@@ -4502,8 +4554,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "lead",
"role": "lead" "email": "sebastian@phpunit.de"
} }
], ],
"description": "FilterIterator implementation that filters files based on a list of suffixes.", "description": "FilterIterator implementation that filters files based on a list of suffixes.",
@@ -4544,8 +4596,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "lead",
"role": "lead" "email": "sebastian@phpunit.de"
} }
], ],
"description": "Simple template engine.", "description": "Simple template engine.",
@@ -4593,8 +4645,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "lead",
"role": "lead" "email": "sebastian@phpunit.de"
} }
], ],
"description": "Utility class for timing", "description": "Utility class for timing",
@@ -4724,8 +4776,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "lead",
"role": "lead" "email": "sebastian@phpunit.de"
} }
], ],
"description": "The PHP Unit Testing framework.", "description": "The PHP Unit Testing framework.",
@@ -4957,16 +5009,16 @@
}, },
{ {
"name": "sebastian/exporter", "name": "sebastian/exporter",
"version": "3.1.0", "version": "3.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git", "url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "234199f4528de6d12aaa58b612e98f7d36adb937" "reference": "06a9a5947f47b3029d76118eb5c22802e5869687"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687",
"reference": "234199f4528de6d12aaa58b612e98f7d36adb937", "reference": "06a9a5947f47b3029d76118eb5c22802e5869687",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -4993,6 +5045,10 @@
"BSD-3-Clause" "BSD-3-Clause"
], ],
"authors": [ "authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{ {
"name": "Jeff Welch", "name": "Jeff Welch",
"email": "whatthejeff@gmail.com" "email": "whatthejeff@gmail.com"
@@ -5001,17 +5057,13 @@
"name": "Volker Dusch", "name": "Volker Dusch",
"email": "github@wallbash.com" "email": "github@wallbash.com"
}, },
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{ {
"name": "Adam Harvey", "name": "Adam Harvey",
"email": "aharvey@php.net" "email": "aharvey@php.net"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
} }
], ],
"description": "Provides the functionality to export PHP variables for visualization", "description": "Provides the functionality to export PHP variables for visualization",
@@ -5020,7 +5072,7 @@
"export", "export",
"exporter" "exporter"
], ],
"time": "2017-04-03T13:19:02+00:00" "time": "2019-08-11T12:43:14+00:00"
}, },
{ {
"name": "sebastian/global-state", "name": "sebastian/global-state",
@@ -5295,8 +5347,8 @@
"authors": [ "authors": [
{ {
"name": "Sebastian Bergmann", "name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de", "role": "lead",
"role": "lead" "email": "sebastian@phpunit.de"
} }
], ],
"description": "Library that helps with managing the version number of Git-hosted PHP projects", "description": "Library that helps with managing the version number of Git-hosted PHP projects",
@@ -5558,8 +5610,8 @@
"authors": [ "authors": [
{ {
"name": "Arne Blankerts", "name": "Arne Blankerts",
"email": "arne@blankerts.de", "role": "Developer",
"role": "Developer" "email": "arne@blankerts.de"
} }
], ],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",

View File

@@ -16,8 +16,11 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
die(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req)); die(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req));
} }
if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) { if (PHP_SAPI === 'cli-server') {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>"); $symfony_server = strpos(getenv('_'), 'symfony') !== false;
if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
}
} }
// Ensure vendor libraries exist // Ensure vendor libraries exist

View File

@@ -10,3 +10,4 @@ Disallow: /user/
Allow: /user/pages/ Allow: /user/pages/
Allow: /user/themes/ Allow: /user/themes/
Allow: /user/images/ Allow: /user/images/
Allow: /

View File

@@ -1,70 +0,0 @@
div.phpdebugbar {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.phpdebugbar pre {
padding: 1rem;
}
.phpdebugbar div.phpdebugbar-header > div > * {
padding: 5px 15px;
}
.phpdebugbar div.phpdebugbar-header > div.phpdebugbar-header-right > * {
padding: 5px 8px;
}
.phpdebugbar div.phpdebugbar-header, .phpdebugbar a.phpdebugbar-restore-btn {
background-image: url(grav.png);
}
.phpdebugbar a.phpdebugbar-restore-btn {
width: 13px;
}
.phpdebugbar a.phpdebugbar-tab.phpdebugbar-active {
background: #3DB9EC;
color: #fff;
margin-top: -1px;
padding-top: 6px;
}
.phpdebugbar .phpdebugbar-widgets-toolbar {
border-top: 1px solid #ddd;
padding-left: 5px;
padding-right: 2px;
padding-top: 2px;
background-color: #fafafa !important;
width: auto !important;
left: 0;
right: 0;
}
.phpdebugbar .phpdebugbar-widgets-toolbar input {
background: transparent !important;
}
.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter {
}
.phpdebugbar input[type=text] {
padding: 0;
display: inline;
}
.phpdebugbar dl.phpdebugbar-widgets-varlist, ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
font-size: 12px;
}
ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
top: 0;
}
.phpdebugbar pre, .phpdebugbar code {
margin: 0;
font-size: 14px;
}

View File

@@ -0,0 +1,2 @@
/** Clockwork Debugger CSS **/
.clockwork-badge{position:fixed;z-index:10;bottom:0;left:0;padding:2px 4px;background-color:#eee;border:1px solid #ccc;border-bottom:0;border-left:0;display:flex;align-items:center}.clockwork-badge:hover{width:auto}.clockwork-badge:hover:after{content:'Grav Clockwork debugger enabled. Install Clockwork Browser extension (Chrome or Firefox), open your Developer tools and then select the Clockwork tab.'}.clockwork-badge:after{margin-left:10px;font-family:Monaco,Consolas,"Lucida Console",monospace;font-size:12px;line-height:1.5;color:#666}.clockwork-badge i{display:block;float:left;height:22px;width:22px;min-width:22px;background-size:contain;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAA/1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeHh4AAAD///8EBAT7+/sLCwv29vYVFRUvLy/t7e3m5ubCwsKxsbE/Pz+mpqZMTEwcHBzy8vLp6emfn5+AgIA2Njbi4uLf39+rq6tzc3NWVlYhISHa2trW1tbS0tLMzMy7u7uZmZmUlJSMjIxvb29kZGRHR0c7Ozt5eXkqKiq1tbWQkJBqampbW1tSUlLHx8eHh4ckJCRDQ0M3wD42AAAAI3RSTlMA/PibTbQ0x76TVAlw4LhZLOuEYCAN9Hjx0a2ppGZEGYw97djhXHwAAATZSURBVFjDlVcHW+MwDO1eFCjj2McNOzvdpXTTXVbL/P+/5SQ7QSSX5Di1X1onfi/Sk+Q4sTDbKqWK+YuznZ2zi3wxVdqK/Zf92M1nT9gnO8rmd398GX6Z3xaoOFoiAQcx3E5efgmeSuN8F6Xg1x3G06l/wjNpMR1B0uif4EhnIuFb+0diIoFXk3IVfokisR+h52GO4JKgyjmfaMhAFNlSaPR7DpwI+lzn/E4QKIqmKIJirxCMP4izBPPZPXhgXwMBYgULw0nfg/BF5scDbslb7QeJ08yqqTEmGYoB95d4H8ETL8+n9wBqrLu6ao3bBsMwAnxISf/9BHcqxNB8Y7cWl3Zz7TAUfPrvAT6AoNEFFXvsjutL01yOuMrtBxnFXsmT/1wQHmdWAFNnI3uI48Yj0FUcHbKf62GfUfr8eeQt7Uk3mQZpZNoVRPEui5vtEz5zFEpgWnyqVBZMc6oaGNriH2hGVZ0OxEvInPeMaZWJBA7vmPbCr5jjws5HBnAUxvDMH40aCIf4G5BjRQSs8E8HFFYf8bGxgDvD55bzGhwWkoBcuIyHR/AMdaCagxXDhtL6tSqoWpd4BMnlIR+Or+rYTK/a3EAGcc6e4AWHISnWv20iCCojsHoVlQdjrMexFF2C7UMg2A2WEGWbQhXN6l3eXC6XGp4b9qxbuEB2EBGBwtocrK90cVG5mbRXm6vmx/0phq1sIAGKDgLOBiN1MrO5a9aDl+D0W6x0Ar9BCTRuIIANa90Y7LrLVRXzwVtDInCqMRWcf2bUOEAsa4wJqFowQALL9EiAtVRk8QC4OW+1pOM9jIaVASwYagyNXDj+W0NcfuZNzjtXOiL0Zzg30Llj+ptfxQs4+vBPNiL5PawFCBkgXpUaVtqGl+A8dgZHL34BcBUQrwPptToW+o37Ku+UH9eYByJIx3YkAeFnMFuGO7S5gEp7YhXxa5OOAM39RXDPXb0qmpROsswZe+twXdU55oUIZAiEv3bD1UFwIYKkmGqytPCDCwKFQCKK0yL7qtSAPX54UAbtsLuBHkb9zyLmPQSNjsSgmQwKUOIfEY8F8t4B34DvndJY9BA8tNBJq1Nev9axmaStFcQLhgYoCTo0salkIaW8OUDdWjMTR2sHPhrAFZqx6cqcKE4pl2BJJ4K6hfwvqNgAnXfKX/HU6X3Zrhnu0k7tLNZtTBRv1hkwTDBY1NzFU6doDYjJbWdQkQhWwuU7/LvhTh3SDoco4ECL4i5dwURbc8NdDZz2IwKicE8d0KIqWetLE3+lL4hvUuGSeRfVWNLfj/gpOw4smBJBkKQHCzlHGwvAj4woB1gq5NGGLSXtORBPnUQPV5/MPVkDMxbpwG7w4x0xL6Ltxka0A/4NBvV09UVk4DoSn/jl2+JQS9q9KYawisAD4CfhsZ4TH3htylsdEHARIQBusqCKyUpymycgbbkkXEXjT3z7/oKQFTFVuZD2FMJHZIDsO5x2d4aAr2jR+GLwZhtAb028/0yJ9J8dE87jQyKObcjtTXT8dH+fDuKF4/eiPwzH44wTf/yUi6wrpRIOZ9lM1EtXAifFI+CJn9+iX/t2xMQwOMth/UZbASi8btAwR9FHWSpJr75g9Oqbin3VDg+SpwlP6k6TB4ex/7JvmcJx8jydy6XPk8eFTKhyfwCgX71MSvaBHgAAAABJRU5ErkJggg==)}

View File

@@ -0,0 +1,2 @@
/** Clockwork Debugger JS **/
document.addEventListener("DOMContentLoaded",function(){var e=document.createElement("div");e.appendChild(document.createElement("i")),e.className="clockwork-badge",document.body.appendChild(e)});

View File

@@ -0,0 +1,70 @@
div.phpdebugbar {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.phpdebugbar pre {
padding: 1rem;
}
.phpdebugbar div.phpdebugbar-header > div > * {
padding: 5px 15px;
}
.phpdebugbar div.phpdebugbar-header > div.phpdebugbar-header-right > * {
padding: 5px 8px;
}
.phpdebugbar div.phpdebugbar-header, .phpdebugbar a.phpdebugbar-restore-btn {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAA/1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeHh4AAAD///8EBAT7+/sLCwv29vYVFRUvLy/t7e3m5ubCwsKxsbE/Pz+mpqZMTEwcHBzy8vLp6emfn5+AgIA2Njbi4uLf39+rq6tzc3NWVlYhISHa2trW1tbS0tLMzMy7u7uZmZmUlJSMjIxvb29kZGRHR0c7Ozt5eXkqKiq1tbWQkJBqampbW1tSUlLHx8eHh4ckJCRDQ0M3wD42AAAAI3RSTlMA/PibTbQ0x76TVAlw4LhZLOuEYCAN9Hjx0a2ppGZEGYw97djhXHwAAATZSURBVFjDlVcHW+MwDO1eFCjj2McNOzvdpXTTXVbL/P+/5SQ7QSSX5Di1X1onfi/Sk+Q4sTDbKqWK+YuznZ2zi3wxVdqK/Zf92M1nT9gnO8rmd398GX6Z3xaoOFoiAQcx3E5efgmeSuN8F6Xg1x3G06l/wjNpMR1B0uif4EhnIuFb+0diIoFXk3IVfokisR+h52GO4JKgyjmfaMhAFNlSaPR7DpwI+lzn/E4QKIqmKIJirxCMP4izBPPZPXhgXwMBYgULw0nfg/BF5scDbslb7QeJ08yqqTEmGYoB95d4H8ETL8+n9wBqrLu6ao3bBsMwAnxISf/9BHcqxNB8Y7cWl3Zz7TAUfPrvAT6AoNEFFXvsjutL01yOuMrtBxnFXsmT/1wQHmdWAFNnI3uI48Yj0FUcHbKf62GfUfr8eeQt7Uk3mQZpZNoVRPEui5vtEz5zFEpgWnyqVBZMc6oaGNriH2hGVZ0OxEvInPeMaZWJBA7vmPbCr5jjws5HBnAUxvDMH40aCIf4G5BjRQSs8E8HFFYf8bGxgDvD55bzGhwWkoBcuIyHR/AMdaCagxXDhtL6tSqoWpd4BMnlIR+Or+rYTK/a3EAGcc6e4AWHISnWv20iCCojsHoVlQdjrMexFF2C7UMg2A2WEGWbQhXN6l3eXC6XGp4b9qxbuEB2EBGBwtocrK90cVG5mbRXm6vmx/0phq1sIAGKDgLOBiN1MrO5a9aDl+D0W6x0Ar9BCTRuIIANa90Y7LrLVRXzwVtDInCqMRWcf2bUOEAsa4wJqFowQALL9EiAtVRk8QC4OW+1pOM9jIaVASwYagyNXDj+W0NcfuZNzjtXOiL0Zzg30Llj+ptfxQs4+vBPNiL5PawFCBkgXpUaVtqGl+A8dgZHL34BcBUQrwPptToW+o37Ku+UH9eYByJIx3YkAeFnMFuGO7S5gEp7YhXxa5OOAM39RXDPXb0qmpROsswZe+twXdU55oUIZAiEv3bD1UFwIYKkmGqytPCDCwKFQCKK0yL7qtSAPX54UAbtsLuBHkb9zyLmPQSNjsSgmQwKUOIfEY8F8t4B34DvndJY9BA8tNBJq1Nev9axmaStFcQLhgYoCTo0salkIaW8OUDdWjMTR2sHPhrAFZqx6cqcKE4pl2BJJ4K6hfwvqNgAnXfKX/HU6X3Zrhnu0k7tLNZtTBRv1hkwTDBY1NzFU6doDYjJbWdQkQhWwuU7/LvhTh3SDoco4ECL4i5dwURbc8NdDZz2IwKicE8d0KIqWetLE3+lL4hvUuGSeRfVWNLfj/gpOw4smBJBkKQHCzlHGwvAj4woB1gq5NGGLSXtORBPnUQPV5/MPVkDMxbpwG7w4x0xL6Ltxka0A/4NBvV09UVk4DoSn/jl2+JQS9q9KYawisAD4CfhsZ4TH3htylsdEHARIQBusqCKyUpymycgbbkkXEXjT3z7/oKQFTFVuZD2FMJHZIDsO5x2d4aAr2jR+GLwZhtAb028/0yJ9J8dE87jQyKObcjtTXT8dH+fDuKF4/eiPwzH44wTf/yUi6wrpRIOZ9lM1EtXAifFI+CJn9+iX/t2xMQwOMth/UZbASi8btAwR9FHWSpJr75g9Oqbin3VDg+SpwlP6k6TB4ex/7JvmcJx8jydy6XPk8eFTKhyfwCgX71MSvaBHgAAAABJRU5ErkJggg==);
}
.phpdebugbar a.phpdebugbar-restore-btn {
width: 13px;
}
.phpdebugbar a.phpdebugbar-tab.phpdebugbar-active {
background: #3DB9EC;
color: #fff;
margin-top: -1px;
padding-top: 6px;
}
.phpdebugbar .phpdebugbar-widgets-toolbar {
border-top: 1px solid #ddd;
padding-left: 5px;
padding-right: 2px;
padding-top: 2px;
background-color: #fafafa !important;
width: auto !important;
left: 0;
right: 0;
}
.phpdebugbar .phpdebugbar-widgets-toolbar input {
background: transparent !important;
}
.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter {
}
.phpdebugbar input[type=text] {
padding: 0;
display: inline;
}
.phpdebugbar dl.phpdebugbar-widgets-varlist, ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
font-family: "DejaVu Sans Mono", Menlo, Monaco, Consolas, Courier, monospace;
font-size: 12px;
}
ul.phpdebugbar-widgets-timeline li span.phpdebugbar-widgets-label {
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
top: 0;
}
.phpdebugbar pre, .phpdebugbar code {
margin: 0;
font-size: 14px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ config:
title: Accounts title: Accounts
icon: fa-users icon: fa-users
authorize: ['admin.users', 'admin.accounts', 'admin.super'] authorize: ['admin.users', 'admin.accounts', 'admin.super']
priority: 6
form: form:
fields: fields:
@@ -36,4 +37,4 @@ form:
flex-readonly@: exists flex-readonly@: exists
readonly: false readonly: false
validate: validate:
required: true required: true

View File

@@ -27,6 +27,7 @@ home:
hide_in_urls: false # Hide the home route in URLs hide_in_urls: false # Hide the home route in URLs
pages: pages:
type: page # EXPERIMENTAL: Page type: page or flex
theme: quark # Default theme (defaults to "quark" theme) theme: quark # Default theme (defaults to "quark" theme)
order: order:
by: default # Order pages by "default", "alpha" or "date" by: default # Order pages by "default", "alpha" or "date"
@@ -125,6 +126,8 @@ log:
debugger: debugger:
enabled: false # Enable Grav debugger and following settings enabled: false # Enable Grav debugger and following settings
provider: clockwork # Debugger provider: debugbar | clockwork
censored: false # Censor potentially sensitive information (POST parameters, cookies, files, configuration and most array/object data in log messages)
shutdown: shutdown:
close_connection: true # Close the connection before calling onShutdown(). false for debugging close_connection: true # Close the connection before calling onShutdown(). false for debugging
@@ -154,15 +157,15 @@ session:
path: path:
gpm: gpm:
releases: stable # Set to either 'stable' or 'testing' releases: testing # Set to either 'stable' or 'testing'
proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128) proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128)
method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help. verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
accounts: accounts:
type: data # Account type: data or flex type: data # EXPERIMENTAL: Account type: data or flex
storage: file # Flex storage type: file or folder storage: file # EXPERIMENTAL: Flex storage type: file or folder
strict_mode: strict_mode:
yaml_compat: true # Grav 1.5+: Enables YAML backwards compatibility yaml_compat: true # Grav 1.5+: Enables YAML backwards compatibility

View File

@@ -8,8 +8,8 @@
// Some standard defines // Some standard defines
define('GRAV', true); define('GRAV', true);
define('GRAV_VERSION', '1.6.13'); define('GRAV_VERSION', '1.7.0-beta.7');
define('GRAV_TESTING', false); define('GRAV_TESTING', true);
define('DS', '/'); define('DS', '/');
if (!defined('GRAV_PHP_MIN')) { if (!defined('GRAV_PHP_MIN')) {

View File

@@ -20,9 +20,9 @@ if (is_file($_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $_SERVER['SCRIPT_N
$grav_index = 'index.php'; $grav_index = 'index.php';
/* Check the GRAV_BASEDIR environment variable and use if set */ /* Check the GRAV_BASEDIR environment variable and use if set */
$grav_basedir = getenv('GRAV_BASEDIR') ?: '';
if (isset($grav_basedir)) { $grav_basedir = getenv('GRAV_BASEDIR') ?: '';
if ($grav_basedir) {
$grav_index = ltrim($grav_basedir, '/') . DIRECTORY_SEPARATOR . $grav_index; $grav_index = ltrim($grav_basedir, '/') . DIRECTORY_SEPARATOR . $grav_index;
$grav_basedir = DIRECTORY_SEPARATOR . trim($grav_basedir, DIRECTORY_SEPARATOR); $grav_basedir = DIRECTORY_SEPARATOR . trim($grav_basedir, DIRECTORY_SEPARATOR);
define('GRAV_ROOT', str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . $grav_basedir); define('GRAV_ROOT', str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . $grav_basedir);

View File

@@ -334,7 +334,7 @@ class Cache extends Getters
* Stores a new cached entry. * Stores a new cached entry.
* *
* @param string $id the id of the cached entry * @param string $id the id of the cached entry
* @param array|object $data the data for the cached entry to store * @param array|object|int $data the data for the cached entry to store
* @param int $lifetime the lifetime to store the entry in seconds * @param int $lifetime the lifetime to store the entry in seconds
*/ */
public function save($id, $data, $lifetime = null) public function save($id, $data, $lifetime = null)

View File

@@ -25,6 +25,9 @@ class Blueprint extends BlueprintForm
/** @var BlueprintSchema */ /** @var BlueprintSchema */
protected $blueprintSchema; protected $blueprintSchema;
/** @var object */
protected $object;
/** @var array */ /** @var array */
protected $defaults; protected $defaults;
@@ -42,6 +45,11 @@ class Blueprint extends BlueprintForm
$this->scope = $scope; $this->scope = $scope;
} }
public function setObject($object)
{
$this->object = $object;
}
/** /**
* Set default values for field types. * Set default values for field types.
* *
@@ -111,6 +119,7 @@ class Blueprint extends BlueprintForm
foreach ($data as $property => $call) { foreach ($data as $property => $call) {
$action = $call['action']; $action = $call['action'];
$method = 'dynamic' . ucfirst($action); $method = 'dynamic' . ucfirst($action);
$call['object'] = $this->object;
if (isset($this->handlers[$action])) { if (isset($this->handlers[$action])) {
$callable = $this->handlers[$action]; $callable = $this->handlers[$action];

View File

@@ -244,8 +244,8 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|| !empty($field['disabled']) || !empty($field['disabled'])
// Field validation is set to be ignored // Field validation is set to be ignored
|| !empty($field['validate']['ignore']) || !empty($field['validate']['ignore'])
// Field is toggleable and the toggle is turned off // Field is overridable and the toggle is turned off
|| (!empty($field['toggleable']) && empty($toggles[$key])) || (!empty($field['overridable']) && empty($toggles[$key]))
) { ) {
continue; continue;
} }
@@ -279,6 +279,12 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
continue; continue;
} }
// Skip overridable fields without value.
// TODO: We need better overridable support, which is not just ignoring required values but also looking if defaults are good.
if (!empty($field['overridable']) && !isset($data[$name])) {
continue;
}
// Check if required. // Check if required.
if (isset($field['validate']['required']) if (isset($field['validate']['required'])
&& $field['validate']['required'] === true) { && $field['validate']['required'] === true) {

View File

@@ -9,6 +9,14 @@
namespace Grav\Common; namespace Grav\Common;
use Clockwork\Clockwork;
use Clockwork\DataSource\MonologDataSource;
use Clockwork\DataSource\PhpDataSource;
use Clockwork\DataSource\PsrMessageDataSource;
use Clockwork\DataSource\XdebugDataSource;
use Clockwork\Helpers\ServerTiming;
use Clockwork\Request\UserData;
use Clockwork\Storage\FileStorage;
use DebugBar\DataCollector\ConfigCollector; use DebugBar\DataCollector\ConfigCollector;
use DebugBar\DataCollector\DataCollectorInterface; use DebugBar\DataCollector\DataCollectorInterface;
use DebugBar\DataCollector\ExceptionsCollector; use DebugBar\DataCollector\ExceptionsCollector;
@@ -22,11 +30,23 @@ use DebugBar\JavascriptRenderer;
use DebugBar\StandardDebugBar; use DebugBar\StandardDebugBar;
use Grav\Common\Config\Config; use Grav\Common\Config\Config;
use Grav\Common\Processors\ProcessorInterface; use Grav\Common\Processors\ProcessorInterface;
use Grav\Common\Twig\TwigClockworkDataSource;
use Grav\Framework\Psr7\Response;
use Monolog\Logger;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Twig\Environment;
use Twig\Template; use Twig\Template;
use Twig\TemplateWrapper; use Twig\TemplateWrapper;
class Debugger class Debugger
{ {
/** @var static */
protected static $instance;
/** @var Grav $grav */ /** @var Grav $grav */
protected $grav; protected $grav;
@@ -39,8 +59,11 @@ class Debugger
/** @var StandardDebugBar $debugbar */ /** @var StandardDebugBar $debugbar */
protected $debugbar; protected $debugbar;
/** @var Clockwork */
protected $clockwork;
/** @var bool */ /** @var bool */
protected $enabled; protected $enabled = false;
protected $initialized = false; protected $initialized = false;
@@ -53,36 +76,38 @@ class Debugger
/** @var callable */ /** @var callable */
protected $errorHandler; protected $errorHandler;
protected $requestTime;
protected $currentTime;
/** @var int */
protected $profiling = 0;
protected $censored = false;
/** /**
* Debugger constructor. * Debugger constructor.
*/ */
public function __construct() public function __construct()
{ {
$currentTime = microtime(true); static::$instance = $this;
$this->currentTime = microtime(true);
if (!\defined('GRAV_REQUEST_TIME')) { if (!\defined('GRAV_REQUEST_TIME')) {
\define('GRAV_REQUEST_TIME', $currentTime); \define('GRAV_REQUEST_TIME', $this->currentTime);
} }
// Enable debugger until $this->init() gets called. $this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME;
$this->enabled = true;
$debugbar = new DebugBar();
$debugbar->addCollector(new PhpInfoCollector());
$debugbar->addCollector(new MessagesCollector());
$debugbar->addCollector(new RequestDataCollector());
$debugbar->addCollector(new TimeDataCollector($_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME));
$debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
$debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $currentTime);
$debugbar['time']->addMeasure('Debugger', $currentTime, microtime(true));
$this->debugbar = $debugbar;
// Set deprecation collector. // Set deprecation collector.
$this->setErrorHandler(); $this->setErrorHandler();
} }
public function getClockwork(): ?Clockwork
{
return $this->enabled ? $this->clockwork : null;
}
/** /**
* Initialize the debugger * Initialize the debugger
* *
@@ -100,25 +125,234 @@ class Debugger
// Enable/disable debugger based on configuration. // Enable/disable debugger based on configuration.
$this->enabled = (bool)$this->config->get('system.debugger.enabled'); $this->enabled = (bool)$this->config->get('system.debugger.enabled');
$this->censored = (bool)$this->config->get('system.debugger.censored', false);
if ($this->enabled()) { if ($this->enabled) {
$this->initialized = true; $this->initialized = true;
$plugins_config = (array)$this->config->get('plugins'); $clockwork = $debugbar = null;
switch ($this->config->get('system.debugger.provider', 'debugbar')) {
case 'clockwork':
$this->clockwork = $clockwork = new Clockwork();
break;
default:
$this->debugbar = $debugbar = new DebugBar();
}
$plugins_config = (array)$this->config->get('plugins');
ksort($plugins_config); ksort($plugins_config);
$debugbar = $this->debugbar; if ($clockwork) {
$debugbar->addCollector(new MemoryCollector()); $log = $this->grav['log'];
$debugbar->addCollector(new ExceptionsCollector()); $clockwork->setStorage(new FileStorage('cache://clockwork'));
$debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config')); if (extension_loaded('xdebug')) {
$debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins')); $clockwork->addDataSource(new XdebugDataSource());
$this->addMessage('Grav v' . GRAV_VERSION); }
if ($log instanceof Logger) {
$clockwork->addDataSource(new MonologDataSource($log));
}
$clockwork->addDataSource(new TwigClockworkDataSource());
$timeLine = $clockwork->getTimeline();
if ($this->requestTime !== GRAV_REQUEST_TIME) {
$timeLine->addEvent('server', 'Server', $this->requestTime, GRAV_REQUEST_TIME);
}
if ($this->currentTime !== GRAV_REQUEST_TIME) {
$timeLine->addEvent('loading', 'Loading', GRAV_REQUEST_TIME, $this->currentTime);
}
$timeLine->addEvent('setup', 'Site Setup', $this->currentTime, microtime(true));
}
if ($this->censored) {
$censored = ['CENSORED' => true];
}
if ($debugbar) {
$debugbar->addCollector(new PhpInfoCollector());
$debugbar->addCollector(new MessagesCollector());
if (!$this->censored) {
$debugbar->addCollector(new RequestDataCollector());
}
$debugbar->addCollector(new TimeDataCollector($this->requestTime));
$debugbar->addCollector(new MemoryCollector());
$debugbar->addCollector(new ExceptionsCollector());
$debugbar->addCollector(new ConfigCollector($censored ?? (array)$this->config->get('system'), 'Config'));
$debugbar->addCollector(new ConfigCollector($censored ?? $plugins_config, 'Plugins'));
$debugbar->addCollector(new ConfigCollector($this->config->get('streams.schemes'), 'Streams'));
if ($this->requestTime !== GRAV_REQUEST_TIME) {
$debugbar['time']->addMeasure('Server', $debugbar['time']->getRequestStartTime(), GRAV_REQUEST_TIME);
}
if ($this->currentTime !== GRAV_REQUEST_TIME) {
$debugbar['time']->addMeasure('Loading', GRAV_REQUEST_TIME, $this->currentTime);
}
$debugbar['time']->addMeasure('Site Setup', $this->currentTime, microtime(true));
}
$this->addMessage('Grav v' . GRAV_VERSION . ' - PHP ' . PHP_VERSION);
$this->config->debug();
if ($clockwork) {
$clockwork->info('System Configuration', $censored ?? $this->config->get('system'));
$clockwork->info('Plugins Configuration', $censored ?? $plugins_config);
$clockwork->info('Streams', $this->config->get('streams.schemes'));
}
} }
return $this; return $this;
} }
public function finalize(): void
{
if ($this->clockwork && $this->enabled) {
$this->stopProfiling('Profiler Analysis');
$this->addMeasures();
$deprecations = $this->getDeprecations();
$count = count($deprecations);
if (!$count) {
return;
}
/** @var UserData $userData */
$userData = $this->clockwork->userData('Deprecated');
$userData->counters([
'Deprecated' => count($deprecations)
]);
foreach ($deprecations as &$deprecation) {
if (0) {
$d = $deprecation;
unset($d['message']);
$this->clockwork->log('deprecated', $deprecation['message'], $d);
}
}
unset($deprecation);
$userData->table('Your site is using following deprecated features', $deprecations);
}
}
public function logRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
if (!$this->enabled || !$this->clockwork) {
return $response;
}
$clockwork = $this->clockwork;
$this->finalize();
$clockwork->getTimeline()->finalize($request->getAttribute('request_time'));
if ($this->censored) {
$censored = 'CENSORED';
$request = $request
->withCookieParams([$censored => ''])
->withUploadedFiles([])
->withHeader('cookie', $censored);
if ($request->getBody()) {
$request = $request->withParsedBody([$censored => '']);
}
}
$clockwork->addDataSource(new PsrMessageDataSource($request, $response));
$clockwork->resolveRequest();
$clockwork->storeRequest();
$clockworkRequest = $clockwork->getRequest();
$response = $response
->withHeader('X-Clockwork-Id', $clockworkRequest->id)
->withHeader('X-Clockwork-Version', $clockwork::VERSION);
$basePath = Grav::instance()['uri']->rootUrl();
if ($basePath) {
$response = $response->withHeader('X-Clockwork-Path', $basePath . '/__clockwork/');
}
return $response->withHeader('Server-Timing', ServerTiming::fromRequest($clockworkRequest)->value());
}
public function debuggerRequest(RequestInterface $request): Response
{
$clockwork = $this->clockwork;
$headers = [
'Content-Type' => 'application/json',
'Grav-Internal-SkipShutdown' => 1
];
$path = $request->getUri()->getPath();
$clockworkDataUri = '#/__clockwork(?:/(?<id>[0-9-]+))?(?:/(?<direction>(?:previous|next)))?(?:/(?<count>\d+))?#';
if (preg_match($clockworkDataUri, $path, $matches) === false) {
$response = ['message' => 'Bad Input'];
return new Response(400, $headers, json_encode($response));
}
$id = $matches['id'] ?? null;
$direction = $matches['direction'] ?? null;
$count = $matches['count'] ?? null;
$storage = $clockwork->getStorage();
if ($direction === 'previous') {
$data = $storage->previous($id, $count);
} elseif ($direction === 'next') {
$data = $storage->next($id, $count);
} elseif ($id === 'latest') {
$data = $storage->latest();
} else {
$data = $storage->find($id);
}
if (preg_match('#(?<id>[0-9-]+|latest)/extended#', $path)) {
$clockwork->extendRequest($data);
}
if (!$data) {
$response = ['message' => 'Not Found'];
return new Response(404, $headers, json_encode($response));
}
$data = is_array($data) ? array_map(function ($item) { return $item->toArray(); }, $data) : $data->toArray();
return new Response(200, $headers, json_encode($data));
}
protected function addMeasures()
{
if (!$this->enabled) {
return;
}
$nowTime = microtime(true);
$clkTimeLine = $this->clockwork ? $this->clockwork->getTimeline() : null;
$debTimeLine = $this->debugbar ? $this->debugbar['time'] : null;
foreach ($this->timers as $name => $data) {
$description = $data[0];
$startTime = $data[1] ?? null;
$endTime = $data[2] ?? $nowTime;
if ($endTime - $startTime < 0.001) {
continue;
}
if ($clkTimeLine) {
$clkTimeLine->addEvent($name, $description ?? $name, $startTime, $endTime);
}
if ($debTimeLine) {
$debTimeLine->addMeasure($description ?? $name, $startTime, $endTime);
}
}
$this->timers = [];
}
/** /**
* Set/get the enabled state of the debugger * Set/get the enabled state of the debugger
* *
@@ -142,7 +376,8 @@ class Debugger
*/ */
public function addAssets() public function addAssets()
{ {
if ($this->enabled()) { if ($this->enabled) {
// Only add assets if Page is HTML // Only add assets if Page is HTML
$page = $this->grav['page']; $page = $this->grav['page'];
@@ -153,23 +388,35 @@ class Debugger
/** @var Assets $assets */ /** @var Assets $assets */
$assets = $this->grav['assets']; $assets = $this->grav['assets'];
// Add jquery library // Clockwork specific assets
$assets->add('jquery', 101); if ($this->clockwork) {
$assets->addCss('/system/assets/debugger/clockwork.css', ['loading' => 'inline']);
$this->renderer = $this->debugbar->getJavascriptRenderer(); $assets->addJs('/system/assets/debugger/clockwork.js', ['loading' => 'inline']);
$this->renderer->setIncludeVendors(false);
// Get the required CSS files
list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
foreach ((array)$css_files as $css) {
$assets->addCss($css);
} }
$assets->addCss('/system/assets/debugger.css');
foreach ((array)$js_files as $js) { // Debugbar specific assets
$assets->addJs($js); if ($this->debugbar) {
// Add jquery library
$assets->add('jquery', 101);
$this->renderer = $this->debugbar->getJavascriptRenderer();
$this->renderer->setIncludeVendors(false);
list($css_files, $js_files) = $this->renderer->getAssets(null, JavascriptRenderer::RELATIVE_URL);
foreach ((array)$css_files as $css) {
$assets->addCss($css);
}
$assets->addCss('/system/assets/debugger/phpdebugbar.css', ['loading' => 'inline']);
foreach ((array)$js_files as $js) {
$assets->addJs($js);
}
} }
} }
return $this; return $this;
@@ -192,7 +439,9 @@ class Debugger
*/ */
public function addCollector($collector) public function addCollector($collector)
{ {
$this->debugbar->addCollector($collector); if ($this->debugbar && !$this->debugbar->hasCollector($collector->getName())) {
$this->debugbar->addCollector($collector);
}
return $this; return $this;
} }
@@ -200,14 +449,18 @@ class Debugger
/** /**
* Returns a data collector * Returns a data collector
* *
* @param DataCollectorInterface $collector * @param string $name
* *
* @return DataCollectorInterface * @return DataCollectorInterface
* @throws \DebugBar\DebugBarException * @throws \DebugBar\DebugBarException
*/ */
public function getCollector($collector) public function getCollector($name)
{ {
return $this->debugbar->getCollector($collector); if ($this->debugbar && $this->debugbar->hasCollector($name)) {
return $this->debugbar->getCollector($name);
}
return null;
} }
/** /**
@@ -217,13 +470,14 @@ class Debugger
*/ */
public function render() public function render()
{ {
if ($this->enabled()) { if ($this->enabled && $this->debugbar) {
// Only add assets if Page is HTML // Only add assets if Page is HTML
$page = $this->grav['page']; $page = $this->grav['page'];
if (!$this->renderer || $page->templateFormat() !== 'html') { if (!$this->renderer || $page->templateFormat() !== 'html') {
return $this; return $this;
} }
$this->addMeasures();
$this->addDeprecations(); $this->addDeprecations();
echo $this->renderer->render(); echo $this->renderer->render();
@@ -239,7 +493,8 @@ class Debugger
*/ */
public function sendDataInHeaders() public function sendDataInHeaders()
{ {
if ($this->enabled()) { if ($this->enabled && $this->debugbar) {
$this->addMeasures();
$this->addDeprecations(); $this->addDeprecations();
$this->debugbar->sendDataInHeaders(); $this->debugbar->sendDataInHeaders();
} }
@@ -254,16 +509,146 @@ class Debugger
*/ */
public function getData() public function getData()
{ {
if (!$this->enabled()) { if (!$this->enabled || !$this->debugbar) {
return null; return null;
} }
$this->addMeasures();
$this->addDeprecations(); $this->addDeprecations();
$this->timers = []; $this->timers = [];
return $this->debugbar->getData(); return $this->debugbar->getData();
} }
/**
* Hierarchical Profiler support.
*
* @param callable $callable
* @param string $message
* @return mixed
*/
public function profile(callable $callable, string $message = null)
{
$this->startProfiling();
$response = $callable();
$this->stopProfiling($message);
return $response;
}
/**
* Start profiling code.
*/
public function startProfiling(): void
{
if ($this->enabled && extension_loaded('tideways_xhprof')) {
$this->profiling++;
if ($this->profiling === 1) {
\tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_NO_BUILTINS);
}
}
}
/**
* Stop profiling code. Returns profiling array or null if profiling couldn't be done.
*
* @param string $message
* @return array|null
*/
public function stopProfiling(string $message = null): ?array
{
$timings = null;
if ($this->enabled && extension_loaded('tideways_xhprof')) {
$profiling = $this->profiling - 1;
if ($profiling === 0) {
$timings = \tideways_xhprof_disable();
$timings = $this->buildProfilerTimings($timings);
if ($this->clockwork) {
/** @var UserData $userData */
$userData = $this->clockwork->userData('Profiler');
$userData->counters([
'Calls' => count($timings)
]);
$userData->table('Profiler', $timings);
} else {
$this->addMessage($message ?? 'Profiler Analysis', 'debug', $timings);
}
}
$this->profiling = max(0, $profiling);
}
return $timings;
}
protected function buildProfilerTimings(array $timings): array
{
// Filter method calls which take almost no time.
$timings = array_filter($timings, function ($value) {
return $value['wt'] > 50;
});
uasort($timings, function (array $a, array $b) {
return $b['wt'] <=> $a['wt'];
});
$table = [];
foreach ($timings as $key => $timing) {
$parts = explode('==>', $key);
$method = $this->parseProfilerCall(array_pop($parts));
$context = $this->parseProfilerCall(array_pop($parts));
// Skip redundant method calls.
if ($context === 'Grav\Framework\RequestHandler\RequestHandler::handle()') {
continue;
}
// Do not profile library calls.
if (strpos($context, 'Grav\\') !== 0) {
continue;
}
$table[] = [
'Context' => $context,
'Method' => $method,
'Calls' => $timing['ct'],
'Time (ms)' => $timing['wt'] / 1000,
];
}
return $table;
}
protected function parseProfilerCall(?string $call)
{
if (null === $call) {
return '';
}
if (strpos($call, '@')) {
[$call,] = explode('@', $call);
}
if (strpos($call, '::')) {
[$class, $call] = explode('::', $call);
}
if (!isset($class)) {
return $call;
}
// It is also possible to display twig files, but they are being logged in views.
/*
if (strpos($class, '__TwigTemplate_') === 0 && class_exists($class)) {
$env = new Environment();
/ ** @var Template $template * /
$template = new $class($env);
return $template->getTemplateName();
}
*/
return "{$class}::{$call}()";
}
/** /**
* Start a timer with an associated name and description * Start a timer with an associated name and description
* *
@@ -274,10 +659,7 @@ class Debugger
*/ */
public function startTimer($name, $description = null) public function startTimer($name, $description = null)
{ {
if (strpos($name, '_') === 0 || $this->enabled()) { $this->timers[$name] = [$description, microtime(true)];
$this->debugbar['time']->startMeasure($name, $description);
$this->timers[] = $name;
}
return $this; return $this;
} }
@@ -291,8 +673,9 @@ class Debugger
*/ */
public function stopTimer($name) public function stopTimer($name)
{ {
if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) { if (isset($this->timers[$name])) {
$this->debugbar['time']->stopMeasure($name); $endTime = microtime(true);
$this->timers[$name][] = $endTime;
} }
return $this; return $this;
@@ -303,14 +686,56 @@ class Debugger
* *
* @param mixed $message * @param mixed $message
* @param string $label * @param string $label
* @param bool $isString * @param mixed|bool $isString
* *
* @return $this * @return $this
*/ */
public function addMessage($message, $label = 'info', $isString = true) public function addMessage($message, $label = 'info', $isString = true)
{ {
if ($this->enabled()) { if ($this->enabled) {
$this->debugbar['messages']->addMessage($message, $label, $isString); if ($this->censored) {
if (!is_scalar($message)) {
$message = 'CENSORED';
}
if (!is_scalar($isString)) {
$isString = ['CENSORED'];
}
}
if ($this->debugbar) {
$this->debugbar['messages']->addMessage($message, $label, is_bool($isString) ? $isString : true);
if (is_array($isString)) {
$this->debugbar['messages']->addMessage($isString, $label, false);
}
}
if ($this->clockwork) {
if (!is_scalar($message)) {
$isString = $message;
$message = '';
} elseif (is_bool($isString)) {
$isString = [];
}
if (!is_array($isString)) {
$isString = [gettype($isString) => $isString];
}
$this->clockwork->log($label, $message, $isString);
}
}
return $this;
}
public function addEvent(string $name, ?Event $event, EventDispatcherInterface $dispatcher)
{
if ($this->enabled) {
if ($this->clockwork) {
$listeners = [];
foreach ($dispatcher->getListeners($name) as $listener) {
$listeners[] = $this->resolveCallable($listener);
}
$this->clockwork->addEvent($name, null, microtime(true), ['listeners' => $listeners]);
}
} }
return $this; return $this;
@@ -319,13 +744,23 @@ class Debugger
/** /**
* Dump exception into the Messages tab of the Debug Bar * Dump exception into the Messages tab of the Debug Bar
* *
* @param \Exception $e * @param \Throwable $e
* @return Debugger * @return Debugger
*/ */
public function addException(\Exception $e) public function addException(\Throwable $e)
{ {
if ($this->initialized && $this->enabled()) { if ($this->initialized && $this->enabled) {
$this->debugbar['exceptions']->addException($e); if ($this->debugbar) {
$this->debugbar['exceptions']->addException($e);
}
if ($this->clockwork) {
/** @var UserData $exceptions */
$exceptions = $this->clockwork->userData('Exceptions');
$exceptions->data(['message' => $e->getMessage()]);
$this->clockwork->alert($e->getMessage(), ['exception' => $e]);
}
} }
return $this; return $this;
@@ -355,7 +790,7 @@ class Debugger
return true; return true;
} }
if (!$this->enabled()) { if (!$this->enabled) {
return true; return true;
} }
@@ -540,6 +975,21 @@ class Debugger
return true; return true;
} }
protected function getDeprecations(): array
{
if (!$this->deprecations) {
return [];
}
$list = [];
/** @var array $deprecated */
foreach ($this->deprecations as $deprecated) {
$list[] = $this->getDepracatedMessage($deprecated)[0];
}
return $list;
}
protected function addDeprecations() protected function addDeprecations()
{ {
if (!$this->deprecations) { if (!$this->deprecations) {
@@ -603,4 +1053,13 @@ class Debugger
return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')'; return $trace['function'] . '(' . implode(', ', $trace['args'] ?? []) . ')';
} }
protected function resolveCallable(callable $callable)
{
if (is_array($callable)) {
return get_class($callable[0]) . '->' . $callable[1] . '()';
}
return 'unknown';
}
} }

View File

@@ -11,6 +11,9 @@ namespace Grav\Common\GPM\Common;
use Grav\Common\Data\Data; use Grav\Common\Data\Data;
/**
* @property string $name
*/
class Package class Package
{ {
/** /**

View File

@@ -16,12 +16,8 @@ use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium; use Grav\Common\Page\Medium\Medium;
use Grav\Common\Processors\AssetsProcessor; use Grav\Common\Processors\AssetsProcessor;
use Grav\Common\Processors\BackupsProcessor; use Grav\Common\Processors\BackupsProcessor;
use Grav\Common\Processors\ConfigurationProcessor;
use Grav\Common\Processors\DebuggerAssetsProcessor; use Grav\Common\Processors\DebuggerAssetsProcessor;
use Grav\Common\Processors\DebuggerProcessor;
use Grav\Common\Processors\ErrorsProcessor;
use Grav\Common\Processors\InitializeProcessor; use Grav\Common\Processors\InitializeProcessor;
use Grav\Common\Processors\LoggerProcessor;
use Grav\Common\Processors\PagesProcessor; use Grav\Common\Processors\PagesProcessor;
use Grav\Common\Processors\PluginsProcessor; use Grav\Common\Processors\PluginsProcessor;
use Grav\Common\Processors\RenderProcessor; use Grav\Common\Processors\RenderProcessor;
@@ -90,10 +86,6 @@ class Grav extends Container
* @var array All middleware processors that are processed in $this->process() * @var array All middleware processors that are processed in $this->process()
*/ */
protected $middleware = [ protected $middleware = [
'configurationProcessor',
'loggerProcessor',
'errorsProcessor',
'debuggerProcessor',
'initializeProcessor', 'initializeProcessor',
'pluginsProcessor', 'pluginsProcessor',
'themesProcessor', 'themesProcessor',
@@ -157,15 +149,13 @@ class Grav extends Container
$this->initialized['setup'] = true; $this->initialized['setup'] = true;
$this->measureTime('_setup', 'Site Setup', function () use ($environment) { // Force environment if passed to the method.
// Force environment if passed to the method. if ($environment) {
if ($environment) { Setup::$environment = $environment;
Setup::$environment = $environment; }
}
$this['setup']; $this['setup'];
$this['streams']; $this['streams'];
});
return $this; return $this;
} }
@@ -186,18 +176,6 @@ class Grav extends Container
$container = new Container( $container = new Container(
[ [
'configurationProcessor' => function () {
return new ConfigurationProcessor($this);
},
'loggerProcessor' => function () {
return new LoggerProcessor($this);
},
'errorsProcessor' => function () {
return new ErrorsProcessor($this);
},
'debuggerProcessor' => function () {
return new DebuggerProcessor($this);
},
'initializeProcessor' => function () { 'initializeProcessor' => function () {
return new InitializeProcessor($this); return new InitializeProcessor($this);
}, },
@@ -237,13 +215,10 @@ class Grav extends Container
] ]
); );
$default = function (ServerRequestInterface $request) { $default = static function () {
return new Response(404); return new Response(404);
}; };
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$collection = new RequestHandler($this->middleware, $default, $container); $collection = new RequestHandler($this->middleware, $default, $container);
$response = $collection->handle($this['request']); $response = $collection->handle($this['request']);
@@ -251,32 +226,65 @@ class Grav extends Container
$this->header($response); $this->header($response);
echo $response->getBody(); echo $response->getBody();
$debugger->render(); $this['debugger']->render();
register_shutdown_function([$this, 'shutdown']); // Response object can turn off all shutdown processing. This can be used for example to speed up AJAX responses.
} // Note that using this feature will also turn off response compression.
if ($response->getHeaderLine('Grav-Internal-SkipShutdown') !== '1') {
/** register_shutdown_function([$this, 'shutdown']);
* Set the system locale based on the language and configuration
*/
public function setLocale()
{
// Initialize Locale if set and configured.
if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
$language = $this['language']->getLanguage();
setlocale(LC_ALL, \strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
} elseif ($this['config']->get('system.default_locale')) {
setlocale(LC_ALL, $this['config']->get('system.default_locale'));
} }
} }
/** /**
* Redirect browser to another location. * Terminates Grav request with a response.
*
* Please use this method instead of calling `die();` or `exit();`. Note that you need to create a response object.
*
* @param ResponseInterface $response
*/
public function close(ResponseInterface $response): void
{
// Make sure nothing extra gets written to the response.
while (ob_get_level()) {
ob_end_clean();
}
// Close the session.
if (isset($this['session'])) {
$this['session']->close();
}
/** @var ServerRequestInterface $request */
$request = $this['request'];
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$response = $debugger->logRequest($request, $response);
// Send the response and terminate.
$this->header($response);
echo $response->getBody();
exit();
}
/**
* @param ResponseInterface $response
* @deprecated 1.7 Do not use
*/
public function exit(ResponseInterface $response): void
{
$this->close($response);
}
/**
* Terminates Grav request and redirects browser to another location.
*
* Please use this method instead of calling `header("Location: {$url}", true, 302); exit();`.
* *
* @param string $route Internal route. * @param string $route Internal route.
* @param int $code Redirection code (30x) * @param int $code Redirection code (30x)
*/ */
public function redirect($route, $code = null) public function redirect($route, $code = null): void
{ {
/** @var Uri $uri */ /** @var Uri $uri */
$uri = $this['uri']; $uri = $this['uri'];
@@ -293,11 +301,7 @@ class Grav extends Container
$code = $this['config']->get('system.pages.redirect_default_code', 302); $code = $this['config']->get('system.pages.redirect_default_code', 302);
} }
if (isset($this['session'])) { if ($uri::isExternal($route)) {
$this['session']->close();
}
if ($uri->isExternal($route)) {
$url = $route; $url = $route;
} else { } else {
$url = rtrim($uri->rootUrl(), '/') . '/'; $url = rtrim($uri->rootUrl(), '/') . '/';
@@ -309,8 +313,9 @@ class Grav extends Container
} }
} }
header("Location: {$url}", true, $code); $response = new Response($code, ['Location' => $url]);
exit();
$this->close($response);
} }
/** /**
@@ -343,12 +348,30 @@ class Grav extends Container
header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}"); header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}");
foreach ($response->getHeaders() as $key => $values) { foreach ($response->getHeaders() as $key => $values) {
// Skip internal Grav headers.
if (strpos($key, 'Grav-Internal-') === 0) {
continue;
}
foreach ($values as $i => $value) { foreach ($values as $i => $value) {
header($key . ': ' . $value, $i === 0); header($key . ': ' . $value, $i === 0);
} }
} }
} }
/**
* Set the system locale based on the language and configuration
*/
public function setLocale()
{
// Initialize Locale if set and configured.
if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
$language = $this['language']->getLanguage();
setlocale(LC_ALL, \strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
} elseif ($this['config']->get('system.default_locale')) {
setlocale(LC_ALL, $this['config']->get('system.default_locale'));
}
}
/** /**
* Fires an event with optional parameters. * Fires an event with optional parameters.
* *
@@ -362,6 +385,10 @@ class Grav extends Container
/** @var EventDispatcher $events */ /** @var EventDispatcher $events */
$events = $this['events']; $events = $this['events'];
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$debugger->addEvent($eventName, $event, $events);
return $events->dispatch($eventName, $event); return $events->dispatch($eventName, $event);
} }
@@ -470,9 +497,7 @@ class Grav extends Container
return $container; return $container;
}; };
$container->measureTime('_services', 'Services', function () use ($container) { $container->registerServices();
$container->registerServices();
});
return $container; return $container;
} }

View File

@@ -190,7 +190,7 @@ class Truncator {
* Clean extra code * Clean extra code
* *
* @param DOMDocument $doc * @param DOMDocument $doc
* @param $container * @param DOMDocument $container
* @return string * @return string
*/ */
private static function getCleanedHTML(DOMDocument $doc, $container) private static function getCleanedHTML(DOMDocument $doc, $container)
@@ -203,8 +203,7 @@ class Truncator {
$doc->appendChild($container->firstChild); $doc->appendChild($container->firstChild);
} }
$html = trim($doc->saveHTML()); return trim($doc->saveHTML());
return $html;
} }
/** /**
@@ -242,17 +241,20 @@ class Truncator {
$ending = '...', $ending = '...',
$exact = false, $exact = false,
$considerHtml = true $considerHtml = true
) { )
{
if ($considerHtml) { if ($considerHtml) {
// if the plain text is shorter than the maximum length, return the whole text // if the plain text is shorter than the maximum length, return the whole text
if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) { if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
return $text; return $text;
} }
// splits all html-tags to scanable lines // splits all html-tags to scanable lines
preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER); preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
$total_length = strlen($ending); $total_length = strlen($ending);
$open_tags = array();
$truncate = ''; $truncate = '';
$open_tags = [];
foreach ($lines as $line_matchings) { foreach ($lines as $line_matchings) {
// if there is any html-tag in this line, handle it and add it (uncounted) to the output // if there is any html-tag in this line, handle it and add it (uncounted) to the output
if (!empty($line_matchings[1])) { if (!empty($line_matchings[1])) {
@@ -308,22 +310,22 @@ class Truncator {
} else { } else {
if (strlen($text) <= $length) { if (strlen($text) <= $length) {
return $text; return $text;
} else {
$truncate = substr($text, 0, $length - strlen($ending));
} }
$truncate = substr($text, 0, $length - strlen($ending));
} }
// if the words shouldn't be cut in the middle... // if the words shouldn't be cut in the middle...
if (!$exact) { if (!$exact) {
// ...search the last occurance of a space... // ...search the last occurance of a space...
$spacepos = strrpos($truncate, ' '); $spacepos = strrpos($truncate, ' ');
if (isset($spacepos)) { if (false !== $spacepos) {
// ...and cut the text in this position // ...and cut the text in this position
$truncate = substr($truncate, 0, $spacepos); $truncate = substr($truncate, 0, $spacepos);
} }
} }
// add the defined ending to the text // add the defined ending to the text
$truncate .= $ending; $truncate .= $ending;
if($considerHtml) { if (isset($open_tags)) {
// close all unclosed html-tags // close all unclosed html-tags
foreach ($open_tags as $tag) { foreach ($open_tags as $tag) {
$truncate .= '</' . $tag . '>'; $truncate .= '</' . $tag . '>';

View File

@@ -16,13 +16,15 @@ use Symfony\Component\Yaml\Yaml;
class YamlLinter class YamlLinter
{ {
public static function lint() public static function lint(string $folder = null)
{ {
$errors = static::lintConfig(); if (null !== $folder) {
$errors = $errors + static::lintPages(); $folder = $folder ?: GRAV_ROOT;
$errors = $errors + static::lintBlueprints();
return static::recurseFolder($folder);
return $errors; }
return array_merge(static::lintConfig(), static::lintPages(), static::lintBlueprints());
} }
public static function lintPages() public static function lintPages()
@@ -47,7 +49,7 @@ class YamlLinter
return static::recurseFolder('blueprints://'); return static::recurseFolder('blueprints://');
} }
public static function recurseFolder($path, $extensions = 'md|yaml') public static function recurseFolder($path, $extensions = '(md|yaml)')
{ {
$lint_errors = []; $lint_errors = [];

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Language; namespace Grav\Common\Language;
use Grav\Common\Debugger;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Config\Config; use Grav\Common\Config\Config;
use Negotiation\AcceptLanguage; use Negotiation\AcceptLanguage;
@@ -16,20 +17,22 @@ use Negotiation\LanguageNegotiator;
class Language class Language
{ {
/** @var Grav */
protected $grav; protected $grav;
protected $enabled = true;
/**
* @var array
*/
protected $languages = [];
protected $page_extensions = [];
protected $fallback_languages = [];
protected $default;
protected $active = null;
/** @var Config $config */ /** @var Config */
protected $config; protected $config;
protected $enabled = true;
/** @var array */
protected $languages = [];
protected $fallback_languages = [];
protected $fallback_extensions = [];
protected $page_extesions = [];
protected $default;
protected $active;
protected $http_accept_language; protected $http_accept_language;
protected $lang_in_url = false; protected $lang_in_url = false;
@@ -58,7 +61,7 @@ class Language
$this->default = reset($this->languages); $this->default = reset($this->languages);
} }
$this->page_extensions = null; $this->resetFallbackPageExtensions();
if (empty($this->languages)) { if (empty($this->languages)) {
$this->enabled = false; $this->enabled = false;
@@ -93,20 +96,22 @@ class Language
public function setLanguages($langs) public function setLanguages($langs)
{ {
$this->languages = $langs; $this->languages = $langs;
$this->init(); $this->init();
} }
/** /**
* Gets a pipe-separated string of available languages * Gets a pipe-separated string of available languages
* *
* @param string|null $delimiter Delimiter to be quoted.
* @return string * @return string
*/ */
public function getAvailable() public function getAvailable($delimiter = null)
{ {
$languagesArray = $this->languages; //Make local copy $languagesArray = $this->languages; //Make local copy
$languagesArray = array_map(function($value) { $languagesArray = array_map(function($value) use ($delimiter) {
return preg_quote($value); return preg_quote($value, $delimiter);
}, $languagesArray); }, $languagesArray);
sort($languagesArray); sort($languagesArray);
@@ -172,6 +177,10 @@ class Language
public function setActive($lang) public function setActive($lang)
{ {
if ($this->validate($lang)) { if ($this->validate($lang)) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage('Active language set to ' . $lang, 'debug');
$this->active = $lang; $this->active = $lang;
return $lang; return $lang;
@@ -196,7 +205,7 @@ class Language
// Try setting language from prefix of URL (/en/blah/blah). // Try setting language from prefix of URL (/en/blah/blah).
if (preg_match($regex, $uri, $matches)) { if (preg_match($regex, $uri, $matches)) {
$this->lang_in_url = true; $this->lang_in_url = true;
$this->active = $matches[2]; $this->setActive($matches[2]);
$uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1); $uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
// Store in session if language is different. // Store in session if language is different.
@@ -210,7 +219,7 @@ class Language
// Try getting language from the session, else no active. // Try getting language from the session, else no active.
if (isset($this->grav['session']) && $this->grav['session']->isStarted() && if (isset($this->grav['session']) && $this->grav['session']->isStarted() &&
$this->config->get('system.languages.session_store_active', true)) { $this->config->get('system.languages.session_store_active', true)) {
$this->active = $this->grav['session']->active_language ?: null; $this->setActive($this->grav['session']->active_language ?: null);
} }
// if still null, try from http_accept_language header // if still null, try from http_accept_language header
if ($this->active === null && if ($this->active === null &&
@@ -221,9 +230,9 @@ class Language
$best_language = $negotiator->getBest($accept, $this->languages); $best_language = $negotiator->getBest($accept, $this->languages);
if ($best_language instanceof AcceptLanguage) { if ($best_language instanceof AcceptLanguage) {
$this->active = $best_language->getType(); $this->setActive($best_language->getType());
} else { } else {
$this->active = $this->getDefault(); $this->setActive($this->getDefault());
} }
} }
@@ -275,52 +284,61 @@ class Language
return (bool) $this->lang_in_url; return (bool) $this->lang_in_url;
} }
/**
* Get full list of used language page extensions: [''=>'.md', 'en'=>'.en.md', ...]
*
* @param string|null $fileExtension
* @return mixed
*/
public function getPageExtensions($fileExtension = null)
{
$fileExtension = $fileExtension ?: CONTENT_EXT;
if (!isset($this->fallback_extensions[$fileExtension])) {
$extensions[''] = $fileExtension;
foreach ($this->languages as $code) {
$extensions[$code] = ".{$code}{$fileExtension}";
}
$this->fallback_extensions[$fileExtension] = $extensions;
}
return $this->fallback_extensions[$fileExtension];
}
/** /**
* Gets an array of valid extensions with active first, then fallback extensions * Gets an array of valid extensions with active first, then fallback extensions
* *
* @param string|null $file_ext * @param string|null $fileExtension
* * @param string|null $languageCode
* @return array * @param bool $assoc Return values in ['en' => '.en.md', ...] format.
* @return array Key is the language code, value is the file extension to be used.
*/ */
public function getFallbackPageExtensions($file_ext = null) public function getFallbackPageExtensions(string $fileExtension = null, string $languageCode = null, bool $assoc = false)
{ {
if (empty($this->page_extensions)) { $fileExtension = $fileExtension ?: CONTENT_EXT;
if (!$file_ext) { $key = $fileExtension . '-' . ($languageCode ?? 'default') . '-' . (int)$assoc;
$file_ext = CONTENT_EXT;
if (!isset($this->fallback_extensions[$key])) {
$all = $this->getPageExtensions($fileExtension);
$fallback = array_flip($this->getFallbackLanguages($languageCode, true));
$list = array_intersect_key($all, $fallback);
if (!$assoc) {
$list = array_values($list);
} }
if ($this->enabled()) { $this->fallback_extensions[$key] = $list;
$valid_lang_extensions = [];
foreach ($this->languages as $lang) {
$valid_lang_extensions[] = '.' . $lang . $file_ext;
}
if ($this->active) { /** @var Debugger $debugger */
$active_extension = '.' . $this->active . $file_ext; $debugger = $this->grav['debugger'];
$key = \array_search($active_extension, $valid_lang_extensions, true); $debugger->addMessage("Language fallback extensions for {$languageCode}", 'debug', $list);
// Default behavior is to find any language other than active
if ($this->config->get('system.languages.pages_fallback_only')) {
$slice = \array_slice($valid_lang_extensions, 0, $key+1);
$valid_lang_extensions = array_reverse($slice);
} else {
unset($valid_lang_extensions[$key]);
array_unshift($valid_lang_extensions, $active_extension);
}
}
$valid_lang_extensions[] = $file_ext;
$this->page_extensions = $valid_lang_extensions;
} else {
$this->page_extensions = (array)$file_ext;
}
} }
return $this->page_extensions; return $this->fallback_extensions[$key];
} }
/** /**
* Resets the page_extensions value. * Resets the fallback_languages value.
* *
* Useful to re-initialize the pages and change site language at runtime, example: * Useful to re-initialize the pages and change site language at runtime, example:
* *
@@ -332,33 +350,77 @@ class Language
*/ */
public function resetFallbackPageExtensions() public function resetFallbackPageExtensions()
{ {
$this->page_extensions = null; $this->fallback_languages = [];
$this->fallback_extensions = [];
$this->page_extesions = [];
} }
/** /**
* Gets an array of languages with active first, then fallback languages * Gets an array of languages with active first, then fallback languages.
* *
*
* @param string|null $languageCode
* @param bool $includeDefault If true, list contains '', which can be used for default
* @return array * @return array
*/ */
public function getFallbackLanguages() public function getFallbackLanguages(string $languageCode = null, bool $includeDefault = false)
{ {
if (empty($this->fallback_languages)) { // Handle default.
if ($this->enabled()) { if ($languageCode === '' || !$this->enabled()) {
$fallback_languages = $this->languages; return [''];
if ($this->active) {
$active_extension = $this->active;
$key = \array_search($active_extension, $fallback_languages, true);
unset($fallback_languages[$key]);
array_unshift($fallback_languages, $active_extension);
}
$this->fallback_languages = $fallback_languages;
}
// always add english in case a translation doesn't exist
$this->fallback_languages[] = 'en';
} }
return $this->fallback_languages; $default = $this->getDefault() ?? 'en';
$active = $languageCode ?? $this->getActive() ?? $default;
$key = $active . '-' . (int)$includeDefault;
if (!isset($this->fallback_languages[$key])) {
$fallback = $this->config->get('system.languages.content_fallback.' . $active);
$fallback_languages = [];
if (null === $fallback && $this->config->get('system.languages.pages_fallback_only', false)) {
// Special fallback list returns itself and all the previous items in reverse order:
// active: 'v2', languages: ['v1', 'v2', 'v3', 'v4'] => ['v2', 'v1', '']
if ($includeDefault) {
$fallback_languages[''] = '';
}
foreach ($this->languages as $code) {
$fallback_languages[$code] = $code;
if ($code === $active) {
break;
}
}
$fallback_languages = array_reverse($fallback_languages);
} else {
if (null === $fallback) {
$fallback = [$default];
} elseif (!is_array($fallback)) {
$fallback = is_string($fallback) && $fallback !== '' ? explode(',', $fallback) : [];
}
array_unshift($fallback, $active);
$fallback = array_unique($fallback);
foreach ($fallback as $code) {
// Default fallback list has active language followed by default language and extensionless file:
// active: 'fi', default: 'en', languages: ['sv', 'en', 'de', 'fi'] => ['fi', 'en', '']
$fallback_languages[$code] = $code;
if ($includeDefault && $code === $default) {
$fallback_languages[''] = '';
}
}
}
$fallback_languages = array_values($fallback_languages);
$this->fallback_languages[$key] = $fallback_languages;
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage("Language fallback for {$active}", 'debug', $fallback_languages);
}
return $this->fallback_languages[$key];
} }
/** /**
@@ -534,4 +596,11 @@ class Language
return LanguageCodes::get($code, $type); return LanguageCodes::get($code, $type);
} }
public function __debugInfo()
{
$vars = get_object_vars($this);
unset($vars['grav'], $vars['config']);
return $vars;
}
} }

View File

@@ -202,4 +202,13 @@ class LanguageCodes
return false; return false;
} }
public static function getList($native = true)
{
$list = [];
foreach (static::$codes as $key => $names) {
$list[$key] = $native ? $names['nativeName'] : $names['name'];
}
return $list;
}
} }

View File

@@ -11,10 +11,11 @@ namespace Grav\Common\Page;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Iterator; use Grav\Common\Iterator;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils; use Grav\Common\Utils;
class Collection extends Iterator class Collection extends Iterator implements PageCollectionInterface
{ {
/** /**
* @var Pages * @var Pages
@@ -51,6 +52,20 @@ class Collection extends Iterator
return $this->params; return $this->params;
} }
/**
* Set parameters to the Collection
*
* @param array $params
*
* @return $this
*/
public function setParams(array $params)
{
$this->params = array_merge($this->params, $params);
return $this;
}
/** /**
* Add a single page to a collection * Add a single page to a collection
* *
@@ -94,10 +109,10 @@ class Collection extends Iterator
* *
* Merge another collection with the current collection * Merge another collection with the current collection
* *
* @param Collection $collection * @param PageCollectionInterface $collection
* @return $this * @return $this
*/ */
public function merge(Collection $collection) public function merge(PageCollectionInterface $collection)
{ {
foreach($collection as $page) { foreach($collection as $page) {
$this->addPage($page); $this->addPage($page);
@@ -109,10 +124,10 @@ class Collection extends Iterator
/** /**
* Intersect another collection with the current collection * Intersect another collection with the current collection
* *
* @param Collection $collection * @param PageCollectionInterface $collection
* @return $this * @return $this
*/ */
public function intersect(Collection $collection) public function intersect(PageCollectionInterface $collection)
{ {
$array1 = $this->items; $array1 = $this->items;
$array2 = $collection->toArray(); $array2 = $collection->toArray();
@@ -124,20 +139,6 @@ class Collection extends Iterator
return $this; return $this;
} }
/**
* Set parameters to the Collection
*
* @param array $params
*
* @return $this
*/
public function setParams(array $params)
{
$this->params = array_merge($this->params, $params);
return $this;
}
/** /**
* Returns current page. * Returns current page.
* *
@@ -240,7 +241,7 @@ class Collection extends Iterator
* *
* @return bool True if item is first. * @return bool True if item is first.
*/ */
public function isFirst($path) public function isFirst($path): bool
{ {
return $this->items && $path === array_keys($this->items)[0]; return $this->items && $path === array_keys($this->items)[0];
} }
@@ -252,7 +253,7 @@ class Collection extends Iterator
* *
* @return bool True if item is last. * @return bool True if item is last.
*/ */
public function isLast($path) public function isLast($path): bool
{ {
return $this->items && $path === array_keys($this->items)[\count($this->items) - 1]; return $this->items && $path === array_keys($this->items)[\count($this->items) - 1];
} }
@@ -301,7 +302,6 @@ class Collection extends Iterator
} }
return $this; return $this;
} }
/** /**
@@ -309,11 +309,13 @@ class Collection extends Iterator
* *
* @param string $path the path the item * @param string $path the path the item
* *
* @return int the index of the current page. * @return int|null The index of the current page, null if not found.
*/ */
public function currentPosition($path) public function currentPosition($path): ?int
{ {
return \array_search($path, \array_keys($this->items), true); $pos = \array_search($path, \array_keys($this->items), true);
return $pos !== false ? $pos : null;
} }
/** /**

View File

@@ -10,9 +10,13 @@
namespace Grav\Common\Page; namespace Grav\Common\Page;
use RocketTheme\Toolbox\ArrayTraits\Constructor; use RocketTheme\Toolbox\ArrayTraits\Constructor;
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccess; use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
class Header implements \ArrayAccess class Header implements \ArrayAccess, ExportInterface
{ {
use NestedArrayAccess, Constructor; use NestedArrayAccessWithGetters, Constructor, Export;
protected $items;
} }

View File

@@ -0,0 +1,263 @@
<?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\Interfaces;
interface PageCollectionInterface extends \Traversable, \ArrayAccess, \Countable, \Serializable
{
/**
* Get the collection params
*
* @return array
*/
public function params();
/**
* Set parameters to the Collection
*
* @param array $params
*
* @return $this
*/
public function setParams(array $params);
/**
* Add a single page to a collection
*
* @param PageInterface $page
*
* @return $this
*/
public function addPage(PageInterface $page);
/**
* Add a page with path and slug
*
* @param string $path
* @param string $slug
* @return $this
*/
//public function add($path, $slug);
/**
*
* Create a copy of this collection
*
* @return static
*/
public function copy();
/**
*
* Merge another collection with the current collection
*
* @param PageCollectionInterface $collection
* @return $this
*/
public function merge(PageCollectionInterface $collection);
/**
* Intersect another collection with the current collection
*
* @param PageCollectionInterface $collection
* @return $this
*/
public function intersect(PageCollectionInterface $collection);
/**
* Split collection into array of smaller collections.
*
* @param int $size
* @return PageCollectionInterface[]
*/
public function batch($size);
/**
* Remove item from the list.
*
* @param PageInterface|string|null $key
*
* @return $this
* @throws \InvalidArgumentException
*/
//public function remove($key = null);
/**
* Reorder collection.
*
* @param string $by
* @param string $dir
* @param array $manual
* @param string $sort_flags
*
* @return $this
*/
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null);
/**
* Check to see if this item is the first in the collection.
*
* @param string $path
*
* @return bool True if item is first.
*/
public function isFirst($path): bool;
/**
* Check to see if this item is the last in the collection.
*
* @param string $path
*
* @return bool True if item is last.
*/
public function isLast($path): bool;
/**
* Gets the previous sibling based on current position.
*
* @param string $path
*
* @return PageInterface The previous item.
*/
public function prevSibling($path);
/**
* Gets the next sibling based on current position.
*
* @param string $path
*
* @return PageInterface The next item.
*/
public function nextSibling($path);
/**
* Returns the adjacent sibling based on a direction.
*
* @param string $path
* @param int $direction either -1 or +1
*
* @return PageInterface|PageCollectionInterface The sibling item.
*/
public function adjacentSibling($path, $direction = 1);
/**
* Returns the item in the current position.
*
* @param string $path the path the item
*
* @return int|null The index of the current page, null if not found.
*/
public function currentPosition($path): ?int;
/**
* Returns the items between a set of date ranges of either the page date field (default) or
* an arbitrary datetime page field where end date is optional
* Dates can be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
* @param string $startDate
* @param bool $endDate
* @param string|null $field
*
* @return $this
* @throws \Exception
*/
public function dateRange($startDate, $endDate = false, $field = null);
/**
* Creates new collection with only visible pages
*
* @return PageCollectionInterface The collection with only visible pages
*/
public function visible();
/**
* Creates new collection with only non-visible pages
*
* @return PageCollectionInterface The collection with only non-visible pages
*/
public function nonVisible();
/**
* Creates new collection with only modular pages
*
* @return PageCollectionInterface The collection with only modular pages
*/
public function modular();
/**
* Creates new collection with only non-modular pages
*
* @return PageCollectionInterface The collection with only non-modular pages
*/
public function nonModular();
/**
* Creates new collection with only published pages
*
* @return PageCollectionInterface The collection with only published pages
*/
public function published();
/**
* Creates new collection with only non-published pages
*
* @return PageCollectionInterface The collection with only non-published pages
*/
public function nonPublished();
/**
* Creates new collection with only routable pages
*
* @return PageCollectionInterface The collection with only routable pages
*/
public function routable();
/**
* Creates new collection with only non-routable pages
*
* @return PageCollectionInterface The collection with only non-routable pages
*/
public function nonRoutable();
/**
* Creates new collection with only pages of the specified type
*
* @param string $type
*
* @return PageCollectionInterface The collection
*/
public function ofType($type);
/**
* Creates new collection with only pages of one of the specified types
*
* @param string[] $types
*
* @return PageCollectionInterface The collection
*/
public function ofOneOfTheseTypes($types);
/**
* Creates new collection with only pages of one of the specified access levels
*
* @param array $accessLevels
*
* @return PageCollectionInterface The collection
*/
public function ofOneOfTheseAccessLevels($accessLevels);
/**
* Get the extended version of this Collection with each page keyed by route
*
* @return array
* @throws \Exception
*/
public function toExtendedArray();
}

View File

@@ -36,6 +36,13 @@ interface PageContentInterface
*/ */
public function summary($size = null, $textOnly = false); public function summary($size = null, $textOnly = false);
/**
* Sets the summary of the page
*
* @param string $summary Summary
*/
public function setSummary($summary);
/** /**
* Gets and Sets the content based on content portion of the .md file * Gets and Sets the content based on content portion of the .md file
* *
@@ -64,7 +71,7 @@ interface PageContentInterface
* *
* @param string|null $var * @param string|null $var
* *
* @return null * @return string
*/ */
public function rawMarkdown($var = null); public function rawMarkdown($var = null);
@@ -167,7 +174,7 @@ interface PageContentInterface
* *
* @param int $var * @param int $var
* *
* @return int|bool * @return string|bool
*/ */
public function order($var = null); public function order($var = null);

View File

@@ -0,0 +1,30 @@
<?php
namespace Grav\Common\Page\Interfaces;
interface PageFormInterface
{
/**
* Return all the forms which are associated to this page.
*
* Forms are returned as [name => blueprint, ...], where blueprint follows the regular form blueprint format.
*
* @return array
*/
//public function getForms(): array;
/**
* Add forms to this page.
*
* @param array $new
* @param bool $override
* @return $this
*/
public function addForms(array $new/*, $override = true*/);
/**
* Alias of $this->getForms();
*
* @return array
*/
public function forms();//: array;
}

View File

@@ -14,6 +14,12 @@ use Grav\Common\Media\Interfaces\MediaInterface;
/** /**
* Class implements page interface. * Class implements page interface.
*/ */
interface PageInterface extends PageContentInterface, PageRoutableInterface, PageTranslateInterface, MediaInterface, PageLegacyInterface interface PageInterface extends
PageContentInterface,
PageFormInterface,
PageRoutableInterface,
PageTranslateInterface,
MediaInterface,
PageLegacyInterface
{ {
} }

View File

@@ -51,13 +51,6 @@ interface PageLegacyInterface
public function httpHeaders(); public function httpHeaders();
/**
* Sets the summary of the page
*
* @param string $summary Summary
*/
public function setSummary($summary);
/** /**
* Get the contentMeta array and initialize content first if it's not already * Get the contentMeta array and initialize content first if it's not already
* *
@@ -69,7 +62,7 @@ interface PageLegacyInterface
* Add an entry to the page's contentMeta array * Add an entry to the page's contentMeta array
* *
* @param string $name * @param string $name
* @param string $value * @param mixed $value
*/ */
public function addContentMeta($name, $value); public function addContentMeta($name, $value);
@@ -229,9 +222,9 @@ interface PageLegacyInterface
* Allows a page to override the output render format, usually the extension provided * Allows a page to override the output render format, usually the extension provided
* in the URL. (e.g. `html`, `json`, `xml`, etc). * in the URL. (e.g. `html`, `json`, `xml`, etc).
* *
* @param null $var * @param string|null $var
* *
* @return null * @return string
*/ */
public function templateFormat($var = null); public function templateFormat($var = null);

View File

@@ -109,7 +109,7 @@ class ImageFile extends Image
* Gets the hash. * Gets the hash.
* @param string $type * @param string $type
* @param int $quality * @param int $quality
* @param [] $extras * @param array $extras
* @return null * @return null
*/ */
public function getHash($type = 'guess', $quality = 80, $extras = []) public function getHash($type = 'guess', $quality = 80, $extras = [])

View File

@@ -15,17 +15,17 @@ use Grav\Common\Data\Blueprint;
use Grav\Common\File\CompiledYamlFile; use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra; use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Media\Traits\MediaTrait; use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Taxonomy; use Grav\Common\Page\Traits\PageFormTrait;
use Grav\Common\Uri; use Grav\Common\Uri;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Common\Yaml; use Grav\Common\Yaml;
use Negotiation\Accept;
use Negotiation\Negotiator;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile; use RocketTheme\Toolbox\File\MarkdownFile;
@@ -33,6 +33,7 @@ define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
class Page implements PageInterface class Page implements PageInterface
{ {
use PageFormTrait;
use MediaTrait; use MediaTrait;
/** /**
@@ -93,11 +94,9 @@ class Page implements PageInterface
protected $ssl; protected $ssl;
protected $template_format; protected $template_format;
protected $debugger; protected $debugger;
/** @var array */
protected $forms;
/** /**
* @var PageInterface Unmodified (original) version of the page. Used for copying and moving the page. * @var PageInterface|null Unmodified (original) version of the page. Used for copying and moving the page.
*/ */
private $_original; private $_original;
@@ -129,7 +128,10 @@ class Page implements PageInterface
*/ */
public function init(\SplFileInfo $file, $extension = null) public function init(\SplFileInfo $file, $extension = null)
{ {
$config = Grav::instance()['config']; $grav = Grav::instance();
/** @var Config $config */
$config = $grav['config'];
// some extension logic // some extension logic
if (empty($extension)) { if (empty($extension)) {
@@ -139,8 +141,13 @@ class Page implements PageInterface
} }
// extract page language from page extension // extract page language from page extension
$language = trim(basename($this->extension(), 'md'), '.') ?: null; $languageCode = trim(basename($this->extension(), 'md'), '.') ?: null;
$this->language($language); if (!$languageCode) {
/** @var Language $language */
$language = $grav['language'];
$languageCode = $language->enabled() ? $language->getDefault() : null;
}
$this->language($languageCode);
$this->hide_home_route = $config->get('system.home.hide_in_urls', false); $this->hide_home_route = $config->get('system.home.hide_in_urls', false);
$this->home_route = $this->adjustRouteCase($config->get('system.home.alias')); $this->home_route = $this->adjustRouteCase($config->get('system.home.alias'));
@@ -188,16 +195,32 @@ class Page implements PageInterface
*/ */
public function translatedLanguages($onlyPublished = false) public function translatedLanguages($onlyPublished = false)
{ {
$filename = substr($this->name, 0, -(strlen($this->extension()))); $grav = Grav::instance();
$config = Grav::instance()['config'];
$languages = $config->get('system.languages.supported', []); /** @var Language $language */
$language = $grav['language'];
$languages = $language->getLanguages();
$defaultCode = $language->getDefault();
$name = substr($this->name, 0, -strlen($this->extension()));
$translatedLanguages = []; $translatedLanguages = [];
foreach ($languages as $language) { foreach ($languages as $languageCode) {
$path = $this->path . DS . $this->folder . DS . $filename . '.' . $language . '.md'; $languageExtension = ".{$languageCode}.md";
if (file_exists($path)) { $path = $this->path . DS . $this->folder . DS . $name . $languageExtension;
$exists = file_exists($path);
// Default language may be saved without language file location.
if (!$exists && $languageCode === $defaultCode) {
$languageExtension = '.md';
$path = $this->path . DS . $this->folder . DS . $name . $languageExtension;
$exists = file_exists($path);
}
if ($exists) {
$aPage = new Page(); $aPage = new Page();
$aPage->init(new \SplFileInfo($path), $language . '.md'); $aPage->init(new \SplFileInfo($path), $languageExtension);
$route = $aPage->header()->routes['default'] ?? $aPage->rawRoute(); $route = $aPage->header()->routes['default'] ?? $aPage->rawRoute();
if (!$route) { if (!$route) {
@@ -208,7 +231,7 @@ class Page implements PageInterface
continue; continue;
} }
$translatedLanguages[$language] = $route; $translatedLanguages[$languageCode] = $route;
} }
} }
@@ -224,22 +247,41 @@ class Page implements PageInterface
*/ */
public function untranslatedLanguages($includeUnpublished = false) public function untranslatedLanguages($includeUnpublished = false)
{ {
$filename = substr($this->name, 0, -strlen($this->extension())); $grav = Grav::instance();
$config = Grav::instance()['config'];
$languages = $config->get('system.languages.supported', []); /** @var Language $language */
$language = $grav['language'];
$languages = $language->getLanguages();
$defaultCode = $language->getDefault();
$name = substr($this->name, 0, -strlen($this->extension()));
$untranslatedLanguages = []; $untranslatedLanguages = [];
foreach ($languages as $language) { foreach ($languages as $languageCode) {
$path = $this->path . DS . $this->folder . DS . $filename . '.' . $language . '.md'; $path = $this->path . DS . $this->folder . DS . $name . '.' . $languageCode . '.md';
if (file_exists($path)) { $exists = file_exists($path);
$aPage = new Page();
$aPage->init(new \SplFileInfo($path), $language . '.md'); // Default language may be saved without language file location.
if ($includeUnpublished && !$aPage->published()) { if (!$exists && $languageCode === $defaultCode) {
$untranslatedLanguages[] = $language; $path = $this->path . DS . $this->folder . DS . $name . '.md';
} $exists = file_exists($path);
} else {
$untranslatedLanguages[] = $language;
} }
if ($exists) {
if ($includeUnpublished) {
continue;
}
$aPage = new Page();
$aPage->init(new \SplFileInfo($path), $languageCode . '.md');
if (!$aPage->published()) {
continue;
}
}
$untranslatedLanguages[] = $languageCode;
} }
return $untranslatedLanguages; return $untranslatedLanguages;
@@ -568,8 +610,7 @@ class Page implements PageInterface
$content = $textOnly ? strip_tags($this->content()) : $this->content(); $content = $textOnly ? strip_tags($this->content()) : $this->content();
$summary_size = $this->summary_size; $summary_size = $this->summary_size;
} else { } else {
$content = strip_tags($this->summary); $content = $textOnly ? strip_tags($this->summary) : $this->summary;
// Use mb_strwidth to deal with the 2 character widths characters
$summary_size = mb_strwidth($content, 'utf-8'); $summary_size = mb_strwidth($content, 'utf-8');
} }
@@ -772,7 +813,7 @@ class Page implements PageInterface
* Add an entry to the page's contentMeta array * Add an entry to the page's contentMeta array
* *
* @param string $name * @param string $name
* @param string $value * @param mixed $value
*/ */
public function addContentMeta($name, $value) public function addContentMeta($name, $value)
{ {
@@ -784,16 +825,12 @@ class Page implements PageInterface
* *
* @param string|null $name * @param string|null $name
* *
* @return string * @return mixed|null
*/ */
public function getContentMeta($name = null) public function getContentMeta($name = null)
{ {
if ($name) { if ($name) {
if (isset($this->content_meta[$name])) { return $this->content_meta[$name] ?? null;
return $this->content_meta[$name];
}
return null;
} }
return $this->content_meta; return $this->content_meta;
@@ -922,13 +959,16 @@ class Page implements PageInterface
return $this->slug(); return $this->slug();
} }
if ($name === 'name') { if ($name === 'name') {
$name = $this->name();
$language = $this->language() ? '.' . $this->language() : ''; $language = $this->language() ? '.' . $this->language() : '';
$name_val = str_replace($language . '.md', '', $this->name()); $pattern = '%(' . preg_quote($language, '%') . ')?\.md$%';
$name = preg_replace($pattern, '', $name);
if ($this->modular()) { if ($this->modular()) {
return 'modular/' . $name_val; return 'modular/' . $name;
} }
return $name_val; return $name;
} }
if ($name === 'media') { if ($name === 'media') {
return $this->media()->all(); return $this->media()->all();
@@ -1205,93 +1245,12 @@ class Page implements PageInterface
return $this->id(); return $this->id();
} }
/**
* Returns normalized list of name => form pairs.
*
* @return array
*/
public function forms()
{
if (null === $this->forms) {
$header = $this->header();
// Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin)
$grav = Grav::instance();
$grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $this, 'header' => $header]));
$rules = $header->rules ?? null;
if (!\is_array($rules)) {
$rules = [];
}
$forms = [];
// First grab page.header.form
$form = $this->normalizeForm($header->form ?? null, null, $rules);
if ($form) {
$forms[$form['name']] = $form;
}
// Append page.header.forms (override singular form if it clashes)
$headerForms = $header->forms ?? null;
if (\is_array($headerForms)) {
foreach ($headerForms as $name => $form) {
$form = $this->normalizeForm($form, $name, $rules);
if ($form) {
$forms[$form['name']] = $form;
}
}
}
$this->forms = $forms;
}
return $this->forms;
}
/**
* @param array $new
*/
public function addForms(array $new)
{
// Initialize forms.
$this->forms();
foreach ($new as $form) {
$form = $this->normalizeForm($form);
if ($form) {
$this->forms[$form['name']] = $form;
}
}
}
protected function normalizeForm($form, $name = null, array $rules = [])
{
if (!\is_array($form)) {
return null;
}
// Ignore numeric indexes on name.
if (!$name || (string)(int)$name === (string)$name) {
$name = null;
}
$name = $name ?? $form['name'] ?? $this->slug();
$formRules = $form['rules'] ?? null;
if (!\is_array($formRules)) {
$formRules = [];
}
return ['name' => $name, 'rules' => $rules + $formRules] + $form;
}
/** /**
* Gets and sets the associated media as found in the page folder. * Gets and sets the associated media as found in the page folder.
* *
* @param Media $var Representation of associated media. * @param Media $var Representation of associated media.
* *
* @return Media Representation of associated media. * @return MediaCollectionInterface|Media Representation of associated media.
*/ */
public function media($var = null) public function media($var = null)
{ {
@@ -1371,67 +1330,32 @@ class Page implements PageInterface
} }
/** /**
* Allows a page to override the output render format, usually the extension provided * Allows a page to override the output render format, usually the extension provided in the URL.
* in the URL. (e.g. `html`, `json`, `xml`, etc). * (e.g. `html`, `json`, `xml`, etc).
* *
* @param null $var * @param string|null $var
* *
* @return null * @return string
*/ */
public function templateFormat($var = null) public function templateFormat($var = null)
{ {
if ($var !== null) { if (null !== $var) {
$this->template_format = $var; $this->template_format = is_string($var) ? $var : null;
return $this->template_format;
} }
if (isset($this->template_format)) { if (!isset($this->template_format)) {
return $this->template_format; $this->template_format = ltrim($this->header->append_url_extension ?? Utils::getPageFormat(), '.');
} }
// Set from URL extension set on page
$page_extension = trim($this->header->append_url_extension ?? '' , '.');
if (!empty($page_extension)) {
$this->template_format = $page_extension;
return $this->template_format;
}
// Set from uri extension
$uri_extension = Grav::instance()['uri']->extension();
if (is_string($uri_extension)) {
$this->template_format = $uri_extension;
return $this->template_format;
}
// Use content negotiation via the `accept:` header
$http_accept = $_SERVER['HTTP_ACCEPT'] ?? null;
if (is_string($http_accept)) {
$negotiator = new Negotiator();
$supported_types = Utils::getSupportPageTypes(['html', 'json']);
$priorities = Utils::getMimeTypes($supported_types);
$media_type = $negotiator->getBest($http_accept, $priorities);
$mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
$this->template_format = Utils::getExtensionByMime($mimetype);
return $this->template_format;
}
// Last chance set a default type
$this->template_format = 'html';
return $this->template_format; return $this->template_format;
} }
/** /**
* Gets and sets the extension field. * Gets and sets the extension field.
* *
* @param null $var * @param string|null $var
* *
* @return null|string * @return string
*/ */
public function extension($var = null) public function extension($var = null)
{ {
@@ -1660,7 +1584,7 @@ class Page implements PageInterface
} }
/** /**
* Returns the state of the debugger override etting for this page * Returns the state of the debugger override setting for this page
* *
* @return mixed * @return mixed
*/ */
@@ -1689,9 +1613,10 @@ class Page implements PageInterface
$this->metadata = []; $this->metadata = [];
$metadata = [];
// Set the Generator tag // Set the Generator tag
$metadata['generator'] = 'GravCMS'; $metadata = [
'generator' => 'GravCMS'
];
// Get initial metadata for the page // Get initial metadata for the page
$metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata')); $metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata'));
@@ -1784,7 +1709,7 @@ class Page implements PageInterface
* *
* @param int $var * @param int $var
* *
* @return int|bool * @return string|bool
*/ */
public function order($var = null) public function order($var = null)
{ {
@@ -2040,7 +1965,7 @@ class Page implements PageInterface
* *
* @param string $var redirect url * @param string $var redirect url
* *
* @return string * @return string|null
*/ */
public function redirect($var = null) public function redirect($var = null)
{ {
@@ -2048,7 +1973,7 @@ class Page implements PageInterface
$this->redirect = $var; $this->redirect = $var;
} }
return $this->redirect; return $this->redirect ?: null;
} }
/** /**
@@ -2671,321 +2596,45 @@ class Page implements PageInterface
public function collection($params = 'content', $pagination = true) public function collection($params = 'content', $pagination = true)
{ {
if (is_string($params)) { if (is_string($params)) {
// Look into a page header field.
$params = (array)$this->value('header.' . $params); $params = (array)$this->value('header.' . $params);
} elseif (!is_array($params)) { } elseif (!is_array($params)) {
throw new \InvalidArgumentException('Argument should be either header variable name or array of parameters'); throw new \InvalidArgumentException('Argument should be either header variable name or array of parameters');
} }
if (!isset($params['items'])) { $context = [
return new Collection(); 'pagination' => $pagination,
} 'self' => $this
];
// See if require published filter is set and use that, if assume published=true /** @var Pages $pages */
$only_published = true; $pages = Grav::instance()['pages'];
if (isset($params['filter']['published']) && $params['filter']['published']) {
$only_published = false;
} elseif (isset($params['filter']['non-published']) && $params['filter']['non-published']) {
$only_published = false;
}
$collection = $this->evaluate($params['items'], $only_published); return $pages->getCollection($params, $context);
if (!$collection instanceof Collection) {
$collection = new Collection();
}
$collection->setParams($params);
/** @var Uri $uri */
$uri = Grav::instance()['uri'];
/** @var Config $config */
$config = Grav::instance()['config'];
$process_taxonomy = $params['url_taxonomy_filters'] ?? $config->get('system.pages.url_taxonomy_filters');
if ($process_taxonomy) {
foreach ((array)$config->get('site.taxonomies') as $taxonomy) {
if ($uri->param(rawurlencode($taxonomy))) {
$items = explode(',', $uri->param($taxonomy));
$collection->setParams(['taxonomies' => [$taxonomy => $items]]);
foreach ($collection as $page) {
// Don't filter modular pages
if ($page->modular()) {
continue;
}
foreach ($items as $item) {
$item = rawurldecode($item);
if (empty($page->taxonomy[$taxonomy]) || !\in_array(htmlspecialchars_decode($item, ENT_QUOTES), $page->taxonomy[$taxonomy], true)
) {
$collection->remove($page->path());
}
}
}
}
}
}
// If a filter or filters are set, filter the collection...
if (isset($params['filter'])) {
// remove any inclusive sets from filer:
$sets = ['published', 'visible', 'modular', 'routable'];
foreach ($sets as $type) {
$var = "non-{$type}";
if (isset($params['filter'][$type], $params['filter'][$var]) && $params['filter'][$type] && $params['filter'][$var]) {
unset ($params['filter'][$type], $params['filter'][$var]);
}
}
foreach ((array)$params['filter'] as $type => $filter) {
switch ($type) {
case 'published':
if ((bool) $filter) {
$collection->published();
}
break;
case 'non-published':
if ((bool) $filter) {
$collection->nonPublished();
}
break;
case 'visible':
if ((bool) $filter) {
$collection->visible();
}
break;
case 'non-visible':
if ((bool) $filter) {
$collection->nonVisible();
}
break;
case 'modular':
if ((bool) $filter) {
$collection->modular();
}
break;
case 'non-modular':
if ((bool) $filter) {
$collection->nonModular();
}
break;
case 'routable':
if ((bool) $filter) {
$collection->routable();
}
break;
case 'non-routable':
if ((bool) $filter) {
$collection->nonRoutable();
}
break;
case 'type':
$collection->ofType($filter);
break;
case 'types':
$collection->ofOneOfTheseTypes($filter);
break;
case 'access':
$collection->ofOneOfTheseAccessLevels($filter);
break;
}
}
}
if (isset($params['dateRange'])) {
$start = $params['dateRange']['start'] ?? 0;
$end = $params['dateRange']['end'] ?? false;
$field = $params['dateRange']['field'] ?? false;
$collection->dateRange($start, $end, $field);
}
if (isset($params['order'])) {
$by = $params['order']['by'] ?? 'default';
$dir = $params['order']['dir'] ?? 'asc';
$custom = $params['order']['custom'] ?? null;
$sort_flags = $params['order']['sort_flags'] ?? null;
if (is_array($sort_flags)) {
$sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
$sort_flags = array_reduce($sort_flags, function ($a, $b) {
return $a | $b;
}, 0); //merge constant values using bit or
}
$collection->order($by, $dir, $custom, $sort_flags);
}
/** @var Grav $grav */
$grav = Grav::instance();
// New Custom event to handle things like pagination.
$grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection]));
// Slice and dice the collection if pagination is required
if ($pagination) {
$params = $collection->params();
$limit = $params['limit'] ?? 0;
$start = !empty($params['pagination']) ? ($uri->currentPage() - 1) * $limit : 0;
if ($limit && $collection->count() > $limit) {
$collection->slice($start, $limit);
}
}
return $collection;
} }
/** /**
* @param string|array $value * @param string|array $value
* @param bool $only_published * @param bool $only_published
* @return mixed * @return Collection
*/ */
public function evaluate($value, $only_published = true) public function evaluate($value, $only_published = true)
{ {
// Parse command. $params = [
if (is_string($value)) { 'items' => $value,
// Format: @command.param 'published' => $only_published
$cmd = $value; ];
$params = []; $context = [
} elseif (is_array($value) && count($value) == 1 && !is_int(key($value))) { 'event' => false,
// Format: @command.param: { attr1: value1, attr2: value2 } 'pagination' => false,
$cmd = (string)key($value); 'url_taxonomy_filters' => false,
$params = (array)current($value); 'self' => $this
} else { ];
$result = [];
foreach ((array)$value as $key => $val) {
if (is_int($key)) {
$result = $result + $this->evaluate($val)->toArray();
} else {
$result = $result + $this->evaluate([$key => $val])->toArray();
}
}
return new Collection($result);
}
/** @var Pages $pages */ /** @var Pages $pages */
$pages = Grav::instance()['pages']; $pages = Grav::instance()['pages'];
$parts = explode('.', $cmd); return $pages->getCollection($params, $context);
$current = array_shift($parts);
/** @var Collection $results */
$results = new Collection();
switch ($current) {
case 'self@':
case '@self':
if (!empty($parts)) {
switch ($parts[0]) {
case 'modular':
// @self.modular: false (alternative to @self.children)
if (!empty($params) && $params[0] === false) {
$results = $this->children()->nonModular();
break;
}
$results = $this->children()->modular();
break;
case 'children':
$results = $this->children()->nonModular();
break;
case 'all':
$results = $this->children();
break;
case 'parent':
$collection = new Collection();
$results = $collection->addPage($this->parent());
break;
case 'siblings':
if (!$this->parent()) {
return new Collection();
}
$results = $this->parent()->children()->remove($this->path());
break;
case 'descendants':
$results = $pages->all($this)->remove($this->path())->nonModular();
break;
}
}
break;
case 'page@':
case '@page':
$page = null;
if (!empty($params)) {
$page = $this->find($params[0]);
}
// safety check in case page is not found
if (!isset($page)) {
return $results;
}
// Handle a @page.descendants
if (!empty($parts)) {
switch ($parts[0]) {
case 'modular':
$results = new Collection();
foreach ($page->children() as $child) {
$results = $results->addPage($child);
}
$results->modular();
break;
case 'page':
case 'self':
$results = new Collection();
$results = $results->addPage($page);
break;
case 'descendants':
$results = $pages->all($page)->remove($page->path())->nonModular();
break;
case 'children':
$results = $page->children()->nonModular();
break;
}
} else {
$results = $page->children()->nonModular();
}
break;
case 'root@':
case '@root':
if (!empty($parts) && $parts[0] === 'descendants') {
$results = $pages->all($pages->root())->nonModular();
} else {
$results = $pages->root()->children()->nonModular();
}
break;
case 'taxonomy@':
case '@taxonomy':
// Gets a collection of pages by using one of the following formats:
// @taxonomy.category: blog
// @taxonomy.category: [ blog, featured ]
// @taxonomy: { category: [ blog, featured ], level: 1 }
/** @var Taxonomy $taxonomy_map */
$taxonomy_map = Grav::instance()['taxonomy'];
if (!empty($parts)) {
$params = [implode('.', $parts) => $params];
}
$results = $taxonomy_map->findTaxonomy($params);
break;
}
if ($only_published) {
$results = $results->published();
}
return $results;
} }
/** /**
@@ -3170,7 +2819,7 @@ class Page implements PageInterface
/** /**
* Gets the action. * Gets the action.
* *
* @return string The Action string. * @return string|null The Action string.
*/ */
public function getAction() public function getAction()
{ {

View File

@@ -13,13 +13,18 @@ use Grav\Common\Cache;
use Grav\Common\Config\Config; use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint; use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Blueprints; use Grav\Common\Data\Blueprints;
use Grav\Common\Debugger;
use Grav\Common\Filesystem\Folder; use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Language\Language; use Grav\Common\Language\Language;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Taxonomy; use Grav\Common\Taxonomy;
use Grav\Common\Uri; use Grav\Common\Uri;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Plugin\Admin; use Grav\Plugin\Admin;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -28,64 +33,46 @@ use Collator;
class Pages class Pages
{ {
/** /** @var Grav */
* @var Grav
*/
protected $grav; protected $grav;
/** /** @var Flex */
* @var array|PageInterface[] protected $flex;
*/
/** @var array|PageInterface[] */
protected $instances; protected $instances;
/** /** @var array */
* @var array|string[]
*/
protected $children; protected $children;
/** /** @var string */
* @var string
*/
protected $base = ''; protected $base = '';
/** /** @var string[] */
* @var array|string[]
*/
protected $baseRoute = []; protected $baseRoute = [];
/** /** @var string[] */
* @var array|string[]
*/
protected $routes = []; protected $routes = [];
/** /** @var array */
* @var array
*/
protected $sort; protected $sort;
/** /** @var Blueprints */
* @var Blueprints
*/
protected $blueprints; protected $blueprints;
/** /** @var bool */
* @var int protected $enable_pages = true;
*/
/** @var int */
protected $last_modified; protected $last_modified;
/** /** @var string[] */
* @var array|string[]
*/
protected $ignore_files; protected $ignore_files;
/** /** @var string[] */
* @var array|string[]
*/
protected $ignore_folders; protected $ignore_folders;
/** /** @var bool */
* @var bool
*/
protected $ignore_hidden; protected $ignore_hidden;
/** @var string */ /** @var string */
@@ -93,16 +80,13 @@ class Pages
protected $pages_cache_id; protected $pages_cache_id;
/** @var bool */
protected $initialized = false; protected $initialized = false;
/** /** @var Types */
* @var Types
*/
static protected $types; static protected $types;
/** /** @var string|null */
* @var string
*/
static protected $home_route; static protected $home_route;
/** /**
@@ -115,6 +99,26 @@ class Pages
$this->grav = $c; $this->grav = $c;
} }
/**
* Method used in admin to disable frontend pages from being initialized.
*/
public function disablePages(): void
{
$this->enable_pages = false;
}
/**
* Method used in admin to later load frontend pages.
*/
public function enablePages(): void
{
if (!$this->enable_pages) {
$this->enable_pages = true;
$this->buildPages();
}
}
/** /**
* Get or set base path for the pages. * Get or set base path for the pages.
* *
@@ -249,6 +253,9 @@ class Pages
$this->ignore_files = $config->get('system.pages.ignore_files'); $this->ignore_files = $config->get('system.pages.ignore_files');
$this->ignore_folders = $config->get('system.pages.ignore_folders'); $this->ignore_folders = $config->get('system.pages.ignore_folders');
$this->ignore_hidden = $config->get('system.pages.ignore_hidden'); $this->ignore_hidden = $config->get('system.pages.ignore_hidden');
if ($config->get('system.pages.type') === 'flex') {
$this->flex = $this->grav['flex_objects'] ?? null;
}
$this->instances = []; $this->instances = [];
$this->children = []; $this->children = [];
@@ -280,11 +287,23 @@ class Pages
/** /**
* Returns a list of all pages. * Returns a list of all pages.
* *
* @return array|PageInterface[] * @return PageInterface[]
*/ */
public function instances() public function instances()
{ {
return $this->instances; if (!$this->flex) {
return $this->instances;
}
$list = [];
foreach ($this->instances as $path => $instance) {
if (!$instance instanceof PageInterface) {
$instance = $this->flex->getObject($instance);
}
$list[$path] = $instance;
}
return $list;
} }
/** /**
@@ -305,18 +324,301 @@ class Pages
*/ */
public function addPage(PageInterface $page, $route = null) public function addPage(PageInterface $page, $route = null)
{ {
if (!isset($this->instances[$page->path()])) { $path = $page->path() ?? '';
$this->instances[$page->path()] = $page; if (!isset($this->instances[$path])) {
$this->instances[$path] = $page;
} }
$route = $page->route($route); $route = $page->route($route);
if ($page->parent()) { if ($page->parent()) {
$this->children[$page->parent()->path()][$page->path()] = ['slug' => $page->slug()]; $parentPath = $page->parent()->path() ?? '';
$this->children[$parentPath][$path] = ['slug' => $page->slug()];
} }
$this->routes[$route] = $page->path(); $this->routes[$route] = $path;
$this->grav->fireEvent('onPageProcessed', new Event(['page' => $page])); $this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
} }
/**
* Get a collection of pages in the given context.
*
* @param array $params
* @param array $context
* @return PageCollectionInterface|Collection
*/
public function getCollection(array $params = [], array $context = [])
{
if (!isset($params['items'])) {
return new Collection();
}
/** @var Config $config */
$config = $this->grav['config'];
$context += [
'event' => true,
'pagination' => true,
'url_taxonomy_filters' => $config->get('system.pages.url_taxonomy_filters'),
'taxonomies' => (array)$config->get('site.taxonomies'),
'pagination_page' => 1,
'self' => null,
];
// Include taxonomies from the URL if requested.
$process_taxonomy = $params['url_taxonomy_filters'] ?? $context['url_taxonomy_filters'];
if ($process_taxonomy) {
/** @var Uri $uri */
$uri = $this->grav['uri'];
foreach ($context['taxonomies'] as $taxonomy) {
$param = $uri->param(rawurlencode($taxonomy));
$items = $param ? explode(',', $param) : [];
foreach ($items as $item) {
$params['taxonomies'][$taxonomy][] = htmlspecialchars_decode(rawurldecode($item), ENT_QUOTES);
}
}
}
$pagination = $params['pagination'] ?? $context['pagination'];
if ($pagination && !isset($params['page'])) {
/** @var Uri $uri */
$uri = $this->grav['uri'];
$context['pagination_page'] = $uri->currentPage();
}
$collection = $this->evaluate($params['items'], $context['self']);
$collection->setParams($params);
// Filter by taxonomies.
foreach ($params['taxonomies'] ?? [] as $taxonomy => $items) {
foreach ($collection as $page) {
// Don't filter modular pages
if ($page->modular()) {
continue;
}
$test = $page->taxonomy()[$taxonomy] ?? [];
foreach ($items as $item) {
if (!$test || !\in_array($item, $test, true)) {
$collection->remove($page->path());
}
}
}
}
// Remove any inclusive sets from filter.
$filters = $params['filter'] ?? [];
// Assume published=true if not set.
if (!isset($filters['published']) && !isset($filters['non-published'])) {
$filters['published'] = true;
}
foreach (['published', 'visible', 'modular', 'routable'] as $type) {
$var = "non-{$type}";
if (isset($filters[$type], $filters[$var]) && $filters[$type] && $filters[$var]) {
unset($filters[$type], $filters[$var]);
}
}
// Filter the collection
foreach ($filters as $type => $filter) {
switch ($type) {
case 'published':
if ((bool)$filter) {
$collection = $collection->published();
}
break;
case 'non-published':
if ((bool)$filter) {
$collection = $collection->nonPublished();
}
break;
case 'visible':
if ((bool)$filter) {
$collection = $collection->visible();
}
break;
case 'non-visible':
if ((bool)$filter) {
$collection = $collection->nonVisible();
}
break;
case 'modular':
if ((bool)$filter) {
$collection = $collection->modular();
}
break;
case 'non-modular':
if ((bool)$filter) {
$collection = $collection->nonModular();
}
break;
case 'routable':
if ((bool)$filter) {
$collection = $collection->routable();
}
break;
case 'non-routable':
if ((bool)$filter) {
$collection = $collection->nonRoutable();
}
break;
case 'type':
$collection = $collection->ofType($filter);
break;
case 'types':
$collection = $collection->ofOneOfTheseTypes($filter);
break;
case 'access':
$collection = $collection->ofOneOfTheseAccessLevels($filter);
break;
}
}
if (isset($params['dateRange'])) {
$start = $params['dateRange']['start'] ?? 0;
$end = $params['dateRange']['end'] ?? false;
$field = $params['dateRange']['field'] ?? false;
$collection = $collection->dateRange($start, $end, $field);
}
if (isset($params['order'])) {
$by = $params['order']['by'] ?? 'default';
$dir = $params['order']['dir'] ?? 'asc';
$custom = $params['order']['custom'] ?? null;
$sort_flags = $params['order']['sort_flags'] ?? null;
if (is_array($sort_flags)) {
$sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
$sort_flags = array_reduce($sort_flags, function ($a, $b) {
return $a | $b;
}, 0); //merge constant values using bit or
}
$collection = $collection->order($by, $dir, $custom, $sort_flags);
}
// New Custom event to handle things like pagination.
if ($context['event']) {
$this->grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection]));
}
// Slice and dice the collection if pagination is required
if ($pagination) {
$params = $collection->params();
$limit = $params['limit'] ?? 0;
$start = !empty($params['pagination']) ? (($params['page'] ?? $context['pagination_page']) - 1) * $limit : 0;
if ($limit && $collection->count() > $limit) {
$collection->slice($start, $limit);
}
}
return $collection;
}
/**
* @param $value
* @param PageInterface|null $self
* @return Collection
*/
protected function evaluate($value, PageInterface $self = null)
{
// Parse command.
if (is_string($value)) {
// Format: @command.param
$cmd = $value;
$params = [];
} elseif (is_array($value) && count($value) === 1 && !is_int(key($value))) {
// Format: @command.param: { attr1: value1, attr2: value2 }
$cmd = (string)key($value);
$params = (array)current($value);
} else {
$result = [];
foreach ((array)$value as $key => $val) {
if (is_int($key)) {
$result = $result + $this->evaluate($val, $self)->toArray();
} else {
$result = $result + $this->evaluate([$key => $val], $self)->toArray();
}
}
return new Collection($result);
}
$parts = explode('.', $cmd);
$scope = array_shift($parts);
$type = $parts[0] ?? null;
/** @var PageInterface|null $page */
$page = null;
switch ($scope) {
case 'self@':
case '@self':
$page = $self;
break;
case 'page@':
case '@page':
$page = isset($params[0]) ? $this->find($params[0]) : null;
break;
case 'root@':
case '@root':
$page = $this->root();
break;
case 'taxonomy@':
case '@taxonomy':
// Gets a collection of pages by using one of the following formats:
// @taxonomy.category: blog
// @taxonomy.category: [ blog, featured ]
// @taxonomy: { category: [ blog, featured ], level: 1 }
/** @var Taxonomy $taxonomy_map */
$taxonomy_map = Grav::instance()['taxonomy'];
if (!empty($parts)) {
$params = [implode('.', $parts) => $params];
}
return $taxonomy_map->findTaxonomy($params);
}
if (!$page) {
return new Collection();
}
// Handle '@page', '@page.modular: false', '@self' and '@self.modular: false'.
if (null === $type || ($type === 'modular' && ($params[0] ?? null) === false)) {
$type = 'children';
}
switch ($type) {
case 'all':
return $page->children();
case 'modular':
return $page->children()->modular();
case 'children':
return $page->children()->nonModular();
case 'page':
case 'self':
return (new Collection())->addPage($page);
case 'parent':
$parent = $page->parent();
$collection = new Collection();
return $parent ? $collection->addPage($parent) : $collection;
case 'siblings':
$parent = $page->parent();
return $parent ? $parent->children()->remove($page->path()) : new Collection();
case 'descendants':
return $this->all($page)->remove($page->path())->nonModular();
default:
// Unknown type; return empty collection.
return new Collection();
}
}
/** /**
* Sort sub-pages in a page. * Sort sub-pages in a page.
* *
@@ -397,7 +699,15 @@ class Pages
*/ */
public function get($path) public function get($path)
{ {
return $this->instances[(string)$path] ?? null; $instance = $this->instances[(string)$path] ?? null;
if (\is_string($instance)) {
$instance = $this->flex ? $this->flex->getObject($instance) : null;
}
if ($instance && !$instance instanceof PageInterface) {
throw new \RuntimeException('Routing failed on unknown type', 500);
}
return $instance;
} }
/** /**
@@ -575,7 +885,7 @@ class Pages
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = $this->grav['locator']; $locator = $this->grav['locator'];
return $this->instances[rtrim($locator->findResource('page://'), DS)]; return $this->get(rtrim($locator->findResource('page://'), '/'));
} }
/** /**
@@ -713,13 +1023,9 @@ class Pages
} else { } else {
$extra = $showSlug ? '(' . $current->slug() . ') ' : ''; $extra = $showSlug ? '(' . $current->slug() . ') ' : '';
$option = str_repeat('&mdash;-', $level). '&rtrif; ' . $extra . $current->title(); $option = str_repeat('&mdash;-', $level). '&rtrif; ' . $extra . $current->title();
} }
$list[$route] = $option; $list[$route] = $option;
} }
if ($limitLevels === false || ($level+1 < $limitLevels)) { if ($limitLevels === false || ($level+1 < $limitLevels)) {
@@ -824,20 +1130,23 @@ class Pages
* *
* @return array * @return array
*/ */
public static function pageTypes() public static function pageTypes($type = null)
{ {
if (isset(Grav::instance()['admin'])) { if (null === $type && isset(Grav::instance()['admin'])) {
/** @var Admin $admin */ /** @var Admin $admin */
$admin = Grav::instance()['admin']; $admin = Grav::instance()['admin'];
/** @var PageInterface $page */ /** @var PageInterface $page */
$page = $admin->getPage($admin->route); $page = $admin->page();
if ($page && $page->modular()) { $type = $page && $page->modular() ? 'modular' : 'standard';
}
switch ($type) {
case 'standard':
return static::types();
case 'modular':
return static::modularTypes(); return static::modularTypes();
}
return static::types();
} }
return []; return [];
@@ -943,26 +1252,152 @@ class Pages
* *
* @internal * @internal
*/ */
protected function buildPages() protected function buildPages(): void
{ {
$this->sort = []; if ($this->enable_pages === false) {
$page = $this->buildRootPage();
$this->instances[$page->path()] = $page;
return;
}
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->startTimer('build-pages', 'Init frontend routes');
$directory = $this->flex ? $this->flex->getDirectory('grav-pages') : null;
if ($directory) {
$this->buildFlexPages($directory);
} else {
$this->buildRegularPages();
}
$debugger->stopTimer('build-pages');
}
protected function buildFlexPages(FlexDirectory $directory)
{
/** @var Config $config */ /** @var Config $config */
$config = $this->grav['config']; $config = $this->grav['config'];
// TODO: right now we are just emulating normal pages, it is inefficient and bad... but works!
$collection = $directory->getIndex();
$cache = $directory->getCache('index');
/** @var Language $language */ /** @var Language $language */
$language = $this->grav['language']; $language = $this->grav['language'];
$this->pages_cache_id = 'pages-' . md5($collection->getCacheChecksum() . $language->getActive() . $config->checksum());
$cached = $cache->get($this->pages_cache_id);
if ($cached && $this->getVersion() === $cached[0]) {
[, $this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort] = $cached;
/** @var Taxonomy $taxonomy */
$taxonomy = $this->grav['taxonomy'];
$taxonomy->taxonomy($taxonomy_map);
return;
}
$this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
$root = $this->buildRootPage();
$root_path = $root->path();
$instances = [$root_path => $root];
if ($config->get('system.pages.events.page')) {
$this->grav->fireEvent('onBuildPagesInitialized');
}
$children = [];
/**
* @var string $key
* @var PageInterface|FlexObjectInterface $page
*/
foreach ($collection as $key => $page) {
if ($config->get('system.pages.events.page')) {
$this->grav->fireEvent('onPageProcessed', new Event(['page' => $page]));
}
$path = $page->path();
if ($path === $root_path) {
continue;
}
$parent = dirname($path);
$instances[$path] = $page->getFlexKey();
$children[$parent][$path] = ['slug' => $page->slug()];
if (!isset($children[$path])) {
$children[$path] = [];
}
}
foreach ($children as $path => $list) {
$page = $instances[$path] ?? null;
if (null === $page) {
continue;
}
if ($config->get('system.pages.events.page')) {
$this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
}
}
$this->instances = $instances;
$this->children = $children;
$this->sort = [];
$this->buildRoutes();
// cache if needed
if (isset($cache)) {
/** @var Taxonomy $taxonomy */
$taxonomy = $this->grav['taxonomy'];
$taxonomy_map = $taxonomy->taxonomy();
// save pages, routes, taxonomy, and sort to cache
$cache->set($this->pages_cache_id, [$this->getVersion(), $this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort]);
}
}
protected function buildRootPage()
{
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
/** @var Config $config */
$config = $grav['config'];
$page = new Page();
$page->path($locator->findResource('page://'));
$page->orderDir($config->get('system.pages.order.dir'));
$page->orderBy($config->get('system.pages.order.by'));
$page->modified(0);
$page->routable(false);
$page->template('default');
$page->extension('.md');
return $page;
}
protected function buildRegularPages()
{
/** @var Config $config */
$config = $this->grav['config'];
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = $this->grav['locator']; $locator = $this->grav['locator'];
$pages_dir = $locator->findResource('page://'); $pages_dir = $locator->findResource('page://');
if ($config->get('system.cache.enabled')) { if ($config->get('system.cache.enabled')) {
/** @var Cache $cache */ /** @var Language $language */
$cache = $this->grav['cache']; $language = $this->grav['language'];
/** @var Taxonomy $taxonomy */
$taxonomy = $this->grav['taxonomy'];
// how should we check for last modified? Default is by file // how should we check for last modified? Default is by file
switch ($this->check_method) { switch ($this->check_method) {
@@ -982,22 +1417,25 @@ class Pages
$this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum()); $this->pages_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum());
list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($this->pages_cache_id); /** @var Cache $cache */
if (!$this->instances) { $cache = $this->grav['cache'];
$this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..'); $cached = $cache->fetch($this->pages_cache_id);
if ($cached && $this->getVersion() === $cached[0]) {
[, $this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort] = $cached;
// recurse pages and cache result /** @var Taxonomy $taxonomy */
$this->resetPages($pages_dir); $taxonomy = $this->grav['taxonomy'];
} else {
// If pages was found in cache, set the taxonomy
$this->grav['debugger']->addMessage('Page cache hit.');
$taxonomy->taxonomy($taxonomy_map); $taxonomy->taxonomy($taxonomy_map);
return;
} }
$this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
} else { } else {
$this->recurse($pages_dir); $this->grav['debugger']->addMessage('Page cache disabled, rebuilding pages..');
$this->buildRoutes();
} }
$this->resetPages($pages_dir);
} }
/** /**
@@ -1007,6 +1445,7 @@ class Pages
*/ */
public function resetPages($pages_dir) public function resetPages($pages_dir)
{ {
$this->sort = [];
$this->recurse($pages_dir); $this->recurse($pages_dir);
$this->buildRoutes(); $this->buildRoutes();
@@ -1018,7 +1457,7 @@ class Pages
$taxonomy = $this->grav['taxonomy']; $taxonomy = $this->grav['taxonomy'];
// save pages, routes, taxonomy, and sort to cache // save pages, routes, taxonomy, and sort to cache
$cache->save($this->pages_cache_id, [$this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort]); $cache->save($this->pages_cache_id, [$this->getVersion(), $this->instances, $this->routes, $this->children, $taxonomy->taxonomy(), $this->sort]);
} }
} }
@@ -1063,7 +1502,7 @@ class Pages
if ($parent && $page->path()) { if ($parent && $page->path()) {
$this->children[$parent->path()][$page->path()] = ['slug' => $page->slug()]; $this->children[$parent->path()][$page->path()] = ['slug' => $page->slug()];
} }
} else { } elseif ($parent !== null) {
throw new \RuntimeException('Fatal error when creating page instances.'); throw new \RuntimeException('Fatal error when creating page instances.');
} }
@@ -1161,7 +1600,6 @@ class Pages
} }
} }
if (!$content_exists) { if (!$content_exists) {
// Set routability to false if no page found // Set routability to false if no page found
$page->routable(false); $page->routable(false);
@@ -1203,44 +1641,49 @@ class Pages
// Get the home route // Get the home route
$home = self::resetHomeRoute(); $home = self::resetHomeRoute();
// Build routes and taxonomy map. // Build routes and taxonomy map.
/** @var PageInterface $page */ /** @var PageInterface $page */
foreach ($this->instances as $page) { foreach ($this->instances as $path => $page) {
if (!$page->root()) { if (\is_string($page)) {
// process taxonomy $page = $this->get($path);
$taxonomy->addTaxonomy($page); }
$route = $page->route(); if (!$page || $page->root()) {
$raw_route = $page->rawRoute(); continue;
$page_path = $page->path(); }
// add regular route // process taxonomy
$this->routes[$route] = $page_path; $taxonomy->addTaxonomy($page);
// add raw route $route = $page->route();
if ($raw_route !== $route) { $raw_route = $page->rawRoute();
$this->routes[$raw_route] = $page_path; $page_path = $page->path();
}
// add canonical route // add regular route
$route_canonical = $page->routeCanonical(); $this->routes[$route] = $page_path;
if ($route_canonical && ($route !== $route_canonical)) {
$this->routes[$route_canonical] = $page_path;
}
// add aliases to routes list if they are provided // add raw route
$route_aliases = $page->routeAliases(); if ($raw_route !== $route) {
if ($route_aliases) { $this->routes[$raw_route] = $page_path;
foreach ($route_aliases as $alias) { }
$this->routes[$alias] = $page_path;
} // add canonical route
$route_canonical = $page->routeCanonical();
if ($route_canonical && ($route !== $route_canonical)) {
$this->routes[$route_canonical] = $page_path;
}
// add aliases to routes list if they are provided
$route_aliases = $page->routeAliases();
if ($route_aliases) {
foreach ($route_aliases as $alias) {
$this->routes[$alias] = $page_path;
} }
} }
} }
// Alias and set default route to home page. // Alias and set default route to home page.
$homeRoute = '/' . $home; $homeRoute = "/{$home}";
if ($home && isset($this->routes[$homeRoute])) { if ($home && isset($this->routes[$homeRoute])) {
$this->routes['/'] = $this->routes[$homeRoute]; $this->routes['/'] = $this->routes[$homeRoute];
$this->get($this->routes[$homeRoute])->route('/'); $this->get($this->routes[$homeRoute])->route('/');
@@ -1272,7 +1715,7 @@ class Pages
} }
foreach ($pages as $key => $info) { foreach ($pages as $key => $info) {
$child = $this->instances[$key] ?? null; $child = $this->get($key);
if (!$child) { if (!$child) {
throw new \RuntimeException("Page does not exist: {$key}"); throw new \RuntimeException("Page does not exist: {$key}");
} }
@@ -1307,7 +1750,10 @@ class Pages
$list[$key] = $child->folder(); $list[$key] = $child->folder();
break; break;
case (is_string($header_query[0])): case (is_string($header_query[0])):
$child_header = new Header((array)$child->header()); $child_header = $child->header();
if (!$child_header instanceof Header) {
$child_header = new Header((array)$child_header);
}
$header_value = $child_header->get($header_query[0]); $header_value = $child_header->get($header_query[0]);
if (is_array($header_value)) { if (is_array($header_value)) {
$list[$key] = implode(',',$header_value); $list[$key] = implode(',',$header_value);
@@ -1408,6 +1854,11 @@ class Pages
return $new; return $new;
} }
protected function getVersion()
{
return $this->flex ? 'flex' : 'page';
}
/** /**
* Get the Pages cache ID * Get the Pages cache ID
* *

View File

@@ -0,0 +1,120 @@
<?php
namespace Grav\Common\Page\Traits;
use Grav\Common\Grav;
use RocketTheme\Toolbox\Event\Event;
trait PageFormTrait
{
private $_forms;
/**
* Return all the forms which are associated to this page.
*
* Forms are returned as [name => blueprint, ...], where blueprint follows the regular form blueprint format.
*
* @return array
*/
public function getForms(): array
{
if (null === $this->_forms) {
$header = $this->header();
// Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin)
$grav = Grav::instance();
$grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $this, 'header' => $header]));
$rules = $header->rules ?? null;
if (!\is_array($rules)) {
$rules = [];
}
$forms = [];
// First grab page.header.form
$form = $this->normalizeForm($header->form ?? null, null, $rules);
if ($form) {
$forms[$form['name']] = $form;
}
// Append page.header.forms (override singular form if it clashes)
$headerForms = $header->forms ?? null;
if (\is_array($headerForms)) {
foreach ($headerForms as $name => $form) {
$form = $this->normalizeForm($form, $name, $rules);
if ($form) {
$forms[$form['name']] = $form;
}
}
}
$this->_forms = $forms;
}
return $this->_forms;
}
/**
* Add forms to this page.
*
* @param array $new
* @param bool $override
* @return $this
*/
public function addForms(array $new, $override = true)
{
// Initialize forms.
$this->forms();
foreach ($new as $name => $form) {
$form = $this->normalizeForm($form, $name);
$name = $form['name'] ?? null;
if ($name && ($override || !isset($this->_forms[$name]))) {
$this->_forms[$name] = $form;
}
}
return $this;
}
/**
* Alias of $this->getForms();
*
* @return array
*/
public function forms(): array
{
return $this->getForms();
}
/**
* @param array|null $form
* @param string|null $name
* @param array $rules
* @return array|null
*/
protected function normalizeForm($form, $name = null, array $rules = []): ?array
{
if (!\is_array($form)) {
return null;
}
// Ignore numeric indexes on name.
if (!$name || (string)(int)$name === (string)$name) {
$name = null;
}
$name = $name ?? $form['name'] ?? $this->slug();
$formRules = $form['rules'] ?? null;
if (!\is_array($formRules)) {
$formRules = [];
}
return ['name' => $name, 'rules' => $rules + $formRules] + $form;
}
abstract public function header($var = null);
abstract public function slug($var = null);
}

View File

@@ -18,7 +18,7 @@ class AssetsProcessor extends ProcessorBase
public $id = '_assets'; public $id = '_assets';
public $title = 'Assets'; public $title = 'Assets';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();
$this->container['assets']->init(); $this->container['assets']->init();

View File

@@ -18,7 +18,7 @@ class BackupsProcessor extends ProcessorBase
public $id = '_backups'; public $id = '_backups';
public $title = 'Backups'; public $title = 'Backups';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();
$backups = $this->container['backups']; $backups = $this->container['backups'];

View File

@@ -1,30 +0,0 @@
<?php
/**
* @package Grav\Common\Processors
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ConfigurationProcessor extends ProcessorBase
{
public $id = '_config';
public $title = 'Configuration';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$this->startTimer();
$this->container['config']->init();
$this->container['plugins']->setup();
$this->stopTimer();
return $handler->handle($request);
}
}

View File

@@ -19,7 +19,7 @@ class DebuggerAssetsProcessor extends ProcessorBase
public $id = 'debugger_assets'; public $id = 'debugger_assets';
public $title = 'Debugger Assets'; public $title = 'Debugger Assets';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();
$this->container['debugger']->addAssets(); $this->container['debugger']->addAssets();

View File

@@ -1,29 +0,0 @@
<?php
/**
* @package Grav\Common\Processors
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class DebuggerProcessor extends ProcessorBase
{
public $id = '_debugger';
public $title = 'Init Debugger';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$this->startTimer();
$this->container['debugger']->init();
$this->stopTimer();
return $handler->handle($request);
}
}

View File

@@ -1,29 +0,0 @@
<?php
/**
* @package Grav\Common\Processors
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ErrorsProcessor extends ProcessorBase
{
public $id = '_errors';
public $title = 'Error Handlers Reset';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$this->startTimer();
$this->container['errors']->resetHandlers();
$this->stopTimer();
return $handler->handle($request);
}
}

View File

@@ -10,25 +10,119 @@
namespace Grav\Common\Processors; namespace Grav\Common\Processors;
use Grav\Common\Config\Config; use Grav\Common\Config\Config;
use Grav\Common\Debugger;
use Grav\Common\Uri; use Grav\Common\Uri;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Framework\Psr7\Response;
use Grav\Framework\Session\Exceptions\SessionException; use Grav\Framework\Session\Exceptions\SessionException;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\SyslogHandler;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
class InitializeProcessor extends ProcessorBase class InitializeProcessor extends ProcessorBase
{ {
public $id = 'init'; public $id = '_init';
public $title = 'Initialize'; public $title = 'Initialize';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $config = $this->initializeConfig();
$this->initializeLogger($config);
$this->initializeErrors();
$this->startTimer('_debugger', 'Init Debugger');
/** @var Debugger $debugger */
$debugger = $this->container['debugger']->init();
// Clockwork integration.
$clockwork = $debugger->getClockwork();
if ($clockwork) {
$server = $request->getServerParams();
// $baseUri = str_replace('\\', '/', dirname(parse_url($server['SCRIPT_NAME'], PHP_URL_PATH)));
// if ($baseUri === '/') {
// $baseUri = '';
// }
$requestTime = $server['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME;
$request = $request->withAttribute('request_time', $requestTime);
// Handle clockwork API calls.
$uri = $request->getUri();
if (Utils::contains($uri->getPath(), '/__clockwork/')) {
return $debugger->debuggerRequest($request);
}
$this->container['clockwork'] = $clockwork;
}
$this->stopTimer('_debugger');
$this->initialize($config);
$this->initializeSession($config);
// Wrap call to next handler so that debugger can profile it.
/** @var Response $response */
$response = $debugger->profile(function () use ($handler, $request) {
return $handler->handle($request);
});
// Log both request and response and return the response.
return $debugger->logRequest($request, $response);
}
protected function initializeConfig(): Config
{
$this->startTimer('_config', 'Configuration');
// Initialize Configuration
$grav = $this->container;
/** @var Config $config */ /** @var Config $config */
$config = $this->container['config']; $config = $grav['config'];
$config->debug(); $config->init();
$grav['plugins']->setup();
$this->stopTimer('_config');
return $config;
}
protected function initializeLogger(Config $config): void
{
$this->startTimer('_logger', 'Logger');
// Initialize Logging
$grav = $this->container;
switch ($config->get('system.log.handler', 'file')) {
case 'syslog':
$log = $grav['log'];
$log->popHandler();
$facility = $config->get('system.log.syslog.facility', 'local6');
$logHandler = new SyslogHandler('grav', $facility);
$formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
$logHandler->setFormatter($formatter);
$log->pushHandler($logHandler);
break;
}
$this->stopTimer('_logger');
}
protected function initializeErrors(): void
{
$this->startTimer('_errors', 'Error Handlers Reset');
// Initialize Error Handlers
$this->container['errors']->resetHandlers();
$this->stopTimer('_errors');
}
protected function initialize(Config $config): void
{
$this->startTimer('_init', 'Initialize');
// Use output buffering to prevent headers from being sent too early. // Use output buffering to prevent headers from being sent too early.
ob_start(); ob_start();
@@ -43,21 +137,6 @@ class InitializeProcessor extends ProcessorBase
date_default_timezone_set($timezone); date_default_timezone_set($timezone);
} }
// FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS.
if (isset($this->container['session']) && $config->get('system.session.initialize', true)) {
// TODO: remove in 2.0.
$this->container['accounts'];
try {
$this->container['session']->init();
} catch (SessionException $e) {
$this->container['session']->init();
$message = 'Session corruption detected, restarting session...';
$this->addMessage($message);
$this->container['messages']->add($message, 'error');
}
}
/** @var Uri $uri */ /** @var Uri $uri */
$uri = $this->container['uri']; $uri = $this->container['uri'];
$uri->init(); $uri->init();
@@ -73,8 +152,29 @@ class InitializeProcessor extends ProcessorBase
} }
$this->container->setLocale(); $this->container->setLocale();
$this->stopTimer();
return $handler->handle($request); $this->stopTimer('_init');
}
protected function initializeSession(Config $config): void
{
// FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS.
if (isset($this->container['session']) && $config->get('system.session.initialize', true)) {
$this->startTimer('_session', 'Start Session');
// TODO: remove in 2.0.
$this->container['accounts'];
try {
$this->container['session']->init();
} catch (SessionException $e) {
$this->container['session']->init();
$message = 'Session corruption detected, restarting session...';
$this->addMessage($message);
$this->container['messages']->add($message, 'error');
}
$this->stopTimer('_session');
}
} }
} }

View File

@@ -1,50 +0,0 @@
<?php
/**
* @package Grav\Common\Processors
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Processors;
use Grav\Common\Config\Config;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\SyslogHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class LoggerProcessor extends ProcessorBase
{
public $id = '_logger';
public $title = 'Logger';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$this->startTimer();
$grav = $this->container;
/** @var Config $config */
$config = $grav['config'];
switch ($config->get('system.log.handler', 'file')) {
case 'syslog':
$log = $grav['log'];
$log->popHandler();
$facility = $config->get('system.log.syslog.facility', 'local6');
$logHandler = new SyslogHandler('grav', $facility);
$formatter = new LineFormatter("%channel%.%level_name%: %message% %extra%");
$logHandler->setFormatter($formatter);
$log->pushHandler($logHandler);
break;
}
$this->stopTimer();
return $handler->handle($request);
}
}

View File

@@ -20,7 +20,7 @@ class PagesProcessor extends ProcessorBase
public $id = 'pages'; public $id = 'pages';
public $title = 'Pages'; public $title = 'Pages';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();

View File

@@ -18,7 +18,7 @@ class PluginsProcessor extends ProcessorBase
public $id = 'plugins'; public $id = 'plugins';
public $title = 'Plugins'; public $title = 'Plugins';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();
// TODO: remove in 2.0. // TODO: remove in 2.0.

View File

@@ -25,21 +25,21 @@ abstract class ProcessorBase implements ProcessorInterface
$this->container = $container; $this->container = $container;
} }
protected function startTimer($id = null, $title = null) protected function startTimer($id = null, $title = null): void
{ {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = $this->container['debugger']; $debugger = $this->container['debugger'];
$debugger->startTimer($id ?? $this->id, $title ?? $this->title); $debugger->startTimer($id ?? $this->id, $title ?? $this->title);
} }
protected function stopTimer($id = null) protected function stopTimer($id = null): void
{ {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = $this->container['debugger']; $debugger = $this->container['debugger'];
$debugger->stopTimer($id ?? $this->id); $debugger->stopTimer($id ?? $this->id);
} }
protected function addMessage($message, $label = 'info', $isString = true) protected function addMessage($message, $label = 'info', $isString = true): void
{ {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = $this->container['debugger']; $debugger = $this->container['debugger'];

View File

@@ -20,7 +20,7 @@ class RenderProcessor extends ProcessorBase
public $id = 'render'; public $id = 'render';
public $title = 'Render'; public $title = 'Render';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();

View File

@@ -20,7 +20,7 @@ class RequestProcessor extends ProcessorBase
public $id = 'request'; public $id = 'request';
public $title = 'Request'; public $title = 'Request';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();

View File

@@ -19,7 +19,7 @@ class SchedulerProcessor extends ProcessorBase
public $id = '_scheduler'; public $id = '_scheduler';
public $title = 'Scheduler'; public $title = 'Scheduler';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();
$scheduler = $this->container['scheduler']; $scheduler = $this->container['scheduler'];

View File

@@ -19,7 +19,7 @@ class TasksProcessor extends ProcessorBase
public $id = 'tasks'; public $id = 'tasks';
public $title = 'Tasks'; public $title = 'Tasks';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();

View File

@@ -18,7 +18,7 @@ class ThemesProcessor extends ProcessorBase
public $id = 'themes'; public $id = 'themes';
public $title = 'Themes'; public $title = 'Themes';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();
$this->container['themes']->init(); $this->container['themes']->init();

View File

@@ -18,7 +18,7 @@ class TwigProcessor extends ProcessorBase
public $id = 'twig'; public $id = 'twig';
public $title = 'Twig'; public $title = 'Twig';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$this->startTimer(); $this->startTimer();
$this->container['twig']->init(); $this->container['twig']->init();

View File

@@ -21,12 +21,20 @@ class Scheduler
/** /**
* The queued jobs. * The queued jobs.
* *
* @var array * @var Job[]
*/ */
private $jobs = []; private $jobs = [];
/** @var Job[] */
private $saved_jobs = []; private $saved_jobs = [];
/** @var Job[] */
private $executed_jobs = []; private $executed_jobs = [];
/** @var Job[] */
private $failed_jobs = []; private $failed_jobs = [];
/** @var Job[] */
private $jobs_run = []; private $jobs_run = [];
private $output_schedule = []; private $output_schedule = [];
private $config; private $config;
@@ -49,6 +57,8 @@ class Scheduler
/** /**
* Load saved jobs from config/scheduler.yaml file * Load saved jobs from config/scheduler.yaml file
*
* @return $this
*/ */
public function loadSavedJobs() public function loadSavedJobs()
{ {
@@ -65,7 +75,7 @@ class Scheduler
} }
if (isset($j['output'])) { if (isset($j['output'])) {
$mode = isset($j['output_mode']) && $j['output_mode'] === 'append' ? true : false; $mode = isset($j['output_mode']) && $j['output_mode'] === 'append';
$job->output($j['output'], $mode); $job->output($j['output'], $mode);
} }
@@ -106,7 +116,7 @@ class Scheduler
/** /**
* Get all jobs if they are disabled or not as one array * Get all jobs if they are disabled or not as one array
* *
* @return array * @return Job[]
*/ */
public function getAllJobs() public function getAllJobs()
{ {
@@ -184,6 +194,8 @@ class Scheduler
* Reset all collected data of last run. * Reset all collected data of last run.
* *
* Call before run() if you call run() multiple times. * Call before run() if you call run() multiple times.
*
* @return $this
*/ */
public function resetRun() public function resetRun()
{ {
@@ -199,7 +211,7 @@ class Scheduler
* Get the scheduler verbose output. * Get the scheduler verbose output.
* *
* @param string $type Allowed: text, html, array * @param string $type Allowed: text, html, array
* @return mixed The return depends on the requested $type * @return string|array The return depends on the requested $type
*/ */
public function getVerboseOutput($type = 'text') public function getVerboseOutput($type = 'text')
{ {
@@ -217,6 +229,8 @@ class Scheduler
/** /**
* Remove all queued Jobs. * Remove all queued Jobs.
*
* @return $this
*/ */
public function clearJobs() public function clearJobs()
{ {
@@ -263,7 +277,7 @@ class Scheduler
/** /**
* Get the Job states file * Get the Job states file
* *
* @return \RocketTheme\Toolbox\File\FileInterface|YamlFile * @return YamlFile
*/ */
public function getJobStates() public function getJobStates()
{ {
@@ -296,7 +310,6 @@ class Scheduler
* Queue a job for execution in the correct queue. * Queue a job for execution in the correct queue.
* *
* @param Job $job * @param Job $job
* @return void
*/ */
private function queueJob(Job $job) private function queueJob(Job $job)
{ {
@@ -309,7 +322,6 @@ class Scheduler
* Add an entry to the scheduler verbose output array. * Add an entry to the scheduler verbose output array.
* *
* @param string $string * @param string $string
* @return void
*/ */
private function addSchedulerVerboseOutput($string) private function addSchedulerVerboseOutput($string)
{ {

View File

@@ -108,7 +108,8 @@ class AccountsServiceProvider implements ServiceProviderInterface
'options' => [ 'options' => [
'formatter' => ['class' => YamlFormatter::class], 'formatter' => ['class' => YamlFormatter::class],
'folder' => 'account://', 'folder' => 'account://',
'pattern' => '{FOLDER}/{KEY:2}/{KEY}/user.yaml', 'file' => 'user',
'pattern' => '{FOLDER}/{KEY:2}/{KEY}/{FILE}{EXT}',
'key' => 'username', 'key' => 'username',
'indexed' => true 'indexed' => true
], ],
@@ -120,7 +121,7 @@ class AccountsServiceProvider implements ServiceProviderInterface
'options' => [ 'options' => [
'formatter' => ['class' => YamlFormatter::class], 'formatter' => ['class' => YamlFormatter::class],
'folder' => 'account://', 'folder' => 'account://',
'pattern' => '{FOLDER}/{KEY}.yaml', 'pattern' => '{FOLDER}/{KEY}{EXT}',
'key' => 'storage_key', 'key' => 'storage_key',
'indexed' => true 'indexed' => true
], ],

View File

@@ -30,11 +30,11 @@ class Theme extends Plugin
/** /**
* Get configuration of the plugin. * Get configuration of the plugin.
* *
* @return Config * @return array
*/ */
public function config() public function config()
{ {
return $this->config["themes.{$this->name}"]; return $this->config["themes.{$this->name}"] ?? [];
} }
/** /**
@@ -42,7 +42,7 @@ class Theme extends Plugin
* *
* @param string $theme_name The name of the theme whose config it should store. * @param string $theme_name The name of the theme whose config it should store.
* *
* @return true * @return bool
*/ */
public static function saveConfig($theme_name) public static function saveConfig($theme_name)
{ {

View File

@@ -13,6 +13,7 @@ use Grav\Common\Config\Config;
use Grav\Common\File\CompiledYamlFile; use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Data\Blueprints; use Grav\Common\Data\Blueprints;
use Grav\Common\Data\Data; use Grav\Common\Data\Data;
use Grav\Framework\Psr7\Response;
use RocketTheme\Toolbox\Event\EventDispatcher; use RocketTheme\Toolbox\Event\EventDispatcher;
use RocketTheme\Toolbox\Event\EventSubscriberInterface; use RocketTheme\Toolbox\Event\EventSubscriberInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -128,7 +129,7 @@ class Themes extends Iterator
* *
* @param string $name * @param string $name
* *
* @return Data * @return Data|null
* @throws \RuntimeException * @throws \RuntimeException
*/ */
public function get($name) public function get($name)
@@ -214,7 +215,9 @@ class Themes extends Iterator
} }
} }
} elseif (!$locator('theme://') && !defined('GRAV_CLI')) { } elseif (!$locator('theme://') && !defined('GRAV_CLI')) {
exit("Theme '$name' does not exist, unable to display page."); $response = new Response(500, [], "Theme '$name' does not exist, unable to display page.");
$grav->close($response);
} }
$this->config->set('theme', $config->get('themes.' . $name)); $this->config->set('theme', $config->get('themes.' . $name));

View File

@@ -28,7 +28,7 @@ class TwigNodeMarkdown extends Node implements NodeOutputInterface
/** /**
* Compiles the node to PHP. * Compiles the node to PHP.
* *
* @param Compiler $compiler A Twig_Compiler instance * @param Compiler $compiler A Twig Compiler instance
*/ */
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
@@ -41,6 +41,6 @@ class TwigNodeMarkdown extends Node implements NodeOutputInterface
->write('$lines = explode("\n", $content);' . PHP_EOL) ->write('$lines = explode("\n", $content);' . PHP_EOL)
->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL) ->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL)
->write('$content = join("\n", $content);' . PHP_EOL) ->write('$content = join("\n", $content);' . PHP_EOL)
->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($content);' . PHP_EOL); ->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($context, $content);' . PHP_EOL);
} }
} }

View File

@@ -33,12 +33,15 @@ class TwigNodeRender extends Node implements NodeCaptureInterface
$tag = null $tag = null
) )
{ {
parent::__construct(['object' => $object, 'layout' => $layout, 'context' => $context], [], $lineno, $tag); $nodes = ['object' => $object, 'layout' => $layout, 'context' => $context];
$nodes = array_filter($nodes);
parent::__construct($nodes, [], $lineno, $tag);
} }
/** /**
* Compiles the node to PHP. * Compiles the node to PHP.
* *
* @param Compiler $compiler A Twig_Compiler instance * @param Compiler $compiler A Twig Compiler instance
* @throws \LogicException * @throws \LogicException
*/ */
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
@@ -46,15 +49,15 @@ class TwigNodeRender extends Node implements NodeCaptureInterface
$compiler->addDebugInfo($this); $compiler->addDebugInfo($this);
$compiler->write('$object = ')->subcompile($this->getNode('object'))->raw(';' . PHP_EOL); $compiler->write('$object = ')->subcompile($this->getNode('object'))->raw(';' . PHP_EOL);
$layout = $this->getNode('layout'); if ($this->hasNode('layout')) {
if ($layout) { $layout = $this->getNode('layout');
$compiler->write('$layout = ')->subcompile($layout)->raw(';' . PHP_EOL); $compiler->write('$layout = ')->subcompile($layout)->raw(';' . PHP_EOL);
} else { } else {
$compiler->write('$layout = null;' . PHP_EOL); $compiler->write('$layout = null;' . PHP_EOL);
} }
$context = $this->getNode('context'); if ($this->hasNode('context')) {
if ($context) { $context = $this->getNode('context');
$compiler->write('$attributes = ')->subcompile($context)->raw(';' . PHP_EOL); $compiler->write('$attributes = ')->subcompile($context)->raw(';' . PHP_EOL);
} else { } else {
$compiler->write('$attributes = null;' . PHP_EOL); $compiler->write('$attributes = null;' . PHP_EOL);

View File

@@ -29,21 +29,24 @@ class TwigNodeScript extends Node implements NodeCaptureInterface
* @param string|null $tag * @param string|null $tag
*/ */
public function __construct( public function __construct(
Node $body = null, ?Node $body,
AbstractExpression $file = null, ?AbstractExpression $file,
AbstractExpression $group = null, ?AbstractExpression $group,
AbstractExpression $priority = null, ?AbstractExpression $priority,
AbstractExpression $attributes = null, ?AbstractExpression $attributes,
$lineno = 0, $lineno = 0,
$tag = null $tag = null
) )
{ {
parent::__construct(['body' => $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes], [], $lineno, $tag); $nodes = ['body' => $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes];
$nodes = array_filter($nodes);
parent::__construct($nodes, [], $lineno, $tag);
} }
/** /**
* Compiles the node to PHP. * Compiles the node to PHP.
* *
* @param Compiler $compiler A Twig_Compiler instance * @param Compiler $compiler A Twig Compiler instance
* @throws \LogicException * @throws \LogicException
*/ */
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
@@ -52,7 +55,7 @@ class TwigNodeScript extends Node implements NodeCaptureInterface
$compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n");
if ($this->getNode('attributes') !== null) { if ($this->hasNode('attributes')) {
$compiler $compiler
->write('$attributes = ') ->write('$attributes = ')
->subcompile($this->getNode('attributes')) ->subcompile($this->getNode('attributes'))
@@ -66,7 +69,7 @@ class TwigNodeScript extends Node implements NodeCaptureInterface
$compiler->write('$attributes = [];' . "\n"); $compiler->write('$attributes = [];' . "\n");
} }
if ($this->getNode('group') !== null) { if ($this->hasNode('group')) {
$compiler $compiler
->write("\$attributes['group'] = ") ->write("\$attributes['group'] = ")
->subcompile($this->getNode('group')) ->subcompile($this->getNode('group'))
@@ -78,14 +81,14 @@ class TwigNodeScript extends Node implements NodeCaptureInterface
->write("}\n"); ->write("}\n");
} }
if ($this->getNode('priority') !== null) { if ($this->hasNode('priority')) {
$compiler $compiler
->write("\$attributes['priority'] = (int)(") ->write("\$attributes['priority'] = (int)(")
->subcompile($this->getNode('priority')) ->subcompile($this->getNode('priority'))
->raw(");\n"); ->raw(");\n");
} }
if ($this->getNode('file') !== null) { if ($this->hasNode('file')) {
$compiler $compiler
->write('$assets->addJs(') ->write('$assets->addJs(')
->subcompile($this->getNode('file')) ->subcompile($this->getNode('file'))

View File

@@ -29,21 +29,24 @@ class TwigNodeStyle extends Node implements NodeCaptureInterface
* @param string|null $tag * @param string|null $tag
*/ */
public function __construct( public function __construct(
Node $body = null, ?Node $body,
AbstractExpression $file = null, ?AbstractExpression $file,
AbstractExpression $group = null, ?AbstractExpression $group,
AbstractExpression $priority = null, ?AbstractExpression $priority,
AbstractExpression $attributes = null, ?AbstractExpression $attributes,
$lineno = 0, $lineno = 0,
$tag = null $tag = null
) )
{ {
parent::__construct(['body' => $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes], [], $lineno, $tag); $nodes = ['body' => $body, 'file' => $file, 'group' => $group, 'priority' => $priority, 'attributes' => $attributes];
$nodes = array_filter($nodes);
parent::__construct($nodes, [], $lineno, $tag);
} }
/** /**
* Compiles the node to PHP. * Compiles the node to PHP.
* *
* @param Compiler $compiler A Twig_Compiler instance * @param Compiler $compiler A Twig Compiler instance
* @throws \LogicException * @throws \LogicException
*/ */
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
@@ -52,7 +55,7 @@ class TwigNodeStyle extends Node implements NodeCaptureInterface
$compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n"); $compiler->write("\$assets = \\Grav\\Common\\Grav::instance()['assets'];\n");
if ($this->getNode('attributes') !== null) { if ($this->hasNode('attributes')) {
$compiler $compiler
->write('$attributes = ') ->write('$attributes = ')
->subcompile($this->getNode('attributes')) ->subcompile($this->getNode('attributes'))
@@ -66,7 +69,7 @@ class TwigNodeStyle extends Node implements NodeCaptureInterface
$compiler->write('$attributes = [];' . "\n"); $compiler->write('$attributes = [];' . "\n");
} }
if ($this->getNode('group') !== null) { if ($this->hasNode('group')) {
$compiler $compiler
->write("\$attributes['group'] = ") ->write("\$attributes['group'] = ")
->subcompile($this->getNode('group')) ->subcompile($this->getNode('group'))
@@ -78,14 +81,14 @@ class TwigNodeStyle extends Node implements NodeCaptureInterface
->write("}\n"); ->write("}\n");
} }
if ($this->getNode('priority') !== null) { if ($this->hasNode('priority')) {
$compiler $compiler
->write("\$attributes['priority'] = (int)(") ->write("\$attributes['priority'] = (int)(")
->subcompile($this->getNode('priority')) ->subcompile($this->getNode('priority'))
->raw(");\n"); ->raw(");\n");
} }
if ($this->getNode('file') !== null) { if ($this->hasNode('file')) {
$compiler $compiler
->write('$assets->addCss(') ->write('$assets->addCss(')
->subcompile($this->getNode('file')) ->subcompile($this->getNode('file'))

View File

@@ -30,13 +30,16 @@ class TwigNodeSwitch extends Node
$tag = null $tag = null
) )
{ {
parent::__construct(array('value' => $value, 'cases' => $cases, 'default' => $default), array(), $lineno, $tag); $nodes = ['value' => $value, 'cases' => $cases, 'default' => $default];
$nodes = array_filter($nodes);
parent::__construct($nodes, [], $lineno, $tag);
} }
/** /**
* Compiles the node to PHP. * Compiles the node to PHP.
* *
* @param Compiler $compiler A Twig_Compiler instance * @param Compiler $compiler A Twig Compiler instance
*/ */
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
{ {
@@ -68,7 +71,7 @@ class TwigNodeSwitch extends Node
->write("}\n"); ->write("}\n");
} }
if ($this->hasNode('default') && $this->getNode('default') !== null) { if ($this->hasNode('default')) {
$compiler $compiler
->write("default:\n") ->write("default:\n")
->write("{\n") ->write("{\n")

View File

@@ -34,7 +34,7 @@ class TwigNodeThrow extends Node
/** /**
* Compiles the node to PHP. * Compiles the node to PHP.
* *
* @param Compiler $compiler A Twig_Compiler instance * @param Compiler $compiler A Twig Compiler instance
* @throws \LogicException * @throws \LogicException
*/ */
public function compile(Compiler $compiler) public function compile(Compiler $compiler)

View File

@@ -28,13 +28,16 @@ class TwigNodeTryCatch extends Node
$tag = null $tag = null
) )
{ {
parent::__construct(['try' => $try, 'catch' => $catch], [], $lineno, $tag); $nodes = ['try' => $try, 'catch' => $catch];
$nodes = array_filter($nodes);
parent::__construct($nodes, [], $lineno, $tag);
} }
/** /**
* Compiles the node to PHP. * Compiles the node to PHP.
* *
* @param Compiler $compiler A Twig_Compiler instance * @param Compiler $compiler A Twig Compiler instance
* @throws \LogicException * @throws \LogicException
*/ */
public function compile(Compiler $compiler) public function compile(Compiler $compiler)
@@ -50,7 +53,7 @@ class TwigNodeTryCatch extends Node
->subcompile($this->getNode('try')) ->subcompile($this->getNode('try'))
; ;
if ($this->hasNode('catch') && null !== $this->getNode('catch')) { if ($this->hasNode('catch')) {
$compiler $compiler
->outdent() ->outdent()
->write('} catch (\Exception $e) {' . "\n") ->write('} catch (\Exception $e) {' . "\n")

View File

@@ -24,9 +24,9 @@ class TwigTokenParserRender extends AbstractTokenParser
/** /**
* Parses a token and returns a node. * Parses a token and returns a node.
* *
* @param Token $token A Twig_Token instance * @param Token $token A Twig Token instance
* *
* @return Node A Twig_Node instance * @return Node A Twig Node instance
*/ */
public function parse(Token $token) public function parse(Token $token)
{ {

View File

@@ -29,9 +29,9 @@ class TwigTokenParserScript extends AbstractTokenParser
/** /**
* Parses a token and returns a node. * Parses a token and returns a node.
* *
* @param Token $token A Twig_Token instance * @param Token $token A Twig Token instance
* *
* @return Node A Twig_Node instance * @return Node A Twig Node instance
*/ */
public function parse(Token $token) public function parse(Token $token)
{ {

View File

@@ -28,9 +28,9 @@ class TwigTokenParserStyle extends AbstractTokenParser
/** /**
* Parses a token and returns a node. * Parses a token and returns a node.
* *
* @param Token $token A Twig_Token instance * @param Token $token A Twig Token instance
* *
* @return Node A Twig_Node instance * @return Node A Twig Node instance
*/ */
public function parse(Token $token) public function parse(Token $token)
{ {

View File

@@ -26,9 +26,9 @@ class TwigTokenParserThrow extends AbstractTokenParser
/** /**
* Parses a token and returns a node. * Parses a token and returns a node.
* *
* @param Token $token A Twig_Token instance * @param Token $token A Twig Token instance
* *
* @return Node A Twig_Node instance * @return Node A Twig Node instance
*/ */
public function parse(Token $token) public function parse(Token $token)
{ {

View File

@@ -30,9 +30,9 @@ class TwigTokenParserTryCatch extends AbstractTokenParser
/** /**
* Parses a token and returns a node. * Parses a token and returns a node.
* *
* @param Token $token A Twig_Token instance * @param Token $token A Twig Token instance
* *
* @return Node A Twig_Node instance * @return Node A Twig Node instance
*/ */
public function parse(Token $token) public function parse(Token $token)
{ {

View File

@@ -18,11 +18,21 @@ use Grav\Common\Page\Pages;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use Phive\Twig\Extensions\Deferred\DeferredExtension; use Phive\Twig\Extensions\Deferred\DeferredExtension;
use Twig\Cache\FilesystemCache;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Extension\CoreExtension;
use Twig\Extension\DebugExtension;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFilter;
use Twig\TwigFunction;
class Twig class Twig
{ {
/** /**
* @var \Twig_Environment * @var Environment
*/ */
public $twig; public $twig;
@@ -47,18 +57,20 @@ class Twig
protected $grav; protected $grav;
/** /**
* @var \Twig_Loader_Filesystem * @var FilesystemLoader
*/ */
protected $loader; protected $loader;
/** /**
* @var \Twig_Loader_Array * @var ArrayLoader
*/ */
protected $loaderArray; protected $loaderArray;
protected $autoescape; protected $autoescape;
protected $profile;
/** /**
* Constructor * Constructor
* *
@@ -102,7 +114,7 @@ class Twig
$core_templates = array_merge($locator->findResources('system://templates'), $locator->findResources('system://templates/testing')); $core_templates = array_merge($locator->findResources('system://templates'), $locator->findResources('system://templates/testing'));
$this->twig_paths = array_merge($this->twig_paths, $core_templates); $this->twig_paths = array_merge($this->twig_paths, $core_templates);
$this->loader = new \Twig_Loader_Filesystem($this->twig_paths); $this->loader = new FilesystemLoader($this->twig_paths);
// Register all other prefixes as namespaces in twig // Register all other prefixes as namespaces in twig
foreach ($locator->getPaths('theme') as $prefix => $_) { foreach ($locator->getPaths('theme') as $prefix => $_) {
@@ -128,13 +140,13 @@ class Twig
$this->grav->fireEvent('onTwigLoader'); $this->grav->fireEvent('onTwigLoader');
$this->loaderArray = new \Twig_Loader_Array([]); $this->loaderArray = new ArrayLoader([]);
$loader_chain = new \Twig_Loader_Chain([$this->loaderArray, $this->loader]); $loader_chain = new ChainLoader([$this->loaderArray, $this->loader]);
$params = $config->get('system.twig'); $params = $config->get('system.twig');
if (!empty($params['cache'])) { if (!empty($params['cache'])) {
$cachePath = $locator->findResource('cache://twig', true, true); $cachePath = $locator->findResource('cache://twig', true, true);
$params['cache'] = new \Twig_Cache_Filesystem($cachePath, \Twig_Cache_Filesystem::FORCE_BYTECODE_INVALIDATION); $params['cache'] = new FilesystemCache($cachePath, FilesystemCache::FORCE_BYTECODE_INVALIDATION);
} }
if (!$config->get('system.strict_mode.twig_compat', true)) { if (!$config->get('system.strict_mode.twig_compat', true)) {
@@ -153,10 +165,10 @@ class Twig
if ($config->get('system.twig.undefined_functions')) { if ($config->get('system.twig.undefined_functions')) {
$this->twig->registerUndefinedFunctionCallback(function ($name) { $this->twig->registerUndefinedFunctionCallback(function ($name) {
if (function_exists($name)) { if (function_exists($name)) {
return new \Twig_SimpleFunction($name, $name); return new TwigFunction($name, $name);
} }
return new \Twig_SimpleFunction($name, function () { return new TwigFunction($name, function () {
}); });
}); });
} }
@@ -164,10 +176,10 @@ class Twig
if ($config->get('system.twig.undefined_filters')) { if ($config->get('system.twig.undefined_filters')) {
$this->twig->registerUndefinedFilterCallback(function ($name) { $this->twig->registerUndefinedFilterCallback(function ($name) {
if (function_exists($name)) { if (function_exists($name)) {
return new \Twig_SimpleFilter($name, $name); return new TwigFilter($name, $name);
} }
return new \Twig_SimpleFilter($name, function () { return new TwigFilter($name, function () {
}); });
}); });
} }
@@ -176,17 +188,21 @@ class Twig
// set default date format if set in config // set default date format if set in config
if ($config->get('system.pages.dateformat.long')) { if ($config->get('system.pages.dateformat.long')) {
/** @var \Twig_Extension_Core $extension */ /** @var CoreExtension $extension */
$extension = $this->twig->getExtension('Twig_Extension_Core'); $extension = $this->twig->getExtension(CoreExtension::class);
$extension->setDateFormat($config->get('system.pages.dateformat.long')); $extension->setDateFormat($config->get('system.pages.dateformat.long'));
} }
// enable the debug extension if required // enable the debug extension if required
if ($config->get('system.twig.debug')) { if ($config->get('system.twig.debug')) {
$this->twig->addExtension(new \Twig_Extension_Debug()); $this->twig->addExtension(new DebugExtension());
} }
$this->twig->addExtension(new TwigExtension()); $this->twig->addExtension(new TwigExtension());
$this->twig->addExtension(new DeferredExtension()); $this->twig->addExtension(new DeferredExtension());
$this->profile = new \Twig\Profiler\Profile();
$this->twig->addExtension(new \Twig\Extension\ProfilerExtension($this->profile));
$this->grav->fireEvent('onTwigExtensions'); $this->grav->fireEvent('onTwigExtensions');
/** @var Pages $pages */ /** @var Pages $pages */
@@ -219,7 +235,7 @@ class Twig
} }
/** /**
* @return \Twig_Environment * @return Environment
*/ */
public function twig() public function twig()
{ {
@@ -227,13 +243,19 @@ class Twig
} }
/** /**
* @return \Twig_Loader_Filesystem * @return FilesystemLoader
*/ */
public function loader() public function loader()
{ {
return $this->loader; return $this->loader;
} }
public function profile()
{
return $this->profile;
}
/** /**
* Adds or overrides a template. * Adds or overrides a template.
* *
@@ -254,7 +276,6 @@ class Twig
* @param string $content Optional content override * @param string $content Optional content override
* *
* @return string The rendered output * @return string The rendered output
* @throws \Twig_Error_Loader
*/ */
public function processPage(PageInterface $item, $content = null) public function processPage(PageInterface $item, $content = null)
{ {
@@ -288,7 +309,7 @@ class Twig
$output = $local_twig->render($name, $twig_vars); $output = $local_twig->render($name, $twig_vars);
} }
} catch (\Twig_Error_Loader $e) { } catch (LoaderError $e) {
throw new \RuntimeException($e->getRawMessage(), 404, $e); throw new \RuntimeException($e->getRawMessage(), 404, $e);
} }
@@ -312,7 +333,7 @@ class Twig
try { try {
$output = $this->twig->render($template, $vars); $output = $this->twig->render($template, $vars);
} catch (\Twig_Error_Loader $e) { } catch (LoaderError $e) {
throw new \RuntimeException($e->getRawMessage(), 404, $e); throw new \RuntimeException($e->getRawMessage(), 404, $e);
} }
@@ -341,7 +362,7 @@ class Twig
try { try {
$output = $this->twig->render($name, $vars); $output = $this->twig->render($name, $vars);
} catch (\Twig_Error_Loader $e) { } catch (LoaderError $e) {
throw new \RuntimeException($e->getRawMessage(), 404, $e); throw new \RuntimeException($e->getRawMessage(), 404, $e);
} }
@@ -386,14 +407,14 @@ class Twig
try { try {
$output = $this->twig->render($template, $vars + $twig_vars); $output = $this->twig->render($template, $vars + $twig_vars);
} catch (\Twig_Error_Loader $e) { } catch (LoaderError $e) {
$error_msg = $e->getMessage(); $error_msg = $e->getMessage();
// Try html version of this template if initial template was NOT html // Try html version of this template if initial template was NOT html
if ($ext !== '.html' . TWIG_EXT) { if ($ext !== '.html' . TWIG_EXT) {
try { try {
$page->templateFormat('html'); $page->templateFormat('html');
$output = $this->twig->render($page->template() . '.html' . TWIG_EXT, $vars + $twig_vars); $output = $this->twig->render($page->template() . '.html' . TWIG_EXT, $vars + $twig_vars);
} catch (\Twig_Error_Loader $e) { } catch (LoaderError $e) {
throw new \RuntimeException($error_msg, 400, $e); throw new \RuntimeException($error_msg, 400, $e);
} }
} else { } else {
@@ -405,9 +426,10 @@ class Twig
} }
/** /**
* Wraps the Twig_Loader_Filesystem addPath method (should be used only in `onTwigLoader()` event * Wraps the FilesystemLoader addPath method (should be used only in `onTwigLoader()` event
* @param string $template_path * @param string $template_path
* @param string $namespace * @param string $namespace
* @throws LoaderError
*/ */
public function addPath($template_path, $namespace = '__main__') public function addPath($template_path, $namespace = '__main__')
{ {
@@ -415,9 +437,10 @@ class Twig
} }
/** /**
* Wraps the Twig_Loader_Filesystem prependPath method (should be used only in `onTwigLoader()` event * Wraps the FilesystemLoader prependPath method (should be used only in `onTwigLoader()` event
* @param string $template_path * @param string $template_path
* @param string $namespace * @param string $namespace
* @throws LoaderError
*/ */
public function prependPath($template_path, $namespace = '__main__') public function prependPath($template_path, $namespace = '__main__')
{ {

View File

@@ -0,0 +1,53 @@
<?php
/**
* @package Grav\Common\Twig
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig;
use Clockwork\DataSource\DataSource;
use Clockwork\Request\Request;
use Clockwork\Request\Timeline;
use Grav\Common\Grav;
class TwigClockworkDataSource extends DataSource
{
/**
* Views data structure
*/
protected $views;
protected $root;
/**
* TwigClockworkDataSource constructor.
*/
public function __construct()
{
$this->views = new Timeline();
}
/**
* Resolves and adds the Twig profiler data to the request
*
* @param Request $request
* @return Request
*/
public function resolve(Request $request)
{
$profile = Grav::instance()['twig']->profile();
if ($profile) {
$processor = new TwigProfileProcessor();
$processor->process($profile, $this->views);
$request->viewsData = $this->views->finalize();
}
return $request;
}
}

View File

@@ -9,7 +9,9 @@
namespace Grav\Common\Twig; namespace Grav\Common\Twig;
class TwigEnvironment extends \Twig_Environment use Twig\Environment;
class TwigEnvironment extends Environment
{ {
use WriteCacheFileTrait; use WriteCacheFileTrait;
} }

View File

@@ -29,9 +29,16 @@ use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Common\Yaml; use Grav\Common\Yaml;
use Grav\Common\Helpers\Base32; use Grav\Common\Helpers\Base32;
use Grav\Framework\Psr7\Response;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface class TwigExtension extends AbstractExtension implements GlobalsInterface
{ {
/** @var Grav */ /** @var Grav */
protected $grav; protected $grav;
@@ -72,60 +79,60 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
public function getFilters() public function getFilters()
{ {
return [ return [
new \Twig_SimpleFilter('*ize', [$this, 'inflectorFilter']), new TwigFilter('*ize', [$this, 'inflectorFilter']),
new \Twig_SimpleFilter('absolute_url', [$this, 'absoluteUrlFilter']), new TwigFilter('absolute_url', [$this, 'absoluteUrlFilter']),
new \Twig_SimpleFilter('contains', [$this, 'containsFilter']), new TwigFilter('contains', [$this, 'containsFilter']),
new \Twig_SimpleFilter('chunk_split', [$this, 'chunkSplitFilter']), new TwigFilter('chunk_split', [$this, 'chunkSplitFilter']),
new \Twig_SimpleFilter('nicenumber', [$this, 'niceNumberFunc']), new TwigFilter('nicenumber', [$this, 'niceNumberFunc']),
new \Twig_SimpleFilter('nicefilesize', [$this, 'niceFilesizeFunc']), new TwigFilter('nicefilesize', [$this, 'niceFilesizeFunc']),
new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFunc']), new TwigFilter('nicetime', [$this, 'nicetimeFunc']),
new \Twig_SimpleFilter('defined', [$this, 'definedDefaultFilter']), new TwigFilter('defined', [$this, 'definedDefaultFilter']),
new \Twig_SimpleFilter('ends_with', [$this, 'endsWithFilter']), new TwigFilter('ends_with', [$this, 'endsWithFilter']),
new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']), new TwigFilter('fieldName', [$this, 'fieldNameFilter']),
new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']), new TwigFilter('ksort', [$this, 'ksortFilter']),
new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']), new TwigFilter('ltrim', [$this, 'ltrimFilter']),
new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]), new TwigFilter('markdown', [$this, 'markdownFunction'], ['needs_context' => true, 'is_safe' => ['html']]),
new \Twig_SimpleFilter('md5', [$this, 'md5Filter']), new TwigFilter('md5', [$this, 'md5Filter']),
new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']), new TwigFilter('base32_encode', [$this, 'base32EncodeFilter']),
new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']), new TwigFilter('base32_decode', [$this, 'base32DecodeFilter']),
new \Twig_SimpleFilter('base64_encode', [$this, 'base64EncodeFilter']), new TwigFilter('base64_encode', [$this, 'base64EncodeFilter']),
new \Twig_SimpleFilter('base64_decode', [$this, 'base64DecodeFilter']), new TwigFilter('base64_decode', [$this, 'base64DecodeFilter']),
new \Twig_SimpleFilter('randomize', [$this, 'randomizeFilter']), new TwigFilter('randomize', [$this, 'randomizeFilter']),
new \Twig_SimpleFilter('modulus', [$this, 'modulusFilter']), new TwigFilter('modulus', [$this, 'modulusFilter']),
new \Twig_SimpleFilter('rtrim', [$this, 'rtrimFilter']), new TwigFilter('rtrim', [$this, 'rtrimFilter']),
new \Twig_SimpleFilter('pad', [$this, 'padFilter']), new TwigFilter('pad', [$this, 'padFilter']),
new \Twig_SimpleFilter('regex_replace', [$this, 'regexReplace']), new TwigFilter('regex_replace', [$this, 'regexReplace']),
new \Twig_SimpleFilter('safe_email', [$this, 'safeEmailFilter']), new TwigFilter('safe_email', [$this, 'safeEmailFilter']),
new \Twig_SimpleFilter('safe_truncate', ['\Grav\Common\Utils', 'safeTruncate']), new TwigFilter('safe_truncate', ['\Grav\Common\Utils', 'safeTruncate']),
new \Twig_SimpleFilter('safe_truncate_html', ['\Grav\Common\Utils', 'safeTruncateHTML']), new TwigFilter('safe_truncate_html', ['\Grav\Common\Utils', 'safeTruncateHTML']),
new \Twig_SimpleFilter('sort_by_key', [$this, 'sortByKeyFilter']), new TwigFilter('sort_by_key', [$this, 'sortByKeyFilter']),
new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']), new TwigFilter('starts_with', [$this, 'startsWithFilter']),
new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils', 'truncate']), new TwigFilter('truncate', ['\Grav\Common\Utils', 'truncate']),
new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']), new TwigFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']),
new \Twig_SimpleFilter('json_decode', [$this, 'jsonDecodeFilter']), new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']),
new \Twig_SimpleFilter('array_unique', 'array_unique'), new TwigFilter('array_unique', 'array_unique'),
new \Twig_SimpleFilter('basename', 'basename'), new TwigFilter('basename', 'basename'),
new \Twig_SimpleFilter('dirname', 'dirname'), new TwigFilter('dirname', 'dirname'),
new \Twig_SimpleFilter('print_r', 'print_r'), new TwigFilter('print_r', 'print_r'),
new \Twig_SimpleFilter('yaml_encode', [$this, 'yamlEncodeFilter']), new TwigFilter('yaml_encode', [$this, 'yamlEncodeFilter']),
new \Twig_SimpleFilter('yaml_decode', [$this, 'yamlDecodeFilter']), new TwigFilter('yaml_decode', [$this, 'yamlDecodeFilter']),
new \Twig_SimpleFilter('nicecron', [$this, 'niceCronFilter']), new TwigFilter('nicecron', [$this, 'niceCronFilter']),
// Translations // Translations
new \Twig_SimpleFilter('t', [$this, 'translate'], ['needs_environment' => true]), new TwigFilter('t', [$this, 'translate'], ['needs_environment' => true]),
new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']), new TwigFilter('tl', [$this, 'translateLanguage']),
new \Twig_SimpleFilter('ta', [$this, 'translateArray']), new TwigFilter('ta', [$this, 'translateArray']),
// Casting values // Casting values
new \Twig_SimpleFilter('string', [$this, 'stringFilter']), new TwigFilter('string', [$this, 'stringFilter']),
new \Twig_SimpleFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]), new TwigFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]),
new \Twig_SimpleFilter('bool', [$this, 'boolFilter']), new TwigFilter('bool', [$this, 'boolFilter']),
new \Twig_SimpleFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]), new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
new \Twig_SimpleFilter('array', [$this, 'arrayFilter']), new TwigFilter('array', [$this, 'arrayFilter']),
// Object Types // Object Types
new \Twig_SimpleFilter('get_type', [$this, 'getTypeFunc']), new TwigFilter('get_type', [$this, 'getTypeFunc']),
new \Twig_SimpleFilter('of_type', [$this, 'ofTypeFunc']) new TwigFilter('of_type', [$this, 'ofTypeFunc'])
]; ];
} }
@@ -137,54 +144,54 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
public function getFunctions() public function getFunctions()
{ {
return [ return [
new \Twig_SimpleFunction('array', [$this, 'arrayFilter']), new TwigFunction('array', [$this, 'arrayFilter']),
new \Twig_SimpleFunction('array_key_value', [$this, 'arrayKeyValueFunc']), new TwigFunction('array_key_value', [$this, 'arrayKeyValueFunc']),
new \Twig_SimpleFunction('array_key_exists', 'array_key_exists'), new TwigFunction('array_key_exists', 'array_key_exists'),
new \Twig_SimpleFunction('array_unique', 'array_unique'), new TwigFunction('array_unique', 'array_unique'),
new \Twig_SimpleFunction('array_intersect', [$this, 'arrayIntersectFunc']), new TwigFunction('array_intersect', [$this, 'arrayIntersectFunc']),
new \Twig_SimpleFunction('authorize', [$this, 'authorize']), new TwigFunction('authorize', [$this, 'authorize']),
new \Twig_SimpleFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), new TwigFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
new \Twig_SimpleFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]), new TwigFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
new \Twig_SimpleFunction('vardump', [$this, 'vardumpFunc']), new TwigFunction('vardump', [$this, 'vardumpFunc']),
new \Twig_SimpleFunction('print_r', 'print_r'), new TwigFunction('print_r', 'print_r'),
new \Twig_SimpleFunction('http_response_code', 'http_response_code'), new TwigFunction('http_response_code', 'http_response_code'),
new \Twig_SimpleFunction('evaluate', [$this, 'evaluateStringFunc'], ['needs_context' => true]), new TwigFunction('evaluate', [$this, 'evaluateStringFunc'], ['needs_context' => true]),
new \Twig_SimpleFunction('evaluate_twig', [$this, 'evaluateTwigFunc'], ['needs_context' => true]), new TwigFunction('evaluate_twig', [$this, 'evaluateTwigFunc'], ['needs_context' => true]),
new \Twig_SimpleFunction('gist', [$this, 'gistFunc']), new TwigFunction('gist', [$this, 'gistFunc']),
new \Twig_SimpleFunction('nonce_field', [$this, 'nonceFieldFunc']), new TwigFunction('nonce_field', [$this, 'nonceFieldFunc']),
new \Twig_SimpleFunction('pathinfo', 'pathinfo'), new TwigFunction('pathinfo', 'pathinfo'),
new \Twig_SimpleFunction('random_string', [$this, 'randomStringFunc']), new TwigFunction('random_string', [$this, 'randomStringFunc']),
new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']), new TwigFunction('repeat', [$this, 'repeatFunc']),
new \Twig_SimpleFunction('regex_replace', [$this, 'regexReplace']), new TwigFunction('regex_replace', [$this, 'regexReplace']),
new \Twig_SimpleFunction('regex_filter', [$this, 'regexFilter']), new TwigFunction('regex_filter', [$this, 'regexFilter']),
new \Twig_SimpleFunction('string', [$this, 'stringFunc']), new TwigFunction('string', [$this, 'stringFunc']),
new \Twig_SimpleFunction('url', [$this, 'urlFunc']), new TwigFunction('url', [$this, 'urlFunc']),
new \Twig_SimpleFunction('json_decode', [$this, 'jsonDecodeFilter']), new TwigFunction('json_decode', [$this, 'jsonDecodeFilter']),
new \Twig_SimpleFunction('get_cookie', [$this, 'getCookie']), new TwigFunction('get_cookie', [$this, 'getCookie']),
new \Twig_SimpleFunction('redirect_me', [$this, 'redirectFunc']), new TwigFunction('redirect_me', [$this, 'redirectFunc']),
new \Twig_SimpleFunction('range', [$this, 'rangeFunc']), new TwigFunction('range', [$this, 'rangeFunc']),
new \Twig_SimpleFunction('isajaxrequest', [$this, 'isAjaxFunc']), new TwigFunction('isajaxrequest', [$this, 'isAjaxFunc']),
new \Twig_SimpleFunction('exif', [$this, 'exifFunc']), new TwigFunction('exif', [$this, 'exifFunc']),
new \Twig_SimpleFunction('media_directory', [$this, 'mediaDirFunc']), new TwigFunction('media_directory', [$this, 'mediaDirFunc']),
new \Twig_SimpleFunction('body_class', [$this, 'bodyClassFunc']), new TwigFunction('body_class', [$this, 'bodyClassFunc']),
new \Twig_SimpleFunction('theme_var', [$this, 'themeVarFunc']), new TwigFunction('theme_var', [$this, 'themeVarFunc']),
new \Twig_SimpleFunction('header_var', [$this, 'pageHeaderVarFunc']), new TwigFunction('header_var', [$this, 'pageHeaderVarFunc']),
new \Twig_SimpleFunction('read_file', [$this, 'readFileFunc']), new TwigFunction('read_file', [$this, 'readFileFunc']),
new \Twig_SimpleFunction('nicenumber', [$this, 'niceNumberFunc']), new TwigFunction('nicenumber', [$this, 'niceNumberFunc']),
new \Twig_SimpleFunction('nicefilesize', [$this, 'niceFilesizeFunc']), new TwigFunction('nicefilesize', [$this, 'niceFilesizeFunc']),
new \Twig_SimpleFunction('nicetime', [$this, 'nicetimeFunc']), new TwigFunction('nicetime', [$this, 'nicetimeFunc']),
new \Twig_SimpleFunction('cron', [$this, 'cronFunc']), new TwigFunction('cron', [$this, 'cronFunc']),
new \Twig_SimpleFunction('xss', [$this, 'xssFunc']), new TwigFunction('xss', [$this, 'xssFunc']),
// Translations // Translations
new \Twig_SimpleFunction('t', [$this, 'translate'], ['needs_environment' => true]), new TwigFunction('t', [$this, 'translate'], ['needs_environment' => true]),
new \Twig_SimpleFunction('tl', [$this, 'translateLanguage']), new TwigFunction('tl', [$this, 'translateLanguage']),
new \Twig_SimpleFunction('ta', [$this, 'translateArray']), new TwigFunction('ta', [$this, 'translateArray']),
// Object Types // Object Types
new \Twig_SimpleFunction('get_type', [$this, 'getTypeFunc']), new TwigFunction('get_type', [$this, 'getTypeFunc']),
new \Twig_SimpleFunction('of_type', [$this, 'ofTypeFunc']) new TwigFunction('of_type', [$this, 'ofTypeFunc'])
]; ];
} }
@@ -457,7 +464,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
/** /**
* Gets a human readable output for cron syntax * Gets a human readable output for cron syntax
* *
* @param $at * @param string $at
* @return string * @return string
*/ */
public function niceCronFilter($at) public function niceCronFilter($at)
@@ -484,7 +491,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
* @param bool $long_strings * @param bool $long_strings
* *
* @param bool $show_tense * @param bool $show_tense
* @return bool * @return string
*/ */
public function nicetimeFunc($date, $long_strings = true, $show_tense = true) public function nicetimeFunc($date, $long_strings = true, $show_tense = true)
{ {
@@ -613,10 +620,11 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
/** /**
* @param string $string * @param string $string
* *
* @param array $context
* @param bool $block Block or Line processing * @param bool $block Block or Line processing
* @return mixed|string * @return mixed|string
*/ */
public function markdownFunction($context = false, $string, $block = true) public function markdownFunction($context, $string, $block = true)
{ {
$page = $context['page'] ?? null; $page = $context['page'] ?? null;
return Utils::processMarkdown($string, $block, $page); return Utils::processMarkdown($string, $block, $page);
@@ -652,7 +660,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*/ */
public function definedDefaultFilter($value, $default = null) public function definedDefaultFilter($value, $default = null)
{ {
return null !== $value ? $value : $default; return $value ?? $default;
} }
/** /**
@@ -734,9 +742,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
} }
/** /**
* @param Environment $twig
* @return string * @return string
*/ */
public function translate(\Twig_Environment $twig) public function translate(Environment $twig)
{ {
// shift off the environment // shift off the environment
$args = func_get_args(); $args = func_get_args();
@@ -818,7 +827,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
public function urlFunc($input, $domain = false) public function urlFunc($input, $domain = false)
{ {
return Utils::url($input, $domain); return Utils::url($input, $domain);
} }
/** /**
* This function will evaluate Twig $twig through the $environment, and return its results. * This function will evaluate Twig $twig through the $environment, and return its results.
@@ -829,8 +838,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*/ */
public function evaluateTwigFunc($context, $twig ) { public function evaluateTwigFunc($context, $twig ) {
$loader = new \Twig_Loader_Filesystem('.'); $loader = new FilesystemLoader('.');
$env = new \Twig_Environment($loader); $env = new Environment($loader);
$template = $env->createTemplate($twig); $template = $env->createTemplate($twig);
@@ -849,15 +858,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
return $this->evaluateTwigFunc($context, "{{ $string }}"); return $this->evaluateTwigFunc($context, "{{ $string }}");
} }
/** /**
* Based on Twig_Extension_Debug / twig_var_dump * Based on Twig\Extension\Debug / twig_var_dump
* (c) 2011 Fabien Potencier * (c) 2011 Fabien Potencier
* *
* @param \Twig_Environment $env * @param Environment $env
* @param string $context * @param array $context
*/ */
public function dump(\Twig_Environment $env, $context) public function dump(Environment $env, $context)
{ {
if (!$env->isDebug() || !$this->debugger) { if (!$env->isDebug() || !$this->debugger) {
return; return;
@@ -880,7 +888,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
$this->debugger->addMessage($data, 'debug'); $this->debugger->addMessage($data, 'debug');
} else { } else {
for ($i = 2; $i < $count; $i++) { for ($i = 2; $i < $count; $i++) {
$this->debugger->addMessage(func_get_arg($i), 'debug'); $var = func_get_arg($i);
$this->debugger->addMessage('Twig Dump', 'debug', $var);
} }
} }
} }
@@ -1105,8 +1114,9 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*/ */
public function redirectFunc($url, $statusCode = 303) public function redirectFunc($url, $statusCode = 303)
{ {
header('Location: ' . $url, true, $statusCode); $response = new Response($statusCode, ['location' => $url]);
exit();
$this->grav->close($response);
} }
/** /**

View File

@@ -0,0 +1,74 @@
<?php
/**
* @package Grav\Common\Twig
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig;
use Grav\Common\Utils;
use Twig\Profiler\Profile;
use Clockwork\Request\Timeline;
class TwigProfileProcessor
{
private $root;
public function process(Profile $profile, Timeline $views, $counter = 0, $prefix = '', $sibling = false)
{
if ($profile->isRoot()) {
$this->root = $profile->getDuration();
$name = $profile->getName();
} else {
if ($profile->isTemplate()) {
$name = $this->formatTemplate($profile, $prefix);
} else {
$name = $this->formatNonTemplate($profile, $prefix);
}
$prefix .= '⎯⎯';
}
$percent = $this->root ? $profile->getDuration() / $this->root * 100 : 0;
$data = [
'tm' => $this->formatTime($profile, $percent),
'mu' => Utils::prettySize($profile->getMemoryUsage())
];
if ($profile->isRoot()) {
$data += ['pmu' => Utils::prettySize($profile->getPeakMemoryUsage())];
}
$views->addEvent(
$counter,
$profile->getTemplate(),
0,
$profile->getDuration(),
[ 'name' => $name, 'data' => $data ]
);
$nCount = count($profile->getProfiles());
foreach ($profile as $i => $p) {
$this->process($p, $views, ++$counter, $prefix, $i + 1 !== $nCount);
}
}
protected function formatTemplate(Profile $profile, $prefix)
{
return sprintf('%s⤍ %s', $prefix, $profile->getTemplate());
}
protected function formatNonTemplate(Profile $profile, $prefix)
{
return sprintf('%s⤍ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName());
}
protected function formatTime(Profile $profile, $percent)
{
return sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent);
}
}

View File

@@ -300,16 +300,17 @@ class Uri
* Get URI parameter. * Get URI parameter.
* *
* @param string $id * @param string $id
* @param bool $default
* *
* @return bool|string * @return bool|string
*/ */
public function param($id) public function param($id, $default = false)
{ {
if (isset($this->params[$id])) { if (isset($this->params[$id])) {
return html_entity_decode(rawurldecode($this->params[$id])); return html_entity_decode(rawurldecode($this->params[$id]));
} }
return false; return $default;
} }
/** /**
@@ -1340,7 +1341,7 @@ class Uri
/** /**
* Check if this is a valid Grav extension * Check if this is a valid Grav extension
* *
* @param $extension * @param string $extension
* @return bool * @return bool
*/ */
public function isValidExtension($extension) public function isValidExtension($extension)
@@ -1357,7 +1358,7 @@ class Uri
/** /**
* Allow overriding of any element (be careful!) * Allow overriding of any element (be careful!)
* *
* @param $data * @param array $data
* @return Uri * @return Uri
*/ */
public function setUriProperties($data) public function setUriProperties($data)

View File

@@ -382,31 +382,6 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
return $this->getBlueprint()->extra($this->toArray()); return $this->getBlueprint()->extra($this->toArray());
} }
/**
* @param string $name
* @return Blueprint
*/
public function getBlueprint(string $name = '')
{
$blueprint = clone parent::getBlueprint($name);
$blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
$params = (array)$call['params'];
$method = array_shift($params);
if (method_exists($this, $method)) {
$value = $this->{$method}(...$params);
if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
} else {
$field[$property] = $value;
}
}
});
return $blueprint->init();
}
/** /**
* Return unmodified data as raw string. * Return unmodified data as raw string.
* *

View File

@@ -59,6 +59,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface
*/ */
public function find($query, $fields = ['username', 'email']): UserInterface public function find($query, $fields = ['username', 'email']): UserInterface
{ {
// FIXME: $fields is incompatible to parent class -- add support for finding value from multiple properties.
foreach ((array)$fields as $field) { foreach ((array)$fields as $field) {
if ($field === 'key') { if ($field === 'key') {
$user = $this->get($query); $user = $this->get($query);

View File

@@ -14,6 +14,8 @@ use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Markdown\Parsedown; use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra; use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Markdown\Excerpts; use Grav\Common\Page\Markdown\Excerpts;
use Negotiation\Accept;
use Negotiation\Negotiator;
use RocketTheme\Toolbox\Event\Event; use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator; use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -663,6 +665,39 @@ abstract class Utils
} }
} }
/**
* Returns the output render format, usually the extension provided in the URL. (e.g. `html`, `json`, `xml`, etc).
*
* @return string
*/
public static function getPageFormat(): string
{
/** @var Uri $uri */
$uri = Grav::instance()['uri'];
// Set from uri extension
$uri_extension = $uri->extension();
if (is_string($uri_extension)) {
return $uri_extension;
}
// Use content negotiation via the `accept:` header
$http_accept = $_SERVER['HTTP_ACCEPT'] ?? null;
if (is_string($http_accept)) {
$negotiator = new Negotiator();
$supported_types = Utils::getSupportPageTypes(['html', 'json']);
$priorities = Utils::getMimeTypes($supported_types);
$media_type = $negotiator->getBest($http_accept, $priorities);
$mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
return Utils::getExtensionByMime($mimetype);
}
return 'html';
}
/** /**
* Return the mimetype based on filename extension * Return the mimetype based on filename extension
* *
@@ -1321,7 +1356,7 @@ abstract class Utils
*/ */
public static function sortArrayByArray(array $array, array $orderArray) public static function sortArrayByArray(array $array, array $orderArray)
{ {
$ordered = array(); $ordered = [];
foreach ($orderArray as $key) { foreach ($orderArray as $key) {
if (array_key_exists($key, $array)) { if (array_key_exists($key, $array)) {
$ordered[$key] = $array[$key]; $ordered[$key] = $array[$key];
@@ -1476,16 +1511,16 @@ abstract class Utils
/** /**
* Parse a readable file size and return a value in bytes * Parse a readable file size and return a value in bytes
* *
* @param string|int $size * @param string|int|float $size
* @return int * @return int
*/ */
public static function parseSize($size) public static function parseSize($size)
{ {
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size); $unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
$size = preg_replace('/[^0-9\.]/', '', $size); $size = (float)preg_replace('/[^0-9\.]/', '', $size);
if ($unit) { if ($unit) {
$size = $size * pow(1024, stripos('bkmgtpezy', $unit[0])); $size *= 1024 ** stripos('bkmgtpezy', $unit[0]);
} }
return (int) abs(round($size)); return (int) abs(round($size));
@@ -1574,7 +1609,7 @@ abstract class Utils
} }
// Packed representation of IP // Packed representation of IP
$ip = inet_pton($ip); $ip = (string)inet_pton($ip);
// Maximum netmask length = same as packed address // Maximum netmask length = same as packed address
$len = 8*strlen($ip); $len = 8*strlen($ip);

View File

@@ -74,7 +74,7 @@ class SchedulerCommand extends ConsoleCommand
// Show jobs list // Show jobs list
$jobs = $scheduler->getAllJobs(); $jobs = $scheduler->getAllJobs();
$job_states = $scheduler->getJobStates()->content(); $job_states = (array)$scheduler->getJobStates()->content();
$rows = []; $rows = [];
$table = new Table($this->output); $table = new Table($this->output);
@@ -113,7 +113,7 @@ class SchedulerCommand extends ConsoleCommand
$io->newLine(); $io->newLine();
} elseif ($this->input->getOption('details')) { } elseif ($this->input->getOption('details')) {
$jobs = $scheduler->getAllJobs(); $jobs = $scheduler->getAllJobs();
$job_states = $scheduler->getJobStates()->content(); $job_states = (array)$scheduler->getJobStates()->content();
$io->title('Job Details'); $io->title('Job Details');

View File

@@ -0,0 +1,137 @@
<?php
/**
* @package Grav\Console\Cli
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Grav;
use Grav\Common\Helpers\LogViewer;
use Grav\Common\Helpers\YamlLinter;
use Grav\Common\Utils;
use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\PhpProcess;
use Symfony\Component\Process\Process;
class ServerCommand extends ConsoleCommand
{
const SYMFONY_SERVER = 'Symfony Server';
const PHP_SERVER = 'Built-in PHP Server';
protected $ip;
protected $port;
protected $io;
protected function configure()
{
$this
->setName('server')
->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Preferred HTTP port rather than auto-find (default is 8000-9000')
->addOption('symfony', null, InputOption::VALUE_NONE, 'Force using Symfony server')
->addOption('php', null, InputOption::VALUE_NONE, 'Force using built-in PHP server')
->setDescription("Runs built-in web-server, Symfony first, then tries PHP's")
->setHelp("Runs built-in web-server, Symfony first, then tries PHP's");
}
protected function serve()
{
$io = $this->io = new SymfonyStyle($this->input, $this->output);
$io->title('Grav Web Server');
// Ensure CLI colors are on
ini_set('cli_server.color', 'on');
// Options
$force_symfony = $this->input->getOption('symfony');
$force_php = $this->input->getOption('php');
// Find PHP
$executableFinder = new PhpExecutableFinder();
$php = $executableFinder->find(false);
$this->ip = '127.0.0.1';
$this->port = intval($this->input->getOption('port') ?? 8000);
// Get an open port
while (!$this->portAvailable($this->ip, $this->port)) {
$this->port += 1;
}
// Setup the commands
$symfony_cmd = ['symfony', 'server:start', '--ansi', '--port=' . $this->port ];
$php_cmd = [$php, '-S', $this->ip.':'.$this->port, 'system/router.php'];
$commands = [
self::SYMFONY_SERVER => $symfony_cmd,
self::PHP_SERVER => $php_cmd
];
if ($force_symfony) {
unset($commands[self::PHP_SERVER]);
} elseif ($force_php) {
unset($commands[self::SYMFONY_SERVER]);
}
foreach($commands as $name => $command) {
$process = $this->runProcess($name, $command);
if (!$process) {
$io->note('Starting ' . $name . '...');
}
// Should only get here if there's an error running
if (!$process->isRunning() && (
($name === self::SYMFONY_SERVER && $force_symfony) ||
($name === self::PHP_SERVER)
)) {
$io->error('Could not start ' . $name);
}
}
}
protected function runProcess($name, $cmd)
{
$process = new Process($cmd);
$process->setTimeout(0);
$process->start();
if ($name === self::PHP_SERVER) {
$this->io->success('Built-in PHP web server listening on http://' . $this->ip . ':' . $this->port . ' (PHP v' . phpversion() . ')');
}
$process->wait(function ($type, $buffer) {
$this->output->write($buffer);
});
return $process;
}
/**
* Simple function test the port
*
* @param $ip
* @param $port
* @return bool
*/
protected function portAvailable($ip, $port) {
$fp = @fsockopen($ip, $port, $errno, $errstr, 0.1);
if (!$fp) {
return true;
} else {
fclose($fp);
return false;
}
}
}

View File

@@ -10,9 +10,7 @@
namespace Grav\Console\Cli; namespace Grav\Console\Cli;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Helpers\LogViewer;
use Grav\Common\Helpers\YamlLinter; use Grav\Common\Helpers\YamlLinter;
use Grav\Common\Utils;
use Grav\Console\ConsoleCommand; use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
@@ -29,6 +27,18 @@ class YamlLinterCommand extends ConsoleCommand
InputOption::VALUE_OPTIONAL, InputOption::VALUE_OPTIONAL,
'The environment to trigger a specific configuration. For example: localhost, mysite.dev, www.mysite.com' 'The environment to trigger a specific configuration. For example: localhost, mysite.dev, www.mysite.com'
) )
->addOption(
'all',
'a',
InputOption::VALUE_NONE,
'Go through the whole Grav installation'
)
->addOption(
'folder',
'f',
InputOption::VALUE_OPTIONAL,
'Go through specific folder'
)
->setDescription('Checks various files for YAML errors') ->setDescription('Checks various files for YAML errors')
->setHelp("Checks various files for YAML errors"); ->setHelp("Checks various files for YAML errors");
} }
@@ -42,41 +52,59 @@ class YamlLinterCommand extends ConsoleCommand
$io->title('Yaml Linter'); $io->title('Yaml Linter');
$io->section('User Configuration'); if ($this->input->getOption('all')) {
$errors = YamlLinter::lintConfig(); $io->section('All');
$errors = YamlLinter::lint('');
if (empty($errors)) { if (empty($errors)) {
$io->success('No YAML Linting issues with configuration'); $io->success('No YAML Linting issues found');
} else {
$this->displayErrors($errors, $io);
}
} elseif ($folder = $this->input->getOption('folder')) {
$io->section($folder);
$errors = YamlLinter::lint($folder);
if (empty($errors)) {
$io->success('No YAML Linting issues found');
} else {
$this->displayErrors($errors, $io);
}
} else { } else {
$this->displayErrors($errors, $io); $io->section('User Configuration');
$errors = YamlLinter::lintConfig();
if (empty($errors)) {
$io->success('No YAML Linting issues with configuration');
} else {
$this->displayErrors($errors, $io);
}
$io->section('Pages Frontmatter');
$errors = YamlLinter::lintPages();
if (empty($errors)) {
$io->success('No YAML Linting issues with pages');
} else {
$this->displayErrors($errors, $io);
}
$io->section('Page Blueprints');
$errors = YamlLinter::lintBlueprints();
if (empty($errors)) {
$io->success('No YAML Linting issues with blueprints');
} else {
$this->displayErrors($errors, $io);
}
} }
$io->section('Pages Frontmatter');
$errors = YamlLinter::lintPages();
if (empty($errors)) {
$io->success('No YAML Linting issues with pages');
} else {
$this->displayErrors($errors, $io);
}
$io->section('Page Blueprints');
$errors = YamlLinter::lintBlueprints();
if (empty($errors)) {
$io->success('No YAML Linting issues with blueprints');
} else {
$this->displayErrors($errors, $io);
}
} }
protected function displayErrors($errors, $io) protected function displayErrors($errors, SymfonyStyle $io)
{ {
$io->error("YAML Linting issues found..."); $io->error('YAML Linting issues found...');
foreach ($errors as $path => $error) { foreach ($errors as $path => $error) {
$io->writeln("<yellow>$path</yellow> - $error"); $io->writeln("<yellow>{$path}</yellow> - {$error}");
} }
} }
} }

View File

@@ -19,7 +19,7 @@ use Symfony\Component\Console\Input\InputOption;
class IndexCommand extends ConsoleCommand class IndexCommand extends ConsoleCommand
{ {
/** @var array */ /** @var Packages */
protected $data; protected $data;
/** @var GPM */ /** @var GPM */
@@ -188,9 +188,9 @@ class IndexCommand extends ConsoleCommand
} }
/** /**
* @param array $data * @param Packages $data
* *
* @return mixed * @return Packages
*/ */
public function filter($data) public function filter($data)
{ {
@@ -245,6 +245,7 @@ class IndexCommand extends ConsoleCommand
/** /**
* @param Packages $packages * @param Packages $packages
* @return Packages
*/ */
public function sort($packages) public function sort($packages)
{ {

View File

@@ -551,10 +551,9 @@ class InstallCommand extends ConsoleCommand
/** /**
* @param Package $package * @param Package $package
* @param string|null $license
* *
* @param string $license * @return string|null
*
* @return string
*/ */
private function downloadPackage($package, $license = null) private function downloadPackage($package, $license = null)
{ {
@@ -586,7 +585,7 @@ class InstallCommand extends ConsoleCommand
$this->output->writeln(' |- Downloading package... <red>error</red> '); $this->output->writeln(' |- Downloading package... <red>error</red> ');
$this->output->writeln(" | '- " . $error); $this->output->writeln(" | '- " . $error);
return false; return null;
} }
Folder::create($this->tmp); Folder::create($this->tmp);

View File

@@ -185,17 +185,17 @@ class SelfupgradeCommand extends ConsoleCommand
{ {
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true); $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/Grav-' . uniqid('', false); $this->tmp = $tmp_dir . '/Grav-' . uniqid('', false);
$output = Response::get($package['download'], [], [$this, 'progress']); $output = Response::get($package->download, [], [$this, 'progress']);
Folder::create($this->tmp); Folder::create($this->tmp);
$this->output->write("\x0D"); $this->output->write("\x0D");
$this->output->write(" |- Downloading upgrade [{$this->formatBytes($package['size'])}]... 100%"); $this->output->write(" |- Downloading upgrade [{$this->formatBytes($package->size)}]... 100%");
$this->output->writeln(''); $this->output->writeln('');
file_put_contents($this->tmp . DS . $package['name'], $output); file_put_contents($this->tmp . DS . $package->name, $output);
return $this->tmp . DS . $package['name']; return $this->tmp . DS . $package->name;
} }
/** /**

View File

@@ -110,7 +110,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
*/ */
public function removeElement($element) public function removeElement($element)
{ {
$key = $this->isAllowedElement($element) ? $element->getKey() : null; $key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null;
if (!$key || !isset($this->entries[$key])) { if (!$key || !isset($this->entries[$key])) {
return false; return false;
@@ -178,7 +178,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
*/ */
public function contains($element) public function contains($element)
{ {
$key = $this->isAllowedElement($element) ? $element->getKey() : null; $key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null;
return $key && isset($this->entries[$key]); return $key && isset($this->entries[$key]);
} }
@@ -196,7 +196,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
*/ */
public function indexOf($element) public function indexOf($element)
{ {
$key = $this->isAllowedElement($element) ? $element->getKey() : null; $key = $this->isAllowedElement($element) ? $this->getCurrentKey($element) : null;
return $key && isset($this->entries[$key]) ? $key : null; return $key && isset($this->entries[$key]) ? $key : null;
} }
@@ -246,10 +246,6 @@ abstract class AbstractIndexCollection implements CollectionInterface
throw new \InvalidArgumentException('Invalid argument $value'); throw new \InvalidArgumentException('Invalid argument $value');
} }
if ($key !== $value->getKey()) {
$value->setKey($key);
}
$this->entries[$key] = $this->getElementMeta($value); $this->entries[$key] = $this->getElementMeta($value);
} }
@@ -262,7 +258,7 @@ abstract class AbstractIndexCollection implements CollectionInterface
throw new \InvalidArgumentException('Invalid argument $element'); throw new \InvalidArgumentException('Invalid argument $element');
} }
$this->entries[$element->getKey()] = $this->getElementMeta($element); $this->entries[$this->getCurrentKey($element)] = $this->getElementMeta($element);
return true; return true;
} }
@@ -477,6 +473,11 @@ abstract class AbstractIndexCollection implements CollectionInterface
$this->entries = $entries; $this->entries = $entries;
} }
protected function getCurrentKey($element)
{
return $element->getKey();
}
/** /**
* @param string $key * @param string $key
* @param mixed $value * @param mixed $value

View File

@@ -0,0 +1,200 @@
<?php
/**
* @package Grav\Framework\Controller
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
declare(strict_types=1);
namespace Grav\Framework\Controller\Traits;
use Grav\Common\Config\Config;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
trait ControllerResponseTrait
{
/**
* Display the current page.
*
* @return Response
*/
protected function createDisplayResponse(): ResponseInterface
{
return new Response(418);
}
/**
* @param string $content
* @param int $code
* @param array $headers
* @return Response
*/
protected function createHtmlResponse(string $content, int $code = null, array $headers = null): ResponseInterface
{
$code = $code ?? 200;
if ($code < 100 || $code > 599) {
$code = 500;
}
$headers = $headers ?? [];
return new Response($code, $headers, $content);
}
/**
* @param array $content
* @param int $code
* @param array $headers
* @return Response
*/
protected function createJsonResponse(array $content, int $code = null, array $headers = null): ResponseInterface
{
$code = $code ?? $content['code'];
if (null === $code || $code < 100 || $code > 599) {
$code = 200;
}
$headers = ($headers ?? []) + [
'Content-Type' => 'application/json',
'Cache-Control' => 'no-cache, no-store, must-revalidate'
];
return new Response($code, $headers, json_encode($content));
}
/**
* @param string $url
* @param int $code
* @return Response
*/
protected function createRedirectResponse(string $url, int $code = null): ResponseInterface
{
if (null === $code || $code < 301 || $code > 307) {
$code = $this->getConfig()->get('system.pages.redirect_default_code', 302);
}
$accept = $this->getAccept(['application/json', 'text/html']);
if ($accept === 'application/json') {
return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]);
}
return new Response($code, ['Location' => $url]);
}
/**
* @param \Throwable $e
* @return \Throwable
*/
protected function createErrorResponse(\Throwable $e): ResponseInterface
{
$validCodes = [
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511
];
if ($e instanceof RequestException) {
$code = $e->getHttpCode();
$reason = $e->getHttpReason();
} else {
$code = $e->getCode();
$reason = null;
}
if (!in_array($code, $validCodes, true)) {
$code = 500;
}
$message = $e->getMessage();
$response = [
'code' => $code,
'status' => 'error',
'message' => $message,
'error' => [
'code' => $code,
'message' => $message
]
];
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
if ($debugger->enabled()) {
$response['error'] += [
'type' => \get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => explode("\n", $e->getTraceAsString())
];
}
$accept = $this->getAccept(['application/json', 'text/html']);
if ($accept === 'text/html') {
$request = $this->getRequest();
$method = $request->getMethod();
// On POST etc, redirect back to the previous page.
if ($method !== 'GET' && $method !== 'HEAD') {
$this->setMessage($message, 'error');
$referer = $request->getHeaderLine('Referer');
return $this->createRedirectResponse($referer, 303);
}
// TODO: improve error page
return $this->createHtmlResponse($response['message'], $code);
}
return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
}
protected function getAccept(array $compare)
{
$accepted = [];
foreach ($this->getRequest()->getHeader('Accept') as $accept) {
foreach (explode(',', $accept) as $item) {
if (!$item) {
continue;
}
$split = explode(';q=', $item);
$mime = array_shift($split);
$priority = array_shift($split) ?? 1.0;
$accepted[$mime] = $priority;
}
}
arsort($accepted);
// TODO: add support for image/* etc
$list = array_intersect($compare, array_keys($accepted));
if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
return reset($compare);
}
return reset($list);
}
/**
* @return ServerRequestInterface
*/
abstract protected function getRequest(): ServerRequestInterface;
/**
* @param string $message
* @param string $type
* @return $this
*/
abstract protected function setMessage($message, $type = 'info');
/**
* @return Config
*/
abstract protected function getConfig(): Config;
}

View File

@@ -262,7 +262,7 @@ class Flex implements \Countable
if (null === $type && null === $keyField) { if (null === $type && null === $keyField) {
// Special handling for quick Flex key lookups. // Special handling for quick Flex key lookups.
$keyField = 'storage_key'; $keyField = 'storage_key';
[$type, $key] = $this->resolveKeyAndType($key, $type); [$key, $type] = $this->resolveKeyAndType($key, $type);
} else { } else {
$type = $this->resolveType($type); $type = $this->resolveType($type);
} }

View File

@@ -13,12 +13,11 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Grav\Common\Debugger; use Grav\Common\Debugger;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Inflector;
use Grav\Common\Twig\Twig; use Grav\Common\Twig\Twig;
use Grav\Common\User\Interfaces\UserInterface; use Grav\Common\User\Interfaces\UserInterface;
use Grav\Framework\Cache\CacheInterface; use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\ContentBlock\ContentBlockInterface;
use Grav\Framework\ContentBlock\HtmlBlock; use Grav\Framework\ContentBlock\HtmlBlock;
use Grav\Framework\Flex\Interfaces\FlexIndexInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface; use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Object\ObjectCollection; use Grav\Framework\Object\ObjectCollection;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface; use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
@@ -51,8 +50,10 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
'getTypePrefix' => true, 'getTypePrefix' => true,
'getType' => true, 'getType' => true,
'getFlexDirectory' => true, 'getFlexDirectory' => true,
'hasFlexFeature' => true,
'getFlexFeatures' => true,
'getCacheKey' => true, 'getCacheKey' => true,
'getCacheChecksum' => true, 'getCacheChecksum' => false,
'getTimestamp' => true, 'getTimestamp' => true,
'hasProperty' => true, 'hasProperty' => true,
'getProperty' => true, 'getProperty' => true,
@@ -92,6 +93,31 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
} }
} }
/**
* {@inheritdoc}
* @see FlexCommonInterface::hasFlexFeature()
*/
public function hasFlexFeature(string $name): bool
{
return in_array($name, $this->getFlexFeatures(), true);
}
/**
* {@inheritdoc}
* @see FlexCommonInterface::hasFlexFeature()
*/
public function getFlexFeatures(): array
{
$implements = class_implements($this);
$list = [];
foreach ($implements as $interface) {
$feature = Inflector::hyphenize(preg_replace('/(.*\\\\)(.*?)Interface$/', '\\2', $interface));
$list[] = $feature;
}
return $list;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexCollectionInterface::search() * @see FlexCollectionInterface::search()
@@ -184,7 +210,16 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
*/ */
public function getCacheChecksum(): string public function getCacheChecksum(): string
{ {
return sha1(json_encode($this->getTimestamps())); /**
* @var string $key
* @var FlexObjectInterface $object
*/
$list = [];
foreach ($this as $key => $object) {
$list[$key] = $object->getCacheChecksum();
}
return sha1(json_encode($list));
} }
/** /**
@@ -258,6 +293,11 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField()); return $this->getFlexDirectory()->getIndex($this->getKeys(), $this->getKeyField());
} }
public function getCollection()
{
return $this;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexCollectionInterface::render() * @see FlexCollectionInterface::render()
@@ -275,16 +315,19 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
$debugger = $grav['debugger']; $debugger = $grav['debugger'];
$debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')'); $debugger->startTimer('flex-collection-' . ($debugKey = uniqid($type, false)), 'Render Collection ' . $type . ' (' . $layout . ')');
$cache = $key = null; $key = null;
foreach ($context as $value) { foreach ($context as $value) {
if (!\is_scalar($value)) { if (!\is_scalar($value)) {
$key = false; $key = false;
break;
} }
} }
if ($key !== false) { if ($key !== false) {
$key = md5($this->getCacheKey() . '.' . $layout . json_encode($context)); $key = md5($this->getCacheKey() . '.' . $layout . json_encode($context));
$cache = $this->getCache('render'); $cache = $this->getCache('render');
} else {
$cache = null;
} }
try { try {
@@ -305,9 +348,9 @@ class FlexCollection extends ObjectCollection implements FlexCollectionInterface
} }
if (!$block) { if (!$block) {
$block = HtmlBlock::create($key); $block = HtmlBlock::create($key ?: null);
$block->setChecksum($checksum); $block->setChecksum($checksum);
if ($key === false) { if (!$key) {
$block->disableCache(); $block->disableCache();
} }

View File

@@ -14,6 +14,7 @@ use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint; use Grav\Common\Data\Blueprint;
use Grav\Common\Debugger; use Grav\Common\Debugger;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Framework\Cache\Adapter\DoctrineCache; use Grav\Framework\Cache\Adapter\DoctrineCache;
use Grav\Framework\Cache\Adapter\MemoryCache; use Grav\Framework\Cache\Adapter\MemoryCache;
@@ -59,6 +60,8 @@ class FlexDirectory implements FlexAuthorizeInterface
protected $storage; protected $storage;
/** @var CacheInterface */ /** @var CacheInterface */
protected $cache; protected $cache;
/** @var FlexObjectInterface[] */
protected $objects;
/** @var string */ /** @var string */
protected $objectClassName; protected $objectClassName;
/** @var string */ /** @var string */
@@ -79,6 +82,18 @@ class FlexDirectory implements FlexAuthorizeInterface
$this->blueprint_file = $blueprint_file; $this->blueprint_file = $blueprint_file;
$this->defaults = $defaults; $this->defaults = $defaults;
$this->enabled = !empty($defaults['enabled']); $this->enabled = !empty($defaults['enabled']);
$this->objects = [];
}
public function isListed(): bool
{
$grav = Grav::instance();
/** @var Flex|null $flex */
$flex = $grav['flex_objects'] ?? null;
$directory = $flex ? $flex->getDirectory($this->type) : null;
return null !== $directory;
} }
/** /**
@@ -222,7 +237,7 @@ class FlexDirectory implements FlexAuthorizeInterface
} }
/** /**
* Returns an object if it exists. * Returns an object if it exists. If no arguments are passed (or both of them are null), method creates a new empty object.
* *
* Note: It is not safe to use the object without checking if the user can access it. * Note: It is not safe to use the object without checking if the user can access it.
* *
@@ -230,8 +245,12 @@ class FlexDirectory implements FlexAuthorizeInterface
* @param string|null $keyField Field to be used as the key. * @param string|null $keyField Field to be used as the key.
* @return FlexObjectInterface|null * @return FlexObjectInterface|null
*/ */
public function getObject($key, string $keyField = null): ?FlexObjectInterface public function getObject($key = null, string $keyField = null): ?FlexObjectInterface
{ {
if (null === $key && null === $keyField) {
return $this->createObject([], '');
}
return $this->getIndex(null, $keyField)->get($key); return $this->getIndex(null, $keyField)->get($key);
} }
@@ -239,9 +258,12 @@ class FlexDirectory implements FlexAuthorizeInterface
* @param array $data * @param array $data
* @param string|null $key * @param string|null $key
* @return FlexObjectInterface * @return FlexObjectInterface
* @deprecated 1.7 Use $object->update()->save() instead.
*/ */
public function update(array $data, string $key = null): FlexObjectInterface public function update(array $data, string $key = null): FlexObjectInterface
{ {
user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->update()->save() instead.', E_USER_DEPRECATED);
$object = null !== $key ? $this->getIndex()->get($key): null; $object = null !== $key ? $this->getIndex()->get($key): null;
$storage = $this->getStorage(); $storage = $this->getStorage();
@@ -285,9 +307,12 @@ class FlexDirectory implements FlexAuthorizeInterface
/** /**
* @param string $key * @param string $key
* @return FlexObjectInterface|null * @return FlexObjectInterface|null
* @deprecated 1.7 Use $object->delete() instead.
*/ */
public function remove(string $key): ?FlexObjectInterface public function remove(string $key): ?FlexObjectInterface
{ {
user_error(__CLASS__ . '::' . __FUNCTION__ . '() should not be used anymore: use $object->delete() instead.', E_USER_DEPRECATED);
$object = $this->getIndex()->get($key); $object = $this->getIndex()->get($key);
if (!$object) { if (!$object) {
return null; return null;
@@ -492,76 +517,127 @@ class FlexDirectory implements FlexAuthorizeInterface
{ {
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = Grav::instance()['debugger']; $debugger = Grav::instance()['debugger'];
$debugger->startTimer('flex-objects', sprintf('Flex: Initializing %d %s', \count($entries), $this->type));
$storage = $this->getStorage();
$cache = $this->getCache('object'); $cache = $this->getCache('object');
// Get storage keys for the objects.
$keys = []; $keys = [];
$rows = []; $rows = [];
$fetch = [];
// Build lookup arrays with storage keys for the objects.
foreach ($entries as $key => $value) { foreach ($entries as $key => $value) {
$k = $value['storage_key']; $k = $value['storage_key'] ?? '';
$v = $this->objects[$k] ?? null;
$keys[$k] = $key; $keys[$k] = $key;
$rows[$k] = null; $rows[$k] = $v;
if (!$v) {
$fetch[] = $k;
}
} }
// Fetch rows from the cache. $loading = \count($fetch);
try {
$rows = $cache->getMultiple(array_keys($rows)); // Attempt to fetch missing rows from the cache.
} catch (InvalidArgumentException $e) { if ($fetch) {
$debugger->addException($e); try {
$debugger->startTimer('flex-objects', sprintf('Flex: Loading %d %s', $loading, $this->type));
$fetched = (array)$cache->getMultiple($fetch);
// Make sure cached objects are up to date: compare against index checksum/timestamp.
foreach ($fetched as $key => $value) {
if ($value instanceof FlexObjectInterface) {
$objectMeta = $value->getMetaData();
} else {
$objectMeta = $value['__META'] ?? [];
}
$indexMeta = $this->index->getMetaData($key);
$indexChecksum = $indexMeta['checksum'] ?? $indexMeta['storage_timestamp'] ?? null;
$objectChecksum = $objectMeta['checksum'] ?? $objectMeta['storage_timestamp'] ?? null;
if ($indexChecksum !== $objectChecksum) {
unset($fetched[$key]);
}
}
// Update cached rows.
$rows = array_replace($rows, $fetched);
} catch (InvalidArgumentException $e) {
$debugger->addException($e);
}
} }
// Read missing rows from the storage. // Read missing rows from the storage.
$storage = $this->getStorage();
$updated = []; $updated = [];
$rows = $storage->readRows($rows, $updated); $rows = $storage->readRows($rows, $updated);
// Create objects from the rows.
$isListed = $this->isListed();
$list = [];
foreach ($rows as $storageKey => $row) {
$usedKey = $keys[$storageKey];
if ($row instanceof FlexObjectInterface) {
$object = $row;
} else {
if ($row === null) {
$debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug');
continue;
}
if (isset($row['__error'])) {
$message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__error']);
$debugger->addException(new \RuntimeException($message));
$debugger->addMessage($message, 'error');
continue;
}
if (!isset($row['__META'])) {
$row['__META'] = [
'storage_key' => $storageKey,
'storage_timestamp' => $entries[$usedKey]['storage_timestamp'] ?? 0,
];
}
$key = $row['__META']['key'] ?? $entries[$usedKey]['key'] ?? $usedKey;
$object = $this->createObject($row, $key, false);
$this->objects[$storageKey] = $object;
if ($isListed) {
// If unserialize works for the object, serialize the object to speed up the loading.
$updated[$storageKey] = $object;
}
}
$list[$usedKey] = $object;
}
// Store updated rows to the cache. // Store updated rows to the cache.
if ($updated) { if ($updated) {
if (!$cache instanceof MemoryCache) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('Flex: Caching %d %s', \count($entries), $this->type), 'debug');
}
try { try {
if (!$cache instanceof MemoryCache) {
$debugger->addMessage(sprintf('Flex: Caching %d %s: %s', \count($updated), $this->type, implode(', ', array_keys($updated))), 'debug');
}
$cache->setMultiple($updated); $cache->setMultiple($updated);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$debugger->addException($e); $debugger->addException($e);
// TODO: log about the issue. // TODO: log about the issue.
} }
} }
// Create objects from the rows. $fetch && $debugger->stopTimer('flex-objects');
$list = [];
foreach ($rows as $storageKey => $row) {
if ($row === null) {
$debugger->addMessage(sprintf('Flex: Object %s was not found from %s storage', $storageKey, $this->type), 'debug');
continue;
}
if (isset($row['__error'])) {
$message = sprintf('Flex: Object %s is broken in %s storage: %s', $storageKey, $this->type, $row['__error']);
$debugger->addException(new \RuntimeException($message));
$debugger->addMessage($message, 'error');
continue;
}
$usedKey = $keys[$storageKey];
$row += [
'storage_key' => $storageKey,
'storage_timestamp' => $entries[$usedKey]['storage_timestamp'],
];
$key = $entries[$usedKey]['key'] ?? $usedKey;
$object = $this->createObject($row, $key, false);
$list[$usedKey] = $object;
}
$debugger->stopTimer('flex-objects');
return $list; return $list;
} }
public function reloadIndex(): void
{
$cache = $this->getCache('index');
$cache->delete('__keys');
}
/** /**
* @param string $type_view * @param string $type_view
* @param string $context * @param string $context
@@ -579,6 +655,13 @@ class FlexDirectory implements FlexAuthorizeInterface
$view = array_shift($parts) ?: ''; $view = array_shift($parts) ?: '';
$blueprint = new Blueprint($this->getBlueprintFile($view)); $blueprint = new Blueprint($this->getBlueprintFile($view));
$blueprint->addDynamicHandler('data', function (array &$field, $property, array &$call) {
$this->dynamicDataField($field, $property, $call);
});
$blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
$this->dynamicFlexField($field, $property, $call);
});
if ($context) { if ($context) {
$blueprint->setContext($context); $blueprint->setContext($context);
} }
@@ -595,6 +678,72 @@ class FlexDirectory implements FlexAuthorizeInterface
return $this->blueprints[$type_view]; return $this->blueprints[$type_view];
} }
/**
* @param array $field
* @param string $property
* @param array $call
*/
protected function dynamicDataField(array &$field, $property, array &$call)
{
$params = $call['params'];
if (\is_array($params)) {
$function = array_shift($params);
} else {
$function = $params;
$params = [];
}
$object = $call['object'];
if ($function === '\Grav\Common\Page\Pages::pageTypes') {
$params = [$object instanceof PageInterface && $object->modular() ? 'modular' : 'standard'];
}
[$o, $f] = explode('::', $function, 2);
$data = null;
if (!$f) {
if (\function_exists($o)) {
$data = \call_user_func_array($o, $params);
}
} else {
if (method_exists($o, $f)) {
$data = \call_user_func_array([$o, $f], $params);
}
}
// If function returns a value,
if (null !== $data) {
if (\is_array($data) && isset($field[$property]) && \is_array($field[$property])) {
// Combine field and @data-field together.
$field[$property] += $data;
} else {
// Or create/replace field with @data-field.
$field[$property] = $data;
}
}
}
/**
* @param array $field
* @param string $property
* @param array $call
*/
protected function dynamicFlexField(array &$field, $property, array &$call)
{
$params = (array)$call['params'];
$object = $call['object'] ?? null;
$method = array_shift($params);
if ($object && method_exists($object, $method)) {
$value = $object->{$method}(...$params);
if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
} else {
$field[$property] = $value;
}
}
}
/** /**
* @return FlexStorageInterface * @return FlexStorageInterface
*/ */

View File

@@ -13,11 +13,11 @@ use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data; use Grav\Common\Data\Data;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Twig\Twig; use Grav\Common\Twig\Twig;
use Grav\Common\Utils;
use Grav\Framework\Flex\Interfaces\FlexFormInterface; use Grav\Framework\Flex\Interfaces\FlexFormInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface; use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Form\Traits\FormTrait; use Grav\Framework\Form\Traits\FormTrait;
use Grav\Framework\Route\Route; use Grav\Framework\Route\Route;
use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
use Twig\Template; use Twig\Template;
@@ -29,6 +29,10 @@ use Twig\TemplateWrapper;
*/ */
class FlexForm implements FlexFormInterface class FlexForm implements FlexFormInterface
{ {
use NestedArrayAccessWithGetters {
NestedArrayAccessWithGetters::get as private traitGet;
NestedArrayAccessWithGetters::set as private traitSet;
}
use FormTrait { use FormTrait {
FormTrait::doSerialize as doTraitSerialize; FormTrait::doSerialize as doTraitSerialize;
FormTrait::doUnserialize as doTraitUnserialize; FormTrait::doUnserialize as doTraitUnserialize;
@@ -40,23 +44,88 @@ class FlexForm implements FlexFormInterface
/** @var FlexObjectInterface */ /** @var FlexObjectInterface */
private $object; private $object;
/** @var string */
private $flexName;
/**
* @param array $options Options to initialize the form instance:
* (string) name: Form name, allows you to use custom form.
* (string) unique_id: Unique id for this form instance.
* (array) form: Custom form fields.
* (FlexObjectInterface) object: Object instance.
* (string) key: Object key, used only if object instance isn't given.
* (FlexDirectory) directory: Flex Directory, mandatory if object isn't given.
*
* @return FlexFormInterface
*/
public static function instance(array $options = [])
{
if (isset($options['object'])) {
$object = $options['object'];
if (!$object instanceof FlexObjectInterface) {
throw new \RuntimeException(__METHOD__ . "(): 'object' should be instance of FlexObjectInterface", 400);
}
} elseif (isset($options['directory'])) {
$directory = $options['directory'];
if (!$directory instanceof FlexDirectory) {
throw new \RuntimeException(__METHOD__ . "(): 'directory' should be instance of FlexDirectory", 400);
}
$key = $options['key'] ?? '';
$object = $directory->getObject($key) ?? $directory->createObject([], $key);
} else {
throw new \RuntimeException(__METHOD__ . "(): You need to pass option 'directory' or 'object'", 400);
}
$name = $options['name'] ?? '';
// There is no reason to pass object and directory.
unset($options['object'], $options['directory']);
return $object->getForm($name, $options);
}
/** /**
* FlexForm constructor. * FlexForm constructor.
* @param string $name * @param string $name
* @param FlexObjectInterface $object * @param FlexObjectInterface $object
* @param array|null $form * @param array $options
*/ */
public function __construct(string $name, FlexObjectInterface $object, array $form = null) public function __construct(string $name, FlexObjectInterface $object, array $options = null)
{ {
$this->name = $name; $this->name = $name;
$this->form = $form;
$uniqueId = $object->exists() ? $object->getStorageKey() : "{$object->getFlexType()}:new";
$this->setObject($object); $this->setObject($object);
$this->setName($object->getFlexType(), $name);
$this->setId($this->getName()); $this->setId($this->getName());
$this->setUniqueId(md5($uniqueId));
$uniqueId = $options['unique_id'] ?? null;
if (!$uniqueId) {
if ($object->exists()) {
$uniqueId = $object->getStorageKey();
} elseif ($object->hasKey()) {
$uniqueId = "{$object->getKey()}:new";
} else {
$uniqueId = "{$object->getFlexType()}:new";
}
$uniqueId = md5($uniqueId);
}
$this->setUniqueId($uniqueId);
$directory = $object->getFlexDirectory();
$this->setFlashLookupFolder($directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
$this->form = $options['form'] ?? null;
$this->initialize();
}
/**
* @return $this
*/
public function initialize()
{
$this->messages = []; $this->messages = [];
$this->submitted = false; $this->submitted = false;
$this->data = null;
$this->files = [];
$this->unsetFlash();
$flash = $this->getFlash(); $flash = $this->getFlash();
if ($flash->exists()) { if ($flash->exists()) {
@@ -65,10 +134,55 @@ class FlexForm implements FlexFormInterface
$this->data = $data ? new Data($data, $this->getBlueprint()) : null; $this->data = $data ? new Data($data, $this->getBlueprint()) : null;
$this->files = $flash->getFilesByFields($includeOriginal); $this->files = $flash->getFilesByFields($includeOriginal);
} else {
$this->data = null;
$this->files = [];
} }
return $this;
}
/**
* @param string $name
* @param mixed $default
* @param string|null $separator
* @return mixed
*/
public function get($name, $default = null, $separator = null)
{
switch (strtolower($name)) {
case 'id':
case 'uniqueid':
case 'name':
case 'noncename':
case 'nonceaction':
case 'action':
case 'data':
case 'files':
case 'errors';
case 'fields':
case 'blueprint':
case 'page':
$method = 'get' . $name;
return $this->{$method}();
}
return $this->traitGet($name, $default, $separator);
}
/**
* @param string $name
* @param mixed $value
* @param string|null $separator
* @return FlexForm
*/
public function set($name, $value, $separator = null)
{
switch (strtolower($name)) {
case 'id':
case 'uniqueid':
$method = 'set' . $name;
return $this->{$method}();
}
return $this->traitSet($name, $value, $separator);
} }
/** /**
@@ -76,10 +190,15 @@ class FlexForm implements FlexFormInterface
*/ */
public function getName(): string public function getName(): string
{ {
$object = $this->getObject(); return $this->flexName;
$name = $this->name ?: 'object'; }
return "flex-{$object->getFlexType()}-{$name}"; protected function setName(string $type, string $name): void
{
// Make sure that both type and name do not have dash (convert dashes to underscores).
$type = str_replace('-', '_', $type);
$name = str_replace('-', '_', $name);
$this->flexName = $name ? "flex-{$type}-{$name}" : "flex-{$type}";
} }
/** /**
@@ -127,6 +246,32 @@ class FlexForm implements FlexFormInterface
return $this->object->getFlexType(); return $this->object->getFlexType();
} }
/**
* Get form flash object.
*
* @return FlexFormFlash
*/
public function getFlash()
{
if (null === $this->flash) {
$grav = Grav::instance();
$config = [
'session_id' => $this->getSessionId(),
'unique_id' => $this->getUniqueId(),
'form_name' => $this->getName(),
'folder' => $this->getFlashFolder(),
'object' => $this->getObject()
];
$this->flash = new FlexFormFlash($config);
$this->flash
->setUrl($grav['uri']->url)
->setUser($grav['user'] ?? null);
}
return $this->flash;
}
/** /**
* @return FlexObjectInterface * @return FlexObjectInterface
*/ */
@@ -150,7 +295,7 @@ class FlexForm implements FlexFormInterface
{ {
if (null === $this->blueprint) { if (null === $this->blueprint) {
try { try {
$blueprint = $this->getObject()->getBlueprint(Utils::isAdminPlugin() ? '' : $this->name); $blueprint = $this->getObject()->getBlueprint($this->name);
if ($this->form) { if ($this->form) {
// We have field overrides available. // We have field overrides available.
$blueprint->extend(['form' => $this->form], true); $blueprint->extend(['form' => $this->form], true);
@@ -330,12 +475,12 @@ class FlexForm implements FlexFormInterface
$this->object = $data['object']; $this->object = $data['object'];
} }
/** /**
* Filter validated data. * Filter validated data.
* *
* @param \ArrayAccess $data * @param \ArrayAccess|Data $data
*/ */
protected function filterData(\ArrayAccess $data): void protected function filterData($data): void
{ {
if ($data instanceof Data) { if ($data instanceof Data) {
$data->filter(true, true); $data->filter(true, true);

View File

@@ -0,0 +1,61 @@
<?php
/**
* @package Grav\Common\Flex
*
* @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Flex;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Form\FormFlash;
class FlexFormFlash extends FormFlash
{
/**
* @var FlexObjectInterface
*/
protected $object;
public function setObject(FlexObjectInterface $object)
{
$this->object = $object;
}
public function getObject(): FlexObjectInterface
{
return $this->object;
}
public function jsonSerialize(): array
{
$serialized = parent::jsonSerialize();
$object = $this->getObject();
if ($object) {
$serialized['object'] = [
'type' => $object->getFlexType(),
'key' => $object->getKey() ?: null,
'storage_key' => $object->exists() ? $object->getStorageKey() : null,
'timestamp' => $object->getTimestamp(),
'serialized' => $object->jsonSerialize()
];
}
return $serialized;
}
protected function init(?array $data, array $config): void
{
parent::init($data, $config);
$object = $config['object'];
if (isset($data['object']['serialized']) && !$object->exists()) {
$object->update($data['object']['serialized']);
}
$this->setObject($object);
}
}

View File

@@ -12,6 +12,7 @@ namespace Grav\Framework\Flex;
use Grav\Common\Debugger; use Grav\Common\Debugger;
use Grav\Common\File\CompiledYamlFile; use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Inflector;
use Grav\Common\Session; use Grav\Common\Session;
use Grav\Framework\Cache\CacheInterface; use Grav\Framework\Cache\CacheInterface;
use Grav\Framework\Collection\CollectionInterface; use Grav\Framework\Collection\CollectionInterface;
@@ -27,6 +28,8 @@ use Psr\SimpleCache\InvalidArgumentException;
class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexIndexInterface class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexIndexInterface
{ {
const VERSION = 1;
/** @var FlexDirectory */ /** @var FlexDirectory */
private $_flexDirectory; private $_flexDirectory;
@@ -80,6 +83,31 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
$this->setKeyField(null); $this->setKeyField(null);
} }
/**
* {@inheritdoc}
* @see FlexCommonInterface::hasFlexFeature()
*/
public function hasFlexFeature(string $name): bool
{
return in_array($name, $this->getFlexFeatures(), true);
}
/**
* {@inheritdoc}
* @see FlexCommonInterface::hasFlexFeature()
*/
public function getFlexFeatures(): array
{
$implements = class_implements($this->getFlexDirectory()->getCollectionClass());
$list = [];
foreach ($implements as $interface) {
$feature = Inflector::hyphenize(preg_replace('/(.*\\\\)(.*?)Interface$/', '\\2', $interface));
$list[] = $feature;
}
return $list;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexCollectionInterface::search() * @see FlexCollectionInterface::search()
@@ -152,7 +180,12 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
*/ */
public function getCacheChecksum(): string public function getCacheChecksum(): string
{ {
return sha1($this->getCacheKey() . json_encode($this->getTimestamps())); $list = [];
foreach ($this->getEntries() as $key => $value) {
$list[$key] = $value['checksum'] ?? $value['storage_timestamp'];
}
return sha1(json_encode($list));
} }
/** /**
@@ -227,6 +260,11 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
return $this; return $this;
} }
public function getCollection()
{
return $this->loadCollection();
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexCollectionInterface::render() * @see FlexCollectionInterface::render()
@@ -328,7 +366,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
} }
// Order by current field. // Order by current field.
if ($ordering === 'DESC') { if (strtoupper($ordering) === 'DESC') {
arsort($search, SORT_NATURAL); arsort($search, SORT_NATURAL);
} else { } else {
asort($search, SORT_NATURAL); asort($search, SORT_NATURAL);
@@ -369,25 +407,27 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
$cacheKey = ''; $cacheKey = '';
} }
$key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey()); $key = "{$flexType}.idx." . sha1($name . '.' . $cacheKey . json_encode($arguments) . $this->getCacheKey());
$checksum = $this->getCacheChecksum();
$cache = $this->getCache('object'); $cache = $this->getCache('object');
try { try {
$result = $cache->get($key); $cached = $cache->get($key);
$test = $cached[0] ?? null;
$result = $test === $checksum ? ($cached[1] ?? null) : null;
// Make sure the keys aren't changed if the returned type is the same index type. // Make sure the keys aren't changed if the returned type is the same index type.
if ($result instanceof self && $flexType === $result->getFlexType()) { if ($result instanceof self && $flexType === $result->getFlexType()) {
$result = $result->withKeyField($this->getKeyField()); $result = $result->withKeyField($this->getKeyField());
} }
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addException($e); $debugger->addException($e);
} }
if (!isset($result)) { if (!isset($result)) {
$collection = $this->loadCollection(); $collection = $this->loadCollection();
$result = $collection->{$name}(...$arguments); $result = $collection->{$name}(...$arguments);
$debugger->addMessage("Cache miss: '{$flexType}::{$name}()'", 'debug');
try { try {
// If flex collection is returned, convert it back to flex index. // If flex collection is returned, convert it back to flex index.
@@ -397,7 +437,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
$cached = $result; $cached = $result;
} }
$cache->set($key, $cached); $cache->set($key, [$checksum, $cached]);
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$debugger->addException($e); $debugger->addException($e);
@@ -408,8 +448,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
$collection = $this->loadCollection(); $collection = $this->loadCollection();
$result = $collection->{$name}(...$arguments); $result = $collection->{$name}(...$arguments);
if (!isset($cachedMethods[$name])) { if (!isset($cachedMethods[$name])) {
$class = \get_class($collection); $debugger->addMessage("Call '{$flexType}:{$name}()' isn't cached", 'debug');
$debugger->addMessage("Call '{$class}:{$name}()' isn't cached", 'debug');
} }
} }
@@ -544,7 +583,23 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
*/ */
protected function getElementMeta($object) protected function getElementMeta($object)
{ {
return $object->getTimestamp(); return $object->getMetaData();
}
protected function getCurrentKey($element)
{
$keyField = $this->getKeyField();
if ($keyField === 'storage_key') {
return $element->getStorageKey();
}
if ($keyField === 'flex_key') {
return $element->getFlexKey();
}
if ($keyField === 'key') {
return $element->getKey();
}
return $element->getKey();
} }
/** /**
@@ -553,7 +608,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
* @param array $entries Updated index * @param array $entries Updated index
* @return array Compiled list of entries * @return array Compiled list of entries
*/ */
protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries): array protected static function updateIndexFile(FlexStorageInterface $storage, array $index, array $entries, array $options = []): array
{ {
// Calculate removed objects. // Calculate removed objects.
$removed = array_diff_key($index, $entries); $removed = array_diff_key($index, $entries);
@@ -595,12 +650,12 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
// Go through all the updated objects and refresh their index data. // Go through all the updated objects and refresh their index data.
$updated = $added = []; $updated = $added = [];
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
if (null !== $row) { if (null !== $row || !empty($options['include_missing'])) {
$entry = ['key' => $key] + $entries[$key]; $entry = $entries[$key] + ['key' => $key];
if ($keyField !== 'storage_key' && isset($row[$keyField])) { if ($keyField !== 'storage_key' && isset($row[$keyField])) {
$entry['key'] = $row[$keyField]; $entry['key'] = $row[$keyField];
} }
static::updateIndexData($entry, $row); static::updateIndexData($entry, $row ?? []);
if (isset($row['__error'])) { if (isset($row['__error'])) {
$entry['__error'] = true; $entry['__error'] = true;
static::onException(new \RuntimeException(sprintf('Object failed to load: %s (%s)', $key, $row['__error']))); static::onException(new \RuntimeException(sprintf('Object failed to load: %s (%s)', $key, $row['__error'])));
@@ -627,7 +682,7 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
static::onChanges($index, $added, $updated, $removed); static::onChanges($index, $added, $updated, $removed);
$indexFile->save(['count' => \count($index), 'index' => $index]); $indexFile->save(['version' => static::VERSION, 'timestamp' => time(), 'count' => \count($index), 'index' => $index]);
$indexFile->unlock(); $indexFile->unlock();
return $index; return $index;
@@ -637,19 +692,30 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
{ {
} }
protected static function loadEntriesFromIndex(FlexStorageInterface $storage) protected static function loadIndex(FlexStorageInterface $storage)
{ {
$indexFile = static::getIndexFile($storage); $indexFile = static::getIndexFile($storage);
$data = []; $data = [];
try { try {
$data = (array)$indexFile->content(); $data = (array)$indexFile->content();
$version = $data['version'] ?? null;
if ($version !== static::VERSION) {
$data = [];
}
} catch (\Exception $e) { } catch (\Exception $e) {
$e = new \RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e); $e = new \RuntimeException(sprintf('Index failed to load: %s', $e->getMessage()), $e->getCode(), $e);
static::onException($e); static::onException($e);
} }
return $data ?: ['version' => static::VERSION, 'timestamp' => 0, 'count' => 0, 'index' => []];
}
protected static function loadEntriesFromIndex(FlexStorageInterface $storage)
{
$data = static::loadIndex($storage);
return $data['index'] ?? []; return $data['index'] ?? [];
} }
@@ -679,17 +745,19 @@ class FlexIndex extends ObjectIndex implements FlexCollectionInterface, FlexInde
protected static function onChanges(array $entries, array $added, array $updated, array $removed) protected static function onChanges(array $entries, array $added, array $updated, array $removed)
{ {
$message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', \count($entries), \count($added), \count($updated), \count($removed)); $addedCount = \count($added);
$updatedCount = \count($updated);
$removedCount = \count($removed);
$grav = Grav::instance(); if ($addedCount + $updatedCount + $removedCount) {
$message = sprintf('Index updated, %d objects (%d added, %d updated, %d removed).', \count($entries), $addedCount, $updatedCount, $removedCount);
/** @var Logger $logger */ $grav = Grav::instance();
$logger = $grav['log'];
$logger->addDebug($message);
/** @var Debugger $debugger */ /** @var Debugger $debugger */
$debugger = $grav['debugger']; $debugger = $grav['debugger'];
$debugger->addMessage($message, 'debug'); $debugger->addMessage($message, 'debug');
}
} }
public function __debugInfo() public function __debugInfo()

View File

@@ -12,6 +12,7 @@ namespace Grav\Framework\Flex;
use Grav\Common\Data\Blueprint; use Grav\Common\Data\Blueprint;
use Grav\Common\Debugger; use Grav\Common\Debugger;
use Grav\Common\Grav; use Grav\Common\Grav;
use Grav\Common\Inflector;
use Grav\Common\Twig\Twig; use Grav\Common\Twig\Twig;
use Grav\Common\Utils; use Grav\Common\Utils;
use Grav\Framework\Cache\CacheInterface; use Grav\Framework\Cache\CacheInterface;
@@ -54,9 +55,13 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
/** @var FlexFormInterface[] */ /** @var FlexFormInterface[] */
private $_forms = []; private $_forms = [];
/** @var array */ /** @var array */
private $_storage; private $_meta;
/** @var array */ /** @var array */
protected $_changes; protected $_changes;
/** @var string */
protected $storage_key;
/** @var int */
protected $storage_timestamp;
/** /**
* @return array * @return array
@@ -68,8 +73,10 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
'getType' => true, 'getType' => true,
'getFlexType' => true, 'getFlexType' => true,
'getFlexDirectory' => true, 'getFlexDirectory' => true,
'hasFlexFeature' => true,
'getFlexFeatures' => true,
'getCacheKey' => true, 'getCacheKey' => true,
'getCacheChecksum' => true, 'getCacheChecksum' => false,
'getTimestamp' => true, 'getTimestamp' => true,
'value' => true, 'value' => true,
'exists' => true, 'exists' => true,
@@ -97,6 +104,11 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
{ {
$this->_flexDirectory = $directory; $this->_flexDirectory = $directory;
if (isset($elements['__META'])) {
$this->setStorage($elements['__META']);
unset($elements['__META']);
}
if ($validate) { if ($validate) {
$blueprint = $this->getFlexDirectory()->getBlueprint(); $blueprint = $this->getFlexDirectory()->getBlueprint();
@@ -110,6 +122,31 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$this->objectConstruct($elements, $key); $this->objectConstruct($elements, $key);
} }
/**
* {@inheritdoc}
* @see FlexCommonInterface::hasFlexFeature()
*/
public function hasFlexFeature(string $name): bool
{
return in_array($name, $this->getFlexFeatures(), true);
}
/**
* {@inheritdoc}
* @see FlexCommonInterface::hasFlexFeature()
*/
public function getFlexFeatures(): array
{
$implements = class_implements($this);
$list = [];
foreach ($implements as $interface) {
$feature = Inflector::hyphenize(preg_replace('/(.*\\\\)(.*?)Interface$/', '\\2', $interface));
$list[] = $feature;
}
return $list;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexObjectInterface::getFlexType() * @see FlexObjectInterface::getFlexType()
@@ -134,7 +171,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function getTimestamp(): int public function getTimestamp(): int
{ {
return $this->_storage['storage_timestamp'] ?? 0; return $this->_meta['storage_timestamp'] ?? 0;
} }
/** /**
@@ -143,7 +180,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function getCacheKey(): string public function getCacheKey(): string
{ {
return $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getStorageKey(); return $this->hasKey() ? $this->getTypePrefix() . $this->getFlexType() . '.' . $this->getKey() : '';
} }
/** /**
@@ -152,7 +189,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function getCacheChecksum(): string public function getCacheChecksum(): string
{ {
return (string)$this->getTimestamp(); return (string)($this->_meta['checksum'] ?? $this->getTimestamp());
} }
/** /**
@@ -185,7 +222,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function getKey() public function getKey()
{ {
return $this->_key ?: $this->getFlexType() . '@@' . spl_object_hash($this); return (string)$this->_key;
} }
/** /**
@@ -194,7 +231,13 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function getFlexKey(): string public function getFlexKey(): string
{ {
return $this->_storage['flex_key'] ?? $this->_flexDirectory->getFlexType() . '.obj:' . $this->getStorageKey(); $key = $this->_meta['flex_key'] ?? null;
if (!$key && $key = $this->getStorageKey()) {
$key = $this->_flexDirectory->getFlexType() . '.obj:' . $key;
}
return (string)$key;
} }
/** /**
@@ -203,7 +246,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function getStorageKey(): string public function getStorageKey(): string
{ {
return $this->_storage['storage_key'] ?? $this->getTypePrefix() . $this->getFlexType() . '@@' . spl_object_hash($this); return (string)($this->storage_key ?? $this->_meta['storage_key'] ?? null);
} }
/** /**
@@ -249,7 +292,11 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
public function searchNestedProperty(string $property, string $search, array $options = null): float public function searchNestedProperty(string $property, string $search, array $options = null): float
{ {
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []); $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$value = $this->getNestedProperty($property); if ($property === 'key') {
$value = $this->getKey();
} else {
$value = $this->getNestedProperty($property);
}
return $this->searchValue($property, $value, $search, $options); return $this->searchValue($property, $value, $search, $options);
} }
@@ -345,7 +392,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function setStorageKey($key = null) public function setStorageKey($key = null)
{ {
$this->_storage['storage_key'] = $key; $this->storage_key = $key ?? '';
return $this; return $this;
} }
@@ -356,7 +403,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function setTimestamp($timestamp = null) public function setTimestamp($timestamp = null)
{ {
$this->_storage['storage_timestamp'] = $timestamp ?? time(); $this->storage_timestamp = $timestamp ?? time();
return $this; return $this;
} }
@@ -379,20 +426,28 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$debugger = $grav['debugger']; $debugger = $grav['debugger'];
$debugger->startTimer('flex-object-' . ($debugKey = uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')'); $debugger->startTimer('flex-object-' . ($debugKey = uniqid($type, false)), 'Render Object ' . $type . ' (' . $layout . ')');
$cache = $key = null; $key = $this->getCacheKey();
foreach ($context as $value) {
if (!\is_scalar($value)) { // Disable caching if context isn't all scalars.
$key = false; if ($key) {
foreach ($context as $value) {
if (!\is_scalar($value)) {
$key = '';
break;
}
} }
} }
if ($key !== false) { if ($key) {
$key = md5($this->getCacheKey() . '.' . $layout . json_encode($context)); // Create a new key which includes layout and context.
$key = md5($key . '.' . $layout . json_encode($context));
$cache = $this->getCache('render'); $cache = $this->getCache('render');
} else {
$cache = null;
} }
try { try {
$data = $cache && $key ? $cache->get($key) : null; $data = $cache ? $cache->get($key) : null;
$block = $data ? HtmlBlock::fromArray($data) : null; $block = $data ? HtmlBlock::fromArray($data) : null;
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
@@ -413,7 +468,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
if (!$block) { if (!$block) {
$block = HtmlBlock::create($key ?: null); $block = HtmlBlock::create($key ?: null);
$block->setChecksum($checksum); $block->setChecksum($checksum);
if ($key === false) { if (!$cache) {
$block->disableCache(); $block->disableCache();
} }
@@ -435,7 +490,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$block->setContent($output); $block->setContent($output);
try { try {
$cache && $key && $block->isCached() && $cache->set($key, $block->toArray()); $cache && $block->isCached() && $cache->set($key, $block->toArray());
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
$debugger->addException($e); $debugger->addException($e);
} }
@@ -531,6 +586,24 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return $this->save(); return $this->save();
} }
/**
* @param string|null $key
* @return FlexObject|FlexObjectInterface
*/
public function createCopy(string $key = null)
{
$this->markAsCopy();
return $this->create($key);
}
protected function markAsCopy()
{
$meta = $this->getMetaData();
$meta['copy'] = true;
$this->_meta = $meta;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexObjectInterface::save() * @see FlexObjectInterface::save()
@@ -539,15 +612,33 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
{ {
$this->triggerEvent('onBeforeSave'); $this->triggerEvent('onBeforeSave');
$result = $this->getFlexDirectory()->getStorage()->replaceRows([$this->getStorageKey() => $this->prepareStorage()]); $storage = $this->getFlexDirectory()->getStorage();
$key = $this->getStorageKey() ?: '@@' . spl_object_hash($this);
$meta = $this->getMetaData();
/** @var string|null $origKey */
$origKey = $meta['storage_key'] ?? null;
if (null !== $origKey && $key !== $origKey) {
if (!empty($meta['copy'])) {
$storage->copyRow($origKey, $key);
} else {
$storage->renameRow($origKey, $key);
}
}
$result = $storage->replaceRows([$key => $this->prepareStorage()]);
$value = reset($result); $value = reset($result);
$storageKey = (string)key($result); $meta = $value['__META'] ?? null;
if ($meta) {
$this->_meta = $meta;
}
$storageKey = $meta['storage_key'] ?? (string)key($result);
if ($value && $storageKey) { if ($value && $storageKey) {
$this->setStorageKey($storageKey); $this->setStorageKey($storageKey);
if (!$this->hasKey()) { $this->setKey($meta['key'] ?? $storageKey);
$this->setKey($storageKey);
}
} }
// FIXME: For some reason locator caching isn't cleared for the file, investigate! // FIXME: For some reason locator caching isn't cleared for the file, investigate!
@@ -564,7 +655,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
} }
try { try {
$this->getFlexDirectory()->clearCache(); $this->getFlexDirectory()->reloadIndex();
if (method_exists($this, 'clearMediaCache')) { if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache(); $this->clearMediaCache();
} }
@@ -587,12 +678,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function delete() public function delete()
{ {
if (!$this->exists()) {
return $this;
}
$this->triggerEvent('onBeforeDelete'); $this->triggerEvent('onBeforeDelete');
$this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]); $this->getFlexDirectory()->getStorage()->deleteRows([$this->getStorageKey() => $this->prepareStorage()]);
try { try {
$this->getFlexDirectory()->clearCache(); $this->getFlexDirectory()->reloadIndex();
if (method_exists($this, 'clearMediaCache')) { if (method_exists($this, 'clearMediaCache')) {
$this->clearMediaCache(); $this->clearMediaCache();
} }
@@ -615,17 +710,20 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
public function getBlueprint(string $name = '') public function getBlueprint(string $name = '')
{ {
return $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name); $blueprint = clone $this->_flexDirectory->getBlueprint($name ? '.' . $name : $name);
$blueprint->setObject($this);
return $blueprint->init();
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexObjectInterface::getForm() * @see FlexObjectInterface::getForm()
*/ */
public function getForm(string $name = '', array $form = null) public function getForm(string $name = '', array $options = null)
{ {
if (!isset($this->_forms[$name])) { if (!isset($this->_forms[$name])) {
$this->_forms[$name] = $this->createFormObject($name, $form); $this->_forms[$name] = $this->createFormObject($name, $options);
} }
return $this->_forms[$name]; return $this->_forms[$name];
@@ -715,6 +813,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
{ {
return [ return [
'type:private' => $this->getFlexType(), 'type:private' => $this->getFlexType(),
'storage_key:protected' => $this->getStorageKey(),
'storage_timestamp:protected' => $this->getTimestamp(),
'key:private' => $this->getKey(), 'key:private' => $this->getKey(),
'elements:private' => $this->getElements(), 'elements:private' => $this->getElements(),
'storage:private' => $this->getStorage() 'storage:private' => $this->getStorage()
@@ -765,12 +865,13 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
{ {
$this->_flexDirectory = $directory; $this->_flexDirectory = $directory;
} }
/** /**
* @param array $storage * @param array $storage
*/ */
protected function setStorage(array $storage) : void protected function setStorage(array $storage) : void
{ {
$this->_storage = $storage; $this->_meta = $storage;
} }
/** /**
@@ -778,7 +879,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
protected function getStorage() : array protected function getStorage() : array
{ {
return $this->_storage ?? []; return $this->_meta ?? [];
} }
/** /**
@@ -853,25 +954,25 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/ */
protected function filterElements(array &$elements): void protected function filterElements(array &$elements): void
{ {
if (!empty($elements['storage_key'])) { if (isset($elements['storage_key'])) {
$this->_storage['storage_key'] = trim($elements['storage_key']); $elements['storage_key'] = trim($elements['storage_key']);
} }
if (!empty($elements['storage_timestamp'])) { if (isset($elements['storage_timestamp'])) {
$this->_storage['storage_timestamp'] = (int)$elements['storage_timestamp']; $elements['storage_timestamp'] = (int)$elements['storage_timestamp'];
} }
unset ($elements['storage_key'], $elements['storage_timestamp'], $elements['_post_entries_save']); unset ($elements['_post_entries_save']);
} }
/** /**
* This methods allows you to override form objects in child classes. * This methods allows you to override form objects in child classes.
* *
* @param string $name Form name * @param string $name Form name
* @param array|null $form Form fields * @param array $options Form optiosn
* @return FlexFormInterface * @return FlexFormInterface
*/ */
protected function createFormObject(string $name, array $form = null) protected function createFormObject(string $name, array $options = null)
{ {
return new FlexForm($name, $this, $form); return new FlexForm($name, $this, $options);
} }
} }

View File

@@ -38,6 +38,21 @@ interface FlexCommonInterface extends RenderInterface
*/ */
public function getFlexDirectory(): FlexDirectory; public function getFlexDirectory(): FlexDirectory;
/**
* Test whether the feature is implemented in the object / collection.
*
* @param string $name
* @return bool
*/
public function hasFlexFeature(string $name): bool;
/**
* Get full list of features the object / collection implements.
*
* @return array
*/
public function getFlexFeatures(): array;
/** /**
* Get last updated timestamp for the object / collection. * Get last updated timestamp for the object / collection.
* *

View File

@@ -165,12 +165,12 @@ interface FlexObjectInterface extends FlexCommonInterface, NestedObjectInterface
* Returns a form instance for the object. * Returns a form instance for the object.
* *
* @param string $name Name of the form. Can be used to create customized forms for different use cases. * @param string $name Name of the form. Can be used to create customized forms for different use cases.
* @param array|null $form Can be used to further customize the form. * @param array|null $options Options can be used to further customize the form.
* *
* @return FlexFormInterface Returns a Form. * @return FlexFormInterface Returns a Form.
* @api * @api
*/ */
public function getForm(string $name = '', array $form = null); public function getForm(string $name = '', array $options = null);
/** /**
* Returns default value suitable to be used in a form for the given property. * Returns default value suitable to be used in a form for the given property.

View File

@@ -29,6 +29,12 @@ interface FlexStorageInterface
*/ */
public function getKeyField(): string; public function getKeyField(): string;
/**
* @param string[] $keys
* @return array
*/
public function getMetaData(array $keys): array;
/** /**
* Returns associated array of all existing storage keys with a timestamp. * Returns associated array of all existing storage keys with a timestamp.
* *
@@ -106,6 +112,14 @@ interface FlexStorageInterface
*/ */
public function replaceRows(array $rows): array; public function replaceRows(array $rows): array;
/**
* @param string $src
* @param string $dst
*
* @return bool
*/
public function copyRow(string $src, string $dst): bool;
/** /**
* @param string $src * @param string $src
* @param string $dst * @param string $dst

View File

@@ -149,6 +149,6 @@ abstract class AbstractFilesystemStorage implements FlexStorageInterface
*/ */
protected function validateKey(string $key): bool protected function validateKey(string $key): bool
{ {
return (bool) preg_match('/^[^\\/\\?\\*:;{}\\\\\\n]+$/u', $key); return $key && (bool) preg_match('/^[^\\/\\?\\*:;{}\\\\\\n]+$/u', $key);
} }
} }

View File

@@ -25,7 +25,7 @@ class FileStorage extends FolderStorage
*/ */
public function __construct(array $options) public function __construct(array $options)
{ {
$this->dataPattern = '{FOLDER}/{KEY}'; $this->dataPattern = '{FOLDER}/{KEY}{EXT}';
if (!isset($options['formatter']) && isset($options['pattern'])) { if (!isset($options['formatter']) && isset($options['pattern'])) {
$options['formatter'] = $this->detectDataFormatter($options['pattern']); $options['formatter'] = $this->detectDataFormatter($options['pattern']);
@@ -69,10 +69,7 @@ class FileStorage extends FolderStorage
continue; continue;
} }
$list[$key] = [ $list[$key] = $this->getObjectMeta($key);
'storage_key' => $key,
'storage_timestamp' => $info->getMTime()
];
} }
ksort($list, SORT_NATURAL); ksort($list, SORT_NATURAL);

View File

@@ -24,14 +24,20 @@ use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
*/ */
class FolderStorage extends AbstractFilesystemStorage class FolderStorage extends AbstractFilesystemStorage
{ {
/** @var string */ /** @var string Folder where all the data is stored. */
protected $dataFolder; protected $dataFolder;
/** @var string */ /** @var string Pattern to access an object. */
protected $dataPattern = '{FOLDER}/{KEY}/item'; protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}';
/** @var string Filename for the object. */
protected $dataFile;
/** @var string File extension for the object. */
protected $dataExt;
/** @var bool */ /** @var bool */
protected $prefixed; protected $prefixed;
/** @var bool */ /** @var bool */
protected $indexed; protected $indexed;
/** @var array */
protected $meta = [];
/** /**
* {@inheritdoc} * {@inheritdoc}
@@ -44,16 +50,20 @@ class FolderStorage extends AbstractFilesystemStorage
$this->initDataFormatter($options['formatter'] ?? []); $this->initDataFormatter($options['formatter'] ?? []);
$this->initOptions($options); $this->initOptions($options);
}
// Make sure that the data folder exists. /**
$folder = $this->resolvePath($this->dataFolder); * @param string[] $keys
if (!file_exists($folder)) { * @return array
try { */
Folder::create($folder); public function getMetaData(array $keys): array
} catch (\RuntimeException $e) { {
throw new \RuntimeException(sprintf('Flex: %s', $e->getMessage())); $list = [];
} foreach ($keys as $key) {
$list[$key] = $this->getObjectMeta($key);
} }
return $list;
} }
/** /**
@@ -87,6 +97,7 @@ class FolderStorage extends AbstractFilesystemStorage
$path = $this->getPathFromKey($key); $path = $this->getPathFromKey($key);
$file = $this->getFile($path); $file = $this->getFile($path);
$list[$key] = $this->saveFile($file, $row); $list[$key] = $this->saveFile($file, $row);
$list[$key]['__META'] = $this->getObjectMeta($key, true);
} }
return $list; return $list;
@@ -100,7 +111,7 @@ class FolderStorage extends AbstractFilesystemStorage
{ {
$list = []; $list = [];
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
if (null === $row || (!\is_object($row) && !\is_array($row))) { if (null === $row || \is_scalar($row)) {
// Only load rows which haven't been loaded before. // Only load rows which haven't been loaded before.
$key = (string)$key; $key = (string)$key;
if (!$this->hasKey($key)) { if (!$this->hasKey($key)) {
@@ -109,6 +120,7 @@ class FolderStorage extends AbstractFilesystemStorage
$path = $this->getPathFromKey($key); $path = $this->getPathFromKey($key);
$file = $this->getFile($path); $file = $this->getFile($path);
$list[$key] = $this->loadFile($file); $list[$key] = $this->loadFile($file);
$list[$key]['__META'] = $this->getObjectMeta($key);
} }
if (null !== $fetched) { if (null !== $fetched) {
$fetched[$key] = $list[$key]; $fetched[$key] = $list[$key];
@@ -137,6 +149,7 @@ class FolderStorage extends AbstractFilesystemStorage
$path = $this->getPathFromKey($key); $path = $this->getPathFromKey($key);
$file = $this->getFile($path); $file = $this->getFile($path);
$list[$key] = $this->saveFile($file, $row); $list[$key] = $this->saveFile($file, $row);
$list[$key]['__META'] = $this->getObjectMeta($key, true);
} }
} }
@@ -150,6 +163,7 @@ class FolderStorage extends AbstractFilesystemStorage
public function deleteRows(array $rows): array public function deleteRows(array $rows): array
{ {
$list = []; $list = [];
$baseMediaPath = $this->getMediaPath();
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
$key = (string)$key; $key = (string)$key;
if (!$this->hasKey($key)) { if (!$this->hasKey($key)) {
@@ -159,11 +173,15 @@ class FolderStorage extends AbstractFilesystemStorage
$file = $this->getFile($path); $file = $this->getFile($path);
$list[$key] = $this->deleteFile($file); $list[$key] = $this->deleteFile($file);
$storage = $this->getStoragePath($key); $storagePath = $this->getStoragePath($key);
$media = $this->getMediaPath($key); $mediaPath = $this->getMediaPath($key);
$this->deleteFolder($storage, true); if ($storagePath) {
$media && $this->deleteFolder($media, true); $this->deleteFolder($storagePath, true);
}
if ($mediaPath && $mediaPath !== $storagePath && $mediaPath !== $baseMediaPath) {
$this->deleteFolder($mediaPath, true);
}
} }
} }
@@ -179,17 +197,37 @@ class FolderStorage extends AbstractFilesystemStorage
$list = []; $list = [];
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
$key = (string)$key; $key = (string)$key;
if (strpos($key, '@@')) { if (strpos($key, '@@') !== false) {
$key = $this->getNewKey(); $key = $this->getNewKey();
} }
$path = $this->getPathFromKey($key); $path = $this->getPathFromKey($key);
$file = $this->getFile($path); $file = $this->getFile($path);
$list[$key] = $this->saveFile($file, $row); $list[$key] = $this->saveFile($file, $row);
$list[$key]['__META'] = $this->getObjectMeta($key, true);
} }
return $list; return $list;
} }
/**
* @param string $src
* @param string $dst
* @return bool
*/
public function copyRow(string $src, string $dst): bool
{
if ($this->hasKey($dst)) {
throw new \RuntimeException("Cannot copy object: key '{$dst}' is already taken");
}
if (!$this->hasKey($src)) {
return false;
}
return $this->copyFolder($this->getStoragePath($src), $this->getStoragePath($dst));
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* @see FlexStorageInterface::renameRow() * @see FlexStorageInterface::renameRow()
@@ -204,7 +242,7 @@ class FolderStorage extends AbstractFilesystemStorage
return false; return false;
} }
return $this->moveFolder($this->getMediaPath($src), $this->getMediaPath($dst)); return $this->moveFolder($this->getStoragePath($src), $this->getStoragePath($dst));
} }
/** /**
@@ -213,10 +251,18 @@ class FolderStorage extends AbstractFilesystemStorage
*/ */
public function getStoragePath(string $key = null): string public function getStoragePath(string $key = null): string
{ {
if (null === $key) { if (null === $key || $key === '') {
$path = $this->dataFolder; $path = $this->dataFolder;
} else { } else {
$path = sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2)); $options = [
$this->dataFolder,
$key,
\mb_substr($key, 0, 2),
'***',
'***'
];
$path = rtrim(explode('***', sprintf($this->dataPattern, ...$options))[0], '/');
} }
return $path; return $path;
@@ -228,7 +274,7 @@ class FolderStorage extends AbstractFilesystemStorage
*/ */
public function getMediaPath(string $key = null): string public function getMediaPath(string $key = null): string
{ {
return null !== $key ? \dirname($this->getStoragePath($key)) : $this->getStoragePath(); return $this->getStoragePath($key);
} }
/** /**
@@ -239,7 +285,26 @@ class FolderStorage extends AbstractFilesystemStorage
*/ */
public function getPathFromKey(string $key): string public function getPathFromKey(string $key): string
{ {
return sprintf($this->dataPattern, $this->dataFolder, $key, substr($key, 0, 2)); $options = [
$this->dataFolder,
$key,
\mb_substr($key, 0, 2),
$this->dataFile,
$this->dataExt
];
return sprintf($this->dataPattern, ...$options);
}
/**
* Get key from the filesystem path.
*
* @param string $path
* @return string
*/
protected function getKeyFromPath(string $path): string
{
return basename($path);
} }
/** /**
@@ -272,6 +337,7 @@ class FolderStorage extends AbstractFilesystemStorage
protected function saveFile(File $file, array $data): array protected function saveFile(File $file, array $data): array
{ {
try { try {
unset($data['__META'], $data['__error']);
$file->save($data); $file->save($data);
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
@@ -294,7 +360,9 @@ class FolderStorage extends AbstractFilesystemStorage
{ {
try { try {
$data = $file->content(); $data = $file->content();
$file->delete(); if ($file->exists()) {
$file->delete();
}
/** @var UniformResourceLocator $locator */ /** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator']; $locator = Grav::instance()['locator'];
@@ -308,6 +376,28 @@ class FolderStorage extends AbstractFilesystemStorage
return $data; return $data;
} }
/**
* @param string $src
* @param string $dst
* @return bool
*/
protected function copyFolder(string $src, string $dst): bool
{
try {
Folder::copy($this->resolvePath($src), $this->resolvePath($dst));
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($src) || $locator->isStream($dst)) {
$locator->clearCache();
}
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Flex copyFolder(%s, %s): %s', $src, $dst, $e->getMessage()));
}
return true;
}
/** /**
* @param string $src * @param string $src
* @param string $dst * @param string $dst
@@ -352,17 +442,6 @@ class FolderStorage extends AbstractFilesystemStorage
} }
} }
/**
* Get key from the filesystem path.
*
* @param string $path
* @return string
*/
protected function getKeyFromPath(string $path): string
{
return basename($path);
}
/** /**
* Returns list of all stored keys in [key => timestamp] pairs. * Returns list of all stored keys in [key => timestamp] pairs.
* *
@@ -386,6 +465,31 @@ class FolderStorage extends AbstractFilesystemStorage
return $list; return $list;
} }
/**
* @param string $key
* @param bool $reload
* @return array
*/
protected function getObjectMeta(string $key, bool $reload = false): array
{
if (!$reload && isset($this->meta[$key])) {
return $this->meta[$key];
}
$filename = $this->getPathFromKey($key);
$modified = is_file($filename) ? filemtime($filename) : 0;
$meta = [
'storage_key' => $key,
'storage_timestamp' => $modified
];
$this->meta[$key] = $meta;
return $meta;
}
protected function buildIndexFromFilesystem($path) protected function buildIndexFromFilesystem($path)
{ {
$flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS; $flags = \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS;
@@ -394,21 +498,15 @@ class FolderStorage extends AbstractFilesystemStorage
$list = []; $list = [];
/** @var \SplFileInfo $info */ /** @var \SplFileInfo $info */
foreach ($iterator as $filename => $info) { foreach ($iterator as $filename => $info) {
if (!$info->isDir()) { if (!$info->isDir() || strpos($info->getFilename(), '.') === 0) {
continue; continue;
} }
$key = $this->getKeyFromPath($filename); $key = $this->getKeyFromPath($filename);
$filename = $this->getPathFromKey($key); $meta = $this->getObjectMeta($key);
$modified = is_file($filename) ? filemtime($filename) : null; if ($meta['storage_timestamp']) {
if (null === $modified) { $list[$key] = $meta;
continue;
} }
$list[$key] = [
'storage_key' => $key,
'storage_timestamp' => $modified
];
} }
return $list; return $list;
@@ -460,11 +558,26 @@ class FolderStorage extends AbstractFilesystemStorage
$pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern; $pattern = !empty($options['pattern']) ? $options['pattern'] : $this->dataPattern;
$this->dataFolder = $options['folder']; $this->dataFolder = $options['folder'];
$this->dataFile = $options['file'] ?? 'item';
$this->dataExt = $extension;
if (\mb_strpos($pattern, '{FILE}') === false && \mb_strpos($pattern, '{EXT}') === false) {
if (isset($options['file'])) {
$pattern .= '/{FILE}{EXT}';
} else {
$this->dataFile = \basename($pattern, $extension);
$pattern = \dirname($pattern) . '/{FILE}{EXT}';
}
}
$this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/')); $this->prefixed = (bool)($options['prefixed'] ?? strpos($pattern, '/{KEY:2}/'));
$this->indexed = (bool)($options['indexed'] ?? false); $this->indexed = (bool)($options['indexed'] ?? false);
$this->keyField = $options['key'] ?? 'storage_key'; $this->keyField = $options['key'] ?? 'storage_key';
$pattern = preg_replace(['/{FOLDER}/', '/{KEY}/', '/{KEY:2}/'], ['%1$s', '%2$s', '%3$s'], $pattern); $pattern = preg_replace(
$this->dataPattern = \dirname($pattern) . '/' . basename($pattern, $extension) . $extension; ['/{FOLDER}/', '/{KEY}/', '/{KEY:2}/', '/{FILE}/', '/{EXT}/'],
['%1$s', '%2$s', '%3$s', '%4$s', '%5$s'],
$pattern
);
$this->dataPattern = $pattern;
} }
} }

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