mirror of
https://github.com/getgrav/grav.git
synced 2025-12-05 15:29:57 +01:00
Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3c82f85c8 | ||
|
|
b17eaba8bf | ||
|
|
0600d6a4d8 | ||
|
|
c51fb1779b | ||
|
|
34b7a28fbe | ||
|
|
a446152631 | ||
|
|
61c2abee35 | ||
|
|
c1d520f1cf | ||
|
|
3bd9e44155 | ||
|
|
7311517d65 | ||
|
|
4568a197e7 | ||
|
|
350134b256 | ||
|
|
1350cf5675 | ||
|
|
21db2e7d4a | ||
|
|
cda08242f1 | ||
|
|
fc8936986f | ||
|
|
ec37fd065f | ||
|
|
47875a4525 | ||
|
|
61adb1e6cf | ||
|
|
907e46631c | ||
|
|
aedf8cda47 | ||
|
|
7a1f5539ed | ||
|
|
49087e9a53 | ||
|
|
a128c7f18d | ||
|
|
c6704d8129 | ||
|
|
c43b375d3b | ||
|
|
9523bab910 | ||
|
|
a8fe62a829 | ||
|
|
4708a46ec9 | ||
|
|
7a99aaa53f | ||
|
|
e54e488f80 | ||
|
|
bf471cc3fa | ||
|
|
fd8c44ba90 | ||
|
|
f9e7f1c08e | ||
|
|
8042caee57 | ||
|
|
3f3f63f411 | ||
|
|
292687ea00 | ||
|
|
aa47cb7b97 | ||
|
|
794237bf30 | ||
|
|
de3aa16aca | ||
|
|
34d001cbef | ||
|
|
21bd51aef9 | ||
|
|
f45afd1f54 | ||
|
|
c975f894ae | ||
|
|
a9b59596d8 | ||
|
|
9333fcc1d6 | ||
|
|
b3426f86a3 | ||
|
|
b3e9682511 | ||
|
|
b3af6c9920 | ||
|
|
2e9fe80e33 | ||
|
|
def389356e | ||
|
|
c5dfa65994 | ||
|
|
c9159695aa | ||
|
|
17d1786e5c | ||
|
|
5437d2db1a | ||
|
|
e390e9901e | ||
|
|
7c946c59f8 | ||
|
|
506c74de55 | ||
|
|
ab9783102e | ||
|
|
a8a8cce25f | ||
|
|
d62e869044 | ||
|
|
6dd5e0fd20 | ||
|
|
c57a29c23f | ||
|
|
2866a51326 | ||
|
|
21f5488d3b | ||
|
|
7b1a188cfe | ||
|
|
fdcf7026d2 | ||
|
|
b8ada23e2b | ||
|
|
5def813a2e | ||
|
|
551a8251f9 | ||
|
|
951ce6f9f8 | ||
|
|
c9448870fa | ||
|
|
1d552ab603 | ||
|
|
e39d01e139 | ||
|
|
d4805bc709 | ||
|
|
35db2f61f7 | ||
|
|
8af1229f65 | ||
|
|
9aa6f5b1f7 | ||
|
|
da8e374443 | ||
|
|
95851e8f52 | ||
|
|
d2350b6786 | ||
|
|
08a2abb713 | ||
|
|
ac62f54aa5 | ||
|
|
fb189a3ce4 | ||
|
|
7e41938317 | ||
|
|
d90b28a399 | ||
|
|
90f5635478 | ||
|
|
acf8724402 | ||
|
|
f1c623c14b | ||
|
|
ee40ad59f2 | ||
|
|
845fac8adf | ||
|
|
896695b30f | ||
|
|
fbfa88739d | ||
|
|
ea191602da | ||
|
|
564287eb21 | ||
|
|
28790197aa | ||
|
|
1db66fd43d | ||
|
|
8d506db73c | ||
|
|
c288d4bd0b | ||
|
|
39247ac7ef | ||
|
|
47b5b10bf4 | ||
|
|
ad1cf15d7c | ||
|
|
6339d9f3cd | ||
|
|
fc36a76fc0 | ||
|
|
18d7fd4c7d | ||
|
|
76e44a1043 | ||
|
|
4c0d107562 | ||
|
|
d359120d81 | ||
|
|
278671deec | ||
|
|
e28360f86a | ||
|
|
75cef03644 | ||
|
|
33be6946f7 | ||
|
|
7f23b088a4 | ||
|
|
c56f9f3277 | ||
|
|
6198d5abf3 | ||
|
|
3f8c51cc01 | ||
|
|
b693ed4071 | ||
|
|
5621f5cdb0 | ||
|
|
83f2097f40 | ||
|
|
879de1d95e | ||
|
|
ec018f40aa | ||
|
|
0866753617 | ||
|
|
5f66f2c4a9 | ||
|
|
40f08a7f8b | ||
|
|
c274337fed | ||
|
|
7d01977a89 | ||
|
|
d058c1d4fc | ||
|
|
1a28155f1c | ||
|
|
7ca7d8e045 | ||
|
|
ec98ddc2df | ||
|
|
72b520745a | ||
|
|
9b4f0ca951 | ||
|
|
a761df80db | ||
|
|
9059904c1a | ||
|
|
568e728d20 | ||
|
|
e56d414357 | ||
|
|
668f8ccdbf | ||
|
|
9281be57fc | ||
|
|
d1e58eb95e | ||
|
|
9f5a15f00a | ||
|
|
3aa47043c9 | ||
|
|
8532db70d2 | ||
|
|
f8106a48ae | ||
|
|
2813934d21 | ||
|
|
defb793b0b | ||
|
|
1fd2162d4f | ||
|
|
094a1bd5ee | ||
|
|
2cf7a5f281 | ||
|
|
2ed451130c | ||
|
|
4d1f88627a | ||
|
|
ed7e51480b | ||
|
|
3e91be9a4d | ||
|
|
3e9bfad78f | ||
|
|
fd34fce3c1 | ||
|
|
0cf684300d | ||
|
|
fe1c808dfd | ||
|
|
aa8c67061c | ||
|
|
8e8de1eeec | ||
|
|
b9fb284a52 | ||
|
|
5efe447861 | ||
|
|
fb8d76922a |
@@ -13,5 +13,5 @@ indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# 2 space indentation
|
||||
[*.{yaml,yml}]
|
||||
[*.{yaml,yml,vue,js,css}]
|
||||
indent_size = 2
|
||||
|
||||
16
.github/workflows/build.yaml
vendored
16
.github/workflows/build.yaml
vendored
@@ -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
1
.gitignore
vendored
@@ -45,3 +45,4 @@ tests/cache/*
|
||||
tests/error.log
|
||||
system/templates/testing/*
|
||||
/user/config/versions.yaml
|
||||
/user/cli/config/security.yaml
|
||||
|
||||
137
CHANGELOG.md
137
CHANGELOG.md
@@ -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
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#  Grav
|
||||
|
||||
[](https://github.com/phpstan/phpstan)
|
||||
[](https://insight.sensiolabs.com/projects/cfd20465-d0f8-4a0a-8444-467f5b5f16ad)
|
||||
[](https://chat.getgrav.org)
|
||||
[](https://github.com/getgrav/grav/actions?query=workflow%3A%22PHP+Tests%22) [](#backers) [](#sponsors)
|
||||
|
||||
|
||||
@@ -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
763
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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>");
|
||||
}
|
||||
|
||||
@@ -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: '* * * * *'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -184,9 +184,9 @@ config:
|
||||
# Fields to be searched
|
||||
fields:
|
||||
- key
|
||||
- slug
|
||||
- menu
|
||||
- title
|
||||
- name
|
||||
|
||||
blueprints:
|
||||
configure:
|
||||
|
||||
@@ -121,7 +121,7 @@ form:
|
||||
underline: true
|
||||
|
||||
folder:
|
||||
type: text
|
||||
type: folder-slug
|
||||
label: PLUGIN_ADMIN.FOLDER_NAME
|
||||
validate:
|
||||
rule: slug
|
||||
|
||||
@@ -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
1986
system/config/mime.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
19
system/src/Grav/Common/Twig/Exception/TwigException.php
Normal file
19
system/src/Grav/Common/Twig/Exception/TwigException.php
Normal 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
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
|
||||
* Interface FlexDirectoryInterface
|
||||
* @package Grav\Framework\Flex\Interfaces
|
||||
*/
|
||||
interface FlexDirectoryInterface
|
||||
interface FlexDirectoryInterface extends FlexAuthorizeInterface
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
107
system/src/Grav/Framework/Mime/MimeTypes.php
Normal file
107
system/src/Grav/Framework/Mime/MimeTypes.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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")');
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user