Merge branch 'develop' into feature/watermark

This commit is contained in:
Andy Miller 2021-10-21 06:30:52 -06:00 committed by GitHub
commit 883c935e05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 7905 additions and 3001 deletions

View File

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

View File

@ -12,11 +12,15 @@ 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:
php-version: 7.3
extensions: opcache, gd
tools: composer:v2
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
@ -38,13 +42,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

View File

@ -25,6 +25,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
extensions: opcache, gd
tools: composer:v2
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

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

View File

@ -1,9 +1,198 @@
# v1.7.11
# v1.7.24
## mm/dd/2021
3. [](#bugfix)
* Fixed a bug in `PermissionsReader` in PHP 7.3
* Fixed `session_store_active` language option (#3464)
* Fixed deprecated warnings on `ArrayAccess` in PHP 8.1
# v1.7.23
## 09/29/2021
1. [](#new)
* Added method `Pages::referrerRoute()` to get the referrer route and language
* Added true unique `Utils::uniqueId()` / `{{ unique_id() }}` utilities with length, prefix, and suffix support
* Added `UserObject::isMyself()` method to check if flex user is currently logged in
* Added support for custom form field options validation with `validate: options: key|ignore`
2. [](#improved)
* Replaced GPL `SVG-Sanitizer` with MIT licensed `DOM-Sanitizer`
* `Uri::referrer()` now accepts third parameter, if set to `true`, it returns route without base or language code [#3411](https://github.com/getgrav/grav/issues/3411)
* Updated vendor libs with latest
* Updated with latest language strings via Crowdin.com
3. [](#bugfix)
* Fixed `Folder::move()` throwing an error when target folder is changed by only appending characters to the end [#3445](https://github.com/getgrav/grav/issues/3445)
* Fixed some phpstan issues (all code back to level 1, Framework level 3)
* Fixed form reset causing image uploads to fail when using Flex
# v1.7.22
## 09/16/2021
1. [](#new)
* Register plugin autoloaders into plugin objects
2. [](#improved)
* Improve Twig 2 compatibility
* Update to customized version of Twig DeferredExtension (Twig 1/2 compatible)
3. [](#bugfix)
* Fixed conflicting `$_original` variable in `Flex Pages`
# 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. [](#improved)
* Improve JSON support for the request
1. [](#bugfix)
* Fixed absolute path support for Windows [#3297](https://github.com/getgrav/grav/issues/3297)
* Fixed adding tags in admin after upgrading Grav [#3315](https://github.com/getgrav/grav/issues/3315)
# v1.7.11
## 04/13/2021
1. [](#new)
* Added configuration options to allow PHP methods to be used in Twig functions (`system.twig.safe_functions`) and filters (`system.twig.safe_filters`)
* Deprecated using PHP methods in Twig without them being in the safe lists
* Prevent dangerous PHP methods from being used as Twig functions and filters
* Restrict filesystem Twig functions to accept only local filesystem and grav streams
1. [](#improved)
* Better GPM detection of unauthorized installations
1. [](#bugfix)
* **IMPORTANT** Fixed security vulnerability with Twig allowing dangerous PHP functions by default [GHSA-g8r4-p96j-xfxc](https://github.com/getgrav/grav/security/advisories/GHSA-g8r4-p96j-xfxc)
* Fixed nxinx appending repeating `?_url=` in some redirects
* Fixed deleting page with language code not removing the folder if it was the last language [#3305](https://github.com/getgrav/grav/issues/3305)
* Fixed fatal error when using markdown links with `image://` stream [#3285](https://github.com/getgrav/grav/issues/3285)

View File

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

View File

@ -44,8 +44,8 @@
"filp/whoops": "~2.9",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.25",
"gregwar/image": "dev-php8",
"gregwar/cache": "dev-php8",
"getgrav/image": "^3.0",
"getgrav/cache": "^2.0",
"donatj/phpuseragentparser": "~1.1",
"pimple/pimple": "~3.3.0",
"rockettheme/toolbox": "~1.5",
@ -55,19 +55,18 @@
"miljar/php-exif": "^0.6",
"composer/ca-bundle": "^1.2",
"dragonmantank/cron-expression": "^1.2",
"phive/twig-extensions-deferred": "^1.0",
"willdurand/negotiation": "^3.0",
"itsgoingd/clockwork": "^5.0",
"enshrined/svg-sanitize": "~0.13",
"symfony/http-client": "^4.4",
"composer/semver": "^1.4"
"composer/semver": "^1.4",
"rhukster/dom-sanitizer": "^1.0"
},
"require-dev": {
"codeception/codeception": "^4.1",
"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,23 +90,10 @@
"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"
"Grav\\": "system/src/Grav",
"Twig\\": "system/src/Twig"
},
"files": [
"system/defines.php"
@ -121,8 +107,8 @@
"scripts": {
"api-17": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.17.md",
"post-create-project-cmd": "bin/grav install",
"phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src",
"phpstan-framework": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
"phpstan": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/phpstan.neon --memory-limit=520M system/src",
"phpstan-framework": "vendor/bin/phpstan analyse -l 3 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer",
"phpstan-plugins": "vendor/bin/phpstan analyse -l 1 -c ./tests/phpstan/plugins.neon --memory-limit=400M user/plugins",
"test": "vendor/bin/codecept run unit",
"test-windows": "vendor\\bin\\codecept run unit"

1147
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
}
if (PHP_SAPI === 'cli-server') {
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'], 'symfony
') !== false;
$symfony_server = stripos(getenv('_'), 'symfony') !== false || stripos($_SERVER['SERVER_SOFTWARE'] ?? '', 'symfony') !== false || stripos($_ENV['SERVER_SOFTWARE'] ?? '', 'symfony') !== false;
if (!isset($_SERVER['PHP_CLI_ROUTER']) && !$symfony_server) {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
}

View File

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

View File

@ -646,7 +646,7 @@ form:
type: toggle
label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT
help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP
highlight: 1
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
@ -1446,6 +1446,10 @@ form:
title: PLUGIN_ADMIN.ADVANCED
underline: true
gpm_section:
type: section
title: PLUGIN_ADMIN.GPM_SECTION
gpm.releases:
type: toggle
label: PLUGIN_ADMIN.GPM_RELEASES
@ -1455,23 +1459,6 @@ form:
stable: PLUGIN_ADMIN.STABLE
testing: PLUGIN_ADMIN.TESTING
gpm.proxy_url:
type: text
size: medium
placeholder: "e.g. 127.0.0.1:3128"
label: PLUGIN_ADMIN.PROXY_URL
help: PLUGIN_ADMIN.PROXY_URL_HELP
gpm.method:
type: toggle
label: PLUGIN_ADMIN.GPM_METHOD
highlight: auto
help: PLUGIN_ADMIN.GPM_METHOD_HELP
options:
auto: PLUGIN_ADMIN.AUTO
fopen: PLUGIN_ADMIN.FOPEN
curl: PLUGIN_ADMIN.CURL
gpm.official_gpm_only:
type: toggle
label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY
@ -1484,17 +1471,80 @@ form:
validate:
type: bool
gpm.verify_peer:
http_section:
type: section
title: PLUGIN_ADMIN.HTTP_SECTION
http.method:
type: toggle
label: PLUGIN_ADMIN.GPM_VERIFY_PEER
label: PLUGIN_ADMIN.GPM_METHOD
highlight: auto
help: PLUGIN_ADMIN.GPM_METHOD_HELP
options:
auto: PLUGIN_ADMIN.AUTO
fopen: PLUGIN_ADMIN.FOPEN
curl: PLUGIN_ADMIN.CURL
http.enable_proxy:
type: toggle
label: PLUGIN_ADMIN.SSL_ENABLE_PROXY
highlight: 1
help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: false
validate:
type: bool
http.proxy_url:
type: text
size: medium
placeholder: "e.g. 127.0.0.1:3128"
label: PLUGIN_ADMIN.PROXY_URL
help: PLUGIN_ADMIN.PROXY_URL_HELP
http.proxy_cert_path:
type: text
size: medium
placeholder: "e.g. /Users/bob/certs/"
label: PLUGIN_ADMIN.PROXY_CERT
help: PLUGIN_ADMIN.PROXY_CERT_HELP
http.verify_peer:
type: toggle
label: PLUGIN_ADMIN.SSL_VERIFY_PEER
highlight: 1
help: PLUGIN_ADMIN.SSL_VERIFY_PEER_HELP
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
http.verify_host:
type: toggle
label: PLUGIN_ADMIN.SSL_VERIFY_HOST
highlight: 1
help: PLUGIN_ADMIN.SSL_VERIFY_HOST_HELP
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
http.concurrent_connections:
type: number
size: x-small
label: PLUGIN_ADMIN.HTTP_CONNECTIONS
help: PLUGIN_ADMIN.HTTP_CONNECTIONS_HELP
validate:
min: 1
max: 20
misc_section:
type: section
title: PLUGIN_ADMIN.MISC_SECTION
reverse_proxy_setup:
type: toggle
label: PLUGIN_ADMIN.REVERSE_PROXY

View File

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

View File

@ -18,6 +18,7 @@ config:
configure:
path: '/accounts/configure'
redirects:
'/groups': '/accounts/groups'
'/accounts': '/accounts/groups'
# Permissions

View File

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

View File

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

1986
system/config/mime.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -96,7 +96,7 @@ cache:
purge_at: '0 4 * * *' # How often to purge old file cache (using new scheduler)
clear_at: '0 3 * * *' # How often to clear cache (using new scheduler)
clear_job_type: 'standard' # Type to clear when processing the scheduled clear job `standard`|`all`
clear_images_by_default: true # By default grav will include processed images in cache clear, this can be disabled
clear_images_by_default: false # By default grav does not include processed images in cache clear, this can be enabled
cli_compatibility: false # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite)
gzip: false # GZip compress the page output
@ -113,6 +113,8 @@ twig:
autoescape: true # Autoescape Twig vars (DEPRECATED, always enabled in strict mode)
undefined_functions: true # Allow undefined functions
undefined_filters: true # Allow undefined filters
safe_functions: [] # List of PHP functions which are allowed to be used as Twig functions
safe_filters: [] # List of PHP functions which are allowed to be used as Twig filters
umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775
assets: # Configuration for Assets Manager (JS, CSS)
@ -129,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
@ -188,11 +190,17 @@ session:
gpm:
releases: stable # Set to either 'stable' or 'testing'
proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128)
method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
http:
method: auto # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
enable_proxy: true # Enable proxy server configuration
proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128)
proxy_cert_path: # Local path to proxy certificate folder containing pem files
concurrent_connections: 5 # Concurrent HTTP connections when multiplexing
verify_peer: true # Enable/Disable SSL verification of peer certificates
verify_host: true # Enable/Disable SSL verification of host certificates
accounts:
type: regular # EXPERIMENTAL: Account type: regular or flex
storage: file # EXPERIMENTAL: Flex storage type: file or folder

View File

@ -1,4 +1,5 @@
<?php
/**
* @package Grav\Core
*
@ -8,7 +9,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.10');
define('GRAV_VERSION', '1.7.23');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', false);
@ -22,59 +23,68 @@ if (!defined('DS')) {
define('DS', '/');
}
// Directories and Paths
// Absolute path to Grav root. This is where Grav is installed into.
if (!defined('GRAV_ROOT')) {
$path = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, getenv('GRAV_ROOT') ?: getcwd()), DS);
define('GRAV_ROOT', $path);
}
// Absolute path to Grav webroot. This is the path where your site is located in.
if (!defined('GRAV_WEBROOT')) {
$path = rtrim(getenv('GRAV_WEBROOT') ?: GRAV_ROOT, DS);
define('GRAV_WEBROOT', $path);
}
// Relative path to user folder. This path needs to be located under GRAV_WEBROOT.
if (!defined('GRAV_USER_PATH')) {
$path = rtrim(getenv('GRAV_USER_PATH') ?: 'user', DS);
define('GRAV_USER_PATH', $path);
}
// Absolute or relative path to system folder. Defaults to GRAV_ROOT/system
// If system folder is outside of webroot, see https://github.com/getgrav/grav/issues/3297#issuecomment-810294972
if (!defined('GRAV_SYSTEM_PATH')) {
$path = rtrim(getenv('GRAV_SYSTEM_PATH') ?: 'system', DS);
define('GRAV_SYSTEM_PATH', $path);
}
// Absolute or relative path to cache folder. Defaults to GRAV_ROOT/cache
if (!defined('GRAV_CACHE_PATH')) {
$path = rtrim(getenv('GRAV_CACHE_PATH') ?: 'cache', DS);
define('GRAV_CACHE_PATH', $path);
}
// Absolute or relative path to logs folder. Defaults to GRAV_ROOT/logs
if (!defined('GRAV_LOG_PATH')) {
$path = rtrim(getenv('GRAV_LOG_PATH') ?: 'logs', DS);
define('GRAV_LOG_PATH', $path);
}
// Absolute or relative path to tmp folder. Defaults to GRAV_ROOT/tmp
if (!defined('GRAV_TMP_PATH')) {
$path = rtrim(getenv('GRAV_TMP_PATH') ?: 'tmp', DS);
define('GRAV_TMP_PATH', $path);
}
// Absolute or relative path to backup folder. Defaults to GRAV_ROOT/backup
if (!defined('GRAV_BACKUP_PATH')) {
$path = rtrim(getenv('GRAV_BACKUP_PATH') ?: 'backup', DS);
define('GRAV_BACKUP_PATH', $path);
}
unset($path);
define('USER_PATH', GRAV_USER_PATH . DS);
define('CACHE_PATH', GRAV_CACHE_PATH . DS);
define('ROOT_DIR', GRAV_ROOT . DS);
define('USER_DIR', GRAV_WEBROOT . '/' . USER_PATH);
define('CACHE_DIR', (!str_starts_with(CACHE_PATH, '/') ? ROOT_DIR : '') . CACHE_PATH);
// INTERNAL: Do not use!
define('USER_DIR', GRAV_WEBROOT . '/' . GRAV_USER_PATH . '/');
define('CACHE_DIR', (!preg_match('`^(/|[a-z]:[\\\/])`ui', GRAV_CACHE_PATH) ? GRAV_ROOT . '/' : '') . GRAV_CACHE_PATH . '/');
// DEPRECATED: Do not use!
define('CACHE_PATH', GRAV_CACHE_PATH . DS);
define('USER_PATH', GRAV_USER_PATH . DS);
define('ROOT_DIR', GRAV_ROOT . DS);
define('ASSETS_DIR', GRAV_WEBROOT . '/assets/');
define('IMAGES_DIR', GRAV_WEBROOT . '/images/');
define('ACCOUNTS_DIR', USER_DIR .'accounts/');
define('PAGES_DIR', USER_DIR .'pages/');
define('DATA_DIR', USER_DIR .'data/');
define('PLUGINS_DIR', USER_DIR .'plugins/');
define('THEMES_DIR', USER_DIR .'themes/');
define('SYSTEM_DIR', (!str_starts_with(GRAV_SYSTEM_PATH, '/') ? ROOT_DIR : '') . GRAV_SYSTEM_PATH);
define('LIB_DIR', SYSTEM_DIR .'src/');
define('VENDOR_DIR', ROOT_DIR .'vendor/');
define('LOG_DIR', (!str_starts_with(GRAV_LOG_PATH, '/') ? ROOT_DIR : '') . GRAV_LOG_PATH . DS);
define('ACCOUNTS_DIR', USER_DIR . 'accounts/');
define('PAGES_DIR', USER_DIR . 'pages/');
define('DATA_DIR', USER_DIR . 'data/');
define('PLUGINS_DIR', USER_DIR . 'plugins/');
define('THEMES_DIR', USER_DIR . 'themes/');
define('SYSTEM_DIR', (!preg_match('`^(/|[a-z]:[\\\/])`ui', GRAV_SYSTEM_PATH) ? GRAV_ROOT . '/' : '') . GRAV_SYSTEM_PATH . '/');
define('LIB_DIR', SYSTEM_DIR . 'src/');
define('VENDOR_DIR', GRAV_ROOT . '/vendor/');
define('LOG_DIR', (!preg_match('`^(/|[a-z]:[\\\/])`ui', GRAV_LOG_PATH) ? GRAV_ROOT . '/' : '') . GRAV_LOG_PATH . '/');
// END DEPRECATED
// Some extensions

View File

@ -51,6 +51,7 @@ GRAV:
VALIDATION_FAIL: '<b>فشل التحقق من صحة:</b>'
INVALID_INPUT: 'إدخال غير صحيح في'
MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:'
XSS_ISSUES: "مشاكل XSS محتملة تم اكتشافها في حقل '%s' '"
MONTHS_OF_THE_YEAR:
- 'كانون الثاني'
- 'شباط'
@ -72,6 +73,8 @@ GRAV:
- 'الجمعة'
- 'السبت'
- 'الأحد'
YES: "نعم"
NO: "لا"
CRON:
EVERY: كل
EVERY_HOUR: كل ساعة
@ -80,3 +83,11 @@ GRAV:
EVERY_DAY_OF_MONTH: كل يوم في الشهر
EVERY_MONTH: ' كل شهر'
TEXT_PERIOD: كل <b />
TEXT_MINS: ' في <b /> دقيقة(دقائق) بعد الساعة'
TEXT_TIME: ' في <b />:<b />'
TEXT_DOW: ' في <b />'
TEXT_MONTH: ' من <b />'
TEXT_DOM: ' في <b />'
ERROR1: الوسم %s غير مدعوم!
ERROR2: عدد عناصر غير صالح.
ERROR4: تعبير غير معروف

View File

@ -15,6 +15,7 @@ GRAV:
BAD_DATE: Data invàlida
AGO: abans
FROM_NOW: des d'ara
JUST_NOW: Ara mateix
SECOND: segon
MINUTE: minut
HOUR: hora
@ -48,6 +49,7 @@ GRAV:
VALIDATION_FAIL: '<b>Ha fallat la validació:</b>'
INVALID_INPUT: 'Entrada no vàlida a'
MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
XSS_ISSUES: "Detectats potencials problemes XSS al camp '%s'"
MONTHS_OF_THE_YEAR:
- 'Gener'
- 'Febrer'
@ -69,3 +71,17 @@ GRAV:
- 'Divendres'
- 'Dissabte'
- 'Diumenge'
YES: "Sí"
NO: "No"
CRON:
EVERY: cada
EVERY_HOUR: cada hora
EVERY_MINUTE: cada minut
EVERY_DAY_OF_WEEK: cada dia de la setmana
EVERY_DAY_OF_MONTH: cada dia del mes
EVERY_MONTH: cada mes
TEXT_PERIOD: Cada <b />
ERROR1: L'etiqueta %s no està suportada!
ERROR2: Nombre d'elements incorrecte
ERROR3: El jquery_element s'ha d'establir a la configuració de jqCron
ERROR4: Expressió no reconeguda

View File

@ -24,6 +24,7 @@ GRAV:
'/(quiz)zes$/i': '\1'
'/(alias|status)es$/i': '\1'
'/([octop|vir])i$/i': '\1us'
'/(n)ews$/i': '\1ouvelles'
INFLECTOR_UNCOUNTABLE:
- 'équipement'
- 'information'
@ -58,10 +59,10 @@ GRAV:
MONTH: mois
YEAR: année
DECADE: décennie
SEC: s
MIN: m
HR: h
WK: sem
SEC: sec.
MIN: min.
HR: hr.
WK: sem.
MO: m
YR: an
DEC: déc
@ -84,6 +85,7 @@ GRAV:
VALIDATION_FAIL: '<b>La validation a échoué :</b>'
INVALID_INPUT: 'Saisie non valide'
MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
XSS_ISSUES: "Erreurs XSS probablement détectées dans le champ '%s'"
MONTHS_OF_THE_YEAR:
- 'janvier'
- 'février'
@ -105,6 +107,8 @@ GRAV:
- 'vendredi'
- 'samedi'
- 'dimanche'
YES: "Oui"
NO: "Non"
CRON:
EVERY: chaque
EVERY_HOUR: toutes les heures
@ -118,7 +122,7 @@ GRAV:
TEXT_DOW: ' sur <b/>'
TEXT_MONTH: ' de <b />'
TEXT_DOM: ' sur <b/>'
ERROR1: La balise %s n'est pas supportée!
ERROR1: La balise %s n'est pas prise en charge !
ERROR2: Nombre invalide d'éléments
ERROR3: L'élément jquery_element doit être défini dans les paramètres jqCron
ERROR4: Expression non reconnue

View File

@ -104,6 +104,7 @@ GRAV:
VALIDATION_FAIL: '<b>Fallou a validación:</b>'
INVALID_INPUT: 'Entrada incorrecta en'
MISSING_REQUIRED_FIELD: 'Falta un campo requirido:'
XSS_ISSUES: "Detectáronse posibles problemas XSS no campo '% s'"
MONTHS_OF_THE_YEAR:
- 'xaneiro'
- 'febreiro'
@ -125,6 +126,8 @@ GRAV:
- 'venres'
- 'sábado'
- 'domingo'
YES: "Si"
NO: "Non"
CRON:
EVERY: cada
EVERY_HOUR: Cada hora

View File

@ -3,26 +3,72 @@ GRAV:
FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Frontmatter tidak valid\n\nLokasi: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
INFLECTOR_PLURALS:
'/(quiz)$/i': '\1zes'
'/^(ox)$/i': '\1en'
'/([m|l])ouse$/i': '\1ice'
'/(matr|vert|ind)ix|ex$/i': '\1ices'
'/(x|ch|ss|sh)$/i': '\1es'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([^aeiouy]|qu)y$/i': '\1ies'
'/(hive)$/i': '\1s'
'/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
'/sis$/i': 'ses'
'/([ti])um$/i': '\1a'
'/(buffal|tomat)o$/i': '\1oes'
'/(bu)s$/i': '\1ses'
'/(alias|status)/i': '\1es'
'/(octop|vir)us$/i': '\1i'
'/(ax|test)is$/i': '\1es'
'/s$/i': 's'
'/$/': 's'
INFLECTOR_SINGULAR:
'/(quiz)zes$/i': '\1'
'/(matr)ices$/i': '\1ix'
'/(vert|ind)ices$/i': '\1ex'
'/^(ox)en/i': '\1'
'/(alias|status)es$/i': '\1'
'/([octop|vir])i$/i': '\1us'
'/(cris|ax|test)es$/i': '\1is'
'/(shoe)s$/i': '\1'
'/(o)es$/i': '\1'
'/(bus)es$/i': '\1'
'/([m|l])ice$/i': '\1ouse'
'/(x|ch|ss|sh)es$/i': '\1'
'/(m)ovies$/i': '\1ovie'
'/(s)eries$/i': '\1eries'
'/([^aeiouy]|qu)ies$/i': '\1y'
'/([lr])ves$/i': '\1f'
'/(tive)s$/i': '\1'
'/(hive)s$/i': '\1'
'/([^f])ves$/i': '\1fe'
'/(^analy)ses$/i': '\1sis'
'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
'/([ti])a$/i': '\1um'
'/(n)ews$/i': '\1ews'
INFLECTOR_UNCOUNTABLE:
- 'peralatan'
- 'informasi'
- 'nasi'
- 'uang'
- 'spesies'
- 'rangkaian'
- 'ikan'
- 'domba'
- 'Peralatan'
- 'Informasi '
- 'Nasi'
- 'Uang'
- 'Jenis'
- 'Seri'
- 'Ikan'
- 'Domba'
INFLECTOR_IRREGULAR:
'person': 'orang-orang'
'man': 'laki-laki'
'child': 'anak-anak'
'sex': 'jenis kelamin'
'person': 'Orang-orang'
'man': 'Pria'
'child': 'Balita'
'sex': 'Jenis Kelamin'
'move': 'pindahkan'
INFLECTOR_ORDINALS:
'default': 'ke'
'first': 'pertama'
'second': 'nd'
'third': 'rd'
NICETIME:
NO_DATE_PROVIDED: Tanggal tidak tersedia
NO_DATE_PROVIDED: Tidak ada tanggal yang disediakan
BAD_DATE: Format tanggal salah
AGO: yang lalu
FROM_NOW: dari saat ini
FROM_NOW: dari sekarang
JUST_NOW: baru saja
SECOND: detik
MINUTE: menit
@ -32,12 +78,12 @@ GRAV:
MONTH: bulan
YEAR: tahun
DECADE: dekade
SEC: dtk
MIN: mnt
HR: j
WK: mng
MO: bln
YR: thn
SEC: detik
MIN: menit
HR: ' jam'
WK: minggu
MO: bulan
YR: tahun
DEC: desimal
SECOND_PLURAL: detik
MINUTE_PLURAL: menit
@ -47,17 +93,18 @@ GRAV:
MONTH_PLURAL: bulan
YEAR_PLURAL: tahun
DECADE_PLURAL: dekade
SEC_PLURAL: dtk
MIN_PLURAL: mnt
HR_PLURAL: j
WK_PLURAL: mgg
MO_PLURAL: bln
YR_PLURAL: thn
SEC_PLURAL: detik
MIN_PLURAL: menit
HR_PLURAL: jam
WK_PLURAL: minggu
MO_PLURAL: bulan
YR_PLURAL: tahun
DEC_PLURAL: dekade
FORM:
VALIDATION_FAIL: '<b>Validasi gagal:</b>'
INVALID_INPUT: 'Input tidak valid di'
MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:'
XSS_ISSUES: "Isu berpotensial XSS terdeteksi dalam baris %s"
MONTHS_OF_THE_YEAR:
- 'Januari'
- 'Februari'
@ -76,22 +123,25 @@ GRAV:
- 'Selasa'
- 'Rabu'
- 'Kamis'
- 'Jumat'
- 'Jum''at'
- 'Sabtu'
- 'Minggu'
YES: "Ya"
NO: "Tidak"
CRON:
EVERY: Setiap
EVERY_HOUR: Setiap jam
EVERY_MINUTE: Setiap menit
EVERY_DAY_OF_WEEK: Setiap hari selama seminggu
EVERY_DAY_OF_MONTH: pada tanggal setiap bulannya
EVERY_DAY_OF_MONTH: Setiap hari dalam sebulan
EVERY_MONTH: setiap bulan
TEXT_PERIOD: Setiap <b />
TEXT_MINS: 'dalam <b /> menit setelah jam yang lalu'
TEXT_TIME: ' pada <b />:<b />'
TEXT_DOW: ' pada <b />'
TEXT_MONTH: ' pada <b />'
TEXT_DOM: ' pada <b />'
ERROR1: Tag %s tidak didukung!
ERROR2: Jumlah elemen tidak valid
ERROR3: jquery_element harus ditetapkan ke pengaturan jqCron
ERROR4: Ekspresi tidak dikenali
ERROR2: Jumlah elemen yang buruk
ERROR3: jquery_element harus diatur ke dalam pengaturan jqCron
ERROR4: Ekspresi tidak dikenal

147
system/languages/mn.yaml Normal file
View File

@ -0,0 +1,147 @@
---
GRAV:
FRONTMATTER_ERROR_PAGE: "---\nГарчиг: %1$s\n---\n\n# Алдаа: Буруу Формат\n\nЗам: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
INFLECTOR_PLURALS:
'/(quiz)$/i': '\1зүүд'
'/^(ox)$/i': '\1ууд'
'/([m|l])ouse$/i': '\1ууд'
'/(matr|vert|ind)ix|ex$/i': '\1иксүүд'
'/(x|ch|ss|sh)$/i': '\1үүд'
'/([^aeiouy]|qu)ies$/i': '\1үүд'
'/([^aeiouy]|qu)y$/i': '\1үүд'
'/(hive)$/i': '\1үүд'
'/(?:([^f])fe|([lr])f)$/i': '\1\2үүд'
'/sis$/i': 'үүд'
'/([ti])um$/i': '\1үүд'
'/(buffal|tomat)o$/i': '\1үүд'
'/(bu)s$/i': '\1үүд'
'/(alias|status)/i': '\1үүд'
'/(octop|vir)us$/i': '\1үүд'
'/(ax|test)is$/i': '\1үүд'
'/s$/i': 'үүд'
'/$/': 'үүд'
INFLECTOR_SINGULAR:
'/(quiz)zes$/i': '\1'
'/(matr)ices$/i': '\1икс'
'/(vert|ind)ices$/i': '\1икс'
'/^(ox)en/i': '\1'
'/(alias|status)es$/i': '\1'
'/([octop|vir])i$/i': '\1'
'/(cris|ax|test)es$/i': '\1'
'/(shoe)s$/i': '\1'
'/(o)es$/i': '\1'
'/(bus)es$/i': '\1'
'/([m|l])ice$/i': '\1'
'/(x|ch|ss|sh)es$/i': '\1'
'/(m)ovies$/i': '\1'
'/(s)eries$/i': '\1'
'/([^aeiouy]|qu)ies$/i': '\1үүд'
'/([lr])ves$/i': '\1'
'/(tive)s$/i': '\1'
'/(hive)s$/i': '\1'
'/([^f])ves$/i': '\1'
'/(^analy)ses$/i': '\1'
'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2үүд'
'/([ti])a$/i': '\1'
'/(n)ews$/i': '\1'
INFLECTOR_UNCOUNTABLE:
- 'тоног төхөөрөмж'
- 'Мэдээлэл'
- 'будаа'
- 'мөнгө'
- 'төрөл зүйл'
- 'цуврал'
- 'загас'
- 'хонь'
INFLECTOR_IRREGULAR:
'person': 'хүмүүс'
'man': 'эрчүүд'
'child': 'хүүхэд'
'sex': 'хүйс'
'move': 'хөдөлгөөн'
INFLECTOR_ORDINALS:
'default': 'th'
'first': 'st'
'second': 'nd'
'third': 'rd'
NICETIME:
NO_DATE_PROVIDED: Огноо алга
BAD_DATE: Буруу огноо
AGO: өмнө
FROM_NOW: одооноос
JUST_NOW: дөнгөж сая
SECOND: секунд
MINUTE: минут
HOUR: цаг
DAY: өдөр
WEEK: долоо хоног
MONTH: сар
YEAR: он
DECADE: арван жил
SEC: сек
MIN: мин
HR: цаг
WK: д.х.
MO: сар
YR: он
DEC: арван жил
SECOND_PLURAL: секунд
MINUTE_PLURAL: минут
HOUR_PLURAL: цаг
DAY_PLURAL: өдрүүд
WEEK_PLURAL: долоо хоногууд
MONTH_PLURAL: сарууд
YEAR_PLURAL: онууд
DECADE_PLURAL: арван жилүүд
SEC_PLURAL: сек.-үүд
MIN_PLURAL: мин.-ууд
HR_PLURAL: цагууд
WK_PLURAL: д.х.-ууд
MO_PLURAL: сарууд
YR_PLURAL: жилүүд
DEC_PLURAL: арван жилүүд
FORM:
VALIDATION_FAIL: '<b>Баталгаажуулалт амжилтгүй боллоо:</b>'
INVALID_INPUT: 'Буруу өгөгдөл дараахид'
MISSING_REQUIRED_FIELD: 'Шаардлагатай талбар дутуу байна:'
XSS_ISSUES: "'%s' талбарт XSS -ийн болзошгүй асуудлууд илэрсэн"
MONTHS_OF_THE_YEAR:
- '1-р сар'
- '2-р сар'
- '3-р сар'
- '4-р сар'
- '5 сар'
- '6 сар'
- '7 сар'
- '8 сар'
- '9 сар'
- '10 сар'
- '11 сар'
- '12 сар'
DAYS_OF_THE_WEEK:
- 'Даваа гараг'
- 'Мягмар гараг'
- 'Лхагва гараг'
- 'Пүрэв гараг'
- 'Баасан гараг'
- 'Бямба гараг'
- 'Ням гараг'
YES: "Тийм"
NO: "Үгүй"
CRON:
EVERY: бүрийн
EVERY_HOUR: цаг бүрийн
EVERY_MINUTE: минут бүрийн
EVERY_DAY_OF_WEEK: долоо хоногийн өдөр болгонд
EVERY_DAY_OF_MONTH: сарын өдөр болгонд
EVERY_MONTH: сар болгон
TEXT_PERIOD: Бүрийн <b />
TEXT_MINS: ' <b /> энэ сүүлийн цагийн минутад'
TEXT_TIME: ' <b />:<b /> -д'
TEXT_DOW: ' <b /> -д'
TEXT_MONTH: ' <b /> -ын'
TEXT_DOM: ' <b /> -т'
ERROR1: '%s -н утга нь дэмжигддэггүй!'
ERROR2: Элементүүдийн тоо хэмжээ буруу
ERROR3: jquery_element нь jqCron тохиргоонд хийгдсэн байх ёстой
ERROR4: Танигдаагүй илэрхийлэл

View File

@ -104,6 +104,7 @@ GRAV:
VALIDATION_FAIL: '<b>Falha na validação:</b>'
INVALID_INPUT: 'Dados inseridos são inválidos em'
MISSING_REQUIRED_FIELD: 'Campo obrigatório em falta:'
XSS_ISSUES: "Potenciais problemas de XSS detectados no campo '%s'"
MONTHS_OF_THE_YEAR:
- 'Janeiro'
- 'Fevereiro'
@ -125,6 +126,8 @@ GRAV:
- 'Sexta-feira'
- 'Sábado'
- 'Domingo'
YES: "Sim"
NO: "Não"
CRON:
EVERY: cada
EVERY_HOUR: cada hora

9
system/languages/si.yaml Normal file
View File

@ -0,0 +1,9 @@
---
GRAV:
INFLECTOR_SINGULAR:
'/(quiz)zes$/i': '\1'
'/^(ox)en/i': '\1'
'/(alias|status)es$/i': '\1'
'/(o)es$/i': '\1'
'/(bus)es$/i': '\1'
'/(x|ch|ss|sh)es$/i': '\1'

View File

@ -82,6 +82,8 @@ GRAV:
- 'Cuma'
- 'Cumartesi'
- 'Pazar'
YES: "Evet"
NO: "Hayır"
CRON:
EVERY: her
EVERY_HOUR: saatte bir

View File

@ -38,7 +38,9 @@ GRAV:
YR_PLURAL:
DEC_PLURAL: 十年
FORM:
MISSING_REQUIRED_FIELD: 遺漏必填欄位:
VALIDATION_FAIL: '<b>確驗證失敗:</b>'
INVALID_INPUT: '無效輸入:'
MISSING_REQUIRED_FIELD: '遺漏必填欄位:'
MONTHS_OF_THE_YEAR:
- '一月'
- '二月'
@ -60,3 +62,16 @@ GRAV:
- '星期五'
- '星期六'
- '星期日'
CRON:
EVERY:
EVERY_HOUR: 每小時
EVERY_MINUTE: 每分鐘
EVERY_DAY_OF_WEEK: 每一天
EVERY_DAY_OF_MONTH: 每一天
EVERY_MONTH: 每個月
TEXT_PERIOD: 每 <b />
TEXT_MINS: ' 的 <b /> 分'
TEXT_TIME: ' <b />:<b />'
TEXT_DOW: ' 的 <b />'
TEXT_MONTH: ' 的 <b />'
TEXT_DOM: ' 的 <b />'

View File

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

View File

@ -15,6 +15,7 @@ use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Object\PropertyObject;
use RocketTheme\Toolbox\File\File;
use SplFileInfo;
/**
@ -91,6 +92,10 @@ abstract class BaseAsset extends PropertyObject
*/
public function init($asset, $options)
{
if (!$asset) {
return false;
}
$config = Grav::instance()['config'];
$uri = Grav::instance()['uri'];
@ -182,16 +187,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 '';
@ -253,6 +263,6 @@ abstract class BaseAsset extends PropertyObject
*/
protected function cssRewrite($file, $dir, $local)
{
return;
return '';
}
}

View File

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

View File

@ -68,8 +68,6 @@ trait AssetUtilsTrait
protected function gatherLinks(array $assets, $css = true)
{
$buffer = '';
foreach ($assets as $id => $asset) {
$local = true;
@ -90,7 +88,7 @@ trait AssetUtilsTrait
}
$relative_dir = dirname($relative_path);
$link = ROOT_DIR . $relative_path;
$link = GRAV_ROOT . '/' . $relative_path;
}
// TODO: looks like this is not being used.
@ -135,7 +133,7 @@ trait AssetUtilsTrait
$imports = [];
$file = (string)preg_replace_callback($regex, function ($matches) use (&$imports) {
$file = (string)preg_replace_callback($regex, static function ($matches) use (&$imports) {
$imports[] = $matches[0];
return '';
@ -156,6 +154,10 @@ trait AssetUtilsTrait
$no_key = ['loading'];
foreach ($this->attributes as $key => $value) {
if ($value === null) {
continue;
}
if (is_numeric($key)) {
$key = $value;
}
@ -196,7 +198,7 @@ trait AssetUtilsTrait
}
if ($this->timestamp) {
if (Utils::contains($asset, '?') || $querystring) {
if ($querystring || Utils::contains($asset, '?')) {
$querystring .= '&' . $this->timestamp;
} else {
$querystring .= '?' . $this->timestamp;

View File

@ -252,7 +252,7 @@ trait TestingAssetsTrait
*/
public function addDir($directory, $pattern = self::DEFAULT_REGEX)
{
$root_dir = rtrim(ROOT_DIR, '/');
$root_dir = GRAV_ROOT;
// Check if $directory is a stream.
if (strpos($directory, '://')) {

View File

@ -222,7 +222,7 @@ class Backups
$backup_root = rtrim(GRAV_ROOT . $backup_root, '/');
}
if (!file_exists($backup_root)) {
if (!$backup_root || !file_exists($backup_root)) {
throw new RuntimeException("Backup location: {$backup_root} does not exist...");
}

View File

@ -141,7 +141,7 @@ class Cache extends Getters
$uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
// Cache key allows us to invalidate all cache on configuration changes.
$this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
$this->key = ($prefix ?: 'g') . '-' . $uniqueness;
$this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
$this->driver_setting = $this->config->get('system.cache.driver');
$this->driver = $this->getCacheDriver();
@ -618,11 +618,7 @@ class Cache extends Getters
*/
public function isVolatileDriver($setting)
{
if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
return true;
}
return false;
return in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'], true);
}
/**

View File

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

View File

@ -37,7 +37,7 @@ class Blueprint extends BlueprintForm
/** @var string|null */
protected $scope;
/** @var BlueprintSchema */
/** @var BlueprintSchema|null */
protected $blueprintSchema;
/** @var object|null */
@ -54,7 +54,7 @@ class Blueprint extends BlueprintForm
*/
public function __clone()
{
if ($this->blueprintSchema) {
if (null !== $this->blueprintSchema) {
$this->blueprintSchema = clone $this->blueprintSchema;
}
}

View File

@ -56,6 +56,15 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
return $this->types[$name] ?? [];
}
/**
* @param string $name
* @return array|null
*/
public function getNestedRules(string $name)
{
return $this->getNested($name);
}
/**
* Validate data against blueprints.
*
@ -317,6 +326,10 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
$toggle = [];
}
// Recursively fetch the items.
$childData = $data[$key] ?? null;
if (null !== $childData && !is_array($childData)) {
throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData)));
}
$data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
} else {
$field = $this->get($value);

View File

@ -264,7 +264,7 @@ class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable,
*/
public function blueprints()
{
if (!$this->blueprints) {
if (null === $this->blueprints) {
$this->blueprints = new Blueprint();
} elseif (is_callable($this->blueprints)) {
// Lazy load blueprints.

View File

@ -238,6 +238,7 @@ class Validation
$value = trim($value);
}
$value = preg_replace("/\r\n|\r/um", "\n", $value);
$len = mb_strlen($value);
$min = (int)($params['min'] ?? 0);
@ -280,7 +281,7 @@ class Validation
$value = trim($value);
}
return $value;
return preg_replace("/\r\n|\r/um", "\n", $value);
}
/**
@ -518,17 +519,32 @@ class Validation
return false;
}
if (isset($params['min']) && $value < $params['min']) {
return false;
$value = (float)$value;
$min = 0;
if (isset($params['min'])) {
$min = (float)$params['min'];
if ($value < $min) {
return false;
}
}
if (isset($params['max']) && $value > $params['max']) {
return false;
if (isset($params['max'])) {
$max = (float)$params['max'];
if ($value > $max) {
return false;
}
}
$min = $params['min'] ?? 0;
if (isset($params['step'])) {
$step = (float)$params['step'];
// Count of how many steps we are above/below the minimum value.
$pos = ($value - $min) / $step;
return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
return is_int(static::filterNumber($pos, $params, $field));
}
return true;
}
/**
@ -592,7 +608,7 @@ class Validation
*/
public static function typeColor($value, array $params, array $field)
{
return preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
return (bool)preg_match('/^\#[0-9a-fA-F]{3}[0-9a-fA-F]{3}?$/u', $value);
}
/**
@ -765,14 +781,22 @@ class Validation
}
// If creating new values is allowed, no further checks are needed.
if (!empty($field['selectize']['create'])) {
$validateOptions = $field['validate']['options'] ?? null;
if (!empty($field['selectize']['create']) || $validateOptions === 'ignore') {
return true;
}
$options = $field['options'] ?? [];
$use = $field['use'] ?? 'values';
if (empty($field['selectize']) || empty($field['multiple'])) {
if ($validateOptions) {
// Use custom options structure.
foreach ($options as &$option) {
$option = $option[$validateOptions] ?? null;
}
unset($option);
$options = array_values($options);
} elseif (empty($field['selectize']) || empty($field['multiple'])) {
$options = array_keys($options);
}
if ($use === 'keys') {
@ -1173,7 +1197,7 @@ class Validation
*/
public static function filterItem_List($value, $params)
{
return array_values(array_filter($value, function ($v) {
return array_values(array_filter($value, static function ($v) {
return !empty($v);
}));
}

View File

@ -332,7 +332,7 @@ class Debugger
return new Response(404, $headers, json_encode($response));
}
$data = is_array($data) ? array_map(function ($item) {
$data = is_array($data) ? array_map(static function ($item) {
return $item->toArray();
}, $data) : $data->toArray();

View File

@ -197,7 +197,7 @@ abstract class Folder
* Shift first directory out of the path.
*
* @param string $path
* @return string
* @return string|null
*/
public static function shift(&$path)
{
@ -371,7 +371,7 @@ abstract class Folder
return;
}
if (strpos($target, $source) === 0) {
if (strpos($target, $source . '/') === 0) {
throw new RuntimeException('Cannot move folder to itself');
}
@ -417,7 +417,8 @@ abstract class Folder
if (!$success) {
$error = error_get_last();
throw new RuntimeException($error['message']);
throw new RuntimeException($error['message'] ?? 'Unknown error');
}
// Make sure that the change will be detected when caching.

View File

@ -57,7 +57,9 @@ class ZipArchiver extends Archiver
throw new InvalidArgumentException('ZipArchiver: Zip PHP module not installed...');
}
if (!file_exists($source)) {
// Get real path for our folder
$rootPath = realpath($source);
if (!$rootPath) {
throw new InvalidArgumentException('ZipArchiver: ' . $source . ' cannot be found...');
}
@ -66,9 +68,6 @@ class ZipArchiver extends Archiver
throw new InvalidArgumentException('ZipArchiver:' . $this->archive_file . ' cannot be created...');
}
// Get real path for our folder
$rootPath = realpath($source);
$files = $this->getArchiveFiles($rootPath);
$status && $status([

View File

@ -13,6 +13,7 @@ namespace Grav\Common\Flex;
use Grav\Common\Flex\Traits\FlexGravTrait;
use Grav\Common\Flex\Traits\FlexObjectTrait;
use Grav\Common\Media\Interfaces\MediaInterface;
use Grav\Framework\Flex\Traits\FlexMediaTrait;
use function is_array;
@ -21,7 +22,7 @@ use function is_array;
*
* @package Grav\Common\Flex
*/
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface
{
use FlexGravTrait;
use FlexObjectTrait;
@ -42,7 +43,7 @@ abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
// Handle media fields.
$settings = $this->getFieldSettings($name);
if ($settings['media_field'] ?? false === true) {
if (($settings['media_field'] ?? false) === true) {
return $this->parseFileProperty($value, $settings);
}

View File

@ -19,7 +19,6 @@ use Grav\Common\Page\Header;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Flex\Pages\FlexPageCollection;
use Collator;
use InvalidArgumentException;
@ -159,7 +158,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
*/
public function addPage(PageInterface $page)
{
if (!$page instanceof FlexObjectInterface) {
if (!$page instanceof PageObject) {
throw new InvalidArgumentException('$page is not a flex page.');
}
@ -192,6 +191,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.
*
@ -392,8 +399,8 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
$i = count($manual);
$new_list = [];
foreach ($list as $key => $dummy) {
$child = $this[$key];
$order = array_search($child->slug, $manual, true);
$child = $this[$key] ?? null;
$order = $child ? array_search($child->slug, $manual, true) : false;
if ($order === false) {
$order = $i++;
}
@ -426,20 +433,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 +456,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 +753,16 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
return $bool ? $this->select($list) : $this->unselect($list);
}
/**
* @param string|null $languageCode
* @param bool|null $fallback
* @return PageIndex
*/
public function withTranslated(string $languageCode = null, bool $fallback = null)
{
return $this->getIndex()->withTranslated($languageCode, $fallback);
}
/**
* Filter pages by given filters.
*

View File

@ -18,6 +18,7 @@ use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Flex\Traits\FlexGravTrait;
use Grav\Common\Flex\Traits\FlexIndexTrait;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Page\Header;
use Grav\Common\Page\Interfaces\PageCollectionInterface;
use Grav\Common\Page\Interfaces\PageInterface;
@ -108,6 +109,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
}
$element = parent::get($key);
if (null === $element) {
return null;
}
if (isset($params)) {
$element = $element->getTranslation(ltrim($params, '.'));
}
@ -164,6 +169,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 +204,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 +228,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 +252,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.
*
@ -271,7 +335,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
*/
protected function filterByParent(array $filters)
{
return parent::filterBy($filters);
/** @var static $index */
$index = parent::filterBy($filters);
return $index;
}
/**
@ -345,6 +412,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
@ -523,13 +680,14 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
$child_count = $tmp->count();
$count = $filters ? $tmp->filterBy($filters, true)->count() : null;
$route = $child->getRoute();
$route = $route ? ($route->toString(false) ?: '/') : '';
$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) ?: null,
'raw' => htmlspecialchars($child->rawRoute()),
],
'modified' => $this->jsDate($child->modified()),
'child_count' => $child_count ?: null,
@ -684,12 +842,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Remove item from the list.
*
* @param PageInterface|string|null $key
*
* @return $this
* @param string $key
* @return PageObject|null
* @throws InvalidArgumentException
*/
public function remove($key = null)
public function remove($key)
{
return $this->getCollection()->remove($key);
}
@ -799,17 +956,17 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Returns the items between a set of date ranges of either the page date field (default) or
* an arbitrary datetime page field where end date is optional
* Dates can be passed in as text that strtotime() can process
* an arbitrary datetime page field where start date and end date are optional
* Dates must be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
* @param string $startDate
* @param bool $endDate
* @param string|null $startDate
* @param string|null $endDate
* @param string|null $field
* @return static
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
public function dateRange($startDate = null, $endDate = null, $field = null)
{
$collection = $this->__call('dateRange', [$startDate, $endDate, $field]);

View File

@ -104,12 +104,12 @@ class PageObject extends FlexPageObject
*/
public function getRoute($query = []): ?Route
{
$route = $this->route();
if (null === $route) {
$path = $this->route();
if (null === $path) {
return null;
}
$route = RouteFactory::createFromString($route);
$route = RouteFactory::createFromString($path);
if ($lang = $route->getLanguage()) {
$grav = Grav::instance();
if (!$grav['config']->get('system.languages.include_default_lang')) {
@ -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
@ -293,7 +311,7 @@ class PageObject extends FlexPageObject
}
// Reset original after save events have all been called.
$this->_original = null;
$this->_originalObject = null;
return $instance;
}
@ -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);
}
@ -411,7 +441,8 @@ class PageObject extends FlexPageObject
// Add missing siblings into the end of the list, keeping the previous ordering between them.
foreach ($siblings as $sibling) {
$basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder'));
$folder = (string)$sibling->getProperty('folder');
$basename = preg_replace('|^\d+\.|', '', $folder);
if (!in_array($basename, $ordering, true)) {
$ordering[] = $basename;
}
@ -421,7 +452,8 @@ class PageObject extends FlexPageObject
$ordering = array_flip(array_values($ordering));
$count = count($ordering);
foreach ($siblings as $sibling) {
$basename = preg_replace('|^\d+\.|', '', $sibling->getProperty('folder'));
$folder = (string)$sibling->getProperty('folder');
$basename = preg_replace('|^\d+\.|', '', $folder);
$newOrder = $ordering[$basename] ?? null;
$newOrder = null !== $newOrder ? $newOrder + 1 : (int)$sibling->order() + $count;
$sibling->order($newOrder);
@ -500,6 +532,8 @@ class PageObject extends FlexPageObject
if ($isNew === true && $name === '') {
// Support onBlueprintCreated event just like in Pages::blueprints($template)
$blueprint->set('initialized', true);
$blueprint->setFilename($template);
Grav::instance()->fireEvent('onBlueprintCreated', new Event(['blueprint' => $blueprint, 'type' => $template]));
}

View File

@ -103,7 +103,10 @@ trait PageLegacyTrait
$parent = $this->parent();
$collection = $parent ? $parent->collection('content', false) : null;
if (null !== $path && $collection instanceof PageCollectionInterface) {
return $collection->adjacentSibling($path, $direction);
$child = $collection->adjacentSibling($path, $direction);
if ($child instanceof PageInterface) {
return $child;
}
}
return false;

View File

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

View File

@ -92,7 +92,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface
} else {
$user = parent::find($query, $field);
}
if ($user) {
if ($user instanceof UserObject) {
return $user;
}
}
@ -123,7 +123,7 @@ class UserCollection extends FlexCollection implements UserCollectionInterface
* @param string $key
* @return string
*/
protected function filterUsername(string $key)
protected function filterUsername(string $key): string
{
$storage = $this->getFlexDirectory()->getStorage();
if (method_exists($storage, 'normalizeKey')) {

View File

@ -62,7 +62,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
* @param FlexStorageInterface $storage
* @return void
*/
public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage)
public static function updateObjectMeta(array &$meta, array $data, FlexStorageInterface $storage): void
{
// Username can also be number and stored as such.
$key = (string)($data['username'] ?? $meta['key'] ?? $meta['storage_key']);
@ -187,7 +187,7 @@ class UserIndex extends FlexIndex implements UserCollectionInterface
* @param array $updated
* @param array $removed
*/
protected static function onChanges(array $entries, array $added, array $updated, array $removed)
protected static function onChanges(array $entries, array $added, array $updated, array $removed): void
{
$message = sprintf('Flex: User index updated, %d objects (%d added, %d updated, %d removed).', count($entries), count($added), count($updated), count($removed));

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Grav\Common\Flex\Types\Users;
use Closure;
use Countable;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
@ -22,7 +23,6 @@ use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Interfaces\MediaUploadInterface;
use Grav\Common\Page\Media;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\MediumFactory;
use Grav\Common\User\Access;
use Grav\Common\User\Authentication;
@ -32,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\Traits\UserTrait;
use Grav\Framework\File\Formatter\JsonFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\Filesystem\Filesystem;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Storage\FileStorage;
@ -76,18 +77,17 @@ class UserObject extends FlexObject implements UserInterface, Countable
use UserTrait;
use UserObjectLegacyTrait;
/** @var Closure|null */
static public $authorizeCallable;
/** @var array|null */
protected $_uploads_original;
/** @var FileInterface|null */
protected $_storage;
/** @var UserGroupIndex */
protected $_groups;
/** @var Access */
protected $_access;
/** @var array|null */
protected $access;
@ -230,6 +230,16 @@ class UserObject extends FlexObject implements UserInterface, Countable
return $this;
}
/**
* @return bool
*/
public function isMyself(): bool
{
$me = $this->getActiveUser();
return $me && $me->authenticated && $this->username === $me->username;
}
/**
* Checks user authorization to the action.
*
@ -264,6 +274,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 +316,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 +721,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
/**
* @param array $files
* @return void
*/
protected function setUpdatedMedia(array $files): void
{
@ -701,10 +729,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 +746,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 +760,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 +784,25 @@ class UserObject extends FlexObject implements UserInterface, Countable
continue;
}
// Calculate path without the retina scaling factor.
$realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
$list[$filename] = [$file, $settings];
$path = str_replace('.', "\n", $field);
if (null !== $data) {
$data['name'] = $filename;
$data['path'] = $filepath;
$this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
$this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
} else {
$this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
$this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
}
}
}
$this->clearMediaCache();
$this->_uploads = $list;
$this->_uploads_original = $list_original;
}

View File

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

View File

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

View File

@ -1,143 +1,3 @@
<?php
/**
* @package Grav\Common\GPM
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM;
use Exception;
use Grav\Common\Utils;
use Grav\Common\Grav;
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use function call_user_func;
use function defined;
use function function_exists;
/**
* Class Response
* @package Grav\Common\GPM
*/
class Response
{
/** @var callable The callback for the progress, either a function or callback in array notation */
public static $callback = null;
/** @var string[] */
private static $headers = [
'User-Agent' => 'Grav CMS'
];
/**
* Makes a request to the URL by using the preferred method
*
* @param string $uri URL to call
* @param array $overrides An array of parameters for both `curl` and `fopen`
* @param callable|null $callback Either a function or callback in array notation
* @return string The response of the request
* @throws TransportExceptionInterface
*/
public static function get($uri = '', $overrides = [], $callback = null)
{
if (empty($uri)) {
throw new TransportException('missing URI');
}
// check if this function is available, if so use it to stop any timeouts
try {
if (Utils::functionExists('set_time_limit')) {
@set_time_limit(0);
}
} catch (Exception $e) {
}
$config = Grav::instance()['config'];
$referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true);
$options = new HttpOptions();
// Set default Headers
$options->setHeaders(array_merge([ 'Referer' => $referer ], self::$headers));
// Disable verify Peer if required
$verify_peer = $config->get('system.gpm.verify_peer', true);
if ($verify_peer !== true) {
$options->verifyPeer($verify_peer);
}
// Set proxy url if provided
$proxy_url = $config->get('system.gpm.proxy_url', false);
if ($proxy_url) {
$options->setProxy($proxy_url);
}
// Use callback if provided
if ($callback) {
self::$callback = $callback;
$options->setOnProgress([Response::class, 'progress']);
}
$preferred_method = $config->get('system.gpm.method', 'auto');
$settings = array_merge_recursive($options->toArray(), $overrides);
switch ($preferred_method) {
case 'curl':
$client = new CurlHttpClient($settings);
break;
case 'fopen':
case 'native':
$client = new NativeHttpClient($settings);
break;
default:
$client = HttpClient::create($settings);
}
$response = $client->request('GET', $uri);
return $response->getContent();
}
/**
* Is this a remote file or not
*
* @param string $file
* @return bool
*/
public static function isRemote($file)
{
return (bool) filter_var($file, FILTER_VALIDATE_URL);
}
/**
* Progress normalized for cURL and Fopen
* Accepts a variable length of arguments passed in by stream method
*
* @return void
*/
public static function progress(int $bytes_transferred, int $filesize, array $info)
{
if ($bytes_transferred > 0) {
$percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize);
$progress = [
'code' => $info['http_code'],
'filesize' => $filesize,
'transferred' => $bytes_transferred,
'percent' => $percent < 100 ? $percent : 100
];
if (self::$callback !== null) {
call_user_func(self::$callback, $progress);
}
}
}
}
// Create alias for the deprecated class.
class_alias(\Grav\Common\HTTP\Response::class, \Grav\Common\GPM\Response::class);

View File

@ -69,6 +69,7 @@ abstract class Getters implements ArrayAccess, Countable
* @param int|string $offset
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
if ($this->gettersVariable) {
@ -84,6 +85,7 @@ abstract class Getters implements ArrayAccess, Countable
* @param int|string $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
if ($this->gettersVariable) {
@ -99,6 +101,7 @@ abstract class Getters implements ArrayAccess, Countable
* @param int|string $offset
* @param mixed $value
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($this->gettersVariable) {
@ -112,6 +115,7 @@ abstract class Getters implements ArrayAccess, Countable
/**
* @param int|string $offset
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
if ($this->gettersVariable) {

View File

@ -9,6 +9,7 @@
namespace Grav\Common;
use Composer\Autoload\ClassLoader;
use Grav\Common\Config\Config;
use Grav\Common\Config\Setup;
use Grav\Common\Helpers\Exif;
@ -134,7 +135,7 @@ class Grav extends Container
*
* @return void
*/
public static function resetInstance()
public static function resetInstance(): void
{
if (self::$instance) {
// @phpstan-ignore-next-line
@ -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) {
@ -234,7 +242,7 @@ class Grav extends Container
*
* @return void
*/
public function process()
public function process(): void
{
if (isset($this->initialized['process'])) {
return;
@ -466,7 +474,7 @@ class Grav extends Container
* @param int $code Redirection code (30x)
* @return void
*/
public function redirectLangSafe($route, $code = null)
public function redirectLangSafe($route, $code = null): void
{
if (!$this['uri']->isExternal($route)) {
$this->redirect($this['pages']->route($route), $code);
@ -481,7 +489,7 @@ class Grav extends Container
* @param ResponseInterface|null $response
* @return void
*/
public function header(ResponseInterface $response = null)
public function header(ResponseInterface $response = null): void
{
if (null === $response) {
/** @var PageInterface $page */
@ -506,7 +514,7 @@ class Grav extends Container
*
* @return void
*/
public function setLocale()
public function setLocale(): void
{
// Initialize Locale if set and configured.
if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
@ -567,7 +575,7 @@ class Grav extends Container
*
* @return void
*/
public function shutdown()
public function shutdown(): void
{
// Prevent user abort allowing onShutdown event to run without interruptions.
if (function_exists('ignore_user_abort')) {
@ -686,7 +694,7 @@ class Grav extends Container
*
* @return void
*/
protected function registerServices()
protected function registerServices(): void
{
foreach (self::$diMap as $serviceKey => $serviceClass) {
if (is_int($serviceKey)) {
@ -753,12 +761,10 @@ class Grav extends Container
// unsupported media type, try to download it...
if ($uri_extension) {
$extension = $uri_extension;
} elseif (isset($path_parts['extension'])) {
$extension = $path_parts['extension'];
} else {
if (isset($path_parts['extension'])) {
$extension = $path_parts['extension'];
} else {
$extension = null;
}
$extension = null;
}
if ($extension) {
@ -773,6 +779,6 @@ class Grav extends Container
return false;
}
return $page;
return $page ?? false;
}
}

View File

@ -0,0 +1,130 @@
<?php
/**
* @package Grav\Common\HTTP
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\HTTP;
use Grav\Common\Grav;
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class Client
{
/** @var callable The callback for the progress, either a function or callback in array notation */
public static $callback = null;
/** @var string[] */
private static $headers = [
'User-Agent' => 'Grav CMS'
];
public static function getClient(array $overrides = [], int $connections = 6, callable $callback = null): HttpClientInterface
{
$config = Grav::instance()['config'];
$options = static::getOptions();
// Use callback if provided
if ($callback) {
self::$callback = $callback;
$options->setOnProgress([Client::class, 'progress']);
}
$settings = array_merge($options->toArray(), $overrides);
$preferred_method = $config->get('system.http.method');
// Try old GPM setting if value is the same as system default
if ($preferred_method === 'auto') {
$preferred_method = $config->get('system.gpm.method', 'auto');
}
switch ($preferred_method) {
case 'curl':
$client = new CurlHttpClient($settings, $connections);
break;
case 'fopen':
case 'native':
$client = new NativeHttpClient($settings, $connections);
break;
default:
$client = HttpClient::create($settings, $connections);
}
return $client;
}
/**
* Get HTTP Options
*
* @return HttpOptions
*/
public static function getOptions(): HttpOptions
{
$config = Grav::instance()['config'];
$referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true);
$options = new HttpOptions();
// Set default Headers
$options->setHeaders(array_merge([ 'Referer' => $referer ], self::$headers));
// Disable verify Peer if required
$verify_peer = $config->get('system.http.verify_peer');
// Try old GPM setting if value is default
if ($verify_peer === true) {
$verify_peer = $config->get('system.gpm.verify_peer', null) ?? $verify_peer;
}
$options->verifyPeer($verify_peer);
// Set verify Host
$verify_host = $config->get('system.http.verify_host', true);
$options->verifyHost($verify_host);
// New setting and must be enabled for Proxy to work
if ($config->get('system.http.enable_proxy', true)) {
// Set proxy url if provided
$proxy_url = $config->get('system.http.proxy_url', $config->get('system.gpm.proxy_url', null));
if ($proxy_url !== null) {
$options->setProxy($proxy_url);
}
// Certificate
$proxy_cert = $config->get('system.http.proxy_cert_path', null);
if ($proxy_cert !== null) {
$options->setCaPath($proxy_cert);
}
}
return $options;
}
/**
* Progress normalized for cURL and Fopen
* Accepts a variable length of arguments passed in by stream method
*
* @return void
*/
public static function progress(int $bytes_transferred, int $filesize, array $info)
{
if ($bytes_transferred > 0) {
$percent = $filesize <= 0 ? 0 : (int)(($bytes_transferred * 100) / $filesize);
$progress = [
'code' => $info['http_code'],
'filesize' => $filesize,
'transferred' => $bytes_transferred,
'percent' => $percent < 100 ? $percent : 100
];
if (self::$callback !== null) {
call_user_func(self::$callback, $progress);
}
}
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* @package Grav\Common\HTTP
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\HTTP;
use Exception;
use Grav\Common\Utils;
use Grav\Common\Grav;
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\HttpOptions;
use Symfony\Component\HttpClient\NativeHttpClient;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use function call_user_func;
use function defined;
/**
* Class Response
* @package Grav\Common\GPM
*/
class Response
{
/**
* Backwards compatible helper method
*
* @param string $uri
* @param array $overrides
* @param callable|null $callback
* @return string
* @throws TransportExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface|ClientExceptionInterface
*/
public static function get(string $uri = '', array $overrides = [], callable $callback = null): string
{
$response = static::request('GET', $uri, $overrides, $callback);
return $response->getContent();
}
/**
* Makes a request to the URL by using the preferred method
*
* @param string $method method to call such as GET, PUT, etc
* @param string $uri URL to call
* @param array $overrides An array of parameters for both `curl` and `fopen`
* @param callable|null $callback Either a function or callback in array notation
* @return ResponseInterface
* @throws TransportExceptionInterface
*/
public static function request(string $method, string $uri, array $overrides = [], callable $callback = null): ResponseInterface
{
if (empty($method)) {
throw new TransportException('missing method (GET, PUT, etc.)');
}
if (empty($uri)) {
throw new TransportException('missing URI');
}
// check if this function is available, if so use it to stop any timeouts
try {
if (Utils::functionExists('set_time_limit')) {
@set_time_limit(0);
}
} catch (Exception $e) {}
$client = Client::getClient($overrides, 6, $callback);
return $client->request($method, $uri);
}
/**
* Is this a remote file or not
*
* @param string $file
* @return bool
*/
public static function isRemote($file): bool
{
return (bool) filter_var($file, FILTER_VALIDATE_URL);
}
}

View File

@ -33,6 +33,9 @@ class Excerpts
public static function processImageHtml($html, PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'img');
if (null === $excerpt) {
return '';
}
$original_src = $excerpt['element']['attributes']['src'];
$excerpt['element']['attributes']['href'] = $original_src;
@ -61,6 +64,9 @@ class Excerpts
public static function processLinkHtml($html, PageInterface $page = null)
{
$excerpt = static::getExcerptFromHtml($html, 'a');
if (null === $excerpt) {
return '';
}
$original_href = $excerpt['element']['attributes']['href'];
$excerpt = static::processLinkExcerpt($excerpt, $page, 'link');
@ -89,7 +95,6 @@ class Excerpts
$excerpt = null;
$inner = [];
/** @var DOMElement $element */
foreach ($elements as $element) {
$attributes = [];
foreach ($element->attributes as $name => $value) {

View File

@ -53,7 +53,6 @@ class LogViewer
*/
public function tail($filepath, $lines = 1)
{
$f = $filepath ? @fopen($filepath, 'rb') : false;
if ($f === false) {
return false;
@ -62,13 +61,12 @@ class LogViewer
$buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
fseek($f, -1, SEEK_END);
if (fread($f, 1) != "\n") {
$lines -= 1;
if (fread($f, 1) !== "\n") {
--$lines;
}
// Start reading
$output = '';
$chunk = '';
// While we would like more
while (ftell($f) > 0 && $lines >= 0) {
// Figure out how far back we should jump
@ -76,7 +74,11 @@ class LogViewer
// Do the jump (backwards, relative to where we are)
fseek($f, -$seek, SEEK_CUR);
// Read a chunk and prepend it to our output
$output = ($chunk = fread($f, $seek)) . $output;
$chunk = fread($f, $seek);
if ($chunk === false) {
throw new \RuntimeException('Cannot read file');
}
$output = $chunk . $output;
// Jump back to where we started reading
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
// Decrease our line counter
@ -123,13 +125,13 @@ class LogViewer
*/
public function parse($line)
{
if (!is_string($line) || strlen($line) === 0) {
return array();
if (!is_string($line) || $line === '') {
return [];
}
preg_match($this->pattern, $line, $data);
if (!isset($data['date'])) {
return array();
return [];
}
preg_match('/(.*)- Trace:(.*)/', $data['message'], $matches);
@ -138,7 +140,7 @@ class LogViewer
$data['trace'] = trim($matches[2]);
}
return array(
return [
'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
'logger' => $data['logger'],
'level' => $data['level'],
@ -146,7 +148,7 @@ class LogViewer
'trace' => isset($data['trace']) ? $this->parseTrace($data['trace']) : null,
'context' => json_decode($data['context'], true),
'extra' => json_decode($data['extra'], true)
);
];
}
/**

View File

@ -230,9 +230,7 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
public function filter(callable $callback = null)
{
foreach ($this->items as $key => $value) {
if ((!$callback && !(bool)$value) ||
($callback && !$callback($value, $key))
) {
if ((!$callback && !(bool)$value) || ($callback && !$callback($value, $key))) {
unset($this->items[$key]);
}
}

View File

@ -86,12 +86,14 @@ class LanguageCodes
'ja-JP' => [ 'name' => 'Japanese', 'nativeName' => '日本語' ], // not iso-639-1
'ka' => [ 'name' => 'Georgian', 'nativeName' => 'ქართული' ],
'kk' => [ 'name' => 'Kazakh', 'nativeName' => 'Қазақ' ],
'km' => [ 'name' => 'Khmer', 'nativeName' => 'Khmer' ],
'kn' => [ 'name' => 'Kannada', 'nativeName' => 'ಕನ್ನಡ' ],
'ko' => [ 'name' => 'Korean', 'nativeName' => '한국어' ],
'ku' => [ 'name' => 'Kurdish', 'nativeName' => 'Kurdî' ],
'la' => [ 'name' => 'Latin', 'nativeName' => 'Latina' ],
'lb' => [ 'name' => 'Luxembourgish', 'nativeName' => 'Lëtzebuergesch' ],
'lg' => [ 'name' => 'Luganda', 'nativeName' => 'Luganda' ],
'lo' => [ 'name' => 'Lao', 'nativeName' => 'Lao' ],
'lt' => [ 'name' => 'Lithuanian', 'nativeName' => 'Lietuvių' ],
'lv' => [ 'name' => 'Latvian', 'nativeName' => 'Latviešu' ],
'mai' => [ 'name' => 'Maithili', 'nativeName' => 'मैथिली মৈথিলী' ],
@ -101,6 +103,7 @@ class LanguageCodes
'ml' => [ 'name' => 'Malayalam', 'nativeName' => 'മലയാളം' ],
'mn' => [ 'name' => 'Mongolian', 'nativeName' => 'Монгол' ],
'mr' => [ 'name' => 'Marathi', 'nativeName' => 'मराठी' ],
'my' => [ 'name' => 'Myanmar (Burmese)', 'nativeName' => 'ဗမာी' ],
'no' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ],
'nb' => [ 'name' => 'Norwegian', 'nativeName' => 'Norsk' ],
'nb-NO' => [ 'name' => 'Norwegian (Bokmål)', 'nativeName' => 'Norsk bokmål' ],
@ -132,6 +135,7 @@ class LanguageCodes
'st' => [ 'name' => 'Southern Sotho', 'nativeName' => 'Sesotho' ],
'sv' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ],
'sv-SE' => [ 'name' => 'Swedish', 'nativeName' => 'Svenska' ],
'sw' => [ 'name' => 'Swahili', 'nativeName' => 'Swahili' ],
'ta' => [ 'name' => 'Tamil', 'nativeName' => 'தமிழ்' ],
'ta-IN' => [ 'name' => 'Tamil (India)', 'nativeName' => 'தமிழ் (இந்தியா)' ],
'ta-LK' => [ 'name' => 'Tamil (Sri Lanka)', 'nativeName' => 'தமிழ் (இலங்கை)' ],
@ -187,12 +191,7 @@ class LanguageCodes
*/
public static function getOrientation($code)
{
if (isset(static::$codes[$code])) {
if (isset(static::$codes[$code]['orientation'])) {
return static::get($code, 'orientation');
}
}
return 'ltr';
return static::$codes[$code]['orientation'] ?? 'ltr';
}
/**
@ -226,11 +225,7 @@ class LanguageCodes
*/
public static function get($code, $type)
{
if (isset(static::$codes[$code][$type])) {
return static::$codes[$code][$type];
}
return false;
return static::$codes[$code][$type] ?? false;
}
/**

View File

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

View File

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

View File

@ -47,7 +47,7 @@ class Collection extends Iterator implements PageCollectionInterface
parent::__construct($items);
$this->params = $params;
$this->pages = $pages ? $pages : Grav::instance()->offsetGet('pages');
$this->pages = $pages ?: Grav::instance()->offsetGet('pages');
}
/**
@ -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.
*
@ -175,6 +187,7 @@ class Collection extends Iterator implements PageCollectionInterface
* @param string $offset
* @return PageInterface|null
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->pages->get($offset) ?: null;
@ -319,30 +332,32 @@ class Collection extends Iterator implements PageCollectionInterface
/**
* Returns the items between a set of date ranges of either the page date field (default) or
* an arbitrary datetime page field where end date is optional
* Dates can be passed in as text that strtotime() can process
* an arbitrary datetime page field where start date and end date are optional
* Dates must be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
* @param string $startDate
* @param bool $endDate
* @param string|null $startDate
* @param string|null $endDate
* @param string|null $field
* @return $this
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
public function dateRange($startDate = null, $endDate = null, $field = null)
{
$start = Utils::date2timestamp($startDate);
$end = $endDate ? Utils::date2timestamp($endDate) : false;
$start = $startDate ? Utils::date2timestamp($startDate) : null;
$end = $endDate ? Utils::date2timestamp($endDate) : null;
$date_range = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
if ($page !== null) {
$date = $field ? strtotime($page->value($field)) : $page->date();
if (!$page) {
continue;
}
if ($date >= $start && (!$end || $date <= $end)) {
$date_range[$path] = $slug;
}
$date = $field ? strtotime($page->value($field)) : $page->date();
if ((!$start || $date >= $start) && (!$end || $date <= $end)) {
$date_range[$path] = $slug;
}
}

View File

@ -158,17 +158,17 @@ interface PageCollectionInterface extends Traversable, ArrayAccess, Countable, S
/**
* Returns the items between a set of date ranges of either the page date field (default) or
* an arbitrary datetime page field where end date is optional
* Dates can be passed in as text that strtotime() can process
* an arbitrary datetime page field where start date and end date are optional
* Dates must be passed in as text that strtotime() can process
* http://php.net/manual/en/function.strtotime.php
*
* @param string $startDate
* @param bool $endDate
* @param string|null $startDate
* @param string|null $endDate
* @param string|null $field
* @return PageCollectionInterface
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null);
public function dateRange($startDate = null, $endDate = null, $field = null);
/**
* Creates new collection with only visible pages

View File

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

View File

@ -63,6 +63,7 @@ class Media extends AbstractMedia
* @param string $offset
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return parent::offsetExists($offset) ?: isset(static::$global[$offset]);
@ -72,6 +73,7 @@ class Media extends AbstractMedia
* @param string $offset
* @return MediaObjectInterface|null
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return parent::offsetGet($offset) ?: static::$global[$offset];
@ -102,12 +104,13 @@ class Media extends AbstractMedia
foreach ($iterator as $file => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
$filename = $info->getFilename();
if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) {
continue;
}
// Find out what type we're dealing with
[$basename, $ext, $type, $extra] = $this->getFileParts($info->getFilename());
[$basename, $ext, $type, $extra] = $this->getFileParts($filename);
if (!in_array(strtolower($ext), $media_types, true)) {
continue;

View File

@ -46,6 +46,7 @@ class GlobalMedia extends AbstractMedia
* @param string $offset
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return parent::offsetExists($offset) ?: !empty($this->resolveStream($offset));
@ -55,6 +56,7 @@ class GlobalMedia extends AbstractMedia
* @param string $offset
* @return MediaObjectInterface|null
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return parent::offsetGet($offset) ?: $this->addMedium($offset);

View File

@ -399,6 +399,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.
*
@ -406,6 +437,7 @@ class ImageMedium extends Medium implements ImageMediaInterface, ImageManipulate
* @param mixed $args
* @return $this|mixed
*/
public function __call($method, $args)
{
if (!in_array($method, static::$magic_actions, true)) {

View File

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

View File

@ -2095,7 +2095,7 @@ class Page implements PageInterface
*/
public function filePathClean()
{
return str_replace(ROOT_DIR, '', $this->filePath());
return str_replace(GRAV_ROOT . DS, '', $this->filePath());
}
/**
@ -2274,11 +2274,11 @@ class Page implements PageInterface
{
if ($var !== null) {
// make sure first level are arrays
array_walk($var, function (&$value) {
array_walk($var, static function (&$value) {
$value = (array) $value;
});
// make sure all values are strings
array_walk_recursive($var, function (&$value) {
array_walk_recursive($var, static function (&$value) {
$value = (string) $value;
});
$this->taxonomy = $var;

View File

@ -196,6 +196,58 @@ class Pages
return $this->baseRoute($lang) . $route;
}
/**
* Get relative referrer route and language code. Returns null if the route isn't within the current base, language (if set) and route.
*
* @example `$langCode = null; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within /admin and updates $langCode
* @example `$langCode = 'en'; $referrer = $pages->referrerRoute($langCode, '/admin');` returns relative referrer url within the /en/admin
*
* @param string|null $langCode Variable to store the language code. If already set, check only against that language.
* @param string $route Optional route within the site.
* @return string|null
* @since 1.7.23
*/
public function referrerRoute(?string &$langCode, string $route = '/'): ?string
{
$referrer = $_SERVER['HTTP_REFERER'] ?? null;
// Start by checking that referrer came from our site.
$root = $this->grav['base_url_absolute'];
if (!is_string($referrer) || !str_starts_with($referrer, $root)) {
return null;
}
/** @var Language $language */
$language = $this->grav['language'];
// Get all language codes and append no language.
if (null === $langCode) {
$languages = $language->enabled() ? $language->getLanguages() : [];
$languages[] = '';
} else {
$languages[] = $langCode;
}
$path_base = rtrim($this->base(), '/');
$path_route = rtrim($route, '/');
// Try to figure out the language code.
foreach ($languages as $code) {
$path_lang = $code ? "/{$code}" : '';
$base = $path_base . $path_lang . $path_route;
if ($referrer === $base || str_starts_with($referrer, "{$base}/")) {
if (null === $langCode) {
$langCode = $code;
}
return substr($referrer, \strlen($base));
}
}
return null;
}
/**
*
* Get base URL for Grav pages.
@ -274,7 +326,7 @@ class Pages
*
* @return void
*/
public function reset()
public function reset(): void
{
$this->initialized = false;
@ -540,9 +592,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);
}
@ -554,7 +606,7 @@ class Pages
if (is_array($sort_flags)) {
$sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
$sort_flags = array_reduce($sort_flags, function ($a, $b) {
$sort_flags = array_reduce($sort_flags, static function ($a, $b) {
return $a | $b;
}, 0); //merge constant values using bit or
}
@ -663,29 +715,39 @@ class Pages
switch ($type) {
case 'all':
return $page->children();
$collection = $page->children();
break;
case 'modules':
case 'modular':
return $page->children()->modules();
$collection = $page->children()->modules();
break;
case 'pages':
case 'children':
return $page->children()->pages();
$collection = $page->children()->pages();
break;
case 'page':
case 'self':
return !$page->root() ? (new Collection())->addPage($page) : new Collection();
$collection = !$page->root() ? (new Collection())->addPage($page) : new Collection();
break;
case 'parent':
$parent = $page->parent();
$collection = new Collection();
return $parent ? $collection->addPage($parent) : $collection;
$collection = $parent ? $collection->addPage($parent) : $collection;
break;
case 'siblings':
$parent = $page->parent();
return $parent ? $parent->children()->remove($page->path()) : new Collection();
$collection = $parent ? $parent->children()->remove($page->path()) : new Collection();
break;
case 'descendants':
return $this->all($page)->remove($page->path())->pages();
$collection = $this->all($page)->remove($page->path())->pages();
break;
default:
// Unknown type; return empty collection.
return new Collection();
$collection = new Collection();
break;
}
return $collection;
}
/**
@ -1761,7 +1823,7 @@ class Pages
// Build regular expression for all the allowed page extensions.
$page_extensions = $language->getFallbackPageExtensions();
$regex = '/^[^\.]*(' . implode('|', array_map(
function ($str) {
static function ($str) {
return preg_quote($str, '/');
},
$page_extensions

View File

@ -10,6 +10,7 @@
namespace Grav\Common;
use ArrayAccess;
use Composer\Autoload\ClassLoader;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data;
use Grav\Common\Page\Interfaces\PageInterface;
@ -42,6 +43,8 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
protected $active = true;
/** @var Blueprint|null */
protected $blueprint;
/** @var ClassLoader|null */
protected $loader;
/**
* By default assign all methods as listeners using the default priority.
@ -79,6 +82,24 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
}
}
/**
* @return ClassLoader|null
* @internal
*/
final public function getAutoloader(): ?ClassLoader
{
return $this->loader;
}
/**
* @param ClassLoader|null $loader
* @internal
*/
final public function setAutoloader(?ClassLoader $loader): void
{
$this->loader = $loader;
}
/**
* @param Config $config
* @return $this
@ -206,6 +227,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
* @param string $offset An offset to check for.
* @return bool Returns TRUE on success or FALSE on failure.
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
if ($offset === 'title') {
@ -223,6 +245,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
* @param string $offset The offset to retrieve.
* @return mixed Can return all value types.
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
if ($offset === 'title') {
@ -241,6 +264,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
* @param mixed $value The value to set.
* @throws LogicException
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');
@ -252,6 +276,7 @@ class Plugin implements EventSubscriberInterface, ArrayAccess
* @param string $offset The offset to unset.
* @throws LogicException
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
throw new LogicException(__CLASS__ . ' blueprints cannot be modified.');

View File

@ -143,7 +143,7 @@ class Plugins extends Iterator
$instance->setConfig($config);
// Register autoloader.
if (method_exists($instance, 'autoload')) {
$instance->autoload();
$instance->setAutoloader($instance->autoload());
}
// Register event listeners.
$events->addSubscriber($instance);
@ -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 clear-cache`", $name)
sprintf("Plugin '%s' enabled but not found! Try clearing cache with `bin/grav clearcache`", $name)
);
return null;
}
return $class;

View File

@ -105,7 +105,7 @@ class InitializeProcessor extends ProcessorBase
// TODO: remove in 2.0.
$this->container['accounts'];
// Initialize session.
// Initialize session (used by URI, see issue #3269).
$this->initializeSession($config);
// Initialize URI (uses session, see issue #3269).

View File

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

View File

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

View File

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

View File

@ -9,10 +9,11 @@
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 Rhukster\DomSanitizer\DOMSanitizer;
use function chr;
use function count;
use function is_array;
@ -33,7 +34,7 @@ class Security
public static function sanitizeSvgString(string $svg): string
{
if (Grav::instance()['config']->get('security.sanitize_svg')) {
$sanitizer = new Sanitizer();
$sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
$sanitized = $sanitizer->sanitize($svg);
if (is_string($sanitized)) {
$svg = $sanitized;
@ -52,13 +53,20 @@ class Security
public static function sanitizeSVG(string $file): void
{
if (file_exists($file) && Grav::instance()['config']->get('security.sanitize_svg')) {
$sanitizer = new Sanitizer();
$sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
$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');
}
}
}
@ -99,7 +107,7 @@ class Security
$content = $page->value('content');
$data = ['header' => $header, 'content' => $content];
$results = Security::detectXssFromArray($data);
$results = static::detectXssFromArray($data);
if (!empty($results)) {
if ($route) {
@ -130,7 +138,7 @@ class Security
$options = static::getXssDefaults();
}
$list = [];
$list = [[]];
foreach ($array as $key => $value) {
if (is_array($value)) {
$list[] = static::detectXssFromArray($value, $prefix . $key . '.', $options);
@ -140,11 +148,7 @@ class Security
}
}
if (!empty($list)) {
return array_merge(...$list);
}
return $list;
return array_merge(...$list);
}
/**
@ -191,7 +195,7 @@ class Security
$string = urldecode($string);
// Convert Hexadecimals
$string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function ($m) {
$string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', static function ($m) {
return chr(hexdec($m[2]));
}, $string);
@ -210,7 +214,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',
@ -231,7 +235,7 @@ class Security
}
}
return false;
return null;
}
public static function getXssDefaults(): array

View File

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

View File

@ -10,6 +10,7 @@
namespace Grav\Common\Service;
use Grav\Common\Uri;
use JsonException;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use Pimple\Container;
@ -18,6 +19,7 @@ use function explode;
use function fopen;
use function function_exists;
use function in_array;
use function is_array;
use function strtolower;
use function trim;
@ -51,18 +53,30 @@ class RequestServiceProvider implements ServiceProviderInterface
$headers = function_exists('getallheaders') ? getallheaders() : $creator::getHeadersFromServer($_SERVER);
$post = null;
if ('POST' === $method) {
if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
foreach ($headers as $headerName => $headerValue) {
if ('content-type' !== strtolower($headerName)) {
continue;
}
if (in_array(
strtolower(trim(explode(';', $headerValue, 2)[0])),
['application/x-www-form-urlencoded', 'multipart/form-data']
)) {
$post = $_POST;
break;
$contentType = strtolower(trim(explode(';', $headerValue, 2)[0]));
switch ($contentType) {
case 'application/x-www-form-urlencoded':
case 'multipart/form-data':
$post = $_POST;
break 2;
case 'application/json':
case 'application/vnd.api+json':
try {
$json = file_get_contents('php://input');
$post = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($post)) {
$post = null;
}
} catch (JsonException $e) {
$post = null;
}
break 2;
}
}
}

View File

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

View File

@ -12,6 +12,7 @@ namespace Grav\Common;
use Grav\Common\Form\FormFlash;
use Grav\Events\SessionStartEvent;
use Grav\Plugin\Form\Forms;
use JsonException;
use function is_string;
/**
@ -128,12 +129,12 @@ class Session extends \Grav\Framework\Session\Session
/** @var Uri $uri */
$uri = $grav['uri'];
/** @var Forms|null $form */
$form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line
$form = $grav['forms']->getActiveForm(); // @phpstan-ignore-line (form plugin)
$sessionField = base64_encode($uri->url);
/** @var FormFlash|null $flash */
$flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line
$flash = $form ? $form->getFlash() : null; // @phpstan-ignore-line (form plugin)
$object = $flash && method_exists($flash, 'getLegacyFiles') ? [$sessionField => $flash->getLegacyFiles()] : null;
}
}
@ -148,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session
* @param mixed $object
* @param int $time
* @return $this
* @throws JsonException
*/
public function setFlashCookieObject($name, $object, $time = 60)
{
setcookie($name, json_encode($object), time() + $time, '/');
setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time));
return $this;
}
@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session
*
* @param string $name
* @return mixed|null
* @throws JsonException
*/
public function getFlashCookieObject($name)
{
if (isset($_COOKIE[$name])) {
$object = json_decode($_COOKIE[$name], false);
setcookie($name, '', time() - 3600, '/');
return $object;
$cookie = $_COOKIE[$name];
setcookie($name, '', $this->getCookieOptions(-42000));
return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR);
}
return null;

View File

@ -105,7 +105,7 @@ class Taxonomy
}
} elseif (is_string($value)) {
if (!empty($key)) {
$taxonomy = $taxonomy . $key;
$taxonomy .= $key;
}
$this->taxonomy_map[$taxonomy][(string) $value][$page->path()] = ['slug' => $page->slug()];
}

View File

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

View File

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

View File

@ -0,0 +1,391 @@
<?php
/**
* @package Grav\Common\Twig
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig\Extension;
use Grav\Common\Grav;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* Class FilesystemExtension
* @package Grav\Common\Twig\Extension
*/
class FilesystemExtension extends AbstractExtension
{
/** @var UniformResourceLocator */
private $locator;
public function __construct()
{
$this->locator = Grav::instance()['locator'];
}
/**
* @return TwigFilter[]
*/
public function getFilters()
{
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']),
];
}
/**
* Return a list of all functions.
*
* @return TwigFunction[]
*/
public function getFunctions()
{
return [
new TwigFunction('file_exists', [$this, 'file_exists']),
new TwigFunction('fileatime', [$this, 'fileatime']),
new TwigFunction('filectime', [$this, 'filectime']),
new TwigFunction('filemtime', [$this, 'filemtime']),
new TwigFunction('filesize', [$this, 'filesize']),
new TwigFunction('filetype', [$this, 'filetype']),
new TwigFunction('is_dir', [$this, 'is_dir']),
new TwigFunction('is_file', [$this, 'is_file']),
new TwigFunction('is_link', [$this, 'is_link']),
new TwigFunction('is_readable', [$this, 'is_readable']),
new TwigFunction('is_writable', [$this, 'is_writable']),
new TwigFunction('is_writeable', [$this, 'is_writable']),
new TwigFunction('lstat', [$this, 'lstat']),
new TwigFunction('getimagesize', [$this, 'getimagesize']),
new TwigFunction('exif_read_data', [$this, 'exif_read_data']),
new TwigFunction('read_exif_data', [$this, 'exif_read_data']),
new TwigFunction('exif_imagetype', [$this, 'exif_imagetype']),
new TwigFunction('hash_file', [$this, 'hash_file']),
new TwigFunction('hash_hmac_file', [$this, 'hash_hmac_file']),
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']),
];
}
/**
* @param string $filename
* @return bool
*/
public function file_exists($filename): bool
{
if (!$this->checkFilename($filename)) {
return false;
}
return file_exists($filename);
}
/**
* @param string $filename
* @return int|false
*/
public function fileatime($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
return fileatime($filename);
}
/**
* @param string $filename
* @return int|false
*/
public function filectime($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
return filectime($filename);
}
/**
* @param string $filename
* @return int|false
*/
public function filemtime($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
return filemtime($filename);
}
/**
* @param string $filename
* @return int|false
*/
public function filesize($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
return filesize($filename);
}
/**
* @param string $filename
* @return string|false
*/
public function filetype($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
return filetype($filename);
}
/**
* @param string $filename
* @return bool
*/
public function is_dir($filename): bool
{
if (!$this->checkFilename($filename)) {
return false;
}
return is_dir($filename);
}
/**
* @param string $filename
* @return bool
*/
public function is_file($filename): bool
{
if (!$this->checkFilename($filename)) {
return false;
}
return is_file($filename);
}
/**
* @param string $filename
* @return bool
*/
public function is_link($filename): bool
{
if (!$this->checkFilename($filename)) {
return false;
}
return is_link($filename);
}
/**
* @param string $filename
* @return bool
*/
public function is_readable($filename): bool
{
if (!$this->checkFilename($filename)) {
return false;
}
return is_readable($filename);
}
/**
* @param string $filename
* @return bool
*/
public function is_writable($filename): bool
{
if (!$this->checkFilename($filename)) {
return false;
}
return is_writable($filename);
}
/**
* @param string $filename
* @return array|false
*/
public function lstat($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
return lstat($filename);
}
/**
* @param string $filename
* @return array|false
*/
public function getimagesize($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
return getimagesize($filename);
}
/**
* @param string $file
* @param string|null $required_sections
* @param bool $as_arrays
* @param bool $read_thumbnail
* @return array|false
*/
public function exif_read_data($file, ?string $required_sections, bool $as_arrays = false, bool $read_thumbnail = false)
{
if (!Utils::functionExists('exif_read_data') || !$this->checkFilename($file)) {
return false;
}
return exif_read_data($file, $required_sections, $as_arrays, $read_thumbnail);
}
/**
* @param string $filename
* @return string|false
*/
public function exif_imagetype($filename)
{
if (!Utils::functionExists('exif_imagetype') || !$this->checkFilename($filename)) {
return false;
}
return @exif_imagetype($filename);
}
/**
* @param string $algo
* @param string $filename
* @param bool $binary
* @return string|false
*/
public function hash_file(string $algo, string $filename, bool $binary = false)
{
if (!$this->checkFilename($filename)) {
return false;
}
return hash_file($algo, $filename, $binary);
}
/**
* @param string $algo
* @param string $data
* @param string $key
* @param bool $binary
* @return string|false
*/
public function hash_hmac_file(string $algo, string $data, string $key, bool $binary = false)
{
if (!$this->checkFilename($data)) {
return false;
}
return hash_hmac_file($algo, $data, $key, $binary);
}
/**
* @param string $filename
* @param bool $binary
* @return string|false
*/
public function md5_file($filename, bool $binary = false)
{
if (!$this->checkFilename($filename)) {
return false;
}
return md5_file($filename, $binary);
}
/**
* @param string $filename
* @param bool $binary
* @return string|false
*/
public function sha1_file($filename, bool $binary = false)
{
if (!$this->checkFilename($filename)) {
return false;
}
return sha1_file($filename, $binary);
}
/**
* @param string $filename
* @return array|false
*/
public function get_meta_tags($filename)
{
if (!$this->checkFilename($filename)) {
return false;
}
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
*/
private function checkFilename($filename): bool
{
return is_string($filename) && (!str_contains($filename, '://') || $this->locator->isStream($filename));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,6 @@ class TwigNodeMarkdown extends Node implements NodeOutputInterface
->write('$lines = explode("\n", $content);' . PHP_EOL)
->write('$content = preg_replace(\'/^\' . $matches[0]. \'/\', "", $lines);' . PHP_EOL)
->write('$content = join("\n", $content);' . PHP_EOL)
->write('echo $this->env->getExtension(\'Grav\Common\Twig\TwigExtension\')->markdownFunction($context, $content);' . PHP_EOL);
->write('echo $this->env->getExtension(\'Grav\Common\Twig\Extension\GravExtension\')->markdownFunction($context, $content);' . PHP_EOL);
}
}

View File

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

View File

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

View File

@ -16,24 +16,30 @@ 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;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RocketTheme\Toolbox\Event\Event;
use Phive\Twig\Extensions\Deferred\DeferredExtension;
use RuntimeException;
use Twig\Cache\FilesystemCache;
use Twig\DeferredExtension\DeferredExtension;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\DebugExtension;
use Twig\Extension\StringLoaderExtension;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Twig\Loader\ExistsLoaderInterface;
use Twig\Loader\FilesystemLoader;
use Twig\Profiler\Profile;
use Twig\TwigFilter;
use Twig\TwigFunction;
use function function_exists;
use function in_array;
use function is_array;
/**
* Class Twig
@ -154,27 +160,53 @@ class Twig
$this->twig = new TwigEnvironment($loader_chain, $params);
if ($config->get('system.twig.undefined_functions')) {
$this->twig->registerUndefinedFunctionCallback(function ($name) {
$this->twig->registerUndefinedFunctionCallback(function ($name) use ($config) {
$allowed = $config->get('system.twig.safe_functions');
if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) {
return new TwigFunction($name, $name);
}
if ($config->get('system.twig.undefined_functions')) {
if (function_exists($name)) {
return new TwigFunction($name, $name);
if (!Utils::isDangerousFunction($name)) {
user_error("PHP function {$name}() was used as Twig function. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_functions`", E_USER_DEPRECATED);
return new TwigFunction($name, $name);
}
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig function. If you really want to use it, please add it to system configuration: `system.twig.safe_functions`"));
}
return new TwigFunction($name, static function () {
});
});
}
return new TwigFunction($name, static function () {});
}
if ($config->get('system.twig.undefined_filters')) {
$this->twig->registerUndefinedFilterCallback(function ($name) {
return false;
});
$this->twig->registerUndefinedFilterCallback(function ($name) use ($config) {
$allowed = $config->get('system.twig.safe_filters');
if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) {
return new TwigFilter($name, $name);
}
if ($config->get('system.twig.undefined_filters')) {
if (function_exists($name)) {
return new TwigFilter($name, $name);
if (!Utils::isDangerousFunction($name)) {
user_error("PHP function {$name}() used as Twig filter. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_filters`", E_USER_DEPRECATED);
return new TwigFilter($name, $name);
}
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig filter. If you really want to use it, please add it to system configuration: `system.twig.safe_filters`"));
}
return new TwigFilter($name, static function () {
});
});
}
return new TwigFilter($name, static function () {});
}
return false;
});
$this->grav->fireEvent('onTwigInitialized');
@ -188,7 +220,8 @@ class Twig
if ($config->get('system.twig.debug')) {
$this->twig->addExtension(new DebugExtension());
}
$this->twig->addExtension(new TwigExtension());
$this->twig->addExtension(new GravExtension());
$this->twig->addExtension(new FilesystemExtension());
$this->twig->addExtension(new DeferredExtension());
$this->twig->addExtension(new StringLoaderExtension());
@ -211,7 +244,7 @@ class Twig
'assets' => $this->grav['assets'],
'taxonomy' => $this->grav['taxonomy'],
'browser' => $this->grav['browser'],
'base_dir' => rtrim(ROOT_DIR, '/'),
'base_dir' => GRAV_ROOT,
'home_url' => $pages->homeUrl($active_language),
'base_url' => $pages->baseUrl($active_language),
'base_url_absolute' => $pages->baseUrl($active_language, true),
@ -372,38 +405,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;
@ -456,25 +514,21 @@ class Twig
$twig_extension = $extension ? '.'. $extension .TWIG_EXT : TEMPLATE_EXT;
$template_file = $this->template($page->template() . $twig_extension);
$page_template = null;
$loader = $this->twig->getLoader();
if ($loader instanceof ExistsLoaderInterface) {
if ($loader->exists($template_file)) {
// template.xxx.twig
$page_template = $template_file;
} elseif ($twig_extension !== TEMPLATE_EXT && $loader->exists($template . TEMPLATE_EXT)) {
// template.html.twig
$page_template = $template . TEMPLATE_EXT;
$format = 'html';
} elseif ($loader->exists($default . $twig_extension)) {
// default.xxx.twig
$page_template = $default . $twig_extension;
} else {
// default.html.twig
$page_template = $default . TEMPLATE_EXT;
$format = 'html';
}
if ($loader->exists($template_file)) {
// template.xxx.twig
$page_template = $template_file;
} elseif ($twig_extension !== TEMPLATE_EXT && $loader->exists($template . TEMPLATE_EXT)) {
// template.html.twig
$page_template = $template . TEMPLATE_EXT;
$format = 'html';
} elseif ($loader->exists($default . $twig_extension)) {
// default.xxx.twig
$page_template = $default . $twig_extension;
} else {
// default.html.twig
$page_template = $default . TEMPLATE_EXT;
$format = 'html';
}
return $page_template;

File diff suppressed because it is too large Load Diff

View File

@ -160,8 +160,8 @@ class Uri
$language = $grav['language'];
// add the port to the base for non-standard ports
if ($this->port !== null && $config->get('system.reverse_proxy_setup') === false) {
$this->base .= ':' . (string)$this->port;
if ($this->port && $config->get('system.reverse_proxy_setup') === false) {
$this->base .= ':' . $this->port;
}
// Handle custom base
@ -176,8 +176,8 @@ class Uri
if (isset($custom_parts['scheme'])) {
$this->base = $custom_parts['scheme'] . '://' . $custom_parts['host'];
$this->port = $custom_parts['port'] ?? null;
if ($this->port !== null && $config->get('system.reverse_proxy_setup') === false) {
$this->base .= ':' . (string)$this->port;
if ($this->port && $config->get('system.reverse_proxy_setup') === false) {
$this->base .= ':' . $this->port;
}
$this->root = $custom_base;
} else {
@ -462,8 +462,8 @@ class Uri
public function port($raw = false)
{
$port = $this->port;
// If not in raw mode and port is not set, figure it out from scheme.
if (!$raw && $port === null) {
// If not in raw mode and port is not set or is 0, figure it out from scheme.
if (!$raw && !$port) {
if ($this->scheme === 'http') {
$this->port = 80;
} elseif ($this->scheme === 'https') {
@ -471,7 +471,7 @@ class Uri
}
}
return $this->port;
return $this->port ?: null;
}
/**
@ -586,33 +586,38 @@ class Uri
/**
* Return relative path to the referrer defaulting to current or given page.
*
* You should set the third parameter to `true` for redirects as long as you came from the same sub-site and language.
*
* @param string|null $default
* @param string|null $attributes
* @param bool $withoutBaseRoute
* @return string
*/
public function referrer($default = null, $attributes = null)
public function referrer($default = null, $attributes = null, bool $withoutBaseRoute = false)
{
$referrer = $_SERVER['HTTP_REFERER'] ?? null;
// Check that referrer came from our site.
$root = $this->rootUrl(true);
if ($referrer) {
// Referrer should always have host set and it should come from the same base address.
if (stripos($referrer, $root) !== 0) {
$referrer = null;
}
if ($withoutBaseRoute) {
/** @var Pages $pages */
$pages = Grav::instance()['pages'];
$base = $pages->baseUrl(null, true);
} else {
$base = $this->rootUrl(true);
}
if (!$referrer) {
// Referrer should always have host set and it should come from the same base address.
if (!is_string($referrer) || !str_starts_with($referrer, $base)) {
$referrer = $default ?: $this->route(true, true);
}
// Relative path from grav root.
$referrer = substr($referrer, strlen($base));
if ($attributes) {
$referrer .= $attributes;
}
// Return relative path.
return substr($referrer, strlen($root));
return $referrer;
}
/**
@ -648,7 +653,7 @@ class Uri
return [
'scheme' => $this->scheme,
'host' => $this->host,
'port' => $this->port,
'port' => $this->port ?: null,
'user' => $this->user,
'pass' => $this->password,
'path' => $path,
@ -665,7 +670,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 +680,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 +697,6 @@ class Uri
$ip = getenv('HTTP_FORWARDED');
} elseif (getenv('REMOTE_ADDR')) {
$ip = getenv('REMOTE_ADDR');
} else {
$ip = 'UNKNOWN';
}
return $ip;
@ -1143,11 +1151,8 @@ class Uri
public static function isValidUrl($url)
{
$regex = '/^(?:(https?|ftp|telnet):)?\/\/((?:[a-z0-9@:.-]|%[0-9A-F]{2}){3,})(?::(\d+))?((?:\/(?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\@]|%[0-9A-F]{2})*)*)(?:\?((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?(?:#((?:[a-z0-9-._~!$&\'\(\)\*\+\,\;\=\:\/?@]|%[0-9A-F]{2})*))?/';
if (preg_match($regex, $url)) {
return true;
}
return false;
return (bool)preg_match($regex, $url);
}
/**
@ -1258,7 +1263,7 @@ class Uri
$this->port = null;
}
if ($this->hasStandardPort()) {
if ($this->port === 0 || $this->hasStandardPort()) {
$this->port = null;
}
@ -1298,7 +1303,7 @@ class Uri
*/
protected function hasStandardPort()
{
return ($this->port === 80 || $this->port === 443);
return (!$this->port || $this->port === 80 || $this->port === 443);
}
/**
@ -1311,11 +1316,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 +1505,7 @@ class Uri
* @param string $delimiter
* @return string
*/
private function processParams($uri, $delimiter = ':')
private function processParams(string $uri, string $delimiter = ':'): string
{
if (strpos($uri, $delimiter) !== false) {
preg_match_all(static::paramsRegex(), $uri, $matches, PREG_SET_ORDER);

View File

@ -57,6 +57,7 @@ class User extends Data implements UserInterface
* @param string $offset
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
$value = parent::offsetExists($offset);
@ -73,6 +74,7 @@ class User extends Data implements UserInterface
* @param string $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
$value = parent::offsetGet($offset);

View File

@ -44,7 +44,7 @@ use function strlen;
*/
abstract class Utils
{
/** @var array */
/** @var array */
protected static $nonces = [];
protected const ROOTURL_REGEX = '{^((?:http[s]?:\/\/[^\/]+)|(?:\/\/[^\/]+))(.*)}';
@ -166,9 +166,9 @@ abstract class Utils
if ($locator->isStream($path)) {
$path = $locator->findResource($path, true);
} elseif (!Utils::startsWith($path, GRAV_ROOT)) {
} elseif (!static::startsWith($path, GRAV_ROOT)) {
$base_url = Grav::instance()['base_url'];
$path = GRAV_ROOT . '/' . ltrim(Utils::replaceFirstOccurrence($base_url, '', $path), '/');
$path = GRAV_ROOT . '/' . ltrim(static::replaceFirstOccurrence($base_url, '', $path), '/');
}
return $path;
@ -178,8 +178,8 @@ abstract class Utils
/**
* Check if the $haystack string starts with the substring $needle
*
* @param string $haystack
* @param string|string[] $needle
* @param string $haystack
* @param string|string[] $needle
* @param bool $case_sensitive
* @return bool
*/
@ -202,8 +202,8 @@ abstract class Utils
/**
* Check if the $haystack string ends with the substring $needle
*
* @param string $haystack
* @param string|string[] $needle
* @param string $haystack
* @param string|string[] $needle
* @param bool $case_sensitive
* @return bool
*/
@ -227,9 +227,9 @@ abstract class Utils
/**
* Check if the $haystack string contains the substring $needle
*
* @param string $haystack
* @param string|string[] $needle
* @param bool $case_sensitive
* @param string $haystack
* @param string|string[] $needle
* @param bool $case_sensitive
* @return bool
*/
public static function contains($haystack, $needle, $case_sensitive = true)
@ -266,19 +266,19 @@ abstract class Utils
{
$regex = str_replace(
array("\*", "\?"), // wildcard chars
array('.*','.'), // regexp chars
array('.*', '.'), // regexp chars
preg_quote($wildcard_pattern, '/')
);
return preg_match('/^'.$regex.'$/is', $haystack);
return preg_match('/^' . $regex . '$/is', $haystack);
}
/**
* Render simple template filling up the variables in it. If value is not defined, leave it as it was.
*
* @param string $template Template string
* @param array $variables Variables with values
* @param array $brackets Optional array of opening and closing brackets or symbols
* @param string $template Template string
* @param array $variables Variables with values
* @param array $brackets Optional array of opening and closing brackets or symbols
* @return string Final string filled with values
*/
public static function simpleTemplate(string $template, array $variables, array $brackets = ['{', '}']): string
@ -376,8 +376,8 @@ abstract class Utils
/**
* Merge two objects into one.
*
* @param object $obj1
* @param object $obj2
* @param object $obj1
* @param object $obj2
*
* @return object
*/
@ -415,7 +415,7 @@ abstract class Utils
*/
public static function arrayRemoveValue(array $search, $value)
{
foreach ((array) $value as $val) {
foreach ((array)$value as $val) {
$key = array_search($val, $search);
if ($key !== false) {
unset($search[$key]);
@ -481,8 +481,8 @@ abstract class Utils
/**
* Array combine but supports different array lengths
*
* @param array $arr1
* @param array $arr2
* @param array $arr1
* @param array $arr2
* @return array|false
*/
public static function arrayCombine($arr1, $arr2)
@ -495,7 +495,7 @@ abstract class Utils
/**
* Array is associative or not
*
* @param array $arr
* @param array $arr
* @return bool
*/
public static function arrayIsAssociative($arr)
@ -517,15 +517,15 @@ abstract class Utils
$now = new DateTime();
$date_formats = [
'd-m-Y H:i' => 'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')',
'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')',
'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. '.$now->format('m/d/Y h:i a').')',
'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')',
'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')',
];
'd-m-Y H:i' => 'd-m-Y H:i (e.g. ' . $now->format('d-m-Y H:i') . ')',
'Y-m-d H:i' => 'Y-m-d H:i (e.g. ' . $now->format('Y-m-d H:i') . ')',
'm/d/Y h:i a' => 'm/d/Y h:i a (e.g. ' . $now->format('m/d/Y h:i a') . ')',
'H:i d-m-Y' => 'H:i d-m-Y (e.g. ' . $now->format('H:i d-m-Y') . ')',
'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. ' . $now->format('h:i a m/d/Y') . ')',
];
$default_format = Grav::instance()['config']->get('system.pages.dateformat.default');
if ($default_format) {
$date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats);
$date_formats = array_merge([$default_format => $default_format . ' (e.g. ' . $now->format($default_format) . ')'], $date_formats);
}
return $date_formats;
@ -552,11 +552,11 @@ abstract class Utils
/**
* Truncate text by number of characters but can cut off words.
*
* @param string $string
* @param int $limit Max number of characters.
* @param bool $up_to_break truncate up to breakpoint after char count
* @param string $break Break point.
* @param string $pad Appended padding to the end of the string.
* @param string $string
* @param int $limit Max number of characters.
* @param bool $up_to_break truncate up to breakpoint after char count
* @param string $break Break point.
* @param string $pad Appended padding to the end of the string.
* @return string
*/
public static function truncate($string, $limit = 150, $up_to_break = false, $break = ' ', $pad = '&hellip;')
@ -582,7 +582,7 @@ abstract class Utils
* Truncate text by number of characters in a "word-safe" manor.
*
* @param string $string
* @param int $limit
* @param int $limit
* @return string
*/
public static function safeTruncate($string, $limit = 150)
@ -594,9 +594,9 @@ abstract class Utils
/**
* Truncate HTML by number of characters. not "word-safe"!
*
* @param string $text
* @param int $length in characters
* @param string $ellipsis
* @param string $text
* @param int $length in characters
* @param string $ellipsis
* @return string
*/
public static function truncateHtml($text, $length = 100, $ellipsis = '...')
@ -607,9 +607,9 @@ abstract class Utils
/**
* Truncate HTML by number of characters in a "word-safe" manor.
*
* @param string $text
* @param int $length in words
* @param string $ellipsis
* @param string $text
* @param int $length in words
* @param string $ellipsis
* @return string
*/
public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...')
@ -628,13 +628,30 @@ abstract class Utils
return substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, $length);
}
/**
* Generates a random string with configurable length, prefix and suffix.
* Unlike the built-in `uniqid()`, this string is non-conflicting and safe
*
* @param int $length
* @param array $options
* @return string
* @throws Exception
*/
public static function uniqueId(int $length = 13, array $options = []): string
{
$options = array_merge(['prefix' => '', 'suffix' => ''], $options);
$bytes = random_bytes(ceil($length / 2));
return $options['prefix'] . substr(bin2hex($bytes), 0, $length) . $options['suffix'];
}
/**
* Provides the ability to download a file to the browser
*
* @param string $file the full path to the file to be downloaded
* @param bool $force_download as opposed to letting browser choose if to download or render
* @param int $sec Throttling, try 0.1 for some speed throttling of downloads
* @param int $bytes Size of chunks to send in bytes. Default is 1024
* @param int $sec Throttling, try 0.1 for some speed throttling of downloads
* @param int $bytes Size of chunks to send in bytes. Default is 1024
* @throws Exception
*/
public static function download($file, $force_download = true, $sec = 0, $bytes = 1024)
@ -645,7 +662,7 @@ abstract class Utils
$file_parts = pathinfo($file);
$mimetype = static::getMimeByExtension($file_parts['extension']);
$size = filesize($file); // File size
$size = filesize($file); // File size
// clean all buffers
while (ob_get_level()) {
@ -742,7 +759,7 @@ abstract class Utils
// Set from uri extension
$uri_extension = $uri->extension();
if (is_string($uri_extension) && $uri->isValidExtension($uri_extension)) {
return($uri_extension);
return ($uri_extension);
}
// Use content negotiation via the `accept:` header
@ -750,13 +767,13 @@ abstract class Utils
if (is_string($http_accept)) {
$negotiator = new Negotiator();
$supported_types = Utils::getSupportPageTypes(['html', 'json']);
$priorities = Utils::getMimeTypes($supported_types);
$supported_types = static::getSupportPageTypes(['html', 'json']);
$priorities = static::getMimeTypes($supported_types);
$media_type = $negotiator->getBest($http_accept, $priorities);
$mimetype = $media_type instanceof Accept ? $media_type->getValue() : '';
return Utils::getExtensionByMime($mimetype);
return static::getExtensionByMime($mimetype);
}
return 'html';
@ -791,13 +808,7 @@ abstract class Utils
$media_types = Grav::instance()['config']->get('media.types');
if (isset($media_types[$extension])) {
if (isset($media_types[$extension]['mime'])) {
return $media_types[$extension]['mime'];
}
}
return $default;
return $media_types[$extension]['mime'] ?? $default;
}
/**
@ -1060,7 +1071,7 @@ abstract class Utils
$pretty_offset = "UTC${offset_prefix}${offset_formatted}";
$timezone_list[$timezone] = "(${pretty_offset}) ".str_replace('_', ' ', $timezone);
$timezone_list[$timezone] = "(${pretty_offset}) " . str_replace('_', ' ', $timezone);
}
return $timezone_list;
@ -1069,11 +1080,11 @@ abstract class Utils
/**
* Recursively filter an array, filtering values by processing them through the $fn function argument
*
* @param array $source the Array to filter
* @param callable $fn the function to pass through each array item
* @param array $source the Array to filter
* @param callable $fn the function to pass through each array item
* @return array
*/
public static function arrayFilterRecursive(Array $source, $fn)
public static function arrayFilterRecursive(array $source, $fn)
{
$result = [];
foreach ($source as $key => $value) {
@ -1093,15 +1104,15 @@ abstract class Utils
/**
* Flatten a multi-dimensional associative array into query params.
*
* @param array $array
* @param string $prepend
* @param array $array
* @param string $prepend
* @return array
*/
public static function arrayToQueryParams($array, $prepend = '')
{
$results = [];
foreach ($array as $key => $value) {
$name = $prepend ? $prepend . '[' . $key . ']' : $key;
$name = $prepend ? $prepend . '[' . $key . ']' : $key;
if (is_array($value)) {
$results = array_merge($results, static::arrayToQueryParams($value, $name));
@ -1138,8 +1149,8 @@ abstract class Utils
/**
* Flatten a multi-dimensional associative array into dot notation
*
* @param array $array
* @param string $prepend
* @param array $array
* @param string $prepend
* @return array
*/
public static function arrayFlattenDotNotation($array, $prepend = '')
@ -1147,9 +1158,9 @@ abstract class Utils
$results = array();
foreach ($array as $key => $value) {
if (is_array($value)) {
$results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend.$key.'.'));
$results = array_merge($results, static::arrayFlattenDotNotation($value, $prepend . $key . '.'));
} else {
$results[$prepend.$key] = $value;
$results[$prepend . $key] = $value;
}
}
@ -1297,7 +1308,7 @@ abstract class Utils
* with reverse proxy setups.
*
* @param string $action
* @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
* @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
* @return string the nonce string
*/
private static function generateNonceString($action, $previousTick = false)
@ -1334,8 +1345,8 @@ abstract class Utils
* Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given
* action is the same for 12 hours.
*
* @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
* @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
* @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
* @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
* @return string the nonce
*/
public static function getNonce($action, $previousTick = false)
@ -1353,7 +1364,7 @@ abstract class Utils
/**
* Verify the passed nonce for the give action
*
* @param string|string[] $nonce the nonce to verify
* @param string|string[] $nonce the nonce to verify
* @param string $action the action to verify the nonce to
* @return boolean verified or not
*/
@ -1434,7 +1445,7 @@ abstract class Utils
while (count($keys) > 1) {
$key = array_shift($keys);
if (! isset($array[$key]) || ! is_array($array[$key])) {
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = array();
}
@ -1725,7 +1736,7 @@ abstract class Utils
$size *= 1024 ** stripos('bkmgtpezy', $unit[0]);
}
return (int) abs(round($size));
return (int)abs(round($size));
}
/**
@ -1739,7 +1750,7 @@ abstract class Utils
{
$enc_url = preg_replace_callback(
'%[^:/@?&=#]+%usD',
function ($matches) {
static function ($matches) {
return urlencode($matches[0]);
},
$url
@ -1770,7 +1781,7 @@ abstract class Utils
public static function processMarkdown($string, $block = true, $page = null)
{
$grav = Grav::instance();
$page = $page ?? $grav['page'] ?? null;
$page = $page ?? $grav['page'] ?? null;
$defaults = [
'markdown' => $grav['config']->get('system.pages.markdown', []),
'images' => $grav['config']->get('system.images', [])
@ -1812,12 +1823,12 @@ abstract class Utils
$ip = (string)inet_pton($ip);
// Maximum netmask length = same as packed address
$len = 8*strlen($ip);
$len = 8 * strlen($ip);
if ($prefix > $len) {
$prefix = $len;
}
$mask = str_repeat('f', $prefix>>2);
$mask = str_repeat('f', $prefix >> 2);
switch ($prefix & 3) {
case 3:
@ -1830,7 +1841,7 @@ abstract class Utils
$mask .= '8';
break;
}
$mask = str_pad($mask, $len>>2, '0');
$mask = str_pad($mask, $len >> 2, '0');
// Packed representation of netmask
$mask = pack('H*', $mask);
@ -1861,4 +1872,244 @@ abstract class Utils
return $types;
}
/**
* @param string $name
* @return bool
*/
public static function isDangerousFunction(string $name): bool
{
static $commandExecutionFunctions = [
'exec',
'passthru',
'system',
'shell_exec',
'popen',
'proc_open',
'pcntl_exec',
];
static $codeExecutionFunctions = [
'assert',
'preg_replace',
'create_function',
'include',
'include_once',
'require',
'require_once'
];
static $callbackFunctions = [
'ob_start' => 0,
'array_diff_uassoc' => -1,
'array_diff_ukey' => -1,
'array_filter' => 1,
'array_intersect_uassoc' => -1,
'array_intersect_ukey' => -1,
'array_map' => 0,
'array_reduce' => 1,
'array_udiff_assoc' => -1,
'array_udiff_uassoc' => [-1, -2],
'array_udiff' => -1,
'array_uintersect_assoc' => -1,
'array_uintersect_uassoc' => [-1, -2],
'array_uintersect' => -1,
'array_walk_recursive' => 1,
'array_walk' => 1,
'assert_options' => 1,
'uasort' => 1,
'uksort' => 1,
'usort' => 1,
'preg_replace_callback' => 1,
'spl_autoload_register' => 0,
'iterator_apply' => 1,
'call_user_func' => 0,
'call_user_func_array' => 0,
'register_shutdown_function' => 0,
'register_tick_function' => 0,
'set_error_handler' => 0,
'set_exception_handler' => 0,
'session_set_save_handler' => [0, 1, 2, 3, 4, 5],
'sqlite_create_aggregate' => [2, 3],
'sqlite_create_function' => 2,
];
static $informationDiscosureFunctions = [
'phpinfo',
'posix_mkfifo',
'posix_getlogin',
'posix_ttyname',
'getenv',
'get_current_user',
'proc_get_status',
'get_cfg_var',
'disk_free_space',
'disk_total_space',
'diskfreespace',
'getcwd',
'getlastmo',
'getmygid',
'getmyinode',
'getmypid',
'getmyuid'
];
static $otherFunctions = [
'extract',
'parse_str',
'putenv',
'ini_set',
'mail',
'header',
'proc_nice',
'proc_terminate',
'proc_close',
'pfsockopen',
'fsockopen',
'apache_child_terminate',
'posix_kill',
'posix_mkfifo',
'posix_setpgid',
'posix_setsid',
'posix_setuid',
];
if (in_array($name, $commandExecutionFunctions)) {
return true;
}
if (in_array($name, $codeExecutionFunctions)) {
return true;
}
if (isset($callbackFunctions[$name])) {
return true;
}
if (in_array($name, $informationDiscosureFunctions)) {
return true;
}
if (in_array($name, $otherFunctions)) {
return true;
}
return static::isFilesystemFunction($name);
}
/**
* @param string $name
* @return bool
*/
public static function isFilesystemFunction(string $name): bool
{
static $fileWriteFunctions = [
'fopen',
'tmpfile',
'bzopen',
'gzopen',
// write to filesystem (partially in combination with reading)
'chgrp',
'chmod',
'chown',
'copy',
'file_put_contents',
'lchgrp',
'lchown',
'link',
'mkdir',
'move_uploaded_file',
'rename',
'rmdir',
'symlink',
'tempnam',
'touch',
'unlink',
'imagepng',
'imagewbmp',
'image2wbmp',
'imagejpeg',
'imagexbm',
'imagegif',
'imagegd',
'imagegd2',
'iptcembed',
'ftp_get',
'ftp_nb_get',
];
static $fileContentFunctions = [
'file_get_contents',
'file',
'filegroup',
'fileinode',
'fileowner',
'fileperms',
'glob',
'is_executable',
'is_uploaded_file',
'parse_ini_file',
'readfile',
'readlink',
'realpath',
'gzfile',
'readgzfile',
'stat',
'imagecreatefromgif',
'imagecreatefromjpeg',
'imagecreatefrompng',
'imagecreatefromwbmp',
'imagecreatefromxbm',
'imagecreatefromxpm',
'ftp_put',
'ftp_nb_put',
'hash_update_file',
'highlight_file',
'show_source',
'php_strip_whitespace',
];
static $filesystemFunctions = [
// read from filesystem
'file_exists',
'fileatime',
'filectime',
'filemtime',
'filesize',
'filetype',
'is_dir',
'is_file',
'is_link',
'is_readable',
'is_writable',
'is_writeable',
'linkinfo',
'lstat',
//'pathinfo',
'getimagesize',
'exif_read_data',
'read_exif_data',
'exif_thumbnail',
'exif_imagetype',
'hash_file',
'hash_hmac_file',
'md5_file',
'sha1_file',
'get_meta_tags',
];
if (in_array($name, $fileWriteFunctions)) {
return true;
}
if (in_array($name, $fileContentFunctions)) {
return true;
}
if (in_array($name, $filesystemFunctions)) {
return true;
}
return false;
}
}

View File

@ -102,11 +102,10 @@ class CleanCommand extends Command
'vendor/dragonmantank/cron-expression/composer.json',
'vendor/dragonmantank/cron-expression/tests',
'vendor/dragonmantank/cron-expression/CHANGELOG.md',
'vendor/enshrined/svg-sanitize/tests',
'vendor/enshrined/svg-sanitize/.gitignore',
'vendor/enshrined/svg-sanitize/.travis.yml',
'vendor/enshrined/svg-sanitize/composer.json',
'vendor/enshrined/svg-sanitize/phpunit.xml',
'vendor/rhukster/dom-sanitizer/tests',
'vendor/rhukster/dom-sanitizer/.gitignore',
'vendor/rhukster/dom-sanitizer/composer.json',
'vendor/rhukster/dom-sanitizer/composer.lock',
'vendor/erusev/parsedown/composer.json',
'vendor/erusev/parsedown/phpunit.xml.dist',
'vendor/erusev/parsedown/.travis.yml',

View File

@ -73,11 +73,11 @@ class InstallCommand extends GravCommand
$io = $this->getIO();
$dependencies_file = '.dependencies';
$this->destination = $input->getArgument('destination') ?: GRAV_ROOT;
$this->destination = $input->getArgument('destination') ?: GRAV_WEBROOT;
// fix trailing slash
$this->destination = rtrim($this->destination, DS) . DS;
$this->user_path = $this->destination . USER_PATH;
$this->user_path = $this->destination . GRAV_USER_PATH . DS;
if ($local_config_file = $this->loadLocalConfig()) {
$io->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
}

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