Compare commits

...

161 Commits

Author SHA1 Message Date
Andy Miller
f3c82f85c8 Merge branch 'release/1.7.21' 2021-09-14 12:59:36 -06:00
Andy Miller
b17eaba8bf prepare for release 2021-09-14 12:59:29 -06:00
Matias Griese
0600d6a4d8 Merge remote-tracking branch 'origin/develop' into develop 2021-09-14 18:28:16 +03:00
Matias Griese
c51fb1779b Fixed Session::setFlashCookieObject() to use the same options as the main session cookie 2021-09-14 18:28:07 +03:00
Andy Miller
34b7a28fbe update changelog 2021-09-13 17:42:30 -06:00
Andy Miller
a446152631 Merge branch 'develop' of github.com:getgrav/grav into develop 2021-09-13 17:41:08 -06:00
Andy Miller
61c2abee35 use a simple cron text field as custom one was confusing 2021-09-13 17:40:55 -06:00
Andy Miller
c1d520f1cf Add date/time to text file output of scheduler 2021-09-13 17:40:39 -06:00
Matias Griese
3bd9e44155 Remove extra space #3432 2021-09-13 16:51:06 +03:00
Matias Griese
7311517d65 Fixed incorrect port :0 with nginx unix socket setup [#3439] 2021-09-13 12:34:42 +03:00
Matias Griese
4568a197e7 Fixed excessive security.yaml file creation [#3432] 2021-09-13 11:53:12 +03:00
Matias Griese
350134b256 Renamed conflicting $object->getOriginal() to $object->getOriginalData() 2021-09-10 19:09:56 +03:00
Matias Griese
1350cf5675 Added $object->getOriginal() to get flex objects data before it was modified with update() 2021-09-09 14:14:42 +03:00
Matias Griese
21db2e7d4a Added support for flex-required@: not exists and flex-required@: '!exists' in blueprints 2021-09-09 14:14:16 +03:00
Matias Griese
cda08242f1 Added file upload/remove support for Flex Forms 2021-09-08 14:34:15 +03:00
Matias Griese
fc8936986f Cast data.search.options to array 2021-09-07 18:28:36 +03:00
Matias Griese
ec37fd065f Throwing exceptions from Twig templates fires onDisplayErrorPage.[code] event allowing better error pages 2021-09-03 18:39:41 +03:00
Matias Griese
47875a4525 Added route and request to onPageNotFound event 2021-09-03 12:23:48 +03:00
Matias Griese
61adb1e6cf More robust way to check number step [#3433] 2021-09-02 20:34:17 +03:00
Matias Griese
907e46631c Added |yaml filter to convert input to YAML 2021-09-02 20:02:07 +03:00
Matias Griese
aedf8cda47 Changelog update [#3433] 2021-09-02 10:57:10 +03:00
Matias Griese
7a1f5539ed Fixed validation of number type 2021-09-02 10:55:27 +03:00
Matias Griese
49087e9a53 Fixed escaping in PageIndex::getLevelListing() 2021-09-02 10:20:26 +03:00
Andy Miller
a128c7f18d Merge branch 'release/1.7.20' 2021-09-01 10:39:40 -06:00
Andy Miller
c6704d8129 Merge tag '1.7.20' into develop
Release v1.7.20
2021-09-01 10:39:40 -06:00
Andy Miller
c43b375d3b prepare for release 2021-09-01 10:39:23 -06:00
Matias Griese
9523bab910 Merge remote-tracking branch 'origin/develop' into develop 2021-08-31 22:25:15 +03:00
Matias Griese
a8fe62a829 Added support for task and action inside JSON request body 2021-08-31 22:25:02 +03:00
Andy Miller
4708a46ec9 Merge branch 'release/1.7.19' 2021-08-31 13:08:46 -06:00
Andy Miller
7a99aaa53f Merge tag '1.7.19' into develop
Release v1.7.19
2021-08-31 13:08:46 -06:00
Andy Miller
e54e488f80 Merge tag '1.7.19' into develop
Release v1.7.19
2021-08-31 12:21:42 -06:00
Andy Miller
bf471cc3fa Merge branch 'release/1.7.19' 2021-08-31 12:21:42 -06:00
Andy Miller
fd8c44ba90 prepare for release 2021-08-31 12:21:31 -06:00
Matias Griese
f9e7f1c08e Flex: Use str_replace() and not strtr() 2021-08-30 09:55:21 +03:00
Andy Miller
8042caee57 fixed svgImageFunction() 2021-08-27 10:26:00 -06:00
Matias Griese
3f3f63f411 Fixed Flex object types not implementing MediaInterface 2021-08-25 18:50:08 +03:00
Matias Griese
292687ea00 Fixed wrong form issue with flex objects after cache clear 2021-08-25 18:31:58 +03:00
Andy Miller
aa47cb7b97 ignore cli/security.yaml 2021-08-18 14:04:21 -06:00
Djamil Legato
794237bf30 Minor indentation config tweaks 2021-08-18 09:31:15 -07:00
Rotzbua
de3aa16aca add mime for .avif image format
new image format developed by google

references:
https://codelabs.developers.google.com/codelabs/avif#0
https://caniuse.com/avif
2021-08-18 11:19:38 +03:00
Rotzbua
34d001cbef fix typo mime type of .aif (#3423)
mime type should be `audio/aiff`
reference: https://en.wikipedia.org/wiki/Audio_Interchange_File_Format
2021-08-13 10:58:35 -04:00
Andy Miller
21bd51aef9 remove sensio 2021-08-13 09:59:03 -04:00
Matias Griese
f45afd1f54 Added support for multiple mime-types per file extension [#3422] 2021-08-13 13:05:32 +03:00
Matias Griese
c975f894ae Composer update 2021-08-11 09:07:57 +03:00
Matias Griese
a9b59596d8 Fixed flex-options@ in blueprints duplicating items in array 2021-08-10 12:38:07 +03:00
Matias Griese
9333fcc1d6 Generalize FolderStorage templating 2021-08-09 21:38:01 +03:00
Matias Griese
b3426f86a3 Merge remote-tracking branch 'origin/develop' into develop 2021-08-09 19:36:23 +03:00
Matias Griese
b3e9682511 Generalize FolderStorage templating 2021-08-09 19:36:14 +03:00
Rotzbua
b3af6c9920 Change mime-type to text/javascript (#3415)
Recommended by whatwg and draft-ietf-dispatch-javascript-mjs-09 (obsoletes RFC4329)
2021-07-31 14:29:22 -06:00
Matias Griese
2e9fe80e33 Initialize $grav['uri] before session 2021-07-30 09:31:33 +03:00
Matias Griese
def389356e Added UserObject::$authorizeCallable to allow $user->authorize() customization 2021-07-29 23:09:25 +03:00
Matias Griese
c5dfa65994 Fixed FlexDirectoryForm serialization 2021-07-29 22:14:45 +03:00
Matias Griese
c9159695aa Fixed FlexForm serialization 2021-07-29 19:59:42 +03:00
Matias Griese
17d1786e5c Fixed broken Twig try tag when catch has not been defined or is empty 2021-07-29 16:12:14 +03:00
Matias Griese
5437d2db1a Fixed Flex Object missing key field value when using FolderStorage 2021-07-28 14:31:55 +03:00
Matias Griese
e390e9901e Allow customization of security.yaml 2021-07-28 14:29:10 +03:00
Matias Griese
7c946c59f8 Include request in onPageTask and onPageAction events (defaults to null) 2021-07-26 17:27:30 +03:00
Matias Griese
506c74de55 Include active form in onPageTask and onPageAction events (defaults to null) 2021-07-26 14:39:40 +03:00
Matias Griese
ab9783102e Fixed broken environment:// stream when it doesn't have configuration 2021-07-23 21:51:17 +03:00
Matias Griese
a8a8cce25f Fixed GPM not using non-standard cache path [#3410] 2021-07-23 09:39:21 +03:00
Matias Griese
d62e869044 Add changelog [#3398] 2021-07-22 17:17:15 +03:00
Karmalakas
6dd5e0fd20 Change key setting in a loop
#531
2021-07-22 17:13:42 +03:00
Karmalakas
c57a29c23f Add setCurrent() method to Page Collection
#531
2021-07-22 17:13:42 +03:00
Matias Griese
2866a51326 Added meta support for UploadedFile class 2021-07-22 17:01:55 +03:00
Matias Griese
21f5488d3b Make MediaUploadTrait::getUploadSettings() public 2021-07-22 17:00:16 +03:00
Matias Griese
7b1a188cfe Fixed error in loadDirectoryConfig() if configuration hasn't been saved [#3409] 2021-07-22 16:59:42 +03:00
Matias Griese
fdcf7026d2 Changelog for #3408 2021-07-20 10:48:22 +03:00
Andy Miller
b8ada23e2b fixes #3408 2021-07-19 22:35:57 -06:00
Andy Miller
5def813a2e Merge branch 'release/1.7.18' 2021-07-19 12:16:38 -06:00
Andy Miller
551a8251f9 Merge tag '1.7.18' into develop
Release v1.7.18
2021-07-19 12:16:38 -06:00
Andy Miller
951ce6f9f8 prepare for release 2021-07-19 12:15:54 -06:00
Matias Griese
c9448870fa Fixed open_basedir() error with some forms 2021-07-08 14:00:53 +03:00
Andy Miller
1d552ab603 updated changelog 2021-07-06 17:46:08 -06:00
Andy Miller
e39d01e139 Support cloudflare + better x-forwarded-for 2021-07-06 17:44:49 -06:00
Robert Bak
d4805bc709 Fixes issue with Symfony local server detection (#3400)
I was just doing the same thing!
2021-07-06 14:40:12 -06:00
Matias Griese
35db2f61f7 Added method FlexObject::resetBlueprints() 2021-07-06 21:39:14 +03:00
Andy Miller
8af1229f65 qurantine bad SVGs 2021-07-01 15:36:26 -06:00
Matias Griese
9aa6f5b1f7 Fixed Failed to save entry: Forbidden when moving a page to a visible page [#3389] 2021-07-01 14:45:24 +03:00
Matias Griese
da8e374443 Fixed Admin becoming unusable when GPM cannot be reached [#3383] 2021-06-24 17:04:04 +03:00
Matias Griese
95851e8f52 Improve page search to include slug [#3316] 2021-06-24 15:34:14 +03:00
Matias Griese
d2350b6786 Fixed error when using Flex SimpleStorage with no entries 2021-06-23 14:45:56 +03:00
Matias Griese
08a2abb713 Added support for loading Flex Directory configuration from main configuration 2021-06-16 21:35:43 +03:00
Andy Miller
ac62f54aa5 Merge branch 'release/1.7.17' 2021-06-15 13:11:35 -06:00
Andy Miller
fb189a3ce4 Merge tag '1.7.17' into develop
Release v1.7.17
2021-06-15 13:11:35 -06:00
Andy Miller
7e41938317 prepare for release 2021-06-15 13:11:26 -06:00
Andy Miller
d90b28a399 fix jquery in tests 2021-06-15 10:20:57 -06:00
Andy Miller
90f5635478 Switch to JQuery 3.x rather than 2.x 2021-06-15 10:15:12 -06:00
Djamil Legato
acf8724402 Added tests for new params support in collections (#3358) 2021-06-14 16:03:22 -07:00
Djamil Legato
f1c623c14b Fixed mutability issue when adding multiple assets with different params (#3358) 2021-06-14 11:55:14 -07:00
Djamil Legato
ee40ad59f2 Ensure simple second argument for priority is also supported in multi-collections with params (fixes #3358) 2021-06-14 10:25:16 -07:00
Matias Griese
845fac8adf Fixed permission check when moving a page [#3382] 2021-06-10 16:27:30 +03:00
Matias Griese
896695b30f Interface FlexDirectoryInterface now extends FlexAuthorizeInterface 2021-06-10 13:55:12 +03:00
Matias Griese
fbfa88739d Changelog update 2021-06-10 13:19:47 +03:00
Matias Griese
ea191602da Fixed missing styles when CSS/JS Pipeline is used and assets/ folder is missing 2021-06-10 13:16:20 +03:00
Matias Griese
564287eb21 Composer update 2021-06-10 09:23:41 +03:00
Matias Griese
28790197aa File frontmatter.yaml isn't part of media, ignore it 2021-06-08 09:28:30 +03:00
Djamil Legato
1db66fd43d Ported support for multi-parameter assignment in asset collection to method add (fixes #3358) 2021-06-04 11:21:17 -07:00
Djamil Legato
8d506db73c Added support specifying custom attributes to assets in a collection (fixes #3358) 2021-06-03 15:59:01 -07:00
Djamil Legato
c288d4bd0b Allow to unset an asset attribute by specifying null (ie, ’defer’ => null) 2021-06-03 15:51:24 -07:00
Djamil Legato
39247ac7ef Updated blueprints inline-docs for clear_images_by_default 2021-06-02 23:35:04 -07:00
Andy Miller
47b5b10bf4 Merge tag '1.7.16' into develop
Release v1.7.16
2021-06-02 12:23:38 -06:00
Andy Miller
ad1cf15d7c Merge branch 'release/1.7.16' 2021-06-02 12:23:37 -06:00
Andy Miller
6339d9f3cd prepare for release 2021-06-02 12:23:27 -06:00
Andy Miller
fc36a76fc0 updated changelog 2021-06-02 12:22:42 -06:00
phmg701
18d7fd4c7d Adding the addFrame method to ImageMedium (#3323)
* Update ImageMediaTrait.php

Add needed magic actions to enable image frame creation.

* Update ImageMedium.php

Adds addFrame method that can be (recursively) called from Twig.
2021-06-01 16:38:40 -06:00
Djamil Legato
76e44a1043 More regex fixes for URI 2021-06-01 15:16:39 -07:00
Andy Miller
4c0d107562 stupid fix for homebrew PHP 7.4.18+ 2021-06-01 14:25:52 -06:00
Djamil Legato
d359120d81 Fixed SRI trying to calculate remote assets, only ever set integrity for local files. Use the SRI provided by the remote source and manually add it in the addJs/addCss call for remote support. (fixes #3358)
Also Fixed wrong SRI paths invoked when Grav instance as a sub folder
2021-05-27 12:26:25 -07:00
Matias Griese
278671deec Fixed flex pages using wrong type in onBlueprintCreated event [#3157] 2021-05-27 14:18:12 +03:00
Matias Griese
e28360f86a Fixed flex pages search using only folder name [#3316] 2021-05-27 13:17:24 +03:00
Matias Griese
75cef03644 Improve error on bad nested form data [#3364] 2021-05-26 11:43:20 +03:00
Matias Griese
33be6946f7 Fixed the first visible child page getting ordering number 999999. [#3365] 2021-05-26 11:16:31 +03:00
Andy Miller
7f23b088a4 set clear_images_by_default to false by default 2021-05-25 11:27:13 -06:00
Matias Griese
c56f9f3277 Fixed pathinfo() twig filter in PHP 7 2021-05-24 10:01:24 +03:00
Matias Griese
6198d5abf3 Improve Plugin and Theme initialization to fix PHP8 bug [#3368] 2021-05-22 12:28:15 +03:00
Andy Miller
3f8c51cc01 Merge tag '1.7.15' into develop
Release v1.7.15
2021-05-19 13:22:30 -06:00
Andy Miller
b693ed4071 Merge branch 'release/1.7.15' 2021-05-19 13:22:29 -06:00
Andy Miller
5621f5cdb0 prepare for release 2021-05-19 13:22:18 -06:00
Andy Miller
83f2097f40 trim extra line breaks 2021-05-18 21:58:37 -06:00
Matias Griese
879de1d95e Rename property for onOutputGenerated and onOutputRendered events 2021-05-12 14:52:41 +03:00
Matias Griese
ec018f40aa Minor update for onOutputGenerated and onOutputRendered events 2021-05-12 14:51:11 +03:00
Matias Griese
0866753617 Added page and content properties to onOutputGenerated and onOutputRendered events 2021-05-12 14:49:34 +03:00
Matias Griese
5f66f2c4a9 Fixed uploading images into Flex Object if field destination is not set 2021-05-12 10:36:25 +03:00
Matias Griese
40f08a7f8b Flex: Mark uploaded media as uploaded 2021-05-10 20:08:18 +03:00
Matias Griese
c274337fed Minor fix on user avatar upload 2021-05-10 18:03:56 +03:00
Matias Griese
7d01977a89 Fixed missing and commonly used methods when using system.twig.undefined_functions = false 2021-05-10 12:27:11 +03:00
Matias Griese
d058c1d4fc Fixed copying page without changing the slug 2021-05-10 11:43:16 +03:00
Matias Griese
1a28155f1c Update docblocks to return the actual type [#3350] 2021-05-07 13:26:24 +03:00
Matias Griese
7ca7d8e045 Allow optional start date in page collections [#3350] 2021-05-07 13:23:38 +03:00
Thibaut HENIN
ec98ddc2df Improve PageCollectionInterface::dateRange signature and documentation. Report changes into implementations. 2021-05-07 13:19:09 +03:00
Thibaut HENIN
72b520745a Simplify Collection::dateRange (same structure as PageCollection::dateRange) 2021-05-07 13:19:09 +03:00
Thibaut HENIN
9b4f0ca951 Fix dateRange when startDate is not provided (value of 0) 2021-05-07 13:19:09 +03:00
Matias Griese
a761df80db Fixed markdown media operations not working when using image:// stream [#3333] [#3349] 2021-05-07 13:09:34 +03:00
Matias Griese
9059904c1a Fixed fatal error with some markdown links 2021-05-07 11:53:32 +03:00
Matias Griese
568e728d20 Fixed twig deprecated TwigFilter messages [#3348] 2021-05-06 15:02:54 +03:00
Andy Miller
e56d414357 better uploader to allow rebuilds 2021-05-05 15:14:45 -06:00
Andy Miller
668f8ccdbf Merge branch 'release/1.7.14' 2021-04-29 10:22:27 -06:00
Andy Miller
9281be57fc Merge tag '1.7.14' into develop
Release v1.7.14
2021-04-29 10:22:27 -06:00
Andy Miller
d1e58eb95e prepare for release 2021-04-29 10:22:16 -06:00
Matias Griese
9f5a15f00a Updating a theme should always keep the custom files 2021-04-29 15:37:44 +03:00
Matias Griese
3aa47043c9 Added option to set flash folder in FlexForm 2021-04-28 13:58:08 +03:00
Matias Griese
8532db70d2 Merge remote-tracking branch 'origin/develop' into develop 2021-04-28 12:44:33 +03:00
Matias Griese
f8106a48ae Added MediaUploadTrait::checkFileMetadata() method 2021-04-28 12:44:21 +03:00
Ricardo
2813934d21 fix for when no valid link attributes are present 2021-04-26 21:20:19 +01:00
Matias Griese
defb793b0b Hide phpstan deprecation errors on Doctrine Cache 2021-04-26 10:54:39 +03:00
Matias Griese
1fd2162d4f Fixed broken exif_imagetype() twig function, composer update 2021-04-26 10:50:35 +03:00
Matias Griese
094a1bd5ee Changelog update 2021-04-26 09:56:39 +03:00
Matias Griese
2cf7a5f281 Fixed broken numeric language codes in Flex Pages [#2131] 2021-04-26 09:53:21 +03:00
Andy Miller
2ed451130c Merge branch 'release/1.7.13' 2021-04-23 12:55:38 -06:00
Andy Miller
4d1f88627a Merge tag '1.7.13' into develop
Release v1.7.13
2021-04-23 12:55:38 -06:00
Andy Miller
ed7e51480b prepare for release 2021-04-23 12:55:28 -06:00
Matias Griese
3e91be9a4d Added support for getting translated collection of Flex Pages using $collection->withTranslated('de') 2021-04-23 15:14:02 +03:00
Matias Griese
3e9bfad78f Added support for user provided folder in Flex $page->copy() 2021-04-23 15:10:43 +03:00
Matias Griese
fd34fce3c1 Fixed a bug in Flex Object refresh() method 2021-04-22 13:49:11 +03:00
Matias Griese
0cf684300d Fixed text field maxlength validation newline issue [#3324] 2021-04-22 11:44:11 +03:00
Djamil Legato
fe1c808dfd Have folder field for pages blueprints use the new folder-slug filed type 2021-04-20 15:53:51 -07:00
Andy Miller
aa8c67061c typo 2021-04-20 14:47:12 -06:00
Andy Miller
8e8de1eeec minor improvement in XSS invalid_protocol regex - #3298 2021-04-20 14:36:53 -06:00
Djamil Legato
b9fb284a52 Moved gregwar/Image and gregwar/Cache in-house to official getgrav/Image and getgrav/Cache packagist packages. This will help environments with very strict proxy setups that don't allow VCS setup. (fixes #3289) 2021-04-19 23:15:22 -07:00
Matias Griese
5efe447861 Fixed The "Grav/Common/Twig/TwigExtension" extension is not enabled when using markdown twig tag [#3317] 2021-04-19 10:20:26 +03:00
Andy Miller
fb8d76922a Merge tag '1.7.12' into develop
Release v1.7.12
2021-04-15 12:03:43 -06:00
78 changed files with 4010 additions and 734 deletions

View File

@@ -13,5 +13,5 @@ indent_size = 4
trim_trailing_whitespace = true
# 2 space indentation
[*.{yaml,yml}]
[*.{yaml,yml,vue,js,css}]
indent_size = 2

View File

@@ -12,6 +12,9 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Extract Tag
run: echo "PACKAGE_VERSION=${{ github.ref }}" >> $GITHUB_ENV
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
@@ -38,13 +41,14 @@ jobs:
run: |
bash ./build-grav.sh
- name: Upload Grav Release Assets
id: upload-release-asset
uses: alexellis/upload-assets@0.2.3
env:
GITHUB_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
- name: Upload packages to release
uses: svenstaro/upload-release-action@v2
with:
asset_paths: '["./grav-dist/*.zip"]'
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.PACKAGE_VERSION }}
file: ./grav-dist/*.zip
overwrite: true
file_glob: true
slack:
name: Slack

1
.gitignore vendored
View File

@@ -45,3 +45,4 @@ tests/cache/*
tests/error.log
system/templates/testing/*
/user/config/versions.yaml
/user/cli/config/security.yaml

View File

@@ -1,3 +1,140 @@
# v1.7.21
## 09/14/2021
1. [](#new)
* Added `|yaml` filter to convert input to YAML
* Added `route` and `request` to `onPageNotFound` event
* Added file upload/remove support for `Flex Forms`
* Added support for `flex-required@: not exists` and `flex-required@: '!exists'` in blueprints
* Added `$object->getOriginalData()` to get flex objects data before it was modified with `update()`
* Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages
2. [](#improved)
* Use a simplified text-based `cron` field for scheduler
* Add timestamp to logging output of scheduler jobs to see when they ran
3. [](#bugfix)
* Fixed escaping in PageIndex::getLevelListing()
* Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433)
* Fixed excessive `security.yaml` file creation [#3432](https://github.com/getgrav/grav/issues/3432)
* Fixed incorrect port :0 with nginx unix socket setup [#3439](https://github.com/getgrav/grav/issues/3439)
* Fixed `Session::setFlashCookieObject()` to use the same options as the main session cookie
# v1.7.20
## 09/01/2021
2. [](#improved)
* Added support for `task` and `action` inside JSON request body
# v1.7.19
## 08/31/2021
1. [](#new)
* Include active form and request in `onPageTask` and `onPageAction` events (defaults to `null`)
* Added `UserObject::$authorizeCallable` to allow `$user->authorize()` customization
2. [](#improved)
* Added meta support for `UploadedFile` class
* Added support for multiple mime-types per file extension [#3422](https://github.com/getgrav/grav/issues/3422)
* Added `setCurrent()` method to Page Collection [#3398](https://github.com/getgrav/grav/pull/3398)
* Initialize `$grav['uri']` before session
3. [](#bugfix)
* Fixed `Warning: Undefined array key "SERVER_SOFTWARE" in index.php` [#3408](https://github.com/getgrav/grav/issues/3408)
* Fixed error in `loadDirectoryConfig()` if configuration hasn't been saved [#3409](https://github.com/getgrav/grav/issues/3409)
* Fixed GPM not using non-standard cache path [#3410](https://github.com/getgrav/grav/issues/3410)
* Fixed broken `environment://` stream when it doesn't have configuration
* Fixed `Flex Object` missing key field value when using `FolderStorage`
* Fixed broken Twig try tag when catch has not been defined or is empty
* Fixed `FlexForm` serialization
* Fixed form validation for numeric values in PHP 8
* Fixed `flex-options@` in blueprints duplicating items in array
* Fixed wrong form issue with flex objects after cache clear
* Fixed Flex object types not implementing `MediaInterface`
* Fixed issue with `svgImageFunction()` that was causing broken output
# v1.7.18
## 07/19/2021
1. [](#improved)
* Added support for loading Flex Directory configuration from main configuration
* Move SVGs that cannot be sanitized to quarantine folder under `log://quarantine`
* Added support for CloudFlare-forwarded client IP in the `URI::ip()` method
1. [](#bugfix)
* Fixed error when using Flex `SimpleStorage` with no entries
* Fixed page search to include slug field [#3316](https://github.com/getgrav/grav/issues/3316)
* Fixed Admin becoming unusable when GPM cannot be reached [#3383](https://github.com/getgrav/grav/issues/3383)
* Fixed `Failed to save entry: Forbidden` when moving a page to a visible page [#3389](https://github.com/getgrav/grav/issues/3389)
* Better support for Symfony local server on linux [#3400](https://github.com/getgrav/grav/pull/3400)
* Fixed `open_basedir()` error with some forms
# v1.7.17
## 06/15/2021
1. [](#new)
* Interface `FlexDirectoryInterface` now extends `FlexAuthorizeInterface`
1. [](#improved)
* Allow to unset an asset attribute by specifying null (ie, `'defer': null`)
* Support specifying custom attributes to assets in a collection [Read more](https://learn.getgrav.org/17/themes/asset-manager#collections-with-attributes?target=_blank) [#3358](https://github.com/getgrav/grav/issues/3358)
* File `frontmatter.yaml` isn't part of media, ignore it
* Switched default `JQuery` collection to use 3.x rather than 2.x
1. [](#bugfix)
* Fixed missing styles when CSS/JS Pipeline is used and `asset://` folder is missing
* Fixed permission check when moving a page [#3382](https://github.com/getgrav/grav/issues/3382)
# v1.7.16
## 06/02/2021
1. [](#new)
* Added 'addFrame()' method to ImageMedium [#3323](https://github.com/getgrav/grav/pull/3323)
1. [](#improved)
* Set `cache.clear_images_by_default` to `false` by default
* Improve error on bad nested form data [#3364](https://github.com/getgrav/grav/issues/3364)
1. [](#bugfix)
* Improve Plugin and Theme initialization to fix PHP8 bug [#3368](https://github.com/getgrav/grav/issues/3368)
* Fixed `pathinfo()` twig filter in PHP7
* Fixed the first visible child page getting ordering number `999999.` [#3365](https://github.com/getgrav/grav/issues/3365)
* Fixed flex pages search using only folder name [#3316](https://github.com/getgrav/grav/issues/3316)
* Fixed flex pages using wrong type in `onBlueprintCreated` event [#3157](https://github.com/getgrav/grav/issues/3157)
* Fixed wrong SRI paths invoked when Grav instance as a sub folder [#3358](https://github.com/getgrav/grav/issues/3358)
* Fixed SRI trying to calculate remote assets, only ever set integrity for local files. Use the SRI provided by the remote source and manually add it in the `addJs/addCss` call for remote support. [#3358](https://github.com/getgrav/grav/issues/3358)
* Fix for weird regex issue with latest PHP versions on Intel Macs causing params to not parse properly in URI object
# v1.7.15
## 05/19/2021
1. [](#improved)
* Allow optional start date in page collections [#3350](https://github.com/getgrav/grav/pull/3350)
* Added `page` and `output` properties to `onOutputGenerated` and `onOutputRendered` events
1. [](#bugfix)
* Fixed twig deprecated TwigFilter messages [#3348](https://github.com/getgrav/grav/issues/3348)
* Fixed fatal error with some markdown links [getgrav/grav-premium-issues#95](https://github.com/getgrav/grav-premium-issues/issues/95)
* Fixed markdown media operations not working when using `image://` stream [#3333](https://github.com/getgrav/grav/issues/3333) [#3349](https://github.com/getgrav/grav/issues/3349)
* Fixed copying page without changing the slug [getgrav/grav-plugin-admin#2135](https://github.com/getgrav/grav-plugin-admin/issues/2139)
* Fixed missing and commonly used methods when using `system.twig.undefined_functions = false` [getgrav/grav-plugin-admin#2138](https://github.com/getgrav/grav-plugin-admin/issues/2138)
* Fixed uploading images into Flex Object if field destination is not set
# v1.7.14
## 04/29/2021
1. [](#new)
* Added `MediaUploadTrait::checkFileMetadata()` method
1. [](#improved)
* Updating a theme should always keep the custom files [getgrav/grav-plugin-admin#2135](https://github.com/getgrav/grav-plugin-admin/issues/2135)
1. [](#bugfix)
* Fixed broken numeric language codes in Flex Pages [#3332](https://github.com/getgrav/grav/issues/3332)
* Fixed broken `exif_imagetype()` twig function
# v1.7.13
## 04/23/2021
1. [](#new)
* Added support for getting translated collection of Flex Pages using `$collection->withTranslated('de')`
1. [](#improved)
* Moved `gregwar/Image` and `gregwar/Cache` in-house to official `getgrav/Image` and `getgrav/Cache` packagist packages. This will help environments with very strict proxy setups that don't allow VCS setup. [#3289](https://github.com/getgrav/grav/issues/3289)
* Improved XSS Invalid Protocol detection regex [#3298](https://github.com/getgrav/grav/issues/3298)
* Added support for user provided folder in Flex `$page->copy()`
1. [](#bugfix)
* Fixed `The "Grav/Common/Twig/TwigExtension" extension is not enabled` when using markdown twig tag [#3317](https://github.com/getgrav/grav/issues/3317)
* Fixed text field maxlength validation newline issue [#3324](https://github.com/getgrav/grav/issues/3324)
* Fixed a bug in Flex Object `refresh()` method
# v1.7.12
## 04/15/2021

View File

@@ -1,7 +1,6 @@
# ![](https://avatars1.githubusercontent.com/u/8237355?v=2&s=50) Grav
[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad/mini.png)](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
[![Discord](https://img.shields.io/discord/501836936584101899.svg?logo=discord&colorB=728ADA&label=Discord%20Chat)](https://chat.getgrav.org)
[![PHP Tests](https://github.com/getgrav/grav/workflows/PHP%20Tests/badge.svg?branch=develop)](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [![OpenCollective](https://opencollective.com/grav/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/grav/sponsors/badge.svg)](#sponsors)

View File

@@ -44,8 +44,8 @@
"filp/whoops": "~2.9",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.25",
"gregwar/image": "dev-php8",
"gregwar/cache": "dev-php8",
"getgrav/image": "^3.0",
"getgrav/cache": "^2.0",
"donatj/phpuseragentparser": "~1.1",
"pimple/pimple": "~3.3.0",
"rockettheme/toolbox": "~1.5",
@@ -67,7 +67,7 @@
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpunit/php-code-coverage": "~9.2",
"victorjonsson/markdowndocs": "dev-master",
"getgrav/markdowndocs": "^2.0",
"codeception/module-asserts": "^1.3",
"codeception/module-phpbrowser": "^1.0",
"symfony/service-contracts": "*"
@@ -91,20 +91,6 @@
"php": "7.3.6"
}
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator"
},
{
"type": "vcs",
"url": "https://github.com/getgrav/Cache"
},
{
"type": "vcs",
"url": "https://github.com/getgrav/Image"
}
],
"autoload": {
"psr-4": {
"Grav\\": "system/src/Grav"

763
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,8 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
}
if (PHP_SAPI === 'cli-server') {
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'symfony
') !== false;
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', '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>");
}

View File

@@ -47,7 +47,8 @@ form:
label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
placeholder: '-lah'
.at:
type: cron
type: text
wrapper_classes: cron-selector
label: PLUGIN_ADMIN.SCHEDULER_RUNAT
help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
placeholder: '* * * * *'

View File

@@ -646,7 +646,7 @@ form:
type: toggle
label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT
help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP
highlight: 1
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO

View File

@@ -184,9 +184,9 @@ config:
# Fields to be searched
fields:
- key
- slug
- menu
- title
- name
blueprints:
configure:

View File

@@ -121,7 +121,7 @@ form:
underline: true
folder:
type: text
type: folder-slug
label: PLUGIN_ADMIN.FOLDER_NAME
validate:
rule: slug

View File

@@ -28,6 +28,10 @@ types:
type: image
thumb: media/thumb-webp.png
mime: image/webp
avif:
type: image
thumb: media/thumb.png
mime: image/avif
gif:
type: animated
thumb: media/thumb-gif.png
@@ -91,7 +95,7 @@ types:
aif:
type: audio
thumb: media/thumb-aif.png
mime: audio/aif
mime: audio/aiff
txt:
type: file
thumb: media/thumb-txt.png
@@ -207,7 +211,7 @@ types:
js:
type: file
thumb: media/thumb-js.png
mime: application/javascript
mime: text/javascript
json:
type: file
thumb: media/thumb-json.png

1986
system/config/mime.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -96,7 +96,7 @@ cache:
purge_at: '0 4 * * *' # How often to purge old file cache (using new scheduler)
clear_at: '0 3 * * *' # How often to clear cache (using new scheduler)
clear_job_type: 'standard' # Type to clear when processing the scheduled clear job `standard`|`all`
clear_images_by_default: true # By default grav will include processed images in cache clear, this can be disabled
clear_images_by_default: false # By default grav does not include processed images in cache clear, this can be enabled
cli_compatibility: false # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite)
gzip: false # GZip compress the page output
@@ -131,7 +131,7 @@ assets: # Configuration for Assets Mana
enable_asset_timestamp: false # Enable asset timestamps
enable_asset_sri: false # Enable asset SRI
collections:
jquery: system://assets/jquery/jquery-2.x.min.js
jquery: system://assets/jquery/jquery-3.x.min.js
errors:
display: 0 # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error

View File

@@ -9,7 +9,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.12');
define('GRAV_VERSION', '1.7.21');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);

View File

@@ -110,7 +110,7 @@ class Assets extends PropertyObject
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$this->assets_dir = $locator->findResource('asset://') . DS;
$this->assets_dir = $locator->findResource('asset://');
$this->assets_url = $locator->findResource('asset://', false);
$this->config($asset_config);
@@ -164,10 +164,19 @@ class Assets extends PropertyObject
// More than one asset
if (is_array($asset)) {
foreach ($asset as $a) {
array_shift($args);
$args = array_merge([$a], $args);
call_user_func_array([$this, 'add'], $args);
foreach ($asset as $index => $location) {
$params = array_slice($args, 1);
if (is_array($location)) {
$params = array_shift($params);
if (is_numeric($params)) {
$params = [ 'priority' => $params ];
}
$params = [array_replace_recursive([], $location, $params)];
$location = $index;
}
$params = array_merge([$location], $params);
call_user_func_array([$this, 'add'], $params);
}
} elseif (isset($this->collections[$asset])) {
array_shift($args);
@@ -201,8 +210,13 @@ class Assets extends PropertyObject
protected function addType($collection, $type, $asset, $options)
{
if (is_array($asset)) {
foreach ($asset as $a) {
$this->addType($collection, $type, $a, $options);
foreach ($asset as $index => $location) {
$assetOptions = $options;
if (is_array($location)) {
$assetOptions = array_replace_recursive([], $options, $location);
$location = $index;
}
$this->addType($collection, $type, $location, $assetOptions);
}
return $this;

View File

@@ -15,6 +15,7 @@ use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Object\PropertyObject;
use RocketTheme\Toolbox\File\File;
use SplFileInfo;
/**
@@ -182,16 +183,21 @@ abstract class BaseAsset extends PropertyObject
public static function integrityHash($input)
{
$grav = Grav::instance();
$uri = $grav['uri'];
$assetsConfig = $grav['config']->get('system.assets');
if ( !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri'] )
{
$dataToHash = file_get_contents( GRAV_WEBROOT . $input);
if (!self::isRemoteLink($input) && !empty($assetsConfig['enable_asset_sri']) && $assetsConfig['enable_asset_sri']) {
$input = preg_replace('#^' . $uri->rootUrl() . '#', '', $input);
$asset = File::instance(GRAV_WEBROOT . $input);
$hash = hash('sha256', $dataToHash, true);
$hash_base64 = base64_encode($hash);
return ' integrity="sha256-' . $hash_base64 . '"';
if ($asset->exists()) {
$dataToHash = $asset->content();
$hash = hash('sha256', $dataToHash, true);
$hash_base64 = base64_encode($hash);
return ' integrity="sha256-' . $hash_base64 . '"';
}
}
return '';

View File

@@ -9,9 +9,9 @@
namespace Grav\Common\Assets;
use Grav\Common\Assets\BaseAsset;
use Grav\Common\Assets\Traits\AssetUtilsTrait;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
@@ -88,7 +88,14 @@ class Pipeline extends PropertyObject
$uri = Grav::instance()['uri'];
$this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
$this->assets_dir = $locator->findResource('asset://') . DS;
$this->assets_dir = $locator->findResource('asset://');
if (!$this->assets_dir) {
// Attempt to create assets folder if it doesn't exist yet.
$this->assets_dir = $locator->findResource('asset://', true, true);
Folder::mkdir($this->assets_dir);
$locator->clearCache();
}
$this->assets_url = $locator->findResource('asset://', false);
}
@@ -119,10 +126,9 @@ class Pipeline extends PropertyObject
$file = $uid . '.css';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
$buffer = null;
if (file_exists($this->assets_dir . $file)) {
$buffer = file_get_contents($this->assets_dir . $file) . "\n";
$filepath = "{$this->assets_dir}/{$file}";
if (file_exists($filepath)) {
$buffer = file_get_contents($filepath) . "\n";
} else {
//if nothing found get out of here!
if (empty($assets)) {
@@ -141,7 +147,7 @@ class Pipeline extends PropertyObject
// Write file
if (trim($buffer) !== '') {
file_put_contents($this->assets_dir . $file, $buffer);
file_put_contents($filepath, $buffer);
}
}
@@ -182,10 +188,9 @@ class Pipeline extends PropertyObject
$file = $uid . '.js';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
$buffer = null;
if (file_exists($this->assets_dir . $file)) {
$buffer = file_get_contents($this->assets_dir . $file) . "\n";
$filepath = "{$this->assets_dir}/{$file}";
if (file_exists($filepath)) {
$buffer = file_get_contents($filepath) . "\n";
} else {
//if nothing found get out of here!
if (empty($assets)) {
@@ -204,7 +209,7 @@ class Pipeline extends PropertyObject
// Write file
if (trim($buffer) !== '') {
file_put_contents($this->assets_dir . $file, $buffer);
file_put_contents($filepath, $buffer);
}
}

View File

@@ -156,6 +156,10 @@ trait AssetUtilsTrait
$no_key = ['loading'];
foreach ($this->attributes as $key => $value) {
if ($value === null) {
continue;
}
if (is_numeric($key)) {
$key = $value;
}

View File

@@ -41,6 +41,9 @@ class Setup extends Data
*/
public static $environment;
/** @var string */
public static $securityFile = 'config://security.yaml';
/** @var array */
protected $streams = [
'user' => [
@@ -390,12 +393,19 @@ class Setup extends Data
if (!$locator->findResource('environment://config', true)) {
// If environment does not have its own directory, remove it from the lookup.
$this->set('streams.schemes.environment.prefixes', ['config' => []]);
$prefixes = $this->get('streams.schemes.environment.prefixes');
$prefixes['config'] = [];
$this->set('streams.schemes.environment.prefixes', $prefixes);
$this->initializeLocator($locator);
}
// Create security.yaml if it doesn't exist.
$filename = $locator->findResource('config://security.yaml', true, true);
// Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
$securityFile = basename(static::$securityFile);
$securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile));
$securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true);
$filename = "{$securityFolder}/{$securityFile}";
$security_file = CompiledYamlFile::instance($filename);
$security_content = (array)$security_file->content();

View File

@@ -317,6 +317,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
$toggle = [];
}
// Recursively fetch the items.
$childData = $data[$key] ?? null;
if (null !== $childData && !is_array($childData)) {
throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData)));
}
$data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
} else {
$field = $this->get($value);

View File

@@ -238,6 +238,7 @@ class Validation
$value = trim($value);
}
$value = preg_replace("/\r\n|\r/um", "\n", $value);
$len = mb_strlen($value);
$min = (int)($params['min'] ?? 0);
@@ -280,7 +281,7 @@ class Validation
$value = trim($value);
}
return $value;
return preg_replace("/\r\n|\r/um", "\n", $value);
}
/**
@@ -518,17 +519,32 @@ class Validation
return false;
}
if (isset($params['min']) && $value < $params['min']) {
return false;
$value = (float)$value;
$min = 0;
if (isset($params['min'])) {
$min = (float)$params['min'];
if ($value < $min) {
return false;
}
}
if (isset($params['max']) && $value > $params['max']) {
return false;
if (isset($params['max'])) {
$max = (float)$params['max'];
if ($value > $max) {
return false;
}
}
$min = $params['min'] ?? 0;
if (isset($params['step'])) {
$step = (float)$params['step'];
// Count of how many steps we are above/below the minimum value.
$pos = ($value - $min) / $step;
return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
return is_int(static::filterNumber($pos, $params, $field));
}
return true;
}
/**

View File

@@ -13,6 +13,7 @@ namespace Grav\Common\Flex;
use Grav\Common\Flex\Traits\FlexGravTrait;
use Grav\Common\Flex\Traits\FlexObjectTrait;
use Grav\Common\Media\Interfaces\MediaInterface;
use Grav\Framework\Flex\Traits\FlexMediaTrait;
use function is_array;
@@ -21,7 +22,7 @@ use function is_array;
*
* @package Grav\Common\Flex
*/
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface
{
use FlexGravTrait;
use FlexObjectTrait;

View File

@@ -192,6 +192,14 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
/**
* Set current page.
*/
public function setCurrent(string $path): void
{
throw new RuntimeException(__METHOD__ . '(): Not Implemented');
}
/**
* Return previous item.
*
@@ -426,20 +434,20 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
/**
* 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
* an arbitrary datetime page field where start date and end date are optional
* Dates must be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
* @param string $startDate
* @param string|false $endDate
* @param string|null $startDate
* @param string|null $endDate
* @param string|null $field
* @return static
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
public function dateRange($startDate = null, $endDate = null, $field = null)
{
$start = Utils::date2timestamp($startDate);
$end = $endDate ? Utils::date2timestamp($endDate) : false;
$start = $startDate ? Utils::date2timestamp($startDate) : null;
$end = $endDate ? Utils::date2timestamp($endDate) : null;
$entries = [];
foreach ($this as $key => $object) {
@@ -449,7 +457,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
$date = $field ? strtotime($object->getNestedProperty($field)) : $object->date();
if ($date >= $start && (!$end || $date <= $end)) {
if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
$entries[$key] = $object;
}
}
@@ -746,6 +754,16 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
return $bool ? $this->select($list) : $this->unselect($list);
}
/**
* @param string|null $languageCode
* @param bool|null $fallback
* @return PageIndex
*/
public function withTranslated(string $languageCode = null, bool $fallback = null)
{
return $this->getIndex()->withTranslated($languageCode, $fallback);
}
/**
* Filter pages by given filters.
*

View File

@@ -18,6 +18,7 @@ use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Flex\Traits\FlexGravTrait;
use Grav\Common\Flex\Traits\FlexIndexTrait;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Page\Header;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
@@ -164,6 +165,31 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
return $root;
}
/**
* @param string|null $languageCode
* @param bool|null $fallback
* @return PageIndex
*/
public function withTranslated(string $languageCode = null, bool $fallback = null)
{
if (null === $languageCode) {
return $this;
}
$entries = $this->translateEntries($this->getEntries(), $languageCode, $fallback);
$params = ['language' => $languageCode, 'language_fallback' => $fallback] + $this->getParams();
return $this->createFrom($entries)->setParams($params);
}
/**
* @return string|null
*/
public function getLanguage(): ?string
{
return $this->_params['language'] ?? null;
}
/**
* Get the collection params
*
@@ -174,6 +200,17 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
return $this->_params ?? [];
}
/**
* Get the collection param
*
* @param string $name
* @return mixed
*/
public function getParam(string $name)
{
return $this->_params[$name] ?? null;
}
/**
* Set parameters to the Collection
*
@@ -187,6 +224,20 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
return $this;
}
/**
* Set a parameter to the Collection
*
* @param string $name
* @param mixed $value
* @return $this
*/
public function setParam(string $name, $value)
{
$this->_params[$name] = $value;
return $this;
}
/**
* Get the collection params
*
@@ -197,6 +248,15 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
return $this->getParams();
}
/**
* {@inheritdoc}
* @see FlexCollectionInterface::getCacheKey()
*/
public function getCacheKey(): string
{
return $this->getTypePrefix() . $this->getFlexType() . '.' . sha1(json_encode($this->getKeys()) . $this->getKeyField() . $this->getLanguage());
}
/**
* Filter pages by given filters.
*
@@ -345,6 +405,96 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
return $index;
}
/**
* @param array $entries
* @param string $lang
* @param bool|null $fallback
* @return array
*/
protected function translateEntries(array $entries, string $lang, bool $fallback = null): array
{
$languages = $this->getFallbackLanguages($lang, $fallback);
foreach ($entries as $key => &$entry) {
// Find out which version of the page we should load.
$translations = $this->getLanguageTemplates((string)$key);
if (!$translations) {
// No translations found, is this a folder?
continue;
}
// Find a translation.
$template = null;
foreach ($languages as $code) {
if (isset($translations[$code])) {
$template = $translations[$code];
break;
}
}
// We couldn't find a translation, remove entry from the list.
if (!isset($code, $template)) {
unset($entries['key']);
continue;
}
// Get the main key without template and langauge.
[$main_key,] = explode('|', $entry['storage_key'] . '|', 2);
// Update storage key and language.
$entry['storage_key'] = $main_key . '|' . $template . '.' . $code;
$entry['lang'] = $code;
}
unset($entry);
return $entries;
}
/**
* @return array
*/
protected function getLanguageTemplates(string $key): array
{
$meta = $this->getMetaData($key);
$template = $meta['template'] ?? 'folder';
$translations = $meta['markdown'] ?? [];
$list = [];
foreach ($translations as $code => $search) {
if (isset($search[$template])) {
// Use main template if possible.
$list[$code] = $template;
} elseif (!empty($search)) {
// Fall back to first matching template.
$list[$code] = key($search);
}
}
return $list;
}
/**
* @param string|null $languageCode
* @param bool|null $fallback
* @return array
*/
protected function getFallbackLanguages(string $languageCode = null, bool $fallback = null): array
{
$fallback = $fallback ?? true;
if (!$fallback && null !== $languageCode) {
return [$languageCode];
}
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
$languageCode = $languageCode ?? '';
if ($languageCode === '' && $fallback) {
return $language->getFallbackLanguages(null, true);
}
return $fallback ? $language->getFallbackLanguages($languageCode, true) : [$languageCode];
}
/**
* @param array $options
* @return array
@@ -524,12 +674,12 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
$count = $filters ? $tmp->filterBy($filters, true)->count() : null;
$route = $child->getRoute();
$payload = [
'item-key' => basename($child->rawRoute() ?? $child->getKey()),
'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
'icon' => $icon,
'title' => htmlspecialchars($child->menu()),
'route' => [
'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
'raw' => $child->rawRoute(),
'display' => htmlspecialchars(($route ? ($route->toString(false) ?: '/') : null) ?? ''),
'raw' => htmlspecialchars($child->rawRoute()),
],
'modified' => $this->jsDate($child->modified()),
'child_count' => $child_count ?: null,
@@ -799,17 +949,17 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* 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
* an arbitrary datetime page field where start date and end date are optional
* Dates must 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 $startDate
* @param string|null $endDate
* @param string|null $field
* @return static
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
public function dateRange($startDate = null, $endDate = null, $field = null)
{
$collection = $this->__call('dateRange', [$startDate, $endDate, $field]);

View File

@@ -262,6 +262,24 @@ class PageObject extends FlexPageObject
$this->getFlexDirectory()->reloadIndex();
}
/**
* @param UserInterface|null $user
*/
public function check(UserInterface $user = null): void
{
parent::check($user);
if ($user && $this->isMoved()) {
$parentKey = $this->getProperty('parent_key');
/** @var PageObject|null $parent */
$parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
if (!$parent || !$parent->isAuthorized('create', null, $user)) {
throw new \RuntimeException('Forbidden', 403);
}
}
}
/**
* @param array|bool $reorder
* @return FlexObject|FlexObjectInterface
@@ -357,6 +375,19 @@ class PageObject extends FlexPageObject
return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
}
/**
* @return bool
*/
protected function isMoved(): bool
{
$storageKey = $this->getMasterKey();
$filesystem = Filesystem::getInstance(false);
$oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
$newParentKey = $this->getProperty('parent_key');
return $this->exists() && $oldParentKey !== $newParentKey;
}
/**
* @param array $ordering
* @return PageCollection|null
@@ -364,10 +395,7 @@ class PageObject extends FlexPageObject
protected function reorderSiblings(array $ordering)
{
$storageKey = $this->getMasterKey();
$filesystem = Filesystem::getInstance(false);
$oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
$newParentKey = $this->getProperty('parent_key');
$isMoved = $oldParentKey !== $newParentKey;
$isMoved = $this->isMoved();
$order = !$isMoved ? $this->order() : false;
if ($order !== false) {
$order = (int)$order;
@@ -385,10 +413,12 @@ class PageObject extends FlexPageObject
// Handle special case where ordering isn't given.
if ($ordering === []) {
if ($order >= 999999) {
// Set ordering to point to be the last item.
// Set ordering to point to be the last item, ignoring the object itself.
$order = 0;
foreach ($siblings as $sibling) {
$order = max($order, (int)$sibling->order());
if ($sibling->getKey() !== $this->getKey()) {
$order = max($order, (int)$sibling->order());
}
}
$this->order($order + 1);
}
@@ -500,6 +530,8 @@ class PageObject extends FlexPageObject
if ($isNew === true && $name === '') {
// Support onBlueprintCreated event just like in Pages::blueprints($template)
$blueprint->set('initialized', true);
$blueprint->setFilename($template);
Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
}

View File

@@ -41,6 +41,14 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
] + parent::getCachedMethods();
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->getProperty('readableName');
}
/**
* Checks user authorization to the action.
*

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Grav\Common\Flex\Types\Users;
use Closure;
use Countable;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
@@ -22,7 +23,6 @@ use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Interfaces\MediaUploadInterface;
use Grav\Common\Page\Media;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Common\User\Access;
use Grav\Common\User\Authentication;
@@ -32,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\Traits\UserTrait;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Storage\FileStorage;
@@ -76,18 +77,17 @@ class UserObject extends FlexObject implements UserInterface, Countable
use UserTrait;
use UserObjectLegacyTrait;
/** @var Closure|null */
static public $authorizeCallable;
/** @var array|null */
protected $_uploads_original;
/** @var FileInterface|null */
protected $_storage;
/** @var UserGroupIndex */
protected $_groups;
/** @var Access */
protected $_access;
/** @var array|null */
protected $access;
@@ -264,6 +264,15 @@ class UserObject extends FlexObject implements UserInterface, Countable
}
}
$authorizeCallable = static::$authorizeCallable;
if ($authorizeCallable instanceof Closure) {
$authorizeCallable->bindTo($this);
$authorized = $authorizeCallable($action, $scope);
if (is_bool($authorized)) {
return $authorized;
}
}
// Check user access.
$access = $this->getAccess();
$authorized = $access->authorize($action, $scope);
@@ -297,6 +306,14 @@ class UserObject extends FlexObject implements UserInterface, Countable
return $value;
}
/**
* @return UserGroupIndex
*/
public function getRoles(): UserGroupIndex
{
return $this->getGroups();
}
/**
* Convert object into an array.
*
@@ -694,6 +711,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
/**
* @param array $files
* @return void
*/
protected function setUpdatedMedia(array $files): void
{
@@ -701,10 +719,16 @@ class UserObject extends FlexObject implements UserInterface, Countable
$locator = Grav::instance()['locator'];
$media = $this->getMedia();
if (!$media instanceof MediaUploadInterface) {
return;
}
$filesystem = Filesystem::getInstance(false);
$list = [];
$list_original = [];
foreach ($files as $field => $group) {
// Ignore files without a field.
if ($field === '') {
continue;
}
@@ -712,7 +736,6 @@ class UserObject extends FlexObject implements UserInterface, Countable
// Load settings for the field.
$settings = $this->getMediaFieldSettings($field);
foreach ($group as $filename => $file) {
if ($file) {
// File upload.
@@ -727,8 +750,8 @@ class UserObject extends FlexObject implements UserInterface, Countable
}
if ($file) {
// Check file upload against media limits.
$filename = $media->checkUploadedFile($file, $filename, $settings);
// Check file upload against media limits (except for max size).
$filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
}
$self = $settings['self'];
@@ -751,19 +774,25 @@ class UserObject extends FlexObject implements UserInterface, Countable
continue;
}
// Calculate path without the retina scaling factor.
$realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
$list[$filename] = [$file, $settings];
$path = str_replace('.', "\n", $field);
if (null !== $data) {
$data['name'] = $filename;
$data['path'] = $filepath;
$this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
$this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
} else {
$this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
$this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
}
}
}
$this->clearMediaCache();
$this->_uploads = $list;
$this->_uploads_original = $list_original;
}

View File

@@ -35,7 +35,11 @@ class GPM extends Iterator
/** @var Remote\Packages|null Remote available Packages */
private $repository;
/** @var Remote\GravCore|null Remove Grav Packages */
public $grav;
private $grav;
/** @var bool */
private $refresh;
/** @var callable|null */
private $callback;
/** @var array Internal cache */
protected $cache;
@@ -55,13 +59,45 @@ class GPM extends Iterator
public function __construct($refresh = false, $callback = null)
{
parent::__construct();
Folder::create(CACHE_DIR . '/gpm');
$this->cache = [];
$this->installed = new Local\Packages();
try {
$this->repository = new Remote\Packages($refresh, $callback);
$this->grav = new Remote\GravCore($refresh, $callback);
} catch (Exception $e) {
$this->refresh = $refresh;
$this->callback = $callback;
}
/**
* Magic getter method
*
* @param string $offset Asset name value
* @return mixed Asset value
*/
public function __get($offset)
{
switch ($offset) {
case 'grav':
return $this->getGrav();
}
return parent::__get($offset);
}
/**
* Magic method to determine if the attribute is set
*
* @param string $offset Asset name value
* @return bool True if the value is set
*/
public function __isset($offset)
{
switch ($offset) {
case 'grav':
return $this->getGrav() !== null;
}
return parent::__isset($offset);
}
/**
@@ -266,11 +302,12 @@ class GPM extends Iterator
{
$items = [];
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return $items;
}
$repository = $this->repository['plugins'];
$plugins = $repository['plugins'];
// local cache to speed things up
if (isset($this->cache[__METHOD__])) {
@@ -278,18 +315,18 @@ class GPM extends Iterator
}
foreach ($this->installed['plugins'] as $slug => $plugin) {
if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
continue;
}
$local_version = $plugin->version ?? 'Unknown';
$remote_version = $repository[$slug]->version;
$remote_version = $plugins[$slug]->version;
if (version_compare($local_version, $remote_version) < 0) {
$repository[$slug]->available = $remote_version;
$repository[$slug]->version = $local_version;
$repository[$slug]->type = $repository[$slug]->release_type;
$items[$slug] = $repository[$slug];
$plugins[$slug]->available = $remote_version;
$plugins[$slug]->version = $local_version;
$plugins[$slug]->type = $plugins[$slug]->release_type;
$items[$slug] = $plugins[$slug];
}
}
@@ -306,19 +343,20 @@ class GPM extends Iterator
*/
public function getLatestVersionOfPackage($package_name)
{
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return null;
}
$repository = $this->repository['plugins'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->available ?: $repository[$package_name]->version;
$plugins = $repository['plugins'];
if (isset($plugins[$package_name])) {
return $plugins[$package_name]->available ?: $plugins[$package_name]->version;
}
//Not a plugin, it's a theme?
$repository = $this->repository['themes'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->available ?: $repository[$package_name]->version;
$themes = $repository['themes'];
if (isset($themes[$package_name])) {
return $themes[$package_name]->available ?: $themes[$package_name]->version;
}
return null;
@@ -356,11 +394,12 @@ class GPM extends Iterator
{
$items = [];
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return $items;
}
$repository = $this->repository['themes'];
$themes = $repository['themes'];
// local cache to speed things up
if (isset($this->cache[__METHOD__])) {
@@ -368,18 +407,18 @@ class GPM extends Iterator
}
foreach ($this->installed['themes'] as $slug => $plugin) {
if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
continue;
}
$local_version = $plugin->version ?? 'Unknown';
$remote_version = $repository[$slug]->version;
$remote_version = $themes[$slug]->version;
if (version_compare($local_version, $remote_version) < 0) {
$repository[$slug]->available = $remote_version;
$repository[$slug]->version = $local_version;
$repository[$slug]->type = $repository[$slug]->release_type;
$items[$slug] = $repository[$slug];
$themes[$slug]->available = $remote_version;
$themes[$slug]->version = $local_version;
$themes[$slug]->type = $themes[$slug]->release_type;
$items[$slug] = $themes[$slug];
}
}
@@ -407,19 +446,20 @@ class GPM extends Iterator
*/
public function getReleaseType($package_name)
{
if (null === $this->repository) {
$repository = $this->getRepository();
if (null === $repository) {
return null;
}
$repository = $this->repository['plugins'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->release_type;
$plugins = $repository['plugins'];
if (isset($plugins[$package_name])) {
return $plugins[$package_name]->release_type;
}
//Not a plugin, it's a theme?
$repository = $this->repository['themes'];
if (isset($repository[$package_name])) {
return $repository[$package_name]->release_type;
$themes = $repository['themes'];
if (isset($themes[$package_name])) {
return $themes[$package_name]->release_type;
}
return null;
@@ -470,7 +510,7 @@ class GPM extends Iterator
*/
public function getRepositoryPlugins()
{
return $this->repository['plugins'] ?? null;
return $this->getRepository()['plugins'] ?? null;
}
/**
@@ -493,7 +533,7 @@ class GPM extends Iterator
*/
public function getRepositoryThemes()
{
return $this->repository['themes'] ?? null;
return $this->getRepository()['themes'] ?? null;
}
/**
@@ -504,9 +544,31 @@ class GPM extends Iterator
*/
public function getRepository()
{
if (null === $this->repository) {
try {
$this->repository = new Remote\Packages($this->refresh, $this->callback);
} catch (Exception $e) {}
}
return $this->repository;
}
/**
* Returns Grav version available in the repository
*
* @return Remote\GravCore|null
*/
public function getGrav()
{
if (null === $this->grav) {
try {
$this->grav = new Remote\GravCore($this->refresh, $this->callback);
} catch (Exception $e) {}
}
return $this->grav;
}
/**
* Searches for a Package in the repository
*

View File

@@ -135,7 +135,10 @@ class Installer
}
if (!$options['sophisticated']) {
if ($options['theme']) {
$isTheme = $options['theme'] ?? false;
// Make sure that themes are always being copied, even if option was not set!
$isTheme = $isTheme || preg_match('|/themes/[^/]+|ui', $install_path);
if ($isTheme) {
self::copyInstall($extracted, $install_path);
} else {
self::moveInstall($extracted, $install_path);

View File

@@ -9,6 +9,7 @@
namespace Grav\Common;
use Composer\Autoload\ClassLoader;
use Grav\Common\Config\Config;
use Grav\Common\Config\Setup;
use Grav\Common\Helpers\Exif;
@@ -152,6 +153,13 @@ class Grav extends Container
{
if (null === self::$instance) {
self::$instance = static::load($values);
/** @var ClassLoader|null $loader */
$loader = self::$instance['loader'] ?? null;
if ($loader) {
// Load fix for Deferred Twig Extension
$loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
}
} elseif ($values) {
$instance = self::$instance;
foreach ($values as $key => $value) {

View File

@@ -56,7 +56,7 @@ trait ImageMediaTrait
'resize', 'forceResize', 'cropResize', 'crop', 'zoomCrop',
'negate', 'brightness', 'contrast', 'grayscale', 'emboss',
'smooth', 'sharp', 'edge', 'colorize', 'sepia', 'enableProgressive',
'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format'
'rotate', 'flip', 'fixOrientation', 'gaussianBlur', 'format', 'create', 'fill', 'merge'
];
/** @var array */

View File

@@ -20,11 +20,13 @@ use Grav\Common\Security;
use Grav\Common\Utils;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Form\FormFlashFile;
use Grav\Framework\Mime\MimeTypes;
use Psr\Http\Message\UploadedFileInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
use function dirname;
use function in_array;
/**
* Implements media upload and delete functionality.
@@ -71,15 +73,6 @@ trait MediaUploadTrait
*/
public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string
{
// Add the defaults to the settings.
$settings = $this->getUploadSettings($settings);
// Destination is always needed (but it can be set in defaults).
$self = $settings['self'] ?? false;
if (!isset($settings['destination']) && $self === false) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED'), 400);
}
// Check if there is an upload error.
switch ($uploadedFile->getError()) {
case UPLOAD_ERR_OK:
@@ -101,10 +94,38 @@ trait MediaUploadTrait
throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400);
}
$metadata = [
'filename' => $uploadedFile->getClientFilename(),
'mime' => $uploadedFile->getClientMediaType(),
'size' => $uploadedFile->getSize(),
];
return $this->checkFileMetadata($metadata, $filename, $settings);
}
/**
* Checks that file metadata meets the requirements. Returns new filename.
*
* @param array $metadata
* @param array|null $settings
* @return string|null
* @throws RuntimeException
*/
public function checkFileMetadata(array $metadata, string $filename = null, array $settings = null): string
{
// Add the defaults to the settings.
$settings = $this->getUploadSettings($settings);
// Destination is always needed (but it can be set in defaults).
$self = $settings['self'] ?? false;
if (!isset($settings['destination']) && $self === false) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED'), 400);
}
if (null === $filename) {
// If no filename is given, use the filename from the uploaded file (path is not allowed).
$folder = '';
$filename = $uploadedFile->getClientFilename() ?? '';
$filename = $metadata['filename'] ?? '';
} else {
// If caller sets the filename, we will accept any custom path.
$folder = dirname($filename);
@@ -128,7 +149,7 @@ trait MediaUploadTrait
$filename = date('YmdHis') . '-' . $filename;
}
}
$filepath = $folder !== '' ? $folder . $filename : $filename;
$filepath = $folder . $filename;
// Check if the filename is allowed.
if (!Utils::checkFilename($filename)) {
@@ -148,23 +169,32 @@ trait MediaUploadTrait
$filesize = $settings['filesize'];
if ($filesize) {
$max_filesize = $filesize * 1048576;
if ($uploadedFile->getSize() > $max_filesize) {
if ($metadata['size'] > $max_filesize) {
// TODO: use own language string
throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
}
} elseif (null === $filesize) {
// Check size against the Grav upload limit.
$grav_limit = Utils::getUploadLimit();
if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) {
if ($grav_limit > 0 && $metadata['size'] > $grav_limit) {
throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
}
}
$grav = Grav::instance();
/** @var MimeTypes $mimeChecker */
$mimeChecker = $grav['mime'];
// Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg)
// Do not trust mime type sent by the browser.
$mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension);
$validExtensions = $mimeChecker->getExtensions($mime);
if (!in_array($extension, $validExtensions, true)) {
throw new RuntimeException('The mime type does not match to file extension', 400);
}
$accepted = false;
$errors = [];
// Do not trust mime type sent by the browser.
$mime = Utils::getMimeByFilename($filename);
foreach ((array)$settings['accept'] as $type) {
// Force acceptance of any file when star notation
if ($type === '*') {
@@ -394,6 +424,17 @@ trait MediaUploadTrait
$uploadedFile->moveTo($filepath);
}
/**
* Get upload settings.
*
* @param array|null $settings Form field specific settings (override).
* @return array
*/
public function getUploadSettings(?array $settings = null): array
{
return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
}
/**
* Internal logic to copy file.
*
@@ -580,17 +621,6 @@ trait MediaUploadTrait
}
}
/**
* Get upload settings.
*
* @param array|null $settings Form field specific settings (override).
* @return array
*/
protected function getUploadSettings(?array $settings = null): array
{
return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
}
/**
* @param string $filename
* @param string $path

View File

@@ -145,6 +145,18 @@ class Collection extends Iterator implements PageCollectionInterface
return $this;
}
/**
* Set current page.
*/
public function setCurrent(string $path): void
{
reset($this->items);
while (($key = key($this->items)) !== null && $key !== $path) {
next($this->items);
}
}
/**
* Returns current page.
*
@@ -319,30 +331,32 @@ class Collection extends Iterator implements PageCollectionInterface
/**
* 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
* an arbitrary datetime page field where start date and end date are optional
* Dates must 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 $startDate
* @param string|null $endDate
* @param string|null $field
* @return $this
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
public function dateRange($startDate = null, $endDate = null, $field = null)
{
$start = Utils::date2timestamp($startDate);
$end = $endDate ? Utils::date2timestamp($endDate) : false;
$start = $startDate ? Utils::date2timestamp($startDate) : null;
$end = $endDate ? Utils::date2timestamp($endDate) : null;
$date_range = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
if ($page !== null) {
$date = $field ? strtotime($page->value($field)) : $page->date();
if (!$page) {
continue;
}
if ($date >= $start && (!$end || $date <= $end)) {
$date_range[$path] = $slug;
}
$date = $field ? strtotime($page->value($field)) : $page->date();
if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
$date_range[$path] = $slug;
}
}

View File

@@ -158,17 +158,17 @@ interface PageCollectionInterface extends Traversable, ArrayAccess, Countable, S
/**
* 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
* an arbitrary datetime page field where start date and end date are optional
* Dates must 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 $startDate
* @param string|null $endDate
* @param string|null $field
* @return PageCollectionInterface
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null);
public function dateRange($startDate = null, $endDate = null, $field = null);
/**
* Creates new collection with only visible pages

View File

@@ -114,7 +114,7 @@ class Excerpts
);
// Valid attributes supported.
$valid_attributes = $grav['config']->get('system.pages.markdown.valid_link_attributes');
$valid_attributes = $grav['config']->get('system.pages.markdown.valid_link_attributes') ?? [];
$skip = [];
// Unless told to not process, go through actions.
@@ -157,7 +157,7 @@ class Excerpts
// Handle custom streams.
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
if ($locator->isStream($url)) {
if ($type === 'link' && $locator->isStream($url)) {
$path = $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
$url_parts['path'] = $grav['base_url_relative'] . '/' . $path;
unset($url_parts['stream'], $url_parts['scheme']);

View File

@@ -102,12 +102,13 @@ class Media extends AbstractMedia
foreach ($iterator as $file => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
$filename = $info->getFilename();
if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) {
continue;
}
// Find out what type we're dealing with
[$basename, $ext, $type, $extra] = $this->getFileParts($info->getFilename());
[$basename, $ext, $type, $extra] = $this->getFileParts($filename);
if (!in_array(strtolower($ext), $media_types, true)) {
continue;

View File

@@ -337,6 +337,37 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
return $this;
}
/**
* Add a frame to image
*
* @return $this
*/
public function addFrame(int $border = 10, string $color = '0x000000')
{
if(is_int(intval($border)) && $border>0 && preg_match('/^0x[a-f0-9]{6}$/i', $color)) { // $border must be an integer and bigger than 0; $color must be formatted as an HEX value (0x??????).
$image = ImageFile::open($this->path());
}
else {
return $this;
}
$dst_width = $image->width()+2*$border;
$dst_height = $image->height()+2*$border;
$frame = ImageFile::create($dst_width, $dst_height);
$frame->__call('fill', [$color]);
$this->image = $frame;
$this->__call('merge', [$image, $border, $border]);
$this->saveImage();
return $this;
}
/**
* Forward the call to the image processing method.
*
@@ -344,6 +375,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
* @param mixed $args
* @return $this|mixed
*/
public function __call($method, $args)
{
if (!in_array($method, static::$magic_actions, true)) {

View File

@@ -41,9 +41,7 @@ class Link implements RenderableInterface, MediaLinkInterface
$this->attributes = $attributes;
$source = $medium->reset()->thumbnail('auto')->display('thumbnail');
// FIXME: Thumbnail can be null, maybe we should not allow that?
if (null === $source) {
if (!$source instanceof MediaObjectInterface) {
throw new RuntimeException('Media has no thumbnail set');
}
@@ -89,10 +87,15 @@ class Link implements RenderableInterface, MediaLinkInterface
throw new BadMethodCallException(get_class($object) . '::' . $method . '() not found.');
}
$this->source = call_user_func_array($callable, $args);
$object = call_user_func_array($callable, $args);
if (!$object instanceof MediaLinkInterface) {
// Don't start nesting links, if user has multiple link calls in his
// actions, we will drop the previous links.
return $this;
}
// Don't start nesting links, if user has multiple link calls in his
// actions, we will drop the previous links.
return $this->source instanceof MediaLinkInterface ? $this->source : $this;
$this->source = $object;
return $object;
}
}

View File

@@ -540,9 +540,9 @@ class Pages
}
if (isset($params['dateRange'])) {
$start = $params['dateRange']['start'] ?? 0;
$end = $params['dateRange']['end'] ?? false;
$field = $params['dateRange']['field'] ?? false;
$start = $params['dateRange']['start'] ?? null;
$end = $params['dateRange']['end'] ?? null;
$field = $params['dateRange']['field'] ?? null;
$collection = $collection->dateRange($start, $end, $field);
}

View File

@@ -287,33 +287,42 @@ class Plugins extends Iterator
{
// NOTE: ALL THE LOCAL VARIABLES ARE USED INSIDE INCLUDED FILE, DO NOT REMOVE THEM!
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
$class = null;
// Start by attempting to load the plugin_name.php file.
$file = $locator->findResource('plugins://' . $name . DS . $name . PLUGIN_EXT);
if (is_file($file)) {
// Local variables available in the file: $grav, $name, $file
$class = include_once $file;
if (!is_object($class) || !is_subclass_of($class, Plugin::class, true)) {
$class = null;
}
}
if (!$class || !is_subclass_of($class, Plugin::class, true)) {
$className = Inflector::camelize($name);
$pluginClassFormat = [
'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
'Grav\\Plugin\\' . $className . 'Plugin',
'Grav\\Plugin\\' . $className
];
// If the class hasn't been initialized yet, guess the class name and create a new instance.
if (null === $class) {
$className = Inflector::camelize($name);
$pluginClassFormat = [
'Grav\\Plugin\\' . ucfirst($name). 'Plugin',
'Grav\\Plugin\\' . $className . 'Plugin',
'Grav\\Plugin\\' . $className
];
foreach ($pluginClassFormat as $pluginClass) {
if (is_subclass_of($pluginClass, Plugin::class, true)) {
$class = new $pluginClass($name, $grav);
break;
}
foreach ($pluginClassFormat as $pluginClass) {
if (is_subclass_of($pluginClass, Plugin::class, true)) {
$class = new $pluginClass($name, $grav);
break;
}
}
} else {
}
// Log a warning if plugin cannot be found.
if (null === $class) {
$grav['log']->addWarning(
sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name)
);
return null;
}
return $class;

View File

@@ -105,12 +105,12 @@ class InitializeProcessor extends ProcessorBase
// TODO: remove in 2.0.
$this->container['accounts'];
// Initialize session.
$this->initializeSession($config);
// Initialize URI (uses session, see issue #3269).
$this->initializeUri($config);
// Initialize session.
$this->initializeSession($config);
// Grav may return redirect response right away.
$redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1);
if ($redirectCode) {

View File

@@ -10,6 +10,8 @@
namespace Grav\Common\Processors;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Grav\Plugin\Form\Forms;
use RocketTheme\Toolbox\Event\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -47,8 +49,17 @@ class PagesProcessor extends ProcessorBase
$page = $this->container['page'];
if (!$page->routable()) {
$exception = new RequestException($request, 'Page Not Found', 404);
$route = $this->container['route'];
// If no page found, fire event
$event = new Event(['page' => $page]);
$event = new Event([
'page' => $page,
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'exception' => $exception,
'route' => $route,
'request' => $request
]);
$event->page = null;
$event = $this->container->fireEvent('onPageNotFound', $event);
@@ -65,12 +76,18 @@ class PagesProcessor extends ProcessorBase
$task = $this->container['task'];
$action = $this->container['action'];
/** @var Forms $forms */
$forms = $this->container['forms'] ?? null;
$form = $forms ? $forms->getActiveForm() : null;
$options = ['page' => $page, 'form' => $form, 'request' => $request];
if ($task) {
$event = new Event(['task' => $task, 'page' => $page]);
$event = new Event(['task' => $task] + $options);
$this->container->fireEvent('onPageTask', $event);
$this->container->fireEvent('onPageTask.' . $task, $event);
} elseif ($action) {
$event = new Event(['action' => $action, 'page' => $page]);
$event = new Event(['action' => $action] + $options);
$this->container->fireEvent('onPageAction', $event);
$this->container->fireEvent('onPageAction.' . $action, $event);
}

View File

@@ -14,6 +14,7 @@ use Grav\Framework\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RocketTheme\Toolbox\Event\Event;
/**
* Class RenderProcessor
@@ -42,23 +43,27 @@ class RenderProcessor extends ProcessorBase
return $output;
}
ob_start();
/** @var PageInterface $page */
$page = $this->container['page'];
// Use internal Grav output.
$container->output = $output;
$container->fireEvent('onOutputGenerated');
ob_start();
$event = new Event(['page' => $page, 'output' => &$container->output]);
$container->fireEvent('onOutputGenerated', $event);
echo $container->output;
$html = ob_get_clean();
// remove any output
$container->output = '';
$this->container->fireEvent('onOutputRendered');
$event = new Event(['page' => $page, 'output' => $html]);
$this->container->fireEvent('onOutputRendered', $event);
$html = ob_get_clean();
/** @var PageInterface $page */
$page = $this->container['page'];
$this->stopTimer();
return new Response($page->httpResponseCode(), $page->httpHeaders(), $html);

View File

@@ -390,7 +390,9 @@ class Job
if (count($this->outputTo) > 0) {
foreach ($this->outputTo as $file) {
$output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
file_put_contents($file, $this->output, $output_mode);
$timestamp = (new DateTime('now'))->format('c');
$output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output;
file_put_contents($file, $output, $output_mode);
}
}

View File

@@ -12,6 +12,7 @@ namespace Grav\Common;
use enshrined\svgSanitize\Sanitizer;
use Exception;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Page\Pages;
use function chr;
use function count;
@@ -56,9 +57,16 @@ class Security
$original_svg = file_get_contents($file);
$clean_svg = $sanitizer->sanitize($original_svg);
// TODO: what to do with bad SVG files which return false?
if ($clean_svg !== false && $clean_svg !== $original_svg) {
// Quarantine bad SVG files and throw exception
if ($clean_svg !== false ) {
file_put_contents($file, $clean_svg);
} else {
$quarantine_file = basename($file);
$quarantine_dir = 'log://quarantine';
Folder::mkdir($quarantine_dir);
file_put_contents("$quarantine_dir/$quarantine_file", $original_svg);
unlink($file);
throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder');
}
}
}
@@ -210,7 +218,7 @@ class Security
'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])([\s\/]on|\sxmlns)[a-z].*=>?#iUu',
// Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols
'invalid_protocols' => '#(' . implode('|', array_map('preg_quote', $invalid_protocols, ['#'])) . '):.*?#iUu',
'invalid_protocols' => '#(' . implode('|', array_map('preg_quote', $invalid_protocols, ['#'])) . '):\S.*?#iUu',
// Match -moz-bindings
'moz_binding' => '#-moz-binding[a-z\x00-\x20]*:#u',

View File

@@ -17,6 +17,7 @@ use Grav\Common\Config\Config;
use Grav\Common\Config\ConfigFileFinder;
use Grav\Common\Config\Setup;
use Grav\Common\Language\Language;
use Grav\Framework\Mime\MimeTypes;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\File\YamlFile;
@@ -56,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface
return $config;
};
$container['mime'] = function ($c) {
/** @var Config $config */
$config = $c['config'];
$mimes = $config->get('mime.types', []);
foreach ($config->get('media.types', []) as $ext => $media) {
if (!empty($media['mime'])) {
$mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? []));
}
}
return MimeTypes::createFromMimes($mimes);
};
$container['languages'] = function ($c) {
return static::languages($c);
};

View File

@@ -12,6 +12,7 @@ namespace Grav\Common\Service;
use Grav\Common\Grav;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Class TaskServiceProvider
@@ -26,7 +27,11 @@ class TaskServiceProvider implements ServiceProviderInterface
public function register(Container $container)
{
$container['task'] = function (Grav $c) {
$task = $_POST['task'] ?? $c['uri']->param('task');
/** @var ServerRequestInterface $request */
$request = $c['request'];
$body = $request->getParsedBody();
$task = $body['task'] ?? $c['uri']->param('task');
if (null !== $task) {
$task = filter_var($task, FILTER_SANITIZE_STRING);
}
@@ -35,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface
};
$container['action'] = function (Grav $c) {
$action = $_POST['action'] ?? $c['uri']->param('action');
/** @var ServerRequestInterface $request */
$request = $c['request'];
$body = $request->getParsedBody();
$action = $body['action'] ?? $c['uri']->param('action');
if (null !== $action) {
$action = filter_var($action, FILTER_SANITIZE_STRING);
}

View File

@@ -12,6 +12,7 @@ namespace Grav\Common;
use Grav\Common\Form\FormFlash;
use Grav\Events\SessionStartEvent;
use Grav\Plugin\Form\Forms;
use JsonException;
use function is_string;
/**
@@ -148,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session
* @param mixed $object
* @param int $time
* @return $this
* @throws JsonException
*/
public function setFlashCookieObject($name, $object, $time = 60)
{
setcookie($name, json_encode($object), time() + $time, '/');
setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time));
return $this;
}
@@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session
*
* @param string $name
* @return mixed|null
* @throws JsonException
*/
public function getFlashCookieObject($name)
{
if (isset($_COOKIE[$name])) {
$object = json_decode($_COOKIE[$name], false);
setcookie($name, '', time() - 3600, '/');
return $object;
$cookie = $_COOKIE[$name];
setcookie($name, '', $this->getCookieOptions(-42000));
return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR);
}
return null;

View File

@@ -224,28 +224,18 @@ class Themes extends Iterator
$grav = $this->grav;
$config = $this->config;
$name = $this->current();
$class = null;
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php");
// Start by attempting to load the theme.php file.
$file = $locator('theme://theme.php') ?: $locator("theme://{$name}.php");
if ($file) {
// Local variables available in the file: $grav, $config, $name, $file
$class = include $file;
if (!$class || !is_subclass_of($class, Plugin::class, true)) {
$className = Inflector::camelize($name);
$themeClassFormat = [
'Grav\\Theme\\' . $className,
'Grav\\Theme\\' . ucfirst($name)
];
foreach ($themeClassFormat as $themeClass) {
if (is_subclass_of($themeClass, Theme::class, true)) {
$class = new $themeClass($grav, $config, $name);
break;
}
}
if (!\is_object($class) || !is_subclass_of($class, Theme::class, true)) {
$class = null;
}
} elseif (!$locator('theme://') && !defined('GRAV_CLI')) {
$response = new Response(500, [], "Theme '$name' does not exist, unable to display page.");
@@ -253,12 +243,28 @@ class Themes extends Iterator
$grav->close($response);
}
$this->config->set('theme', $config->get('themes.' . $name));
// If the class hasn't been initialized yet, guess the class name and create a new instance.
if (null === $class) {
$themeClassFormat = [
'Grav\\Theme\\' . Inflector::camelize($name),
'Grav\\Theme\\' . ucfirst($name)
];
if (empty($class)) {
foreach ($themeClassFormat as $themeClass) {
if (is_subclass_of($themeClass, Theme::class, true)) {
$class = new $themeClass($grav, $config, $name);
break;
}
}
}
// Finally if everything else fails, just create a new instance from the default Theme class.
if (null === $class) {
$class = new Theme($grav, $config, $name);
}
$this->config->set('theme', $config->get('themes.' . $name));
return $class;
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @package Grav\Common\Twig\Exception
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig\Exception;
/**
* TwigException gets thrown when you use {% throw code message %} in twig.
*
* This allows Grav to catch 401, 403 and 404 exceptions and display proper error page.
*/
class TwigException extends \RuntimeException
{
}

View File

@@ -13,11 +13,12 @@ use Grav\Common\Grav;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* Class TwigExtension
* @package Grav\Common\Twig
* Class FilesystemExtension
* @package Grav\Common\Twig\Extension
*/
class FilesystemExtension extends AbstractExtension
{
@@ -30,11 +31,35 @@ class FilesystemExtension extends AbstractExtension
}
/**
* @return TwigFunction[]
* @return TwigFilter[]
*/
public function getFilters()
{
return $this->getFunctions();
return [
new TwigFilter('file_exists', [$this, 'file_exists']),
new TwigFilter('fileatime', [$this, 'fileatime']),
new TwigFilter('filectime', [$this, 'filectime']),
new TwigFilter('filemtime', [$this, 'filemtime']),
new TwigFilter('filesize', [$this, 'filesize']),
new TwigFilter('filetype', [$this, 'filetype']),
new TwigFilter('is_dir', [$this, 'is_dir']),
new TwigFilter('is_file', [$this, 'is_file']),
new TwigFilter('is_link', [$this, 'is_link']),
new TwigFilter('is_readable', [$this, 'is_readable']),
new TwigFilter('is_writable', [$this, 'is_writable']),
new TwigFilter('is_writeable', [$this, 'is_writable']),
new TwigFilter('lstat', [$this, 'lstat']),
new TwigFilter('getimagesize', [$this, 'getimagesize']),
new TwigFilter('exif_read_data', [$this, 'exif_read_data']),
new TwigFilter('read_exif_data', [$this, 'exif_read_data']),
new TwigFilter('exif_imagetype', [$this, 'exif_imagetype']),
new TwigFilter('hash_file', [$this, 'hash_file']),
new TwigFilter('hash_hmac_file', [$this, 'hash_hmac_file']),
new TwigFilter('md5_file', [$this, 'md5_file']),
new TwigFilter('sha1_file', [$this, 'sha1_file']),
new TwigFilter('get_meta_tags', [$this, 'get_meta_tags']),
new TwigFilter('pathinfo', [$this, 'pathinfo']),
];
}
/**
@@ -67,6 +92,7 @@ class FilesystemExtension extends AbstractExtension
new TwigFunction('md5_file', [$this, 'md5_file']),
new TwigFunction('sha1_file', [$this, 'sha1_file']),
new TwigFunction('get_meta_tags', [$this, 'get_meta_tags']),
new TwigFunction('pathinfo', [$this, 'pathinfo']),
];
}
@@ -265,7 +291,7 @@ class FilesystemExtension extends AbstractExtension
return false;
}
return @exif_imagetype();
return @exif_imagetype($filename);
}
/**
@@ -340,6 +366,20 @@ class FilesystemExtension extends AbstractExtension
return get_meta_tags($filename);
}
/**
* @param string $path
* @param int|null $flags
* @return string|string[]
*/
public function pathinfo($path, $flags = null)
{
if (null !== $flags) {
return pathinfo($path, (int)$flags);
}
return pathinfo($path);
}
/**
* @param string $filename
* @return bool

View File

@@ -63,8 +63,8 @@ use function is_string;
use function strlen;
/**
* Class TwigExtension
* @package Grav\Common\Twig
* Class GravExtension
* @package Grav\Common\Twig\Extension
*/
class GravExtension extends AbstractExtension implements GlobalsInterface
{
@@ -76,7 +76,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
protected $config;
/**
* TwigExtension constructor.
* GravExtension constructor.
*/
public function __construct()
{
@@ -155,10 +155,15 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
new TwigFilter('bool', [$this, 'boolFilter']),
new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
new TwigFilter('array', [$this, 'arrayFilter']),
new TwigFilter('yaml', [$this, 'yamlFilter']),
// Object Types
new TwigFilter('get_type', [$this, 'getTypeFunc']),
new TwigFilter('of_type', [$this, 'ofTypeFunc'])
new TwigFilter('of_type', [$this, 'ofTypeFunc']),
// PHP methods
new TwigFilter('count', 'count'),
new TwigFilter('array_diff', 'array_diff'),
];
}
@@ -220,7 +225,18 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
// Object Types
new TwigFunction('get_type', [$this, 'getTypeFunc']),
new TwigFunction('of_type', [$this, 'ofTypeFunc'])
new TwigFunction('of_type', [$this, 'ofTypeFunc']),
// PHP methods
new TwigFunction('is_numeric', 'is_numeric'),
new TwigFunction('is_iterable', 'is_iterable'),
new TwigFunction('is_countable', 'is_countable'),
new TwigFunction('is_null', 'is_null'),
new TwigFunction('is_string', 'is_string'),
new TwigFunction('is_array', 'is_array'),
new TwigFunction('is_object', 'is_object'),
new TwigFunction('count', 'count'),
new TwigFunction('array_diff', 'array_diff'),
];
}
@@ -792,6 +808,17 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
return (array)$input;
}
/**
* @param array|object $value
* @param int|null $inline
* @param int|null $indent
* @return string
*/
public function yamlFilter($value, $inline = null, $indent = null): string
{
return Yaml::dump($value, $inline, $indent);
}
/**
* @param Environment $twig
* @return string
@@ -1484,7 +1511,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
}
//Look for existing class
$svg = preg_replace_callback('/^<svg[^>]*(class=\")([^"]*)(\")[^>]*>/', function($matches) use ($classes, &$matched) {
$svg = preg_replace_callback('/^<svg[^>]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) {
if (isset($matches[2])) {
$new_classes = $matches[2] . $classes;
$matched = true;
@@ -1500,7 +1527,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
$svg = str_replace('<svg ', "<svg class=\"$classes\" ", $svg);
}
return $svg;
return trim($svg);
}
return null;

View File

@@ -47,6 +47,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($context, $content);' . PHP_EOL);
->write('echo $this->env->getExtension(\'Grav\Common\Twig\Extension\GravExtension\')->markdownFunction($context, $content);' . PHP_EOL);
}
}

View File

@@ -43,7 +43,7 @@ class TwigNodeThrow extends Node
$compiler->addDebugInfo($this);
$compiler
->write('throw new \RuntimeException(')
->write('throw new \Grav\Common\Twig\Exception\TwigException(')
->subcompile($this->getNode('message'))
->write(', ')
->write($this->getAttribute('code') ?: 500)

View File

@@ -49,16 +49,15 @@ class TwigNodeTryCatch extends Node
$compiler
->indent()
->subcompile($this->getNode('try'));
->subcompile($this->getNode('try'))
->outdent()
->write('} catch (\Exception $e) {' . "\n")
->indent()
->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
->write('$context[\'e\'] = $e;' . "\n");
if ($this->hasNode('catch')) {
$compiler
->outdent()
->write('} catch (\Exception $e) {' . "\n")
->indent()
->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
->write('$context[\'e\'] = $e;' . "\n")
->subcompile($this->getNode('catch'));
$compiler->subcompile($this->getNode('catch'));
}
$compiler

View File

@@ -16,6 +16,7 @@ use Grav\Common\Language\Language;
use Grav\Common\Language\LanguageCodes;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
use Grav\Common\Twig\Exception\TwigException;
use Grav\Common\Twig\Extension\FilesystemExtension;
use Grav\Common\Twig\Extension\GravExtension;
use Grav\Common\Utils;
@@ -26,6 +27,7 @@ use RuntimeException;
use Twig\Cache\FilesystemCache;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\DebugExtension;
use Twig\Extension\StringLoaderExtension;
@@ -404,38 +406,63 @@ class Twig
*/
public function processSite($format = null, array $vars = [])
{
// set the page now its been processed
$this->grav->fireEvent('onTwigSiteVariables');
/** @var Pages $pages */
$pages = $this->grav['pages'];
/** @var PageInterface $page */
$page = $this->grav['page'];
$content = $page->content();
$twig_vars = $this->twig_vars;
$twig_vars['theme'] = $this->grav['config']->get('theme');
$twig_vars['pages'] = $pages->root();
$twig_vars['page'] = $page;
$twig_vars['header'] = $page->header();
$twig_vars['media'] = $page->media();
$twig_vars['content'] = $content;
// determine if params are set, if so disable twig cache
$params = $this->grav['uri']->params(null, true);
if (!empty($params)) {
$this->twig->setCache(false);
}
// Get Twig template layout
$template = $this->getPageTwigTemplate($page, $format);
$page->templateFormat($format);
try {
$grav = $this->grav;
// set the page now its been processed
$grav->fireEvent('onTwigSiteVariables');
/** @var Pages $pages */
$pages = $grav['pages'];
/** @var PageInterface $page */
$page = $grav['page'];
$twig_vars = $this->twig_vars;
$twig_vars['theme'] = $grav['config']->get('theme');
$twig_vars['pages'] = $pages->root();
$twig_vars['page'] = $page;
$twig_vars['header'] = $page->header();
$twig_vars['media'] = $page->media();
$twig_vars['content'] = $page->content();
// determine if params are set, if so disable twig cache
$params = $grav['uri']->params(null, true);
if (!empty($params)) {
$this->twig->setCache(false);
}
// Get Twig template layout
$template = $this->getPageTwigTemplate($page, $format);
$page->templateFormat($format);
$output = $this->twig->render($template, $vars + $twig_vars);
} catch (LoaderError $e) {
$error_msg = $e->getMessage();
throw new RuntimeException($error_msg, 400, $e);
throw new RuntimeException($e->getMessage(), 400, $e);
} catch (RuntimeError $e) {
$prev = $e->getPrevious();
if ($prev instanceof TwigException) {
$code = $prev->getCode() ?: 500;
// Fire onPageNotFound event.
$event = new Event([
'page' => $page,
'code' => $code,
'message' => $prev->getMessage(),
'exception' => $prev,
'route' => $grav['route'],
'request' => $grav['request']
]);
$event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event);
$newPage = $event['page'];
if ($newPage && $newPage !== $page) {
unset($grav['page']);
$grav['page'] = $newPage;
return $this->processSite($newPage->templateFormat(), $vars);
}
}
throw $e;
}
return $output;

View File

@@ -665,7 +665,7 @@ class Uri
*/
public static function paramsRegex()
{
return '/\/([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/';
return '/\/{1,}([^\:\#\/\?]*' . Grav::instance()['config']->get('system.param_sep') . '[^\:\#\/\?]*)/';
}
/**
@@ -675,10 +675,15 @@ class Uri
*/
public static function ip()
{
$ip = 'UNKNOWN';
if (getenv('HTTP_CLIENT_IP')) {
$ip = getenv('HTTP_CLIENT_IP');
} elseif (getenv('HTTP_CF_CONNECTING_IP')) {
$ip = getenv('HTTP_CF_CONNECTING_IP');
} elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
$ips = array_map('trim', explode(',', getenv('HTTP_X_FORWARDED_FOR')));
$ip = array_shift($ips);
} elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
$ip = getenv('HTTP_X_FORWARDED');
} elseif (getenv('HTTP_FORWARDED_FOR')) {
@@ -687,8 +692,6 @@ class Uri
$ip = getenv('HTTP_FORWARDED');
} elseif (getenv('REMOTE_ADDR')) {
$ip = getenv('REMOTE_ADDR');
} else {
$ip = 'UNKNOWN';
}
return $ip;
@@ -1258,7 +1261,7 @@ class Uri
$this->port = null;
}
if ($this->hasStandardPort()) {
if ($this->port === 0 || $this->hasStandardPort()) {
$this->port = null;
}
@@ -1311,11 +1314,13 @@ class Uri
if ($parts === false) {
throw new RuntimeException('Malformed URL: ' . $url);
}
$port = (int)($parts['port'] ?? 0);
$this->scheme = $parts['scheme'] ?? null;
$this->user = $parts['user'] ?? null;
$this->password = $parts['pass'] ?? null;
$this->host = $parts['host'] ?? null;
$this->port = isset($parts['port']) ? (int)$parts['port'] : null;
$this->port = $port ?: null;
$this->path = $parts['path'] ?? '';
$this->query = $parts['query'] ?? '';
$this->fragment = $parts['fragment'] ?? null;
@@ -1498,7 +1503,7 @@ class Uri
* @param string $delimiter
* @return string
*/
private function processParams($uri, $delimiter = ':')
private function processParams(string $uri, string $delimiter = ':'): string
{
if (strpos($uri, $delimiter) !== false) {
preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER);

View File

@@ -47,7 +47,7 @@ use function is_callable;
* @package Grav\Framework\Flex
* @template T
*/
class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
class FlexDirectory implements FlexDirectoryInterface
{
use FlexAuthorizeTrait;
@@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$filename = $locator->findResource($this->getDirectoryConfigUri($name), true);
$uri = $this->getDirectoryConfigUri($name);
// If configuration is found in main configuration, use it.
if (str_starts_with($uri, 'config://')) {
$path = str_replace('/', '.', substr($uri, 9, -5));
return (array)$grav['config']->get($path);
}
// Load the configuration file.
$filename = $locator->findResource($uri, true);
if ($filename === false) {
return [];
}
@@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
* @param array $call
* @return void
*/
protected function dynamicFlexField(array &$field, $property, array $call)
protected function dynamicFlexField(array &$field, $property, array $call): void
{
$params = (array)$call['params'];
$object = $call['object'] ?? null;
$method = array_shift($params);
$not = false;
if (str_starts_with($method, '!')) {
$method = substr($method, 1);
$not = true;
} elseif (str_starts_with($method, 'not ')) {
$method = substr($method, 4);
$not = true;
}
$method = trim($method);
if ($object && method_exists($object, $method)) {
$value = $object->{$method}(...$params);
if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
$field[$property] = array_merge_recursive($field[$property], $value);
$value = $this->mergeArrays($field[$property], $value);
}
$field[$property] = $not ? !$value : $value;
}
}
/**
* @param array $array1
* @param array $array2
* @return array
*/
protected function mergeArrays(array $array1, array $array2): array
{
foreach ($array2 as $key => $value) {
if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
$array1[$key] = $this->mergeArrays($array1[$key], $value);
} else {
$field[$property] = $value;
$array1[$key] = $value;
}
}
return $array1;
}
/**

View File

@@ -318,11 +318,11 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
}
/**
* @param string $field
* @param string $filename
* @param string|null $field
* @param string|null $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
{
return null;
}
@@ -453,7 +453,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'form' => $this->form,
'directory' => $this->directory,
'flexName' => $this->flexName
];
}
@@ -465,7 +467,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
{
$this->doTraitUnserialize($data);
$this->form = $data['form'];
$this->directory = $data['directory'];
$this->flexName = $data['flexName'];
}
/**

View File

@@ -103,7 +103,14 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$this->name = $name;
$this->setObject($object);
$this->setName($object->getFlexType(), $name);
if (isset($options['form']['name'])) {
// Use custom form name.
$this->flexName = $options['form']['name'];
} else {
// Use standard form name.
$this->setName($object->getFlexType(), $name);
}
$this->setId($this->getName());
$uniqueId = $options['unique_id'] ?? null;
@@ -119,7 +126,7 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
}
$this->setUniqueId($uniqueId);
$directory = $object->getFlexDirectory();
$this->setFlashLookupFolder($directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
$this->setFlashLookupFolder($options['flash_folder'] ?? $directory->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]');
$this->form = $options['form'] ?? null;
if (!empty($options['reset'])) {
@@ -371,22 +378,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
/** @var Route $route */
$route = Grav::instance()['route'];
return $route->withExtension('json')->withGravParam('task', 'media.upload');
}
return $object->route('/edit.json/task:media.upload');
}
/**
* @param string $field
* @param string $filename
* @param string|null $field
* @param string|null $filename
* @return Route|null
*/
public function getFileDeleteAjaxRoute($field, $filename): ?Route
public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
{
$object = $this->getObject();
if (!method_exists($object, 'route')) {
return null;
/** @var Route $route */
$route = Grav::instance()['route'];
return $route->withExtension('json')->withGravParam('task', 'media.delete');
}
return $object->route('/edit.json/task:media.delete');
@@ -536,7 +549,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
protected function doSerialize(): array
{
return $this->doTraitSerialize() + [
'items' => $this->items,
'form' => $this->form,
'object' => $this->object,
'flexName' => $this->flexName,
'submitMethod' => $this->submitMethod,
];
}
@@ -548,7 +565,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
{
$this->doTraitUnserialize($data);
$this->object = $data['object'];
$this->items = $data['items'] ?? null;
$this->form = $data['form'] ?? null;
$this->object = $data['object'] ?? null;
$this->flexName = $data['flexName'] ?? null;
$this->submitMethod = $data['submitMethod'] ?? null;
}
/**

View File

@@ -44,6 +44,7 @@ use function is_array;
use function is_object;
use function is_scalar;
use function is_string;
use function json_encode;
/**
* Class FlexObject
@@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
/** @var array */
private $_meta;
/** @var array */
protected $_original;
/** @var array */
protected $_changes;
/** @var string */
protected $storage_key;
@@ -196,9 +199,10 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
/**
* Refresh object from the storage.
*
* @param bool $keepMissing
* @return bool True if the object was refreshed
*/
public function refresh(): bool
public function refresh(bool $keepMissing = false): bool
{
$key = $this->getStorageKey();
if ('' === $key) {
@@ -216,20 +220,36 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return false;
}
// Get current elements (if requested).
$current = $keepMissing ? $this->getElements() : [];
// Get elements from the filesystem.
$elements = $storage->readRows([$key => null])[$key] ?? null;
if (null !== $elements || isset($elements['__ERROR'])) {
$meta = $elements['_META'] ?? $meta;
if (null !== $elements) {
$meta = $elements['__META'] ?? $meta;
unset($elements['__META']);
$this->filterElements($elements);
$newKey = $meta['key'] ?? $this->getKey();
if ($meta) {
$this->setMetaData($meta);
}
$this->objectConstruct($elements, $newKey);
}
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage("Refreshed {$this->getFlexType()} object {$this->getKey()}", 'debug');
if ($current) {
// Inject back elements which are missing in the filesystem.
$data = $this->getBlueprint()->flattenData($current);
foreach ($data as $property => $value) {
if (strpos($property, '.') === false) {
$this->defProperty($property, $value);
} else {
$this->defNestedProperty($property, $value);
}
}
}
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage("Refreshed {$this->getFlexType()} object {$this->getKey()}", 'debug');
}
return true;
}
@@ -281,7 +301,11 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
$weight = 0;
foreach ($properties as $property) {
$weight += $this->searchNestedProperty($property, $search, $options);
if (strpos($property, '.')) {
$weight += $this->searchNestedProperty($property, $search, $options);
} else {
$weight += $this->searchProperty($property, $search, $options);
}
}
return $weight > 0 ? min($weight, 1) : 0;
@@ -348,7 +372,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function searchProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
$value = $this->getProperty($property);
return $this->searchValue($property, $value, $search, $options);
@@ -362,7 +386,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function searchNestedProperty(string $property, string $search, array $options = null): float
{
$options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
$options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
if ($property === 'key') {
$value = $this->getKey();
} else {
@@ -419,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return 0;
}
/**
* Get original data before update
*
* @return array
*/
public function getOriginalData(): array
{
return $this->_original ?? [];
}
/**
* Get any changes based on data sent to update
*
@@ -632,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
}
// Store the changes
$this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
$this->_original = $this->getElements();
$this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements);
}
if ($files && method_exists($this, 'setUpdatedMedia')) {
@@ -670,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return $this->create($key);
}
/**
* @param UserInterface|null $user
*/
public function check(UserInterface $user = null): void
{
// If user has been provided, check if the user has permissions to save this object.
if ($user && !$this->isAuthorized('save', null, $user)) {
throw new \RuntimeException('Forbidden', 403);
}
}
/**
* {@inheritdoc}
* @see FlexObjectInterface::save()
@@ -788,11 +834,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
*/
public function getForm(string $name = '', array $options = null)
{
if (!isset($this->_forms[$name])) {
$this->_forms[$name] = $this->createFormObject($name, $options);
$hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR));
if (!isset($this->_forms[$hash])) {
$this->_forms[$hash] = $this->createFormObject($name, $options);
}
return $this->_forms[$name];
return $this->_forms[$hash];
}
/**
@@ -1042,6 +1089,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
return $action;
}
/**
* Method to reset blueprints if the type changes.
*
* @return void
* @since 1.7.18
*/
protected function resetBlueprints(): void
{
$this->_blueprint = [];
}
// DEPRECATED METHODS
/**

View File

@@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
* Interface FlexDirectoryInterface
* @package Grav\Framework\Flex\Interfaces
*/
interface FlexDirectoryInterface
interface FlexDirectoryInterface extends FlexAuthorizeInterface
{
/**
* @return bool

View File

@@ -38,8 +38,8 @@ interface FlexFormInterface extends Serializable, FormInterface
/**
* Get route for deleting files by AJAX.
*
* @param string $field Field where the file is associated into.
* @param string $filename Filename for the file.
* @param string|null $field Field where the file is associated into.
* @param string|null $filename Filename for the file.
* @return Route|null Returns Route object or null if file uploads are not enabled.
*/
public function getFileDeleteAjaxRoute($field, $filename);

View File

@@ -320,14 +320,22 @@ trait PageLegacyTrait
// Find non-existing key.
$parentKey = $parent ? $parent->getKey() : '';
$key = trim($parentKey . '/' . basename($this->getKey()), '/');
$key = preg_replace('/-\d+$/', '', $key);
$i = 1;
do {
$i++;
$test = "{$key}-{$i}";
} while ($index->containsKey($test));
$key = $test;
if ($this instanceof FlexPageObject) {
$key = trim($parentKey . '/' . $this->folder(), '/');
$key = preg_replace(static::PAGE_ORDER_PREFIX_REGEX, '', $key);
} else {
$key = trim($parentKey . '/' . basename($this->getKey()), '/');
}
if ($index->containsKey($key)) {
$key = preg_replace('/\d+$/', '', $key);
$i = 1;
do {
$i++;
$test = "{$key}{$i}";
} while ($index->containsKey($test));
$key = $test;
}
$folder = basename($key);
// Get the folder name.

View File

@@ -40,6 +40,8 @@ class FolderStorage extends AbstractFilesystemStorage
protected $dataFolder;
/** @var string Pattern to access an object. */
protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}';
/** @var string[] */
protected $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
/** @var string Filename for the object. */
protected $dataFile;
/** @var string File extension for the object. */
@@ -380,6 +382,12 @@ class FolderStorage extends AbstractFilesystemStorage
if (isset($data[0])) {
throw new RuntimeException('Broken object file');
}
// Add key field to the object.
$keyField = $this->keyField;
if ($keyField !== 'storage_key' && !isset($data[$keyField])) {
$data[$keyField] = $key;
}
} catch (RuntimeException $e) {
$data = ['__ERROR' => $e->getMessage()];
} finally {
@@ -692,9 +700,7 @@ class FolderStorage extends AbstractFilesystemStorage
$this->keyLen = (int)($options['key_len'] ?? 32);
$this->caseSensitive = (bool)($options['case_sensitive'] ?? true);
$variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
$pattern = Utils::simpleTemplate($pattern, $variables);
$pattern = Utils::simpleTemplate($pattern, $this->variables);
if (!$pattern) {
throw new RuntimeException('Bad storage folder pattern');
}

View File

@@ -455,7 +455,7 @@ class SimpleStorage extends AbstractFilesystemStorage
$content = (array) $file->content();
if ($this->prefix) {
$data = new Data($content);
$content = $data->get($this->prefix);
$content = $data->get($this->prefix, []);
}
$file->free();

View File

@@ -120,7 +120,7 @@ trait FlexMediaTrait
// Load settings for the field.
$schema = $this->getBlueprint()->schema();
$settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null;
if (!isset($settings) || !is_array($settings)) {
if (!is_array($settings)) {
return null;
}
@@ -373,7 +373,7 @@ trait FlexMediaTrait
if (is_array($upload)) {
// Uses new format with [UploadedFileInterface, array].
$settings = $upload[1];
if ($settings['destination'] === $media->getPath()) {
if (isset($settings['destination']) && $settings['destination'] === $media->getPath()) {
$upload = $upload[0];
} else {
$upload = false;
@@ -383,6 +383,7 @@ trait FlexMediaTrait
$medium = $upload ? MediumFactory::fromUploadedFile($upload) : null;
$updated = true;
if ($medium) {
$medium->uploaded = true;
$media->add($filename, $medium);
} elseif (is_callable([$media, 'hide'])) {
$media->hide($filename);

View File

@@ -120,7 +120,7 @@ class FormFlash implements FormFlashInterface
protected function loadStoredForm(): ?array
{
$file = $this->getTmpIndex();
$exists = $file->exists();
$exists = $file && $file->exists();
$data = null;
if ($exists) {
@@ -246,8 +246,10 @@ class FormFlash implements FormFlashInterface
if ($force || $this->data || $this->files) {
// Only save if there is data or files to be saved.
$file = $this->getTmpIndex();
$file->save($this->jsonSerialize());
$this->exists = true;
if ($file) {
$file->save($this->jsonSerialize());
$this->exists = true;
}
} elseif ($this->exists) {
// Delete empty form flash if it exists (it carries no information).
return $this->delete();
@@ -476,12 +478,14 @@ class FormFlash implements FormFlashInterface
}
/**
* @return YamlFile
* @return ?YamlFile
*/
protected function getTmpIndex(): YamlFile
protected function getTmpIndex(): ?YamlFile
{
$tmpDir = $this->getTmpDir();
// Do not use CompiledYamlFile as the file can change multiple times per second.
return YamlFile::instance($this->getTmpDir() . '/index.yaml');
return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null;
}
/**
@@ -503,7 +507,9 @@ class FormFlash implements FormFlashInterface
{
// Make sure that index file cache gets always cleared.
$file = $this->getTmpIndex();
$file->free();
if ($file) {
$file->free();
}
$tmpDir = $this->getTmpDir();
if ($tmpDir && file_exists($tmpDir)) {

View File

@@ -0,0 +1,107 @@
<?php declare(strict_types=1);
/**
* @package Grav\Framework\Mime
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Mime;
use function in_array;
/**
* Class to handle mime-types.
*/
class MimeTypes
{
/** @var array */
protected $extensions;
/** @var array */
protected $mimes;
/**
* Create a new mime types instance with the given mappings.
*
* @param array $mimes An associative array containing ['ext' => ['mime/type', 'mime/type2']]
*/
public static function createFromMimes(array $mimes): self
{
$extensions = [];
foreach ($mimes as $ext => $list) {
foreach ($list as $mime) {
$list = $extensions[$mime] ?? [];
if (!in_array($ext, $list, true)) {
$list[] = $ext;
$extensions[$mime] = $list;
}
}
}
return new static($extensions, $mimes);
}
/**
* @param string $extension
* @return string|null
*/
public function getMimeType(string $extension): ?string
{
$extension = $this->cleanInput($extension);
return $this->mimes[$extension][0] ?? null;
}
/**
* @param string $mime
* @return string|null
*/
public function getExtension(string $mime): ?string
{
$mime = $this->cleanInput($mime);
return $this->extensions[$mime][0] ?? null;
}
/**
* @param string $extension
* @return array
*/
public function getMimeTypes(string $extension): array
{
$extension = $this->cleanInput($extension);
return $this->mimes[$extension] ?? [];
}
/**
* @param string $mime
* @return array
*/
public function getExtensions(string $mime): array
{
$mime = $this->cleanInput($mime);
return $this->extensions[$mime] ?? [];
}
/**
* @param string $input
* @return string
*/
protected function cleanInput(string $input): string
{
return strtolower(trim($input));
}
/**
* @param array $extensions
* @param array $mimes
*/
protected function __construct(array $extensions, array $mimes)
{
$this->extensions = $extensions;
$this->mimes = $mimes;
}
}

View File

@@ -9,7 +9,6 @@
namespace Grav\Framework\Object;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Grav\Framework\Collection\ArrayCollection;
use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;

View File

@@ -23,6 +23,9 @@ class UploadedFile implements UploadedFileInterface
{
use UploadedFileDecoratorTrait;
/** @var array */
private $meta = [];
/**
* @param StreamInterface|string|resource $streamOrFile
* @param int $size
@@ -34,4 +37,34 @@ class UploadedFile implements UploadedFileInterface
{
$this->uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType);
}
/**
* @param array $meta
* @return $this
*/
public function setMeta(array $meta)
{
$this->meta = $meta;
return $this;
}
/**
* @param array $meta
* @return $this
*/
public function addMeta(array $meta)
{
$this->meta = array_merge($this->meta, $meta);
return $this;
}
/**
* @return array
*/
public function getMeta(): array
{
return $this->meta;
}
}

View File

@@ -338,23 +338,12 @@ class Session implements SessionInterface
{
$name = $this->getName();
if (null !== $name) {
$params = session_get_cookie_params();
$cookie_options = array (
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite']
);
$this->removeCookie();
setcookie(
session_name(),
'',
$cookie_options
$this->getCookieOptions(-42000)
);
}
@@ -463,27 +452,36 @@ class Session implements SessionInterface
}
/**
* @return void
* Store something in cookie temporarily.
*
* @param int|null $lifetime
* @return array
*/
protected function setCookie(): void
public function getCookieOptions(int $lifetime = null): array
{
$params = session_get_cookie_params();
$cookie_options = array (
'expires' => time() + $params['lifetime'],
return [
'expires' => time() + ($lifetime ?? $params['lifetime']),
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite']
);
];
}
/**
* @return void
*/
protected function setCookie(): void
{
$this->removeCookie();
setcookie(
session_name(),
session_id(),
$cookie_options
$this->getCookieOptions()
);
}

View File

@@ -0,0 +1,70 @@
<?php
// Fix too many ob_get_clean() calls when exception is thrown inside the template.
namespace Phive\Twig\Extensions\Deferred;
class DeferredExtension extends \Twig_Extension
{
/**
* @var array
*/
private $blocks = array();
/**
* {@inheritdoc}
*/
public function getTokenParsers()
{
return array(new DeferredTokenParser());
}
/**
* {@inheritdoc}
*/
public function getNodeVisitors()
{
return array(new DeferredNodeVisitor());
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'deferred';
}
public function defer(\Twig_Template $template, $blockName)
{
ob_start();
$templateName = $template->getTemplateName();
$this->blocks[$templateName][] = [ob_get_level(), $blockName];
}
public function resolve(\Twig_Template $template, array $context, array $blocks)
{
$templateName = $template->getTemplateName();
if (empty($this->blocks[$templateName])) {
return;
}
while ($block = array_pop($this->blocks[$templateName])) {
[$level, $blockName] = $block;
if (ob_get_level() !== $level) {
continue;
}
$buffer = ob_get_clean();
$blocks[$blockName] = array($template, 'block_'.$blockName.'_deferred');
$template->displayBlock($blockName, $context, $blocks);
echo $buffer;
}
if ($parent = $template->getParent($context)) {
$this->resolve($parent, $context, $blocks);
}
}
}

View File

@@ -124,8 +124,11 @@ parameters:
# Support for deprecated features
-
message: '#Instantiation of deprecated class Doctrine\\Common\\Cache\\MemcacheCache#'
message: '#Instantiation of deprecated class Doctrine\\Common\\Cache\\(\w+)Cache#'
path: '*/system/src/Grav/Common/Cache.php'
-
message: '#Instantiation of deprecated class Doctrine\\Common\\Cache\\(\w+)Cache#'
path: '*/system/src/Grav/Common/GPM/Remote/*.php'
-
message: '#Call to deprecated method order#'
path: '*/system/src/Grav/Common/Page/Pages.php'

View File

@@ -340,7 +340,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$this->assets->reset();
$this->assets->addJs('jquery', ['loading' => 'async']);
$js = $this->assets->js();
self::assertSame('<script src="/system/assets/jquery/jquery-2.x.min.js" async></script>' . PHP_EOL, $js);
self::assertSame('<script src="/system/assets/jquery/jquery-3.x.min.js" async></script>' . PHP_EOL, $js);
//Test priority too
$this->assets->reset();
@@ -348,7 +348,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$this->assets->addJs('test.js', ['loading' => 'async', 'priority' => 2]);
$js = $this->assets->js();
self::assertSame('<script src="/test.js" async></script>' . PHP_EOL .
'<script src="/system/assets/jquery/jquery-2.x.min.js" async></script>' . PHP_EOL, $js);
'<script src="/system/assets/jquery/jquery-3.x.min.js" async></script>' . PHP_EOL, $js);
//Test multiple groups
$this->assets->reset();
@@ -357,7 +357,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$js = $this->assets->js();
self::assertSame('<script src="/test.js" async></script>' . PHP_EOL, $js);
$js = $this->assets->js('footer');
self::assertSame('<script src="/system/assets/jquery/jquery-2.x.min.js" async></script>' . PHP_EOL, $js);
self::assertSame('<script src="/system/assets/jquery/jquery-3.x.min.js" async></script>' . PHP_EOL, $js);
//Test adding array of assets
//Test priority too
@@ -365,7 +365,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$this->assets->addJs(['jquery', 'test.js'], ['loading' => 'async']);
$js = $this->assets->js();
self::assertSame('<script src="/system/assets/jquery/jquery-2.x.min.js" async></script>' . PHP_EOL .
self::assertSame('<script src="/system/assets/jquery/jquery-3.x.min.js" async></script>' . PHP_EOL .
'<script src="/test.js" async></script>' . PHP_EOL, $js);
}
@@ -473,6 +473,59 @@ class AssetsTest extends \Codeception\TestCase\Test
'<link href="/test.css" type="text/css" rel="stylesheet" async>' . PHP_EOL, $css);
}
public function testAddingAssetPropertiesWithArrayFromCollectionAndParameters(): void
{
$this->assets->registerCollection('collection_multi_params', [
'foo.js' => [ 'defer' => true ],
'bar.js' => [ 'integrity' => 'sha512-abc123' ],
'foobar.css' => [ 'defer' => null, 'loading' => null ]
]);
// # Test adding properties with array
$this->assets->addJs('collection_multi_params', ['loading' => 'async']);
$js = $this->assets->js();
// expected output
$expected = [
'<script src="/foo.js" async defer="1"></script>',
'<script src="/bar.js" async integrity="sha512-abc123"></script>',
'<script src="/foobar.css"></script>',
];
self::assertCount(count($expected), array_filter(explode("\n", $js)));
self::assertSame(implode("\n", $expected) . PHP_EOL, $js);
// # Test priority as second argument + render JS should not have any css
$this->assets->reset();
$this->assets->add('low_priority.js', 1);
$this->assets->add('collection_multi_params', 2);
$js = $this->assets->js();
// expected output
$expected = [
'<script src="/foo.js" defer="1"></script>',
'<script src="/bar.js" integrity="sha512-abc123"></script>',
'<script src="/low_priority.js"></script>',
];
self::assertCount(3, array_filter(explode("\n", $js)));
self::assertSame(implode("\n", $expected) . PHP_EOL, $js);
// # Test rendering CSS, should not have any JS
$this->assets->reset();
$this->assets->add('collection_multi_params', [ 'class' => '__classname' ]);
$css = $this->assets->css();
// expected output
$expected = [
'<link href="/foobar.css" type="text/css" rel="stylesheet" class="__classname">',
];
self::assertCount(1, array_filter(explode("\n", $css)));
self::assertSame(implode("\n", $expected) . PHP_EOL, $css);
}
public function testPriorityOfAssets(): void
{
$this->assets->reset();
@@ -573,7 +626,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$this->assets->reset();
$this->assets->addAsyncJs('jquery');
$js = $this->assets->js();
self::assertSame('<script src="/system/assets/jquery/jquery-2.x.min.js" async></script>' . PHP_EOL, $js);
self::assertSame('<script src="/system/assets/jquery/jquery-3.x.min.js" async></script>' . PHP_EOL, $js);
}
public function testAddDeferJs(): void
@@ -581,7 +634,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$this->assets->reset();
$this->assets->addDeferJs('jquery');
$js = $this->assets->js();
self::assertSame('<script src="/system/assets/jquery/jquery-2.x.min.js" defer></script>' . PHP_EOL, $js);
self::assertSame('<script src="/system/assets/jquery/jquery-3.x.min.js" defer></script>' . PHP_EOL, $js);
}
public function testTimestamps(): void
@@ -663,7 +716,7 @@ class AssetsTest extends \Codeception\TestCase\Test
{
self::assertIsArray($this->assets->getCollections());
self::assertContains('jquery', array_keys($this->assets->getCollections()));
self::assertContains('system://assets/jquery/jquery-2.x.min.js', $this->assets->getCollections());
self::assertContains('system://assets/jquery/jquery-3.x.min.js', $this->assets->getCollections());
}
public function testExists(): void
@@ -679,6 +732,27 @@ class AssetsTest extends \Codeception\TestCase\Test
self::assertContains('debugger', array_keys($this->assets->getCollections()));
}
public function testRegisterCollectionWithParameters(): void
{
$this->assets->registerCollection('collection_multi_params', [
'foo.js' => [ 'defer' => true ],
'bar.js' => [ 'integrity' => 'sha512-abc123' ],
'foobar.css' => [ 'defer' => null ],
]);
self::assertTrue($this->assets->exists('collection_multi_params'));
$collection = $this->assets->getCollections()['collection_multi_params'];
self::assertArrayHasKey('foo.js', $collection);
self::assertArrayHasKey('bar.js', $collection);
self::assertArrayHasKey('foobar.css', $collection);
self::assertArrayHasKey('defer', $collection['foo.js']);
self::assertArrayHasKey('defer', $collection['foobar.css']);
self::assertNull($collection['foobar.css']['defer']);
self::assertTrue($collection['foo.js']['defer']);
}
public function testReset(): void
{
$this->assets->addInlineJs('alert("test")');

View File

@@ -2,23 +2,23 @@
use Codeception\Util\Fixtures;
use Grav\Common\Grav;
use Grav\Common\Twig\TwigExtension;
use Grav\Common\Twig\Extension\GravExtension;
/**
* Class TwigExtensionTest
* Class GravExtensionTest
*/
class TwigExtensionTest extends \Codeception\TestCase\Test
class GravExtensionTest extends \Codeception\TestCase\Test
{
/** @var Grav $grav */
protected $grav;
/** @var TwigExtension $twig_ext */
/** @var GravExtension $twig_ext */
protected $twig_ext;
protected function _before(): void
{
$this->grav = Fixtures::get('grav');
$this->twig_ext = new TwigExtension();
$this->twig_ext = new GravExtension();
}
public function testInflectorFilter(): void