Compare commits

...

320 Commits

Author SHA1 Message Date
Andy Miller
7dafa2a207 prepare for beta.10 release 2019-10-03 14:31:28 -06:00
Matias Griese
5253aa6aef Fixed user avatar file is not being randomized 2019-10-03 22:40:03 +03:00
Matias Griese
1b34530a4a Merge remote-tracking branch 'origin/1.7' into 1.7 2019-10-03 22:11:24 +03:00
Matias Griese
ae41b6f5ff Fixed Flex User Avatar not fully backwards compatible with old user 2019-10-03 22:11:13 +03:00
Andy Miller
4d09a345a4 Merge branch 'develop' into 1.7
# Conflicts:
#	CHANGELOG.md
2019-10-03 13:06:45 -06:00
Matias Griese
8ac3451fbc Revert chReverted setting language for every page during initialization 2019-10-03 21:41:58 +03:00
Matias Griese
15f1f2a03d Revert chReverted setting language for every page during initialization 2019-10-03 19:17:22 +03:00
Matias Griese
ffa9ef6a7e Grav 1.7: Fixed prev/next page missing pages if pagination was turned on in page header 2019-10-03 15:16:11 +03:00
Matias Griese
a75c0cbe62 Fixed wrong Grav param separator when using Route class 2019-10-03 10:56:05 +03:00
Andy Miller
c795ead402 Updated changelog 2019-10-01 17:52:14 -06:00
Andy Miller
91270c9c66 CSVFormatter null char support 2019-10-01 17:51:29 -06:00
Andy Miller
9259c2f660 Updated changelog 2019-10-01 17:51:17 -06:00
Andy Miller
138d1e93e3 CSVFormatter null char support 2019-10-01 17:51:02 -06:00
Matias Griese
5db9f16174 Flex: Removed extra exists check when creating object (messes up "non-existing" pages) 2019-09-27 13:26:12 +03:00
Andy Miller
342eac1047 Smarter CSV handling 2019-09-26 18:35:25 -06:00
Andy Miller
88c859fde2 move try outside for loop (less memory) 2019-09-26 15:10:35 -06:00
Andy Miller
8b5bce0b6d prepare for b.9 release 2019-09-26 13:37:09 -06:00
Andy Miller
09c4cf66f5 Merge branch '1.7' of github.com:getgrav/grav into 1.7 2019-09-26 13:26:38 -06:00
Andy Miller
46b859b8df better error for badly formatted line in CsvFormatter::decode() 2019-09-26 13:26:33 -06:00
Matias Griese
a7c23c58e5 Merge remote-tracking branch 'origin/1.7' into 1.7 2019-09-26 14:54:01 +03:00
Matias Griese
d206711354 Flex storage changes 2019-09-26 14:53:52 +03:00
Matias Griese
0007100a97 Removed row copy/move logic from Flex object, it belongs to storage 2019-09-26 14:49:45 +03:00
Matias Griese
3d999501a0 Fixed Grav parameter handling in RouteFactory::createFromString() 2019-09-26 14:47:45 +03:00
Andy Miller
52f83775e3 clarification 2019-09-24 18:10:39 -06:00
Andy Miller
a35cf5a830 Added a new {% cache %} tag eliminating need for twigcache extension 2019-09-24 18:10:02 -06:00
Matias Griese
39837d0826 Improved Flex FolderStorage class to better hide storage specific logic 2019-09-20 18:33:07 +03:00
Matias Griese
66bd6a4046 Fixed error when activating Flex Accounts in GRAV system configuration (PHP 7.1) 2019-09-20 12:06:17 +03:00
Matias Griese
4e9d3395e0 Improve blueprint initialization in Flex Objects (fixes content aware fields) 2019-09-20 11:59:13 +03:00
Andy Miller
248c7764f0 Merge branch 'develop' into 1.7
# Conflicts:
#	system/defines.php
2019-09-19 16:44:01 -06:00
Andy Miller
f72eb1b002 Merge tag '1.6.16' into develop
Release v1.6.16

# gpg: Signature made Thu Sep 19 16:42:43 2019 MDT
# gpg:                using RSA key 6AA7DB4F23BD1A7280C4CC63E82B8D0EAB94EFB9
# gpg: Good signature from "Andy Miller <rhuk@mac.com>" [ultimate]
2019-09-19 16:42:44 -06:00
Andy Miller
25caa5138a Merge branch 'release/1.6.16' 2019-09-19 16:42:43 -06:00
Andy Miller
dffb227df6 prepare for release 2019-09-19 16:42:21 -06:00
Matias Griese
7c161d5cbc Comment out couple of language debug messages 2019-09-19 22:47:53 +03:00
Matias Griese
7b2313ef0b Fixed initial Flex Object state when creating a new objects in a form 2019-09-19 21:58:28 +03:00
Matias Griese
1ad2a3f212 Make flex to skip missing flex types 2019-09-19 21:06:32 +03:00
Matias Griese
1f2363b623 Minor improvement for the new Route::withLanguate() method 2019-09-18 12:42:05 +03:00
Andy Miller
d1c069c2ee composer updates 2019-09-17 13:21:27 -06:00
Matias Griese
67e772a5ce Merge remote-tracking branch 'origin/1.7' into 1.7 2019-09-17 22:08:10 +03:00
Matias Griese
59bb167bd4 Merge branch 'develop' of github.com:getgrav/grav into 1.7
 Conflicts:
	CHANGELOG.md
2019-09-17 22:07:32 +03:00
Matias Griese
5c9eb1cdb8 Fixed Badly encoded JSON data warning when uploading files [#2663] 2019-09-17 22:02:21 +03:00
Andy Miller
94a54d2a82 Using phpdebugbar format to log messages (clockwork compatible) 2019-09-17 11:21:05 -06:00
Matias Griese
f201d48112 Fixed Flex Object saving causing the key to be lost for some types 2019-09-17 19:42:28 +03:00
Matias Griese
df51b64b35 Fixed bug in object saving if using streams 2019-09-17 19:03:48 +03:00
Matias Griese
93ff915737 Fixed avatars not being displayed with flex users [#2431] 2019-09-17 15:19:42 +03:00
Matias Griese
e30ab9a043 Fixed Flex user creation if file storage is being used [#2444] 2019-09-17 11:37:09 +03:00
Matias Griese
8e59a08f9e Pages::buildFlexPages(): better compatibility to old pages 2019-09-17 11:18:30 +03:00
Andy Miller
5a874006b4 Merge branch 'develop' into 1.7
# Conflicts:
#	system/src/Grav/Common/Page/Page.php
2019-09-16 16:01:22 -06:00
Matias Griese
5d639cc633 Improved language support for Route class 2019-09-16 15:50:40 +03:00
Matias Griese
ab7038b49e Flex media: set timestamps only when files were uploaded 2019-09-16 12:43:53 +03:00
Newb I the Newbd
651b354d3e Correct non-published collection filter evaluation (#2668) 2019-09-14 10:10:24 -06:00
Matias Griese
90c2079529 Fixed wrong Pages::dispatch() calls (with redirect) when we really meant to call Pages::find() 2019-09-13 22:39:46 +03:00
Matias Griese
fa064301a2 Fixed 404 error when you click to non-routable menu item with children: redirect to the first child instead 2019-09-13 22:27:38 +03:00
Matias Griese
5424047d02 Fixed Page::modular() and Page::modularTwig() returning null for folders and other non-initialized pages 2019-09-13 21:50:06 +03:00
Matias Griese
ce2b80aeb9 Added bin/grav page-system-validator [-r|--record] [-c|--check] to test Flex Pages 2019-09-13 21:45:54 +03:00
Matias Griese
e91ae5542d Made Common\Page\Header class serializable 2019-09-13 11:17:54 +03:00
Andy Miller
922f263005 refactored the code 2019-09-12 11:15:21 -06:00
Andy Miller
01af81a0a6 fixed array types 2019-09-12 10:37:41 -06:00
Andy Miller
bd8c274f38 Support Clockwork and PHPDebugBar for dump() 2019-09-12 10:36:08 -06:00
Andy Miller
05fb69daa7 Fixed broken Twig dump() 2019-09-11 21:34:17 -06:00
Matias Griese
21f95eba53 Added Route::withLanguage() method 2019-09-11 22:36:49 +03:00
Matias Griese
4ef56054ee Minor fix on ControllerResponseTrait::createJsonResponse() 2019-09-10 11:15:47 +03:00
Matias Griese
bec9292a5c Flex folder storage improvements 2019-09-09 14:28:27 +03:00
Matias Griese
0fa6328816 Fix undefined variable when there are no translations 2019-09-09 14:08:36 +03:00
Matias Griese
b30240c340 Improve language fallback logic 2019-09-06 22:14:37 +03:00
Matias Griese
59eb3b4cb2 Flex Pages: Quick and dirty multi-language support in frontend 2019-09-06 21:43:43 +03:00
Matias Griese
5fa047c1b7 Fixed sorting in Flex Pages 2019-09-06 21:00:24 +03:00
Matias Griese
30481da51d Regression: Fixed translations when language code is used for non-language purposes 2019-09-06 15:21:09 +03:00
Matias Griese
beb8f09a9d Flex FolderStorage: Added support for partial delete 2019-09-05 17:20:08 +03:00
Matias Griese
603bb6c878 Make it possible to save multiple variations of the Flex Object 2019-09-05 16:54:59 +03:00
Matias Griese
dac3e57fd4 Fixed error in Session::getFlashObject() if Flex Form is being used 2019-09-05 15:45:29 +03:00
Andy Miller
b601d2c8fd check for file existence 2019-09-03 12:18:32 -06:00
Andy Miller
ab75201f11 Added a new sanitize_svg security config option [default: true] 2019-09-03 12:07:47 -06:00
Andy Miller
45b1b0a2ef Added Security::sanitizeSVG function + update CHANGELOG 2019-09-03 11:53:25 -06:00
Matias Griese
ca2f657c98 Oops, remove print_r() 2019-09-03 14:20:39 +03:00
Matias Griese
5c33882f5b Added extra master key parameter for FlexObjectInterface::getStorageKey() 2019-09-03 13:46:50 +03:00
Matias Griese
0f85214693 Minor fix/improvement on loading Flex objects 2019-09-03 13:41:53 +03:00
Matias Griese
4570c00041 Added FlexTranslateInterface 2019-09-03 13:40:53 +03:00
Matias Griese
4a0c8846e2 Regression: Fixed language fallback 2019-09-03 13:36:19 +03:00
Andy Miller
41e51cd86d Support StaticImageMedium formats for avatars 2019-08-31 23:26:08 -06:00
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
20b9ca56fa Merge tag '1.6.14' into develop
Release v1.6.14
2019-08-18 09:52:55 -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
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
136 changed files with 7207 additions and 3498 deletions

View File

@@ -1,3 +1,166 @@
# v1.7.0-beta.10
## 10/03/2019
1. [](#new)
1. [](#improved)
* Flex: Removed extra exists check when creating object (messes up "non-existing" pages)
* Support customizable null character replacement in `CSVFormatter::decode()`
1. [](#bugfix)
* Fixed wrong Grav param separator when using `Route` class
* Fixed Flex User Avatar not fully backwards compatible with old user
* Grav 1.7: Fixed prev/next page missing pages if pagination was turned on in page header
* Grav 1.7: Reverted setting language for every page during initialization
* Grav 1.7: Fixed numeric language inconsistencies
# v1.7.0-beta.9
## 09/26/2019
1. [](#new)
* Added a new `{% cache %}` Twig tag eliminating need for `twigcache` extension.
1. [](#improved)
* Improved blueprint initialization in Flex Objects (fixes content aware fields)
* Improved Flex FolderStorage class to better hide storage specific logic
* Exception will output a badly formatted line in `CsvFormatter::decode()`
1. [](#bugfix)
* Fixed error when activating Flex Accounts in GRAV system configuration (PHP 7.1)
* Fixed Grav parameter handling in `RouteFactory::createFromString()`
# v1.7.0-beta.8
## 09/19/2019
1. [](#new)
* Added new `Security::sanitizeSVG()` function
* Backwards compatibility break: `FlexStorageInterface::getStoragePath()` and `getMediaPath()` can now return null
1. [](#improved)
* Several FlexObject loading improvements
* Added `bin/grav page-system-validator [-r|--record] [-c|--check]` to test Flex Pages
* Improved language support for `Route` class
1. [](#bugfix)
* Regression: Fixed language fallback
* Regression: Fixed translations when language code is used for non-language purposes
* Regression: Allow SVG avatar images for users
* Fixed error in `Session::getFlashObject()` if Flex Form is being used
* Fixed broken Twig `dump()`
* Fixed `Page::modular()` and `Page::modularTwig()` returning `null` for folders and other non-initialized pages
* Fixed 404 error when you click to non-routable menu item with children: redirect to the first child instead
* Fixed wrong `Pages::dispatch()` calls (with redirect) when we really meant to call `Pages::find()`
* Fixed avatars not being displayed with flex users [#2431](https://github.com/getgrav/grav/issues/2431)
* Fixed initial Flex Object state when creating a new objects in a form
# 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
* 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.17
## mm/dd/2019
1. [](#improved)
* Safer file handling + customizable null char replacment in `CsvFormatter::decode()`
# v1.6.16
## 09/19/2019
1. [](#bugfix)
* Fixed Flex user creation if file storage is being used [#2444](https://github.com/getgrav/grav/issues/2444)
* Fixed `Badly encoded JSON data` warning when uploading files [#2663](https://github.com/getgrav/grav/issues/2663)
# v1.6.15
## 08/20/2019
1. [](#improved)
* Improved robots.txt [#2632](https://github.com/getgrav/grav/issues/2632)
1. [](#bugfix)
* Fixed broken markdown Twig tag [#2635](https://github.com/getgrav/grav/issues/2635)
* Force Symfony 4.2 in Grav 1.6 to remove a bunch of deprecated messages
# v1.6.14
## 08/18/2019

View File

@@ -3,7 +3,7 @@
use Grav\Common\Composer;
use Grav\Common\Grav;
use League\CLImate\CLImate;
use Grav\Console\Cli;
use Symfony\Component\Console\Application;
\define('GRAV_CLI', true);
@@ -52,16 +52,18 @@ if (!file_exists(GRAV_ROOT . '/index.php')) {
$app = new Application('Grav CLI Application', GRAV_VERSION);
$app->addCommands(array(
new \Grav\Console\Cli\InstallCommand(),
new \Grav\Console\Cli\ComposerCommand(),
new \Grav\Console\Cli\SandboxCommand(),
new \Grav\Console\Cli\CleanCommand(),
new \Grav\Console\Cli\ClearCacheCommand(),
new \Grav\Console\Cli\BackupCommand(),
new \Grav\Console\Cli\NewProjectCommand(),
new \Grav\Console\Cli\SchedulerCommand(),
new \Grav\Console\Cli\SecurityCommand(),
new \Grav\Console\Cli\LogViewerCommand(),
new \Grav\Console\Cli\YamlLinterCommand(),
new Cli\InstallCommand(),
new Cli\ComposerCommand(),
new Cli\SandboxCommand(),
new Cli\CleanCommand(),
new Cli\ClearCacheCommand(),
new Cli\BackupCommand(),
new Cli\NewProjectCommand(),
new Cli\SchedulerCommand(),
new Cli\SecurityCommand(),
new Cli\LogViewerCommand(),
new Cli\YamlLinterCommand(),
new Cli\ServerCommand(),
new Cli\PageSystemValidatorCommand(),
));
$app->run();

View File

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

529
composer.lock generated

File diff suppressed because it is too large Load Diff

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));
}
if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
if (PHP_SAPI === 'cli-server') {
$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

View File

@@ -10,3 +10,4 @@ Disallow: /user/
Allow: /user/pages/
Allow: /user/themes/
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

View File

@@ -105,3 +105,15 @@ form:
validate:
type: commalist
sanitize_svg:
type: toggle
label: PLUGIN_ADMIN.SANITIZE_SVG
help: PLUGIN_ADMIN.SANITIZE_SVG_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -36,3 +36,4 @@ uploads_dangerous_extensions:
- htm
- js
- exe
sanitize_svg: true

View File

@@ -27,6 +27,7 @@ home:
hide_in_urls: false # Hide the home route in URLs
pages:
type: page # EXPERIMENTAL: Page type: page or flex
theme: quark # Default theme (defaults to "quark" theme)
order:
by: default # Order pages by "default", "alpha" or "date"
@@ -125,6 +126,8 @@ log:
debugger:
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:
close_connection: true # Close the connection before calling onShutdown(). false for debugging
@@ -154,15 +157,15 @@ session:
path:
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)
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.
official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
accounts:
type: data # Account type: data or flex
storage: file # Flex storage type: file or folder
type: data # EXPERIMENTAL: Account type: data or flex
storage: file # EXPERIMENTAL: Flex storage type: file or folder
strict_mode:
yaml_compat: true # Grav 1.5+: Enables YAML backwards compatibility

View File

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

View File

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

View File

@@ -334,7 +334,7 @@ class Cache extends Getters
* Stores a new 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
*/
public function save($id, $data, $lifetime = null)

View File

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

View File

@@ -244,8 +244,8 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
|| !empty($field['disabled'])
// Field validation is set to be ignored
|| !empty($field['validate']['ignore'])
// Field is toggleable and the toggle is turned off
|| (!empty($field['toggleable']) && empty($toggles[$key]))
// Field is overridable and the toggle is turned off
|| (!empty($field['overridable']) && empty($toggles[$key]))
) {
continue;
}
@@ -279,6 +279,12 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
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.
if (isset($field['validate']['required'])
&& $field['validate']['required'] === true) {

View File

@@ -9,6 +9,14 @@
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\DataCollectorInterface;
use DebugBar\DataCollector\ExceptionsCollector;
@@ -22,11 +30,23 @@ use DebugBar\JavascriptRenderer;
use DebugBar\StandardDebugBar;
use Grav\Common\Config\Config;
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\TemplateWrapper;
class Debugger
{
/** @var static */
protected static $instance;
/** @var Grav $grav */
protected $grav;
@@ -39,8 +59,11 @@ class Debugger
/** @var StandardDebugBar $debugbar */
protected $debugbar;
/** @var Clockwork */
protected $clockwork;
/** @var bool */
protected $enabled;
protected $enabled = false;
protected $initialized = false;
@@ -53,36 +76,38 @@ class Debugger
/** @var callable */
protected $errorHandler;
protected $requestTime;
protected $currentTime;
/** @var int */
protected $profiling = 0;
protected $censored = false;
/**
* Debugger constructor.
*/
public function __construct()
{
$currentTime = microtime(true);
static::$instance = $this;
$this->currentTime = microtime(true);
if (!\defined('GRAV_REQUEST_TIME')) {
\define('GRAV_REQUEST_TIME', $currentTime);
\define('GRAV_REQUEST_TIME', $this->currentTime);
}
// Enable debugger until $this->init() gets called.
$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;
$this->requestTime = $_SERVER['REQUEST_TIME_FLOAT'] ?? GRAV_REQUEST_TIME;
// Set deprecation collector.
$this->setErrorHandler();
}
public function getClockwork(): ?Clockwork
{
return $this->enabled ? $this->clockwork : null;
}
/**
* Initialize the debugger
*
@@ -100,25 +125,234 @@ class Debugger
// Enable/disable debugger based on configuration.
$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;
$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);
$debugbar = $this->debugbar;
$debugbar->addCollector(new MemoryCollector());
$debugbar->addCollector(new ExceptionsCollector());
$debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
$debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
$this->addMessage('Grav v' . GRAV_VERSION);
if ($clockwork) {
$log = $this->grav['log'];
$clockwork->setStorage(new FileStorage('cache://clockwork'));
if (extension_loaded('xdebug')) {
$clockwork->addDataSource(new XdebugDataSource());
}
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;
}
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
*
@@ -142,7 +376,8 @@ class Debugger
*/
public function addAssets()
{
if ($this->enabled()) {
if ($this->enabled) {
// Only add assets if Page is HTML
$page = $this->grav['page'];
@@ -153,23 +388,35 @@ class Debugger
/** @var Assets $assets */
$assets = $this->grav['assets'];
// Add jquery library
$assets->add('jquery', 101);
$this->renderer = $this->debugbar->getJavascriptRenderer();
$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);
// Clockwork specific assets
if ($this->clockwork) {
$assets->addCss('/system/assets/debugger/clockwork.css', ['loading' => 'inline']);
$assets->addJs('/system/assets/debugger/clockwork.js', ['loading' => 'inline']);
}
$assets->addCss('/system/assets/debugger.css');
foreach ((array)$js_files as $js) {
$assets->addJs($js);
// Debugbar specific assets
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;
@@ -192,7 +439,9 @@ class Debugger
*/
public function addCollector($collector)
{
$this->debugbar->addCollector($collector);
if ($this->debugbar && !$this->debugbar->hasCollector($collector->getName())) {
$this->debugbar->addCollector($collector);
}
return $this;
}
@@ -200,14 +449,18 @@ class Debugger
/**
* Returns a data collector
*
* @param DataCollectorInterface $collector
* @param string $name
*
* @return DataCollectorInterface
* @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()
{
if ($this->enabled()) {
if ($this->enabled && $this->debugbar) {
// Only add assets if Page is HTML
$page = $this->grav['page'];
if (!$this->renderer || $page->templateFormat() !== 'html') {
return $this;
}
$this->addMeasures();
$this->addDeprecations();
echo $this->renderer->render();
@@ -239,7 +493,8 @@ class Debugger
*/
public function sendDataInHeaders()
{
if ($this->enabled()) {
if ($this->enabled && $this->debugbar) {
$this->addMeasures();
$this->addDeprecations();
$this->debugbar->sendDataInHeaders();
}
@@ -254,16 +509,146 @@ class Debugger
*/
public function getData()
{
if (!$this->enabled()) {
if (!$this->enabled || !$this->debugbar) {
return null;
}
$this->addMeasures();
$this->addDeprecations();
$this->timers = [];
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
*
@@ -274,10 +659,7 @@ class Debugger
*/
public function startTimer($name, $description = null)
{
if (strpos($name, '_') === 0 || $this->enabled()) {
$this->debugbar['time']->startMeasure($name, $description);
$this->timers[] = $name;
}
$this->timers[$name] = [$description, microtime(true)];
return $this;
}
@@ -291,8 +673,9 @@ class Debugger
*/
public function stopTimer($name)
{
if (\in_array($name, $this->timers, true) && (strpos($name, '_') === 0 || $this->enabled())) {
$this->debugbar['time']->stopMeasure($name);
if (isset($this->timers[$name])) {
$endTime = microtime(true);
$this->timers[$name][] = $endTime;
}
return $this;
@@ -303,14 +686,60 @@ class Debugger
*
* @param mixed $message
* @param string $label
* @param bool $isString
* @param mixed|bool $isString
*
* @return $this
*/
public function addMessage($message, $label = 'info', $isString = true)
{
if ($this->enabled()) {
$this->debugbar['messages']->addMessage($message, $label, $isString);
if ($this->enabled) {
if ($this->censored) {
if (!is_scalar($message)) {
$message = 'CENSORED';
}
if (!is_scalar($isString)) {
$isString = ['CENSORED'];
}
}
if ($this->debugbar) {
if (is_array($isString)) {
$message = $isString;
$isString = false;
} elseif (is_string($isString)) {
$message = $isString;
$isString = true;
}
$this->debugbar['messages']->addMessage($message, $label, $isString);
}
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;
@@ -319,13 +748,23 @@ class Debugger
/**
* Dump exception into the Messages tab of the Debug Bar
*
* @param \Exception $e
* @param \Throwable $e
* @return Debugger
*/
public function addException(\Exception $e)
public function addException(\Throwable $e)
{
if ($this->initialized && $this->enabled()) {
$this->debugbar['exceptions']->addException($e);
if ($this->initialized && $this->enabled) {
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;
@@ -355,7 +794,7 @@ class Debugger
return true;
}
if (!$this->enabled()) {
if (!$this->enabled) {
return true;
}
@@ -540,6 +979,21 @@ class Debugger
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()
{
if (!$this->deprecations) {
@@ -603,4 +1057,13 @@ class Debugger
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;
/**
* @property string $name
*/
class Package
{
/**

View File

@@ -16,12 +16,8 @@ use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Processors\AssetsProcessor;
use Grav\Common\Processors\BackupsProcessor;
use Grav\Common\Processors\ConfigurationProcessor;
use Grav\Common\Processors\DebuggerAssetsProcessor;
use Grav\Common\Processors\DebuggerProcessor;
use Grav\Common\Processors\ErrorsProcessor;
use Grav\Common\Processors\InitializeProcessor;
use Grav\Common\Processors\LoggerProcessor;
use Grav\Common\Processors\PagesProcessor;
use Grav\Common\Processors\PluginsProcessor;
use Grav\Common\Processors\RenderProcessor;
@@ -90,10 +86,6 @@ class Grav extends Container
* @var array All middleware processors that are processed in $this->process()
*/
protected $middleware = [
'configurationProcessor',
'loggerProcessor',
'errorsProcessor',
'debuggerProcessor',
'initializeProcessor',
'pluginsProcessor',
'themesProcessor',
@@ -157,15 +149,13 @@ class Grav extends Container
$this->initialized['setup'] = true;
$this->measureTime('_setup', 'Site Setup', function () use ($environment) {
// Force environment if passed to the method.
if ($environment) {
Setup::$environment = $environment;
}
// Force environment if passed to the method.
if ($environment) {
Setup::$environment = $environment;
}
$this['setup'];
$this['streams'];
});
$this['setup'];
$this['streams'];
return $this;
}
@@ -186,18 +176,6 @@ class Grav extends 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 () {
return new InitializeProcessor($this);
},
@@ -237,13 +215,10 @@ class Grav extends Container
]
);
$default = function (ServerRequestInterface $request) {
$default = static function () {
return new Response(404);
};
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$collection = new RequestHandler($this->middleware, $default, $container);
$response = $collection->handle($this['request']);
@@ -251,32 +226,65 @@ class Grav extends Container
$this->header($response);
echo $response->getBody();
$debugger->render();
$this['debugger']->render();
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'));
// 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']);
}
}
/**
* 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 int $code Redirection code (30x)
*/
public function redirect($route, $code = null)
public function redirect($route, $code = null): void
{
/** @var Uri $uri */
$uri = $this['uri'];
@@ -293,11 +301,7 @@ class Grav extends Container
$code = $this['config']->get('system.pages.redirect_default_code', 302);
}
if (isset($this['session'])) {
$this['session']->close();
}
if ($uri->isExternal($route)) {
if ($uri::isExternal($route)) {
$url = $route;
} else {
$url = rtrim($uri->rootUrl(), '/') . '/';
@@ -309,8 +313,9 @@ class Grav extends Container
}
}
header("Location: {$url}", true, $code);
exit();
$response = new Response($code, ['Location' => $url]);
$this->close($response);
}
/**
@@ -343,12 +348,30 @@ class Grav extends Container
header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}");
foreach ($response->getHeaders() as $key => $values) {
// Skip internal Grav headers.
if (strpos($key, 'Grav-Internal-') === 0) {
continue;
}
foreach ($values as $i => $value) {
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.
*
@@ -362,6 +385,10 @@ class Grav extends Container
/** @var EventDispatcher $events */
$events = $this['events'];
/** @var Debugger $debugger */
$debugger = $this['debugger'];
$debugger->addEvent($eventName, $event, $events);
return $events->dispatch($eventName, $event);
}
@@ -470,9 +497,7 @@ class Grav extends Container
return $container;
};
$container->measureTime('_services', 'Services', function () use ($container) {
$container->registerServices();
});
$container->registerServices();
return $container;
}
@@ -528,7 +553,7 @@ class Grav extends Container
$path_parts = pathinfo($path);
/** @var PageInterface $page */
$page = $this['pages']->dispatch($path_parts['dirname'], true);
$page = $this['pages']->find($path_parts['dirname'], true);
if ($page) {
$media = $page->media()->all();

View File

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

View File

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

View File

@@ -9,6 +9,8 @@
namespace Grav\Common;
use Grav\Common\Language\Language;
/**
* This file was originally part of the Akelos Framework
*/
@@ -24,6 +26,7 @@ class Inflector
public static function init()
{
if (empty(static::$plural)) {
/** @var Language $language */
$language = Grav::instance()['language'];
static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true) ?: [];
static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true) ?: [];

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Language;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Config\Config;
use Negotiation\AcceptLanguage;
@@ -16,20 +17,22 @@ use Negotiation\LanguageNegotiator;
class Language
{
/** @var 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 $enabled = true;
/** @var array */
protected $languages = [];
protected $fallback_languages = [];
protected $fallback_extensions = [];
protected $page_extesions = [];
protected $default;
protected $active;
protected $http_accept_language;
protected $lang_in_url = false;
@@ -42,7 +45,12 @@ class Language
{
$this->grav = $grav;
$this->config = $grav['config'];
$this->languages = $this->config->get('system.languages.supported', []);
$languages = $this->config->get('system.languages.supported', []);
foreach ($languages as &$language) {
$language = (string)$language;
}
$this->languages = $languages;
$this->init();
}
@@ -58,7 +66,7 @@ class Language
$this->default = reset($this->languages);
}
$this->page_extensions = null;
$this->resetFallbackPageExtensions();
if (empty($this->languages)) {
$this->enabled = false;
@@ -93,20 +101,22 @@ class Language
public function setLanguages($langs)
{
$this->languages = $langs;
$this->init();
}
/**
* Gets a pipe-separated string of available languages
*
* @param string|null $delimiter Delimiter to be quoted.
* @return string
*/
public function getAvailable()
public function getAvailable($delimiter = null)
{
$languagesArray = $this->languages; //Make local copy
$languagesArray = array_map(function($value) {
return preg_quote($value);
$languagesArray = array_map(function($value) use ($delimiter) {
return preg_quote($value, $delimiter);
}, $languagesArray);
sort($languagesArray);
@@ -143,6 +153,7 @@ class Language
*/
public function setDefault($lang)
{
$lang = (string)$lang;
if ($this->validate($lang)) {
$this->default = $lang;
@@ -171,7 +182,12 @@ class Language
*/
public function setActive($lang)
{
$lang = (string)$lang;
if ($this->validate($lang)) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage('Active language set to ' . $lang, 'debug');
$this->active = $lang;
return $lang;
@@ -196,7 +212,7 @@ class Language
// Try setting language from prefix of URL (/en/blah/blah).
if (preg_match($regex, $uri, $matches)) {
$this->lang_in_url = true;
$this->active = $matches[2];
$this->setActive($matches[2]);
$uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
// Store in session if language is different.
@@ -210,7 +226,7 @@ class Language
// Try getting language from the session, else no active.
if (isset($this->grav['session']) && $this->grav['session']->isStarted() &&
$this->config->get('system.languages.session_store_active', true)) {
$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 ($this->active === null &&
@@ -221,9 +237,9 @@ class Language
$best_language = $negotiator->getBest($accept, $this->languages);
if ($best_language instanceof AcceptLanguage) {
$this->active = $best_language->getType();
$this->setActive($best_language->getType());
} else {
$this->active = $this->getDefault();
$this->setActive($this->getDefault());
}
}
@@ -275,52 +291,67 @@ class Language
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
*
* @param string|null $file_ext
*
* @return array
* @param string|null $fileExtension
* @param string|null $languageCode
* @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)) {
if (!$file_ext) {
$file_ext = CONTENT_EXT;
$fileExtension = $fileExtension ?: CONTENT_EXT;
$key = $fileExtension . '-' . ($languageCode ?? 'default') . '-' . (int)$assoc;
if (!isset($this->fallback_extensions[$key])) {
$all = $this->getPageExtensions($fileExtension);
$list = [];
$fallback = $this->getFallbackLanguages($languageCode, true);
foreach ($fallback as $code) {
$ext = $all[$code] ?? null;
if (null !== $ext) {
$list[$code] = $ext;
}
}
if (!$assoc) {
$list = array_values($list);
}
if ($this->enabled()) {
$valid_lang_extensions = [];
foreach ($this->languages as $lang) {
$valid_lang_extensions[] = '.' . $lang . $file_ext;
}
$this->fallback_extensions[$key] = $list;
if ($this->active) {
$active_extension = '.' . $this->active . $file_ext;
$key = \array_search($active_extension, $valid_lang_extensions, true);
// 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;
}
/** @var Debugger $debugger */
//$debugger = $this->grav['debugger'];
//$debugger->addMessage("Language fallback extensions for {$languageCode}", 'debug', $list);
}
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:
*
@@ -332,33 +363,77 @@ class Language
*/
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
*/
public function getFallbackLanguages()
public function getFallbackLanguages(string $languageCode = null, bool $includeDefault = false)
{
if (empty($this->fallback_languages)) {
if ($this->enabled()) {
$fallback_languages = $this->languages;
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';
// Handle default.
if ($languageCode === '' || !$this->enabled()) {
return [''];
}
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];
}
/**
@@ -394,18 +469,12 @@ class Language
}
if ($this->config->get('system.languages.translations', true)) {
if ($this->enabled() && $lookup) {
if (empty($languages)) {
if ($this->config->get('system.languages.translations_fallback', true)) {
$languages = $this->getFallbackLanguages();
} else {
$languages = (array)$this->getLanguage();
}
}
} else {
$languages = ['en'];
if ($this->enabled() && $lookup && empty($languages)) {
$languages = $this->getTranslatedLanguages();
}
$languages = $languages ?: ['en'];
foreach ((array)$languages as $lang) {
$translation = $this->getTranslation($lang, $lookup, $array_support);
@@ -439,18 +508,12 @@ class Language
public function translateArray($key, $index, $languages = null, $html_out = false)
{
if ($this->config->get('system.languages.translations', true)) {
if ($this->enabled() && $key) {
if (empty($languages)) {
if ($this->config->get('system.languages.translations_fallback', true)) {
$languages = $this->getFallbackLanguages();
} else {
$languages = (array)$this->getDefault();
}
}
} else {
$languages = ['en'];
if ($this->enabled() && $key && empty($languages)) {
$languages = $this->getTranslatedLanguages();
}
$languages = $languages ?: ['en'];
foreach ((array)$languages as $lang) {
$translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
if ($translation_array && array_key_exists($index, $translation_array)) {
@@ -534,4 +597,27 @@ class Language
return LanguageCodes::get($code, $type);
}
public function __debugInfo()
{
$vars = get_object_vars($this);
unset($vars['grav'], $vars['config']);
return $vars;
}
/**
* @return array
*/
protected function getTranslatedLanguages(): array
{
if ($this->config->get('system.languages.translations_fallback', true)) {
$languages = $this->getFallbackLanguages();
} else {
$languages = [$this->getLanguage()];
}
$languages[] = 'en';
return array_values(array_unique($languages));
}
}

View File

@@ -202,4 +202,13 @@ class LanguageCodes
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

@@ -19,6 +19,7 @@ use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
trait MediaTrait
{
protected $media;
protected $_loadMedia = true;
/**
* Get filesystem path to the associated media.
@@ -40,11 +41,14 @@ trait MediaTrait
/**
* Get URI ot the associated media. Method will return null if path isn't URI.
*
* @return null|string
* @return string|null
*/
public function getMediaUri()
{
$folder = $this->getMediaFolder();
if (!$folder) {
return null;
}
if (strpos($folder, '://')) {
return $folder;
@@ -73,7 +77,7 @@ trait MediaTrait
// Use cached media if possible.
$cacheKey = md5('media' . $this->getCacheKey());
if (!$media = $cache->get($cacheKey)) {
$media = new Media($this->getMediaFolder(), $this->getMediaOrder());
$media = new Media($this->getMediaFolder(), $this->getMediaOrder(), $this->_loadMedia);
$cache->set($cacheKey, $media);
}
$this->media = $media;

View File

@@ -11,10 +11,11 @@ namespace Grav\Common\Page;
use Grav\Common\Grav;
use Grav\Common\Iterator;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils;
class Collection extends Iterator
class Collection extends Iterator implements PageCollectionInterface
{
/**
* @var Pages
@@ -51,6 +52,20 @@ class Collection extends Iterator
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
*
@@ -94,10 +109,10 @@ class Collection extends Iterator
*
* Merge another collection with the current collection
*
* @param Collection $collection
* @param PageCollectionInterface $collection
* @return $this
*/
public function merge(Collection $collection)
public function merge(PageCollectionInterface $collection)
{
foreach($collection as $page) {
$this->addPage($page);
@@ -109,10 +124,10 @@ class Collection extends Iterator
/**
* Intersect another collection with the current collection
*
* @param Collection $collection
* @param PageCollectionInterface $collection
* @return $this
*/
public function intersect(Collection $collection)
public function intersect(PageCollectionInterface $collection)
{
$array1 = $this->items;
$array2 = $collection->toArray();
@@ -124,20 +139,6 @@ class Collection extends Iterator
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.
*
@@ -240,7 +241,7 @@ class Collection extends Iterator
*
* @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];
}
@@ -252,7 +253,7 @@ class Collection extends Iterator
*
* @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];
}
@@ -301,7 +302,6 @@ class Collection extends Iterator
}
return $this;
}
/**
@@ -309,11 +309,13 @@ class Collection extends Iterator
*
* @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,18 @@
namespace Grav\Common\Page;
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, \JsonSerializable
{
use NestedArrayAccess, Constructor;
use NestedArrayAccessWithGetters, Constructor, Export;
protected $items;
public function jsonSerialize()
{
return $this->toArray();
}
}

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);
/**
* 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
*
@@ -64,7 +71,7 @@ interface PageContentInterface
*
* @param string|null $var
*
* @return null
* @return string
*/
public function rawMarkdown($var = null);
@@ -167,7 +174,7 @@ interface PageContentInterface
*
* @param int $var
*
* @return int|bool
* @return string|bool
*/
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.
*/
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();
/**
* 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
*
@@ -69,7 +62,7 @@ interface PageLegacyInterface
* Add an entry to the page's contentMeta array
*
* @param string $name
* @param string $value
* @param mixed $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
* 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);

View File

@@ -153,7 +153,7 @@ interface PageRoutableInterface
/**
* Returns the item in the current position.
*
* @return int the index of the current page.
* @return int|null The index of the current page.
*/
public function currentPosition();

View File

@@ -185,7 +185,7 @@ class Excerpts
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
/** @var PageInterface $ext_page */
$ext_page = $grav['pages']->dispatch($page_route, true);
$ext_page = $grav['pages']->find($page_route, true);
if ($ext_page) {
$media = $ext_page->getMedia();
} else {

View File

@@ -78,21 +78,21 @@ class Media extends AbstractMedia
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$config = Grav::instance()['config'];
$locator = Grav::instance()['locator'];
$exif_reader = isset(Grav::instance()['exif']) ? Grav::instance()['exif']->getReader() : false;
$media_types = array_keys(Grav::instance()['config']->get('media.types'));
$path = $this->getPath();
// Handle special cases where page doesn't exist in filesystem.
if (!is_dir($this->getPath())) {
if (!$path || !is_dir($path)) {
return;
}
$iterator = new \FilesystemIterator($this->getPath(), \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
$iterator = new \FilesystemIterator($path, \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
$media = [];
/** @var \DirectoryIterator $info */
foreach ($iterator as $path => $info) {
foreach ($iterator as $file => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
continue;
@@ -106,9 +106,9 @@ class Media extends AbstractMedia
}
if ($type === 'alternative') {
$media["{$basename}.{$ext}"][$type][$extra] = ['file' => $path, 'size' => $info->getSize()];
$media["{$basename}.{$ext}"][$type][$extra] = ['file' => $file, 'size' => $info->getSize()];
} else {
$media["{$basename}.{$ext}"][$type] = ['file' => $path, 'size' => $info->getSize()];
$media["{$basename}.{$ext}"][$type] = ['file' => $file, 'size' => $info->getSize()];
}
}
@@ -124,6 +124,7 @@ class Media extends AbstractMedia
$alt['file']->set('size', $alt['size']);
}
}
unset($alt);
}
$file_path = null;

View File

@@ -109,7 +109,7 @@ class ImageFile extends Image
* Gets the hash.
* @param string $type
* @param int $quality
* @param [] $extras
* @param array $extras
* @return null
*/
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\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Page\Markdown\Excerpts;
use Grav\Common\Taxonomy;
use Grav\Common\Page\Traits\PageFormTrait;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Yaml;
use Negotiation\Accept;
use Negotiation\Negotiator;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile;
@@ -33,6 +33,7 @@ define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
class Page implements PageInterface
{
use PageFormTrait;
use MediaTrait;
/**
@@ -93,11 +94,9 @@ class Page implements PageInterface
protected $ssl;
protected $template_format;
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;
@@ -188,16 +187,32 @@ class Page implements PageInterface
*/
public function translatedLanguages($onlyPublished = false)
{
$filename = substr($this->name, 0, -(strlen($this->extension())));
$config = Grav::instance()['config'];
$languages = $config->get('system.languages.supported', []);
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
$languages = $language->getLanguages();
$defaultCode = $language->getDefault();
$name = substr($this->name, 0, -strlen($this->extension()));
$translatedLanguages = [];
foreach ($languages as $language) {
$path = $this->path . DS . $this->folder . DS . $filename . '.' . $language . '.md';
if (file_exists($path)) {
foreach ($languages as $languageCode) {
$languageExtension = ".{$languageCode}.md";
$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->init(new \SplFileInfo($path), $language . '.md');
$aPage->init(new \SplFileInfo($path), $languageExtension);
$route = $aPage->header()->routes['default'] ?? $aPage->rawRoute();
if (!$route) {
@@ -208,7 +223,7 @@ class Page implements PageInterface
continue;
}
$translatedLanguages[$language] = $route;
$translatedLanguages[$languageCode] = $route;
}
}
@@ -224,22 +239,41 @@ class Page implements PageInterface
*/
public function untranslatedLanguages($includeUnpublished = false)
{
$filename = substr($this->name, 0, -strlen($this->extension()));
$config = Grav::instance()['config'];
$languages = $config->get('system.languages.supported', []);
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
$languages = $language->getLanguages();
$defaultCode = $language->getDefault();
$name = substr($this->name, 0, -strlen($this->extension()));
$untranslatedLanguages = [];
foreach ($languages as $language) {
$path = $this->path . DS . $this->folder . DS . $filename . '.' . $language . '.md';
if (file_exists($path)) {
$aPage = new Page();
$aPage->init(new \SplFileInfo($path), $language . '.md');
if ($includeUnpublished && !$aPage->published()) {
$untranslatedLanguages[] = $language;
}
} else {
$untranslatedLanguages[] = $language;
foreach ($languages as $languageCode) {
$path = $this->path . DS . $this->folder . DS . $name . '.' . $languageCode . '.md';
$exists = file_exists($path);
// Default language may be saved without language file location.
if (!$exists && $languageCode === $defaultCode) {
$path = $this->path . DS . $this->folder . DS . $name . '.md';
$exists = file_exists($path);
}
if ($exists) {
if ($includeUnpublished) {
continue;
}
$aPage = new Page();
$aPage->init(new \SplFileInfo($path), $languageCode . '.md');
if (!$aPage->published()) {
continue;
}
}
$untranslatedLanguages[] = $languageCode;
}
return $untranslatedLanguages;
@@ -568,8 +602,7 @@ class Page implements PageInterface
$content = $textOnly ? strip_tags($this->content()) : $this->content();
$summary_size = $this->summary_size;
} else {
$content = strip_tags($this->summary);
// Use mb_strwidth to deal with the 2 character widths characters
$content = $textOnly ? strip_tags($this->summary) : $this->summary;
$summary_size = mb_strwidth($content, 'utf-8');
}
@@ -772,7 +805,7 @@ class Page implements PageInterface
* Add an entry to the page's contentMeta array
*
* @param string $name
* @param string $value
* @param mixed $value
*/
public function addContentMeta($name, $value)
{
@@ -784,16 +817,12 @@ class Page implements PageInterface
*
* @param string|null $name
*
* @return string
* @return mixed|null
*/
public function getContentMeta($name = null)
{
if ($name) {
if (isset($this->content_meta[$name])) {
return $this->content_meta[$name];
}
return null;
return $this->content_meta[$name] ?? null;
}
return $this->content_meta;
@@ -922,13 +951,16 @@ class Page implements PageInterface
return $this->slug();
}
if ($name === 'name') {
$name = $this->name();
$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()) {
return 'modular/' . $name_val;
return 'modular/' . $name;
}
return $name_val;
return $name;
}
if ($name === 'media') {
return $this->media()->all();
@@ -1205,93 +1237,12 @@ class Page implements PageInterface
return $this->id();
}
/**
* Returns normalized list of name => form pairs.
*
* @return array
*/
public function forms()
{
if (null === $this->forms) {
$header = $this->header();
// Call event to allow filling the page header form dynamically (e.g. use case: Comments plugin)
$grav = Grav::instance();
$grav->fireEvent('onFormPageHeaderProcessed', new Event(['page' => $this, 'header' => $header]));
$rules = $header->rules ?? null;
if (!\is_array($rules)) {
$rules = [];
}
$forms = [];
// First grab page.header.form
$form = $this->normalizeForm($header->form ?? null, null, $rules);
if ($form) {
$forms[$form['name']] = $form;
}
// Append page.header.forms (override singular form if it clashes)
$headerForms = $header->forms ?? null;
if (\is_array($headerForms)) {
foreach ($headerForms as $name => $form) {
$form = $this->normalizeForm($form, $name, $rules);
if ($form) {
$forms[$form['name']] = $form;
}
}
}
$this->forms = $forms;
}
return $this->forms;
}
/**
* @param array $new
*/
public function addForms(array $new)
{
// Initialize forms.
$this->forms();
foreach ($new as $form) {
$form = $this->normalizeForm($form);
if ($form) {
$this->forms[$form['name']] = $form;
}
}
}
protected function normalizeForm($form, $name = null, array $rules = [])
{
if (!\is_array($form)) {
return null;
}
// Ignore numeric indexes on name.
if (!$name || (string)(int)$name === (string)$name) {
$name = null;
}
$name = $name ?? $form['name'] ?? $this->slug();
$formRules = $form['rules'] ?? null;
if (!\is_array($formRules)) {
$formRules = [];
}
return ['name' => $name, 'rules' => $rules + $formRules] + $form;
}
/**
* Gets and sets the associated media as found in the page folder.
*
* @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)
{
@@ -1371,67 +1322,32 @@ class Page implements PageInterface
}
/**
* Allows a page to override the output render format, usually the extension provided
* in the URL. (e.g. `html`, `json`, `xml`, etc).
* Allows a page to override the output render format, usually the extension provided in the URL.
* (e.g. `html`, `json`, `xml`, etc).
*
* @param null $var
* @param string|null $var
*
* @return null
* @return string
*/
public function templateFormat($var = null)
{
if ($var !== null) {
$this->template_format = $var;
return $this->template_format;
if (null !== $var) {
$this->template_format = is_string($var) ? $var : null;
}
if (isset($this->template_format)) {
return $this->template_format;
if (!isset($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;
}
/**
* Gets and sets the extension field.
*
* @param null $var
* @param string|null $var
*
* @return null|string
* @return string
*/
public function extension($var = null)
{
@@ -1660,7 +1576,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
*/
@@ -1689,9 +1605,10 @@ class Page implements PageInterface
$this->metadata = [];
$metadata = [];
// Set the Generator tag
$metadata['generator'] = 'GravCMS';
$metadata = [
'generator' => 'GravCMS'
];
// Get initial metadata for the page
$metadata = array_merge($metadata, Grav::instance()['config']->get('site.metadata'));
@@ -1784,7 +1701,7 @@ class Page implements PageInterface
*
* @param int $var
*
* @return int|bool
* @return string|bool
*/
public function order($var = null)
{
@@ -2040,7 +1957,7 @@ class Page implements PageInterface
*
* @param string $var redirect url
*
* @return string
* @return string|null
*/
public function redirect($var = null)
{
@@ -2048,7 +1965,7 @@ class Page implements PageInterface
$this->redirect = $var;
}
return $this->redirect;
return $this->redirect ?: null;
}
/**
@@ -2107,7 +2024,7 @@ class Page implements PageInterface
$this->path = dirname($var, 2);
}
return $this->path . '/' . $this->folder . '/' . ($this->name ?: '');
return rtrim($this->path . '/' . $this->folder . '/' . ($this->name() ?: ''), '/');
}
/**
@@ -2349,7 +2266,7 @@ class Page implements PageInterface
}
}
return $this->modular_twig;
return $this->modular_twig ?? false;
}
/**
@@ -2497,7 +2414,7 @@ class Page implements PageInterface
/**
* Returns the item in the current position.
*
* @return int the index of the current page.
* @return int|null The index of the current page.
*/
public function currentPosition()
{
@@ -2538,7 +2455,7 @@ class Page implements PageInterface
if (isset($routes[$uri_path])) {
/** @var PageInterface $child_page */
$child_page = $pages->dispatch($uri->route())->parent();
$child_page = $pages->find($uri->route())->parent();
if ($child_page) {
while (!$child_page->root()) {
if ($this->path() === $child_page->path()) {
@@ -2671,321 +2588,48 @@ class Page implements PageInterface
public function collection($params = 'content', $pagination = true)
{
if (is_string($params)) {
// Look into a page header field.
$params = (array)$this->value('header.' . $params);
} elseif (!is_array($params)) {
throw new \InvalidArgumentException('Argument should be either header variable name or array of parameters');
}
if (!isset($params['items'])) {
return new Collection();
if (!$pagination) {
$params['pagination'] = false;
}
$context = [
'pagination' => $pagination,
'self' => $this
];
// See if require published filter is set and use that, if assume published=true
$only_published = true;
if (isset($params['filter']['published']) && $params['filter']['published']) {
$only_published = false;
} elseif (isset($params['filter']['non-published']) && $params['filter']['non-published']) {
$only_published = false;
}
/** @var Pages $pages */
$pages = Grav::instance()['pages'];
$collection = $this->evaluate($params['items'], $only_published);
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;
return $pages->getCollection($params, $context);
}
/**
* @param string|array $value
* @param bool $only_published
* @return mixed
* @return Collection
*/
public function evaluate($value, $only_published = true)
{
// 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)->toArray();
} else {
$result = $result + $this->evaluate([$key => $val])->toArray();
}
}
return new Collection($result);
}
$params = [
'items' => $value,
'published' => $only_published
];
$context = [
'event' => false,
'pagination' => false,
'url_taxonomy_filters' => false,
'self' => $this
];
/** @var Pages $pages */
$pages = Grav::instance()['pages'];
$parts = explode('.', $cmd);
$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;
return $pages->getCollection($params, $context);
}
/**
@@ -3170,7 +2814,7 @@ class Page implements PageInterface
/**
* Gets the action.
*
* @return string The Action string.
* @return string|null The Action string.
*/
public function getAction()
{

View File

@@ -13,13 +13,18 @@ use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Blueprints;
use Grav\Common\Debugger;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Taxonomy;
use Grav\Common\Uri;
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 RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -28,64 +33,46 @@ use Collator;
class Pages
{
/**
* @var Grav
*/
/** @var Grav */
protected $grav;
/**
* @var array|PageInterface[]
*/
/** @var Flex */
protected $flex;
/** @var array|PageInterface[] */
protected $instances;
/**
* @var array|string[]
*/
/** @var array */
protected $children;
/**
* @var string
*/
/** @var string */
protected $base = '';
/**
* @var array|string[]
*/
/** @var string[] */
protected $baseRoute = [];
/**
* @var array|string[]
*/
/** @var string[] */
protected $routes = [];
/**
* @var array
*/
/** @var array */
protected $sort;
/**
* @var Blueprints
*/
/** @var Blueprints */
protected $blueprints;
/**
* @var int
*/
/** @var bool */
protected $enable_pages = true;
/** @var int */
protected $last_modified;
/**
* @var array|string[]
*/
/** @var string[] */
protected $ignore_files;
/**
* @var array|string[]
*/
/** @var string[] */
protected $ignore_folders;
/**
* @var bool
*/
/** @var bool */
protected $ignore_hidden;
/** @var string */
@@ -93,16 +80,13 @@ class Pages
protected $pages_cache_id;
/** @var bool */
protected $initialized = false;
/**
* @var Types
*/
/** @var Types */
static protected $types;
/**
* @var string
*/
/** @var string|null */
static protected $home_route;
/**
@@ -115,6 +99,26 @@ class Pages
$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.
*
@@ -249,6 +253,9 @@ class Pages
$this->ignore_files = $config->get('system.pages.ignore_files');
$this->ignore_folders = $config->get('system.pages.ignore_folders');
$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->children = [];
@@ -280,11 +287,23 @@ class Pages
/**
* Returns a list of all pages.
*
* @return array|PageInterface[]
* @return PageInterface[]
*/
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)
{
if (!isset($this->instances[$page->path()])) {
$this->instances[$page->path()] = $page;
$path = $page->path() ?? '';
if (!isset($this->instances[$path])) {
$this->instances[$path] = $page;
}
$route = $page->route($route);
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]));
}
/**
* 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.
*
@@ -397,7 +699,16 @@ class Pages
*/
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;
$instance = $instance->hasTranslation() ? $instance->getTranslation() : $instance;
}
if ($instance && !$instance instanceof PageInterface) {
throw new \RuntimeException('Routing failed on unknown type', 500);
}
return $instance;
}
/**
@@ -425,7 +736,7 @@ class Pages
public function ancestor($route, $path = null)
{
if ($path !== null) {
$page = $this->dispatch($route, true);
$page = $this->find($route, true);
if ($page && $page->path() === $path) {
return $page;
@@ -452,7 +763,7 @@ class Pages
{
if ($field !== null) {
$page = $this->dispatch($route, true);
$page = $this->find($route, true);
$parent = $page ? $page->parent() : null;
if ($parent && $parent->value('header.' . $field) !== null) {
@@ -513,13 +824,18 @@ class Pages
// fall back and check site based redirects
if (!$page || ($page && !$page->routable())) {
// Redirect to the first child (placeholder page)
if ($redirect && $page && count($children = $page->children()->visible()) > 0) {
$this->grav->redirectLangSafe($children->first()->route());
}
/** @var Config $config */
$config = $this->grav['config'];
// See if route matches one in the site configuration
$site_route = $config->get("site.routes.{$route}");
if ($site_route) {
$page = $this->dispatch($site_route, $all);
$page = $this->dispatch($site_route, $all, $redirect);
} else {
/** @var Uri $uri */
@@ -551,7 +867,7 @@ class Pages
try {
$found = preg_replace($pattern, $replace, $source_url);
if ($found !== $source_url) {
$page = $this->dispatch($found, $all);
$page = $this->dispatch($found, $all, $redirect);
}
} catch (ErrorException $e) {
$this->grav['log']->error('site.routes: ' . $pattern . '-> ' . $e->getMessage());
@@ -575,7 +891,7 @@ class Pages
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
return $this->instances[rtrim($locator->findResource('page://'), DS)];
return $this->get(rtrim($locator->findResource('page://'), '/'));
}
/**
@@ -713,13 +1029,9 @@ class Pages
} else {
$extra = $showSlug ? '(' . $current->slug() . ') ' : '';
$option = str_repeat('&mdash;-', $level). '&rtrif; ' . $extra . $current->title();
}
$list[$route] = $option;
}
if ($limitLevels === false || ($level+1 < $limitLevels)) {
@@ -824,20 +1136,23 @@ class Pages
*
* @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 */
$admin = Grav::instance()['admin'];
/** @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::types();
}
return [];
@@ -943,26 +1258,156 @@ class Pages
*
* @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 */
$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 */
$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 Flex Pages..');
$root = $this->buildRootPage();
$root_path = $root->path();
$this->instances = [$root_path => $root];
$this->children = [];
$this->sort = [];
if ($config->get('system.pages.events.page')) {
$this->grav->fireEvent('onBuildPagesInitialized');
}
/**
* @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();
// FIXME: We really need to do better than this.
$translated = $page->hasTranslation() ? $page->getTranslation() : false;
if (!$translated || $path === $root_path) {
continue;
}
$parent = dirname($path);
$this->instances[$path] = $page->getFlexKey();
// FIXME: ... better...
$this->children[$parent][$path] = ['slug' => $translated->slug()];
if (!isset($this->children[$path])) {
$this->children[$path] = [];
}
}
foreach ($this->children as $path => $list) {
$page = $this->get($path);
if (null === $page) {
continue;
}
// Call onFolderProcessed event.
if ($config->get('system.pages.events.page')) {
$this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
}
// Sort the children.
$this->children[$path] = $this->sort($page);
}
$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 */
$locator = $this->grav['locator'];
$pages_dir = $locator->findResource('page://');
if ($config->get('system.cache.enabled')) {
/** @var Cache $cache */
$cache = $this->grav['cache'];
/** @var Taxonomy $taxonomy */
$taxonomy = $this->grav['taxonomy'];
/** @var Language $language */
$language = $this->grav['language'];
// how should we check for last modified? Default is by file
switch ($this->check_method) {
@@ -982,22 +1427,25 @@ class Pages
$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);
if (!$this->instances) {
$this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
/** @var Cache $cache */
$cache = $this->grav['cache'];
$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
$this->resetPages($pages_dir);
} else {
// If pages was found in cache, set the taxonomy
$this->grav['debugger']->addMessage('Page cache hit.');
/** @var Taxonomy $taxonomy */
$taxonomy = $this->grav['taxonomy'];
$taxonomy->taxonomy($taxonomy_map);
return;
}
$this->grav['debugger']->addMessage('Page cache missed, rebuilding pages..');
} else {
$this->recurse($pages_dir);
$this->buildRoutes();
$this->grav['debugger']->addMessage('Page cache disabled, rebuilding pages..');
}
$this->resetPages($pages_dir);
}
/**
@@ -1007,6 +1455,7 @@ class Pages
*/
public function resetPages($pages_dir)
{
$this->sort = [];
$this->recurse($pages_dir);
$this->buildRoutes();
@@ -1018,7 +1467,7 @@ class Pages
$taxonomy = $this->grav['taxonomy'];
// 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 +1512,7 @@ class Pages
if ($parent && $page->path()) {
$this->children[$parent->path()][$page->path()] = ['slug' => $page->slug()];
}
} else {
} elseif ($parent !== null) {
throw new \RuntimeException('Fatal error when creating page instances.');
}
@@ -1161,7 +1610,6 @@ class Pages
}
}
if (!$content_exists) {
// Set routability to false if no page found
$page->routable(false);
@@ -1203,44 +1651,49 @@ class Pages
// Get the home route
$home = self::resetHomeRoute();
// Build routes and taxonomy map.
/** @var PageInterface $page */
foreach ($this->instances as $page) {
if (!$page->root()) {
// process taxonomy
$taxonomy->addTaxonomy($page);
foreach ($this->instances as $path => $page) {
if (\is_string($page)) {
$page = $this->get($path);
}
$route = $page->route();
$raw_route = $page->rawRoute();
$page_path = $page->path();
if (!$page || $page->root()) {
continue;
}
// add regular route
$this->routes[$route] = $page_path;
// process taxonomy
$taxonomy->addTaxonomy($page);
// add raw route
if ($raw_route !== $route) {
$this->routes[$raw_route] = $page_path;
}
$route = $page->route();
$raw_route = $page->rawRoute();
$page_path = $page->path();
// add canonical route
$route_canonical = $page->routeCanonical();
if ($route_canonical && ($route !== $route_canonical)) {
$this->routes[$route_canonical] = $page_path;
}
// add regular route
$this->routes[$route] = $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;
}
// add raw route
if ($raw_route !== $route) {
$this->routes[$raw_route] = $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.
$homeRoute = '/' . $home;
$homeRoute = "/{$home}";
if ($home && isset($this->routes[$homeRoute])) {
$this->routes['/'] = $this->routes[$homeRoute];
$this->get($this->routes[$homeRoute])->route('/');
@@ -1272,7 +1725,7 @@ class Pages
}
foreach ($pages as $key => $info) {
$child = $this->instances[$key] ?? null;
$child = $this->get($key);
if (!$child) {
throw new \RuntimeException("Page does not exist: {$key}");
}
@@ -1307,7 +1760,10 @@ class Pages
$list[$key] = $child->folder();
break;
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]);
if (is_array($header_value)) {
$list[$key] = implode(',',$header_value);
@@ -1408,6 +1864,11 @@ class Pages
return $new;
}
protected function getVersion()
{
return $this->flex ? 'flex' : 'page';
}
/**
* 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 $title = 'Assets';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$this->container['assets']->init();

View File

@@ -18,7 +18,7 @@ class BackupsProcessor extends ProcessorBase
public $id = '_backups';
public $title = 'Backups';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$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 $title = 'Debugger Assets';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();
$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;
use Grav\Common\Config\Config;
use Grav\Common\Debugger;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Psr7\Response;
use Grav\Framework\Session\Exceptions\SessionException;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\SyslogHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class InitializeProcessor extends ProcessorBase
{
public $id = 'init';
public $id = '_init';
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 */
$config = $this->container['config'];
$config->debug();
$config = $grav['config'];
$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.
ob_start();
@@ -43,21 +137,6 @@ class InitializeProcessor extends ProcessorBase
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 */
$uri = $this->container['uri'];
$uri->init();
@@ -73,8 +152,29 @@ class InitializeProcessor extends ProcessorBase
}
$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 $title = 'Pages';
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->startTimer();

View File

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

View File

@@ -25,21 +25,21 @@ abstract class ProcessorBase implements ProcessorInterface
$this->container = $container;
}
protected function startTimer($id = null, $title = null)
protected function startTimer($id = null, $title = null): void
{
/** @var Debugger $debugger */
$debugger = $this->container['debugger'];
$debugger->startTimer($id ?? $this->id, $title ?? $this->title);
}
protected function stopTimer($id = null)
protected function stopTimer($id = null): void
{
/** @var Debugger $debugger */
$debugger = $this->container['debugger'];
$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 */
$debugger = $this->container['debugger'];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,11 +9,35 @@
namespace Grav\Common;
use enshrined\svgSanitize\Sanitizer;
use Grav\Common\Page\Pages;
class Security
{
/**
* Sanitize SVG for XSS code
*
* @param $file
*/
public static function sanitizeSVG($file)
{
if (Grav::instance()['config']->get('security.sanitize_svg') && file_exists($file)) {
$sanitizer = new Sanitizer();
$original_svg = file_get_contents($file);
$clean_svg = $sanitizer->sanitize($original_svg);
file_put_contents($file, $clean_svg);
}
}
/**
* Detect XSS code in Grav pages
*
* @param Pages $pages
* @param bool $route
* @param callable|null $status
* @return array
*/
public static function detectXssFromPages(Pages $pages, $route = true, callable $status = null)
{
$routes = $pages->routes();
@@ -51,7 +75,6 @@ class Security
} else {
$list[$page->filePathClean()] = $results;
}
}
} catch (\Exception $e) {
@@ -63,6 +86,8 @@ class Security
}
/**
* Detect XSS in an array or strings such as $_POST or $_GET
*
* @param array $array Array such as $_POST or $_GET
* @param string $prefix Prefix for returned values.
* @return array Returns flatten list of potentially dangerous input values, such as 'data.content'.
@@ -89,6 +114,7 @@ class Security
/**
* Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to
*
* return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into
* their content.
*

View File

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

View File

@@ -125,7 +125,7 @@ class Session extends \Grav\Framework\Session\Session
/** @var FormFlash $flash */
$flash = $form ? $form->getFlash() : null;
$object = $flash ? [$sessionField => $flash->getLegacyFiles()] : null;
$object = $flash && method_exists($flash, 'getLegacyFiles') ? [$sessionField => $flash->getLegacyFiles()] : null;
}
}

View File

@@ -30,11 +30,11 @@ class Theme extends Plugin
/**
* Get configuration of the plugin.
*
* @return Config
* @return array
*/
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.
*
* @return true
* @return bool
*/
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\Data\Blueprints;
use Grav\Common\Data\Data;
use Grav\Framework\Psr7\Response;
use RocketTheme\Toolbox\Event\EventDispatcher;
use RocketTheme\Toolbox\Event\EventSubscriberInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
@@ -128,7 +129,7 @@ class Themes extends Iterator
*
* @param string $name
*
* @return Data
* @return Data|null
* @throws \RuntimeException
*/
public function get($name)
@@ -214,7 +215,9 @@ class Themes extends Iterator
}
}
} 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));

View File

@@ -0,0 +1,61 @@
<?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\Node;
use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Node;
class TwigNodeCache extends Node
{
/**
* @param AbstractExpression $key
* @param AbstractExpression $lifetime
* @param Node $body
* @param integer $lineno
* @param string $tag
*/
public function __construct(AbstractExpression $key, AbstractExpression $lifetime, Node $body, $lineno, $tag = null)
{
parent::__construct(array('key' => $key, 'body' => $body, 'lifetime' => $lifetime), array(), $lineno, $tag);
}
/**
* {@inheritDoc}
*/
public function compile(Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write("\$cache = \\Grav\\Common\\Grav::instance()['cache'];\n")
->write("\$key = \"twigcache-\" . ")
->subcompile($this->getNode('key'))
->write(";\n")
->write("\$lifetime = ")
->subcompile($this->getNode('lifetime'))
->write(";\n")
->write("\$cache_body = \$cache->fetch(\$key);\n")
->write("if (\$cache_body === false) {\n")
->indent()
->write("ob_start();\n")
->indent()
->subcompile($this->getNode('body'))
->outdent()
->write("\n")
->write("\$cache_body = ob_get_clean();\n")
->write("\$cache->save(\$key, \$cache_body, \$lifetime);\n")
->outdent()
->write("}\n")
->write("echo \$cache_body;\n")
;
}
}

View File

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

View File

@@ -33,12 +33,15 @@ class TwigNodeRender extends Node implements NodeCaptureInterface
$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.
*
* @param Compiler $compiler A Twig_Compiler instance
* @param Compiler $compiler A Twig Compiler instance
* @throws \LogicException
*/
public function compile(Compiler $compiler)
@@ -46,15 +49,15 @@ class TwigNodeRender extends Node implements NodeCaptureInterface
$compiler->addDebugInfo($this);
$compiler->write('$object = ')->subcompile($this->getNode('object'))->raw(';' . PHP_EOL);
$layout = $this->getNode('layout');
if ($layout) {
if ($this->hasNode('layout')) {
$layout = $this->getNode('layout');
$compiler->write('$layout = ')->subcompile($layout)->raw(';' . PHP_EOL);
} else {
$compiler->write('$layout = null;' . PHP_EOL);
}
$context = $this->getNode('context');
if ($context) {
if ($this->hasNode('context')) {
$context = $this->getNode('context');
$compiler->write('$attributes = ')->subcompile($context)->raw(';' . PHP_EOL);
} else {
$compiler->write('$attributes = null;' . PHP_EOL);

View File

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

View File

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

View File

@@ -30,13 +30,16 @@ class TwigNodeSwitch extends Node
$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.
*
* @param Compiler $compiler A Twig_Compiler instance
* @param Compiler $compiler A Twig Compiler instance
*/
public function compile(Compiler $compiler)
{
@@ -68,7 +71,7 @@ class TwigNodeSwitch extends Node
->write("}\n");
}
if ($this->hasNode('default') && $this->getNode('default') !== null) {
if ($this->hasNode('default')) {
$compiler
->write("default:\n")
->write("{\n")

View File

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

View File

@@ -28,13 +28,16 @@ class TwigNodeTryCatch extends Node
$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.
*
* @param Compiler $compiler A Twig_Compiler instance
* @param Compiler $compiler A Twig Compiler instance
* @throws \LogicException
*/
public function compile(Compiler $compiler)
@@ -50,7 +53,7 @@ class TwigNodeTryCatch extends Node
->subcompile($this->getNode('try'))
;
if ($this->hasNode('catch') && null !== $this->getNode('catch')) {
if ($this->hasNode('catch')) {
$compiler
->outdent()
->write('} catch (\Exception $e) {' . "\n")

View File

@@ -0,0 +1,61 @@
<?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\TokenParser;
use Grav\Common\Twig\Node\TwigNodeCache;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
/**
* Adds ability to cache Twig between tags.
*
* {% cache 'unique-key' 600 %}
* {{ some_complex_work() }}
* {% endcache %}
*/
class TwigTokenParserCache extends AbstractTokenParser
{
/**
* {@inheritDoc}
*/
public function parse(Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$key = $this->parser->getExpressionParser()->parseExpression();
$lifetime =$this->parser->getExpressionParser()->parseExpression();
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideCacheEnd'), true);
$stream->expect(Token::BLOCK_END_TYPE);
return new TwigNodeCache($key, $lifetime, $body, $lineno, $this->getTag());
}
/**
* Decide if current token marks end of cache block.
*
* @param Token $token
* @return bool
*/
public function decideCacheEnd(Token $token)
{
return $token->test('endcache');
}
/**
* {@inheritDoc}
*/
public function getTag()
{
return 'cache';
}
}

View File

@@ -24,9 +24,9 @@ class TwigTokenParserRender extends AbstractTokenParser
/**
* 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)
{

View File

@@ -29,9 +29,9 @@ class TwigTokenParserScript extends AbstractTokenParser
/**
* 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)
{

View File

@@ -28,9 +28,9 @@ class TwigTokenParserStyle extends AbstractTokenParser
/**
* 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)
{

View File

@@ -26,9 +26,9 @@ class TwigTokenParserThrow extends AbstractTokenParser
/**
* 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)
{

View File

@@ -30,9 +30,9 @@ class TwigTokenParserTryCatch extends AbstractTokenParser
/**
* 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)
{

View File

@@ -18,11 +18,21 @@ use Grav\Common\Page\Pages;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RocketTheme\Toolbox\Event\Event;
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
{
/**
* @var \Twig_Environment
* @var Environment
*/
public $twig;
@@ -47,18 +57,20 @@ class Twig
protected $grav;
/**
* @var \Twig_Loader_Filesystem
* @var FilesystemLoader
*/
protected $loader;
/**
* @var \Twig_Loader_Array
* @var ArrayLoader
*/
protected $loaderArray;
protected $autoescape;
protected $profile;
/**
* Constructor
*
@@ -102,7 +114,7 @@ class Twig
$core_templates = array_merge($locator->findResources('system://templates'), $locator->findResources('system://templates/testing'));
$this->twig_paths = array_merge($this->twig_paths, $core_templates);
$this->loader = new \Twig_Loader_Filesystem($this->twig_paths);
$this->loader = new FilesystemLoader($this->twig_paths);
// Register all other prefixes as namespaces in twig
foreach ($locator->getPaths('theme') as $prefix => $_) {
@@ -128,13 +140,13 @@ class Twig
$this->grav->fireEvent('onTwigLoader');
$this->loaderArray = new \Twig_Loader_Array([]);
$loader_chain = new \Twig_Loader_Chain([$this->loaderArray, $this->loader]);
$this->loaderArray = new ArrayLoader([]);
$loader_chain = new ChainLoader([$this->loaderArray, $this->loader]);
$params = $config->get('system.twig');
if (!empty($params['cache'])) {
$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)) {
@@ -153,10 +165,10 @@ class Twig
if ($config->get('system.twig.undefined_functions')) {
$this->twig->registerUndefinedFunctionCallback(function ($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')) {
$this->twig->registerUndefinedFilterCallback(function ($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
if ($config->get('system.pages.dateformat.long')) {
/** @var \Twig_Extension_Core $extension */
$extension = $this->twig->getExtension('Twig_Extension_Core');
/** @var CoreExtension $extension */
$extension = $this->twig->getExtension(CoreExtension::class);
$extension->setDateFormat($config->get('system.pages.dateformat.long'));
}
// enable the debug extension if required
if ($config->get('system.twig.debug')) {
$this->twig->addExtension(new \Twig_Extension_Debug());
$this->twig->addExtension(new DebugExtension());
}
$this->twig->addExtension(new TwigExtension());
$this->twig->addExtension(new DeferredExtension());
$this->profile = new \Twig\Profiler\Profile();
$this->twig->addExtension(new \Twig\Extension\ProfilerExtension($this->profile));
$this->grav->fireEvent('onTwigExtensions');
/** @var Pages $pages */
@@ -219,7 +235,7 @@ class Twig
}
/**
* @return \Twig_Environment
* @return Environment
*/
public function twig()
{
@@ -227,13 +243,19 @@ class Twig
}
/**
* @return \Twig_Loader_Filesystem
* @return FilesystemLoader
*/
public function loader()
{
return $this->loader;
}
public function profile()
{
return $this->profile;
}
/**
* Adds or overrides a template.
*
@@ -254,7 +276,6 @@ class Twig
* @param string $content Optional content override
*
* @return string The rendered output
* @throws \Twig_Error_Loader
*/
public function processPage(PageInterface $item, $content = null)
{
@@ -288,7 +309,7 @@ class Twig
$output = $local_twig->render($name, $twig_vars);
}
} catch (\Twig_Error_Loader $e) {
} catch (LoaderError $e) {
throw new \RuntimeException($e->getRawMessage(), 404, $e);
}
@@ -312,7 +333,7 @@ class Twig
try {
$output = $this->twig->render($template, $vars);
} catch (\Twig_Error_Loader $e) {
} catch (LoaderError $e) {
throw new \RuntimeException($e->getRawMessage(), 404, $e);
}
@@ -341,7 +362,7 @@ class Twig
try {
$output = $this->twig->render($name, $vars);
} catch (\Twig_Error_Loader $e) {
} catch (LoaderError $e) {
throw new \RuntimeException($e->getRawMessage(), 404, $e);
}
@@ -386,14 +407,14 @@ class Twig
try {
$output = $this->twig->render($template, $vars + $twig_vars);
} catch (\Twig_Error_Loader $e) {
} catch (LoaderError $e) {
$error_msg = $e->getMessage();
// Try html version of this template if initial template was NOT html
if ($ext !== '.html' . TWIG_EXT) {
try {
$page->templateFormat('html');
$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);
}
} 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 $namespace
* @throws LoaderError
*/
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 $namespace
* @throws LoaderError
*/
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;
class TwigEnvironment extends \Twig_Environment
use Twig\Environment;
class TwigEnvironment extends Environment
{
use WriteCacheFileTrait;
}

View File

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

@@ -207,6 +207,7 @@ class Uri
RouteFactory::setRoot($this->root_path);
RouteFactory::setLanguage($language->getLanguageURLPrefix());
RouteFactory::setParamValueDelimiter($config->get('system.param_sep'));
}
/**
@@ -300,16 +301,17 @@ class Uri
* Get URI parameter.
*
* @param string $id
* @param bool $default
*
* @return bool|string
*/
public function param($id)
public function param($id, $default = false)
{
if (isset($this->params[$id])) {
return html_entity_decode(rawurldecode($this->params[$id]));
}
return false;
return $default;
}
/**
@@ -668,6 +670,7 @@ class Uri
public static function getCurrentRoute()
{
if (!static::$currentRoute) {
/** @var Uri $uri */
$uri = Grav::instance()['uri'];
static::$currentRoute = RouteFactory::createFromParts($uri->toArray());
}
@@ -1340,7 +1343,7 @@ class Uri
/**
* Check if this is a valid Grav extension
*
* @param $extension
* @param string $extension
* @return bool
*/
public function isValidExtension($extension)
@@ -1357,7 +1360,7 @@ class Uri
/**
* Allow overriding of any element (be careful!)
*
* @param $data
* @param array $data
* @return Uri
*/
public function setUriProperties($data)

View File

@@ -207,7 +207,7 @@ class User extends Data implements UserInterface
/**
* Return media object for the User's avatar.
*
* @return ImageMedium|null
* @return Medium|null
* @deprecated 1.6 Use ->getAvatarImage() method instead.
*/
public function getAvatarMedia()

View File

@@ -13,4 +13,13 @@ use Grav\Framework\Flex\Storage\FileStorage;
class UserFileStorage extends FileStorage
{
/**
* {@inheritdoc}
* @see FlexStorageInterface::getMediaPath()
*/
public function getMediaPath(string $key = null): ?string
{
// There is no media support for file storage (fallback to common location).
return null;
}
}

View File

@@ -9,25 +9,28 @@
namespace Grav\Common\User\FlexUser;
use Grav\Common\Data\Blueprint;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Media;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Common\User\Authentication;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\Traits\UserTrait;
use Grav\Common\Utils;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Storage\FileStorage;
use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
use Grav\Framework\Flex\Traits\FlexMediaTrait;
use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Media\Interfaces\MediaManipulationInterface;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\File\FileInterface;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* Flex User
@@ -50,7 +53,10 @@ use RocketTheme\Toolbox\File\FileInterface;
*/
class User extends FlexObject implements UserInterface, MediaManipulationInterface, \Countable
{
use FlexMediaTrait;
use FlexMediaTrait {
getMedia as private getFlexMedia;
getMediaFolder as private getFlexMediaFolder;
}
use FlexAuthorizeTrait;
use UserTrait;
@@ -382,31 +388,6 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
return $this->getBlueprint()->extra($this->toArray());
}
/**
* @param string $name
* @return Blueprint
*/
public function getBlueprint(string $name = '')
{
$blueprint = clone parent::getBlueprint($name);
$blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
$params = (array)$call['params'];
$method = array_shift($params);
if (method_exists($this, $method)) {
$value = $this->{$method}(...$params);
if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
} else {
$field[$property] = $value;
}
}
});
return $blueprint->init();
}
/**
* Return unmodified data as raw string.
*
@@ -446,6 +427,15 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
*/
public function save()
{
// TODO: We may want to handle this in the storage layer in the future.
$key = $this->getStorageKey();
if (!$key || strpos($key, '@@')) {
$storage = $this->getFlexDirectory()->getStorage();
if ($storage instanceof FileStorage) {
$this->setStorageKey($this->getKey());
}
}
$password = $this->getProperty('password');
if (null !== $password) {
$this->unsetProperty('password');
@@ -554,6 +544,50 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
return \count($this->jsonSerialize());
}
public function getMedia()
{
$media = $this->getFlexMedia();
// Deal with shared avatar folder.
$path = $this->getAvatarFile();
if ($path && !$media[$path] && is_file($path)) {
$medium = MediumFactory::fromFile($path);
if ($medium) {
$media->add($path, $medium);
$name = basename($path);
if ($name !== $path) {
$media->add($name, $medium);
}
}
}
return $media;
}
public function getMediaFolder(): ?string
{
$folder = $this->getFlexMediaFolder();
if (!$folder) {
// Shared media!
$this->_loadMedia = false;
$folder = $this->getBlueprint()->fields()['avatar']['destination'] ?? 'user://accounts/avatars';
}
return $folder;
}
protected function getAvatarFile(): ?string
{
$avatars = $this->getElement('avatar');
if (\is_array($avatars) && $avatars) {
$avatar = array_shift($avatars);
return $avatar['path'] ?? null;
}
return null;
}
/**
* Gets the associated media collection (original images).
*
@@ -561,7 +595,12 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
*/
protected function getOriginalMedia()
{
return (new Media($this->getMediaFolder() . '/original', $this->getMediaOrder()))->setTimestamps();
$folder = $this->getMediaFolder();
if ($folder) {
$folder .= '/original';
}
return (new Media($folder, $this->getMediaOrder()))->setTimestamps();
}
/**
@@ -573,7 +612,38 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
$list_original = [];
foreach ($files as $field => $group) {
foreach ($group as $filename => $file) {
if (strpos($field, '/original')) {
if ($file) {
$filename = $file->getClientFilename();
/** @var FormFlashFile $file */
$data = $file->jsonSerialize();
unset($data['tmp_name'], $data['path']);
} else {
$data = null;
}
$settings = $this->getBlueprint()->schema()->getProperty($field);
// Generate random name if required
if ($settings['random_name'] ?? false) {
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$data['name'] = $filename = Utils::generateRandomString(15) . '.' . $extension;
}
// For shared media folder we need to keep path for backwards compatibility.
$folder = $this->getMediaFolder();
if ($this->_loadMedia) {
$filepath = $filename;
} else {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$filepath = $locator->findResource($folder, false, true) . '/' . $filename;
if ($data) {
$data['path'] = $filepath;
}
}
if ($this->_loadMedia && strpos($field, '/original')) {
// Special handling for original images.
$list_original[$filename] = $file;
continue;
@@ -581,15 +651,10 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
$list[$filename] = $file;
if ($file) {
/** @var FormFlashFile $file */
$data = $file->jsonSerialize();
$path = $file->getClientFilename();
unset($data['tmp_name'], $data['path']);
$this->setNestedProperty("{$field}\n{$path}", $data, "\n");
if ($data) {
$this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
} else {
$this->unsetNestedProperty("{$field}\n{$filename}", "\n");
$this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
}
}
}
@@ -651,7 +716,7 @@ class User extends FlexObject implements UserInterface, MediaManipulationInterfa
$imageFile = $originalMedia[$filename] ?? $thumbFile;
if ($thumbFile) {
$list[$filename] = [
'name' => $filename,
'name' => $info['name'],
'type' => $info['type'],
'size' => $info['size'],
'image_url' => $imageFile->url(),

View File

@@ -59,6 +59,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface
*/
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) {
if ($field === 'key') {
$user = $this->get($query);

View File

@@ -13,6 +13,7 @@ use Grav\Common\Data\Blueprint;
use Grav\Common\Data\DataInterface;
use Grav\Common\Media\Interfaces\MediaInterface;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
/**
@@ -185,9 +186,9 @@ interface UserInterface extends DataInterface, MediaInterface, \ArrayAccess, \Js
*
* Note: if there's no local avatar image for the user, you should call getAvatarUrl() to get the external avatar URL.
*
* @return ImageMedium|null
* @return Medium|null
*/
public function getAvatarImage(): ?ImageMedium;
public function getAvatarImage(): ?Medium;
/**
* Return the User's avatar URL.

View File

@@ -11,6 +11,8 @@ namespace Grav\Common\User\Traits;
use Grav\Common\Grav;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\StaticImageMedium;
use Grav\Common\User\Authentication;
use Grav\Common\Utils;
@@ -107,9 +109,9 @@ trait UserTrait
*
* Note: if there's no local avatar image for the user, you should call getAvatarUrl() to get the external avatar URL.
*
* @return ImageMedium|null
* @return Medium|null
*/
public function getAvatarImage(): ?ImageMedium
public function getAvatarImage(): ?Medium
{
$avatars = $this->get('avatar');
if (\is_array($avatars) && $avatars) {
@@ -119,7 +121,8 @@ trait UserTrait
$name = $avatar['name'] ?? null;
$image = $name ? $media[$name] : null;
if ($image instanceof ImageMedium) {
if ($image instanceof ImageMedium ||
$image instanceof StaticImageMedium) {
return $image;
}
}

View File

@@ -14,6 +14,8 @@ use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Markdown\Excerpts;
use Negotiation\Accept;
use Negotiation\Negotiator;
use RocketTheme\Toolbox\Event\Event;
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
*
@@ -1321,7 +1356,7 @@ abstract class Utils
*/
public static function sortArrayByArray(array $array, array $orderArray)
{
$ordered = array();
$ordered = [];
foreach ($orderArray as $key) {
if (array_key_exists($key, $array)) {
$ordered[$key] = $array[$key];
@@ -1476,16 +1511,16 @@ abstract class Utils
/**
* Parse a readable file size and return a value in bytes
*
* @param string|int $size
* @param string|int|float $size
* @return int
*/
public static function parseSize($size)
{
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
$size = preg_replace('/[^0-9\.]/', '', $size);
$size = (float)preg_replace('/[^0-9\.]/', '', $size);
if ($unit) {
$size = $size * pow(1024, stripos('bkmgtpezy', $unit[0]));
$size *= 1024 ** stripos('bkmgtpezy', $unit[0]);
}
return (int) abs(round($size));
@@ -1574,7 +1609,7 @@ abstract class Utils
}
// Packed representation of IP
$ip = inet_pton($ip);
$ip = (string)inet_pton($ip);
// Maximum netmask length = same as packed address
$len = 8*strlen($ip);

View File

@@ -0,0 +1,273 @@
<?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\Config\Config;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Uri;
use Grav\Console\ConsoleCommand;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Console\Input\InputOption;
class PageSystemValidatorCommand extends ConsoleCommand
{
protected $tests = [
// Content
'header' => [[]],
'summary' => [[], [200], [200, true]],
'content' => [[]],
'getRawContent' => [[]],
'rawMarkdown' => [[]],
'value' => [['content'], ['route'], ['order'], ['ordering'], ['folder'], ['slug'], ['name'], /*['frontmatter'],*/ ['header.menu'], ['header.slug']],
'title' => [[]],
'menu' => [[]],
'visible' => [[]],
'published' => [[]],
'publishDate' => [[]],
'unpublishDate' => [[]],
'process' => [[]],
'slug' => [[]],
'order' => [[]],
//'id' => [[]],
'modified' => [[]],
'lastModified' => [[]],
'folder' => [[]],
'date' => [[]],
'dateformat' => [[]],
'taxonomy' => [[]],
'shouldProcess' => [['twig'], ['markdown']],
'isPage' => [[]],
'isDir' => [[]],
'exists' => [[]],
// Forms
'forms' => [[]],
// Routing
'urlExtension' => [[]],
'routable' => [[]],
'link' => [[], [false], [true]],
'permalink' => [[]],
'canonical' => [[], [false], [true]],
'url' => [[], [true], [true, true], [true, true, false], [false, false, true, false]],
'route' => [[]],
'rawRoute' => [[]],
'routeAliases' => [[]],
'routeCanonical' => [[]],
'redirect' => [[]],
'relativePagePath' => [[]],
'path' => [[]],
//'folder' => [[]],
'parent' => [[]],
'topParent' => [[]],
'currentPosition' => [[]],
'active' => [[]],
'activeChild' => [[]],
'home' => [[]],
'root' => [[]],
// Translations
'translatedLanguages' => [[], [false], [true]],
'untranslatedLanguages' => [[], [false], [true]],
'language' => [[]],
// Legacy
'raw' => [[]],
'frontmatter' => [[]],
'httpResponseCode' => [[]],
'httpHeaders' => [[]],
'blueprintName' => [[]],
'name' => [[]],
'childType' => [[]],
'template' => [[]],
'templateFormat' => [[]],
'extension' => [[]],
'expires' => [[]],
'cacheControl' => [[]],
'ssl' => [[]],
'metadata' => [[]],
'eTag' => [[]],
'filePath' => [[]],
'filePathClean' => [[]],
'orderDir' => [[]],
'orderBy' => [[]],
'orderManual' => [[]],
'maxCount' => [[]],
'modular' => [[]],
'modularTwig' => [[]],
//'children' => [[]],
'isFirst' => [[]],
'isLast' => [[]],
'prevSibling' => [[]],
'nextSibling' => [[]],
'adjacentSibling' => [[]],
'ancestor' => [[]],
//'inherited' => [[]],
//'inheritedField' => [[]],
'find' => [['/']],
//'collection' => [[]],
//'evaluate' => [[]],
'folderExists' => [[]],
//'getOriginal' => [[]],
//'getAction' => [[]],
];
protected function configure()
{
$this
->setName('page-system-validator')
->setDescription('Page validator can be used to compare site before/after update and when migrating to Flex Pages.')
->addOption('record', 'r', InputOption::VALUE_NONE, 'Record results')
->addOption('check', 'c', InputOption::VALUE_NONE, 'Compare site against previously recorded results')
->setHelp('The <info>page-system-validator</info> command can be used to test the pages before and after upgrade');
}
protected function serve()
{
$this->output->writeln('');
$this->grav = $grav = Grav::instance();
$grav->setup();
// Initialize
$grav['uri']->init();
/** @var Config $config */
$config = $grav['config'];
$config->init();
$grav['plugins']->setup();
$grav['debugger']->init();
// Initialize the timezone.
$timezone = $config->get('system.timezone');
if ($timezone) {
date_default_timezone_set($timezone);
}
/** @var Uri $uri */
$uri = $grav['uri'];
$uri->init();
$grav->setLocale();
$grav['language']->setActive('en');
// Plugins
$grav['accounts'];
$grav['plugins']->init();
$grav->fireEvent('onPluginsInitialized');
// Themes
$grav['themes']->init();
// Twig
$grav['twig']->init();
// Pages
$grav['pages']->init();
$grav->fireEvent('onPagesInitialized', new Event(['pages' => $grav['pages']]));
if ($this->input->getOption('record')) {
$this->output->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
$this->output->writeln('<magenta>Record tests</magenta>');
$this->output->writeln('');
$results = $this->record();
$file = $this->getFile('pages-old');
$file->save($results);
$this->output->writeln('Recorded tests to ' . $file->filename());
} elseif ($this->input->getOption('check')) {
$this->output->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
$this->output->writeln('<magenta>Run tests</magenta>');
$this->output->writeln('');
$new = $this->record();
$file = $this->getFile('pages-new');
$file->save($new);
$this->output->writeln('Recorded tests to ' . $file->filename());
$file = $this->getFile('pages-old');
$old = $file->content();
$results = $this->check($old, $new);
$file = $this->getFile('diff');
$file->save($results);
$this->output->writeln('Recorded results to ' . $file->filename());
} else {
$this->output->writeln('<green>page-system-validator [-r|--record] [-c|--check]</green>');
}
$this->output->writeln('');
}
private function record()
{
$pages = $this->grav['pages'];
$all = $pages->all();
$results = [];
foreach ($all as $page) {
foreach ($this->tests as $method => $params) {
$params = $params ?: [[]];
foreach ($params as $p) {
$result = $page->$method(...$p);
if (in_array($method, ['summary', 'content', 'getRawContent'], true)) {
$result = preg_replace('/name="(form-nonce|__unique_form_id__)" value="[^"]+"/', 'name="\\1" value="DYNAMIC"', $result);
$result = preg_replace('`src=("|\'|&quot;)/images/./././././[^"]+\\1`', 'src="\\1images/GENERATED\\1', $result);
$result = preg_replace('/\?\d{10}/', '?1234567890', $result);
} elseif ($method === 'httpHeaders' && isset($result['Expires'])) {
$result['Expires'] = 'Thu, 19 Sep 2019 13:10:24 GMT (REPLACED AS DYNAMIC)';
} elseif ($result instanceof PageInterface) {
$result = $result->rawRoute();
} elseif (is_object($result)) {
$result = json_decode(json_encode($result), true);
}
$ps = [];
foreach ($p as $val) {
$ps[] = (string)var_export($val, true);
}
$pstr = implode(', ', $ps);
$call = "->{$method}({$pstr})";
$results[$page->rawRoute()][$call] = $result;
}
}
}
return json_decode(json_encode($results), true);
}
private function check(array $old, array $new)
{
$errors = [];
foreach ($old as $path => $page) {
if (!isset($new[$path])) {
$errors[$path] = 'PAGE REMOVED';
continue;
}
foreach ($page as $method => $test) {
if (($new[$path][$method] ?? null) !== $test) {
$errors[$path][$method] = ['old' => $test, 'new' => $new[$path][$method]];
}
}
}
return $errors;
}
/**
* @param string $name
* @return CompiledYamlFile
*/
private function getFile(string $name)
{
return CompiledYamlFile::instance('cache://tests/' . $name . '.yaml');
}
}

View File

@@ -74,7 +74,7 @@ class SchedulerCommand extends ConsoleCommand
// Show jobs list
$jobs = $scheduler->getAllJobs();
$job_states = $scheduler->getJobStates()->content();
$job_states = (array)$scheduler->getJobStates()->content();
$rows = [];
$table = new Table($this->output);
@@ -113,7 +113,7 @@ class SchedulerCommand extends ConsoleCommand
$io->newLine();
} elseif ($this->input->getOption('details')) {
$jobs = $scheduler->getAllJobs();
$job_states = $scheduler->getJobStates()->content();
$job_states = (array)$scheduler->getJobStates()->content();
$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;
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\Style\SymfonyStyle;
@@ -29,6 +27,18 @@ class YamlLinterCommand extends ConsoleCommand
InputOption::VALUE_OPTIONAL,
'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')
->setHelp("Checks various files for YAML errors");
}
@@ -42,41 +52,59 @@ class YamlLinterCommand extends ConsoleCommand
$io->title('Yaml Linter');
$io->section('User Configuration');
$errors = YamlLinter::lintConfig();
if ($this->input->getOption('all')) {
$io->section('All');
$errors = YamlLinter::lint('');
if (empty($errors)) {
$io->success('No YAML Linting issues with configuration');
if (empty($errors)) {
$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 {
$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) {
$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
{
/** @var array */
/** @var Packages */
protected $data;
/** @var GPM */
@@ -188,9 +188,9 @@ class IndexCommand extends ConsoleCommand
}
/**
* @param array $data
* @param Packages $data
*
* @return mixed
* @return Packages
*/
public function filter($data)
{
@@ -245,6 +245,7 @@ class IndexCommand extends ConsoleCommand
/**
* @param Packages $packages
* @return Packages
*/
public function sort($packages)
{

View File

@@ -551,10 +551,9 @@ class InstallCommand extends ConsoleCommand
/**
* @param Package $package
* @param string|null $license
*
* @param string $license
*
* @return string
* @return string|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(" | '- " . $error);
return false;
return null;
}
Folder::create($this->tmp);

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