Compare commits

...

89 Commits

Author SHA1 Message Date
Andy Miller
916b7cfc44 Merge branch 'release/1.7.0' 2021-01-19 13:13:21 -07:00
Andy Miller
317efc182a Prepare for release 2021-01-19 13:13:10 -07:00
Andy Miller
02b3b9a4d7 Merge branch 'release/1.7.0' 2021-01-19 12:54:16 -07:00
Andy Miller
4bcfcfc4f8 Merge tag '1.7.0' into develop
Release v1.7.0
2021-01-19 12:54:16 -07:00
Andy Miller
57d0fc3774 switch to 0.2.3 2021-01-19 12:45:23 -07:00
Andy Miller
2d1bca11df use github secrets token 2021-01-19 12:28:01 -07:00
Andy Miller
ac0de93139 Merge tag '1.7.0' into develop
Release v1.7.0
2021-01-19 12:10:16 -07:00
Andy Miller
9e39860868 Merge branch 'release/1.7.0' 2021-01-19 12:10:15 -07:00
Andy Miller
b13a4d331c Prepare for release 2021-01-19 12:10:05 -07:00
Andy Miller
065cd5b7b7 Prepare for release 2021-01-19 12:08:10 -07:00
Andy Miller
1d34b5389e wrong workflow! 2021-01-19 08:37:10 -07:00
Andy Miller
23df7cc90d Try different token 2021-01-19 08:34:59 -07:00
Andy Miller
c9a5d5a406 Merge branch '1.7' of github.com:getgrav/grav into 1.7 2021-01-19 08:33:46 -07:00
Andy Miller
a5d2c86e59 set token 2021-01-19 08:33:42 -07:00
Andy Miller
f095cc760b fix periods 2021-01-19 08:33:34 -07:00
Matias Griese
c256b0675d Updated README.md, removed UPGRADE-1.7.md 2021-01-19 11:27:14 +02:00
Matias Griese
a7af12d026 Added support for locking the start and limit in a Page Collection 2021-01-19 11:14:55 +02:00
Matias Griese
d7995e9be4 Fixed page collection pagination not behaving as it did in Grav 1.6 2021-01-19 10:48:04 +02:00
Matias Griese
af2f4b66b9 Fixed ERR_TOO_MANY_REDIRECTS with HTTPS = 'On' [#3155] 2021-01-18 20:38:58 +02:00
Matias Griese
15464aa10c Fixed ignore parent in BlueprintSchema 2021-01-18 20:22:08 +02:00
Matias Griese
7cfc02e7d9 Use PHP 7.3 in build 2021-01-18 20:10:13 +02:00
Matias Griese
c6b0a50e4e Composer update 2021-01-18 20:09:50 +02:00
Matias Griese
ac4f093590 Fixed streams in setup.php being overridden by system/streams.yaml [#2450] 2021-01-18 11:20:21 +02:00
Matias Griese
a03746145b Merge branch 'develop' of github.com:getgrav/grav into 1.7 2021-01-15 22:29:49 +02:00
Matias Griese
3a3b84af61 Fixed page collections containing dummy items for untranslated default language [#2985] 2021-01-15 22:21:39 +02:00
Matias Griese
3ba68bf5c5 Fixed multiple issues with system.language.translations: false 2021-01-15 20:36:58 +02:00
Matias Griese
454e9c1d8e Fixed Flex Pages cache not invalidating if saving an old Page object [#3152] 2021-01-15 15:44:49 +02:00
Nico
ee9fb4aeec Add german translations for GRAV.YES and GRAV.NO 2021-01-15 09:52:29 +02:00
Matias Griese
ba2c224d5d Changelog update 2021-01-14 22:53:26 +02:00
Matias Griese
515eab7dad Fixed ordering when changing parent of new page 2021-01-14 22:43:20 +02:00
Matias Griese
18b0e01a3e Fixed version history issue 2021-01-14 20:50:40 +02:00
Matias Griese
57c6414d1c Fixed PageObject::getRoute() with pages which have no route 2021-01-14 20:49:37 +02:00
Matias Griese
fdfcaaeb39 Merge branch 'develop' of github.com:getgrav/grav into 1.7
 Conflicts:
	system/src/Grav/Common/Processors/ConfigurationProcessor.php
2021-01-14 17:16:27 +02:00
Matias Griese
561b91894c Fixed version history 2021-01-14 17:13:58 +02:00
Andy Miller
aea01a3937 missed a change in regular pages 2021-01-11 16:32:10 -07:00
Andy Miller
9b34178332 Merge branch 'develop' into 1.7
# Conflicts:
#	composer.lock
#	system/defines.php
#	system/src/Grav/Common/Page/Medium/ImageMedium.php
#	system/src/Grav/Common/Page/Page.php
#	system/src/Grav/Common/Twig/TwigExtension.php
2021-01-11 16:29:39 -07:00
Matias Griese
eced8facb8 Merge remote-tracking branch 'origin/1.7' into 1.7 2021-01-12 00:51:52 +02:00
Matias Griese
074e13325e Fixed Inflector methods when translation is missing GRAV.INFLECTOR_* translations 2021-01-12 00:51:43 +02:00
angusmcb
d7378f87c5 Added 'format' to magic actions for images (#3144) 2021-01-11 15:49:37 -07:00
Andy Miller
4091c16e97 don’t use array_support 2021-01-11 15:36:03 -07:00
Andy Miller
56408b37ac Fix for string->array conversion with translations: false admin#1896 2021-01-11 15:10:25 -07:00
Nico
fd18eeb62c Add grav twig extensions to its own evaluate and evaluate_twig filters (#3139)
I wanted to use `evaluate_twig()` with a template the uses `theme_var()` or also a simpler twig function `string`. I made them available with this simple call.
2021-01-11 12:47:43 -07:00
Nico
cbd4ed311f Set twig_first setting to the correct default (#3138)
I've also verified this via code, the correct default is `false`. It is also documented here:
https://learn.getgrav.org/16/basics/grav-configuration#pages
2021-01-11 12:46:37 -07:00
Andy Miller
8122703e3b Initialize pages + themes so email will work 2021-01-11 11:40:43 -07:00
Andy Miller
2920b6143b improve scheduler 2021-01-11 11:30:18 -07:00
Matias Griese
8870463afc Composer update 2021-01-11 10:49:50 +02:00
Matias Griese
23fa2324a8 Minor tweaks on phpstan 2021-01-09 09:58:03 +02:00
Matias Griese
0df1082778 Keep on fixing CLI commands 2021-01-07 12:39:38 +02:00
Matias Griese
6647b8da3c Composer update 2021-01-07 11:59:37 +02:00
Matias Griese
f0b9411e19 Fix Console::addOption() breaking --env and --lang 2021-01-06 20:27:46 +02:00
Matias Griese
5c1cc5cdd7 Changelog update 2021-01-06 19:36:52 +02:00
Matias Griese
df9eb60ffa Remove duplicated --env in yamllinter console command 2021-01-06 19:23:32 +02:00
Matias Griese
9893792768 Improve all console commands 2021-01-06 19:13:19 +02:00
Matias Griese
379033aae4 Further improve CLI commands 2021-01-06 13:45:19 +02:00
Matias Griese
193ab52a35 Better --env support for bin/grav and bin/gpm console commands 2021-01-06 13:11:43 +02:00
Matias Griese
9eb6662db8 Changelog update 2021-01-06 11:12:36 +02:00
Matias Griese
5e48146f59 Simplify QFN 2021-01-05 18:50:28 +02:00
Matias Griese
627702a3a1 Merge remote-tracking branch 'origin/1.7' into 1.7 2021-01-05 12:02:00 +02:00
Matias Griese
d9ee6de3d0 Phpstan phpdoc improvements 2021-01-05 11:59:19 +02:00
Andy Miller
14df5a6d5f updated robots.txt 2021-01-04 13:41:32 -07:00
Andy Miller
2738107b1e composer update for matthiasmullie/minify 2020-12-28 11:04:24 -07:00
Andy Miller
e53f26ca13 composer update for matthiasmullie/minify 2020-12-28 11:03:45 -07:00
thekenshow
d3e80d62e5 Fixed Link to XSS Security Docs (#3115)
* Fixed Link to XSS Security Docs

Was linking to .local server.

* Fixed link reference

Co-authored-by: Djamil Legato <djamil+github@djamil.it>
2020-12-28 08:13:29 -08:00
Andy Miller
b7aa20ed88 vendor updates 2020-12-24 11:33:37 -07:00
Andy Miller
26da4a1aa3 Updated minify library to include calc() fix 2020-12-24 11:32:23 -07:00
Matias Griese
4345a629f1 Added FlexCollection::getDistinctValues() to get all the assigned values from the field 2020-12-23 21:05:58 +02:00
Matias Griese
5bb4ca7822 Fixed Clockwork missing dumped arrays and objects 2020-12-23 21:03:36 +02:00
Matias Griese
79ee06f518 Fixed Clockwork missing dumped arrays and objects 2020-12-23 20:21:55 +02:00
Matias Griese
bc2435efe9 Fixed Flex sorting issues 2020-12-23 19:23:53 +02:00
Matias Griese
504a29f496 Added support for relative paths in PageObject::getLevelListing() [#3110] 2020-12-23 14:09:55 +02:00
Matias Griese
0948e9db9d Fixed Filesystem::normalize() with dot-dot paths 2020-12-23 13:46:49 +02:00
Matias Griese
c298464314 Composer update 2020-12-23 11:44:30 +02:00
Matias Griese
67a00a799c Return value fixes for Session class 2020-12-23 11:40:32 +02:00
Matias Griese
589c9e4445 Enabled ETag setting by default for 304 responses 2020-12-22 18:43:13 +02:00
Matias Griese
d1925c8935 Fixed pages with session messages should never be cached [#3108] 2020-12-22 18:06:28 +02:00
Matias Griese
2923658bb9 Use PHP 7.4 serialization 2020-12-22 15:12:09 +02:00
Matias Griese
c333da60d6 Fixed unserialize Framework\File classes 2020-12-22 15:10:33 +02:00
Matias Griese
cf62db1329 Fixed unserialize in MarkdownFormatter class 2020-12-22 13:53:19 +02:00
Matias Griese
625a39a892 Merge remote-tracking branch 'origin/1.7' into 1.7 2020-12-18 20:53:47 +02:00
Matias Griese
ffc93a77a9 Make it possible to use an absolute path when loading a blueprint 2020-12-18 20:53:38 +02:00
Andy Miller
0cee2b6a97 hide errors with exif 2020-12-17 21:12:15 -07:00
Matias Griese
a24ec2c433 Better fix for system.custom_base_url, which does not affect other urls 2020-12-17 10:02:55 +02:00
Matias Griese
e5727462e7 Fixed port issue with system.custom_base_url 2020-12-16 20:54:56 +02:00
Matias Griese
c1cb4e192f Added support for overriding configuration by using environment variables 2020-12-16 16:38:15 +02:00
Andy Miller
b0c12063a1 should skip release of prerelease 2020-12-15 11:12:54 -07:00
Andy Miller
d4a20c71c2 Merge branch 'release/1.6.31' 2020-12-14 21:28:53 -07:00
Andy Miller
ec68068b97 Merge tag '1.6.31' into develop
Release v1.6.31
2020-12-14 21:28:53 -07:00
Andy Miller
d9c1445542 prepare for release 2020-12-14 21:28:42 -07:00
Djamil Legato
6a4686d17b Update composer first thing 2020-12-10 11:19:27 -08:00
206 changed files with 3387 additions and 2468 deletions

View File

@@ -6,7 +6,7 @@ on:
jobs:
build:
if: "!github.event.release.prerelease"
runs-on: ubuntu-latest
steps:
@@ -15,9 +15,11 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.1
php-version: 7.3
extensions: opcache, gd
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
- name: Install Dependencies
run: |
@@ -38,9 +40,9 @@ jobs:
- name: Upload Grav Release Assets
id: upload-release-asset
uses: alexellis/upload-assets@0.2.2
uses: alexellis/upload-assets@0.2.3
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
with:
asset_paths: '["./grav-dist/*.zip"]'
@@ -59,6 +61,6 @@ jobs:
author_name: 'Github Action Build'
text: '🚚 Automated Build Failure'
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: env.WORKFLOW_CONCLUSION == 'failure'

View File

@@ -2,9 +2,9 @@ name: PHP Tests
on:
push:
branches: [ 1.7 ]
branches: [ develop ]
pull_request:
branches: [ 1.7 ]
branches: [ develop ]
jobs:
@@ -26,6 +26,11 @@ jobs:
php-version: ${{ matrix.php }}
extensions: opcache, gd
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
- name: Update composer
run: composer update
- name: Validate composer.json and composer.lock
run: composer validate
@@ -59,9 +64,9 @@ jobs:
status: failure
fields: repo,message,author,action
icon_emoji: ':octocat:'
author_name: 'Github Action Tests (1.7)'
author_name: 'Github Action Tests'
text: '💥 Automated Test Failure'
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GLOBAL_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: env.WORKFLOW_CONCLUSION == 'failure'

View File

@@ -1,3 +1,43 @@
# v1.7.0
## 01/19/2021
1. [](#new)
* Requires **PHP 7.3.6**
* Read about this release in the [Grav 1.7 Released](https://getgrav.org/blog/grav-1.7-released) blog post
* Read the full list of all changes in the [Changelog on GitHub](https://github.com/getgrav/grav/blob/1.7.0/CHANGELOG.md)
* Please read [Grav 1.7 Upgrade Guide](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide) before upgrading!
* Added support for overriding configuration by using environment variables
* Use PHP 7.4 serialization (the old `Serializable` methods are now final and cannot be overridden)
* Enabled `ETag` setting by default for 304 responses
* Added `FlexCollection::getDistinctValues()` to get all the assigned values from the field
* `Flex Pages` method `$page->header()` returns `\Grav\Common\Page\Header` object, old `Page` class still returns `stdClass`
1. [](#improved)
* Make it possible to use an absolute path when loading a blueprint
* Make serialize methods final in `ContentBlock`, `AbstractFile`, `FormTrait`, `ObjectCollectionTrait` and `ObjectTrait`
* Added support for relative paths in `PageObject::getLevelListing()` [#3110](https://github.com/getgrav/grav/issues/3110)
* Better `--env` and `--lang` support for `bin/grav`, `bin/gpm` and `bin/plugin` console commands
* **BC BREAK** Shorthand for `--env`: `-e` will not work anymore as it conflicts with some plugins
* Added support for locking the `start` and `limit` in a Page Collection
1. [](#bugfix)
* Fixed port issue with `system.custom_base_url`
* Hide errors with `exif_read_data` in `ImageFile`
* Fixed unserialize in `MarkdownFormatter` and `Framework\File` classes
* Fixed pages with session messages should never be cached [#3108](https://github.com/getgrav/grav/issues/3108)
* Fixed `Filesystem::normalize()` with dot-dot paths
* Fixed Flex sorting issues [grav-plugin-flex-objects#92](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/92)
* Fixed Clockwork missing dumped arrays and objects
* Fixed fatal error in PHP 8 when trying to access root page
* Fixed Array->String conversion error when `languages:translations: false` [admin#1896](https://github.com/getgrav/grav-plugin-admin/issues/1896)
* Fixed `Inflector` methods when translation is missing `GRAV.INFLECTOR_*` translations
* Fixed exception when changing parent of new page [grav-plugin-admin#2018](https://github.com/getgrav/grav-plugin-admin/issues/2018)
* Fixed ordering issue with moving pages [grav-plugin-admin#2015](https://github.com/getgrav/grav-plugin-admin/issues/2015)
* Fixed Flex Pages cache not invalidating if saving an old `Page` object [#3152](https://github.com/getgrav/grav/issues/3152)
* Fixed multiple issues with `system.language.translations: false`
* Fixed page collections containing dummy items for untranslated default language [#2985](https://github.com/getgrav/grav/issues/2985)
* Fixed streams in `setup.php` being overridden by `system/streams.yaml` [#2450](https://github.com/getgrav/grav/issues/2450)
* Fixed `ERR_TOO_MANY_REDIRECTS` with HTTPS = 'On' [#3155](https://github.com/getgrav/grav/issues/3155)
* Fixed page collection pagination not behaving as it did in Grav 1.6
# v1.7.0-rc.20
## 12/15/2020
@@ -47,7 +87,7 @@
* Allow `JsonFormatter` options to be passed as a string
* Hide Flex Pages frontend configuration (not ready for production use)
* Improve Flex configuration: gather views together in blueprint
* Added XSS detection to all forms. See [documentation](http://learn.grav.local/17/forms/forms/form-options#xss-checks)
* Added XSS detection to all forms. See [documentation](https://learn.getgrav.org/17/forms/forms/form-options#xss-checks)
* Better handling of missing repository index [grav-plugin-admin#1916](https://github.com/getgrav/grav-plugin-admin/issues/1916)
* Added support for having all sites / environments under `user/env` folder [#3072](https://github.com/getgrav/grav/issues/3072)
* Added `FlexObject::refresh()` method to make sure object is up to date
@@ -548,7 +588,7 @@
* Optimization: Combine some early Grav processors into a single one
# v1.6.31
## mm/dd/2020
## 12/14/2020
1. [](#improved)
* Allow all CSS and JS via `robots.txt` [#2006](https://github.com/getgrav/grav/issues/2006) [#3067](https://github.com/getgrav/grav/issues/3067)

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2018 Grav
Copyright (c) 2021 Grav
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -24,6 +24,10 @@ The underlying architecture of Grav is designed to use well-established and _bes
- PHP 7.3.6 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
- Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements
# Documentation
The full documentation can be found from [learn.getgrav.org](https://learn.getgrav.org).
# QuickStart
These are the options to get Grav:
@@ -84,6 +88,11 @@ To update plugins and themes:
$ bin/gpm update
```
## Upgrading from older version
* [Upgrading to Grav 1.7](https://learn.getgrav.org/16/advanced/grav-development/grav-17-upgrade-guide)
* [Upgrading to Grav 1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-16-upgrade-guide)
* [Upgrading from Grav <1.6](https://learn.getgrav.org/16/advanced/grav-development/grav-15-upgrade-guide)
# Contributing
We appreciate any contribution to Grav, whether it is related to bugs, grammar, or simply a suggestion or improvement! Please refer to the [Contributing guide](CONTRIBUTING.md) for more guidance on this topic.
@@ -128,7 +137,14 @@ See [LICENSE](LICENSE.txt)
# Running Tests
First install the dev dependencies by running `composer update` from the Grav root.
First install the dev dependencies by running `composer install` from the Grav root.
Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.
Windows users should use the `composer test-windows` command.
You can also run a single unit test file, e.g. `composer test tests/unit/Grav/Common/AssetsTest.php`
To run phpstan tests, you should run:
* `composer phpstan` for global tests
* `composer phpstan-framework` for more strict tests
* `composer phpstan-plugins` to test all installed plugins

View File

@@ -1,356 +0,0 @@
# UPGRADE FROM 1.6 TO 1.7.RC17
Grav 1.7 REQUIRES PHP 7.3.6
## ADMINISTRATORS
### YAML files
* Please run `bin/grav yamllinter` to find any YAML parsing errors in your site. You should run this command before and after upgrade. Grav falls back to older YAML parser if it detects an error, but it will slow down your site.
## Forms
* **BC BREAK** Fixed `validation: strict`. Please search through all your forms if you were using this feature. If you were, either remove the line or test if the form still works.
* Added configuration option `system.strict_mode.blueprint_compat` to maintain old `validation: strict` behavior
* If you disable compatibiity, form validation will be much more strict (recommended, but may break existing forms)
* DOCUMENT NEW FORM FLASH!
### Pages
* **BC BREAK** Fixed 404 error page when you go to non-routable page with routable child pages under it. Now you get redirected to the first routable child page instead. This is probably what you wanted in the first place. If you do not want this new behavior, you need to **TODO**
### Media
* Support for `webp` image format
* Markdown: Added support for native `loading=lazy` attributes on images. Can be set in `system.images.defaults` or per md image with `?loading=lazy`
* Added ability to `noprocess` specific items only in Link/Image Excerpts, e.g. `http://foo.com/page?id=foo&target=_blank&noprocess=id`
### Multi-language
* Improved language support
* **BC BREAK** Please check that your fallback languages are correct. Old implementation had a fallback to any other language, now only default language is being used unless you use `system.languages.content_fallback` configuration option to override the default behavior.
### Admin
* If you upgrade from older 1.7 RC, you need to go to Flex Objects plugin settings and turn on `Pages`, `User Accounts` and `User Groups` directories (upgrading 1.6 automatically turns them on)
* Disabling `User Accounts` and `User Groups` directories in Flex Objects plugin should be kept enabled; fine tuned **ACL** may not work without
### Sessions
* Session ID now changes on login to prevent session fixation issues
### CLI
* Added new `bin/grav server` CLI command to easily run Symfony or PHP built-in web servers
* Added new `bin/grav page-system-validator [-r|--record] [-c|--check]` CLI command to test Flex Pages
* Improved `Scheduler` cron command check and more useful CLI information
* Added new `-r <job-id>` option for Scheduler CLI command to force-run a job
* Improved `bin/grav yamllinter` CLI command by adding an option to find YAML Linting issues from the whole site or custom folder
### Configuration
* Added new configuration option `system.debugger.provider` to choose between debugbar and clockwork
* Added new configuration option `system.debugger.censored` to hide potentially sensitive information
* Added new configuration option `system.pages.type` to enable Flex Pages
* Added new configuration option `system.languages.include_default_lang_file_extension` to keep default language in `.md` files if set to `false`
* Added new configuration option `system.languages.content_fallback` to set fallback content languages individually for every language
* Added new configuration option `security.sanitize_svg` to remove potentially dangerous code from SVG files
* Added system configuration support for `HTTP_X_Forwarded` headers (host disabled by default)
* Added new configuration option `system.strict_mode.blueprint_compat` to maintain old `validation: strict` behavior
### Debugging
* Added support for [Clockwork](https://underground.works/clockwork) developer tools (now default debugger)
* Added support for [Tideways XHProf](https://github.com/tideways/php-xhprof-extension) PHP Extension for profiling method calls
* Added Twig profiling for Clockwork debugger
## DEVELOPERS
### Use composer autoloader
* Please add `composer.json` file to your plugin and run `composer update --no-dev` (and remember to keep it updated):
composer.json
```json
{
"name": "getgrav/grav-plugin-example",
"type": "grav-plugin",
"description": "Example plugin for Grav CMS",
"keywords": ["example", "plugin"],
"homepage": "https://github.com/getgrav/grav-plugin-example",
"license": "MIT",
"authors": [
{
"name": "...",
"email": "...",
"homepage": "...",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/getgrav/grav-plugin-example/issues",
"docs": "https://github.com/getgrav/grav-plugin-example/blob/master/README.md"
},
"require": {
"php": ">=7.1.3"
},
"autoload": {
"psr-4": {
"Grav\\Plugin\\Example\\": "classes/",
"Grav\\Plugin\\Console\\": "cli/"
},
"classmap": [
"example.php"
]
},
"config": {
"platform": {
"php": "7.1.3"
}
}
}
```
See [Composer schema](https://getcomposer.org/doc/04-schema.md)
* Please use autoloader instead of `require` in the code:
example.php
```php
/**
* @return array
*/
public static function getSubscribedEvents(): array
{
return [
'onPluginsInitialized' => [
// This is only required in Grav 1.6. Grav 1.7 automatically calls $plugin->autolaod() method.
['autoload', 100000],
]
];
}
/**
* Composer autoload.
*
* @return \Composer\Autoload\ClassLoader
*/
public function autoload(): \Composer\Autoload\ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
```
* Plugins & Themes: Call `$plugin->autoload()` and `$theme->autoload()` automatically when object gets initialized
* Make sure your code does not use `require` or `include` for loading classes
### Plugin/Theme Blueprints (`blueprints.yaml`)
* Please add:
```yaml
slug: folder-name
type: plugin|theme
```
* Make sure you update your dependencies. I recommend setting Grav to either 1.6 or 1.7 and update your code/vendor to PHP 7.1
```yaml
dependencies:
- { name: grav, version: '>=1.6.0' }
```
### Sessions
* Added `Session::regenerateId()` method to properly prevent session fixation issues
### ACL
* `user.authorize()` now requires user to be authorized (passed 2FA check), unless the rule contains `login` in its name.
* Added support for more advanced ACL (CRUD)
* **BC BREAK** `user.authorize()` and Flex `object.isAuthorized()` now have two deny states: `false` and `null`.
Make sure you do not have strict checks against false: `$user->authorize($action) === false` (PHP) or `user.authorize(action) is same as(false)` (Twig).
For the negative checks you should be using `!user->authorize($action)` (PHP) or `not user.authorize(action)` (Twig).
The change has been done to allow strong deny rules by chaining the actions if previous ones do not match: `user.authorize(action1) ?? user.authorize(action2) ?? user.authorize(action3)`.
Note that Twig function `authorize()` will still **keeps** the old behavior!
### Pages
* Added experimental support for `Flex Pages` in the frontend (not recommended to use yet)
* Admin uses `Flex Pages` by default (can be disabled from `Flex-Objects` plugin)
* Added page specific admin permissions support for `Flex Pages`
* Added root page support for `Flex Pages`
* Fixed wrong `Pages::dispatch()` calls (with redirect) when we really meant to call `Pages::find()`
* Added `Pages::getCollection()` method
* Moved `collection()` and `evaluate()` logic from `Page` class into `Pages` class
* **DEPRECATED** `$page->modular()` in favor of `$page->isModule()`
* **DEPRECATED** `PageCollectionInterface::nonModular()` in favor of `PageCollectionInterface::pages()`
* **DEPRECATED** `PageCollectionInterface::modular()` in favor of `PageCollectionInterface::modules()`
* **BC BREAK** Fixed `Page::modular()` and `Page::modularTwig()` returning `null` for folders and other non-initialized pages. Should not affect your code unless you were checking against `false` or `null`.
* **BC BREAK** Always use `\Grav\Common\Page\Interfaces\PageInterface` instead of `\Grav\Common\Page\Page` in method signatures
* Admin now uses `Flex Pages` by default, collection will behave in slightly different way
* **BC BREAK** `$page->topParent()` may return page itself instead of null
### Media
* Added `MediaTrait::freeMedia()` method to free media (and memory)
* Added support for uploading and deleting images directly in `Media` by using PSR-7
* Adjusted asset types to enable extension of assets in class
* **BC BREAK** Media no longer extends `Getters`, accessing `$media->$filename` no longer works, use `$media[$filename]` instead!
### Markdown
* **BC BREAK** Upgraded Parsedown to 1.7 for Parsedown-Extra 0.8. Plugins that extend Parsedown may need a fix to render as HTML
* Added new `Excerpts::processLinkHtml()` method
### Users
* Added experimental support for `Flex Users` in the frontend (not recommended to use yet)
* Admin uses `Flex Users` by default (can be disabled from `Flex-Objects` plugin)
* Improved `Flex Users`: obey blueprints and allow Flex to be used in admin only
* Improved `Flex Users`: user and group ACL now supports deny permissions
* Changed `UserInterface::authorize()` to return `null` having the same meaning as `false` if access is denied because of no matching rule
* **DEPRECATED** `\Grav\Common\User\Group` in favor of `$grav['user_groups']`, which contains Flex UserGroup collection
* **BC BREAK** Always use `\Grav\Common\User\Interfaces\UserInterface` instead of `\Grav\Common\User\User` in method signatures
### Flex
* Do not use `Framework` Flex classes directly, it's better to use or extend classes under `Grav\Common\Flex\Types\Generic` namespace
* Added `$grav['flex']` to access all registered Flex Directories
* Added `FlexRegisterEvent` which triggers when `$grav['flex']` is being accessed the first time
* Added `hasFlexFeature()` method to test if `FlexObject` or `FlexCollection` implements a given feature
* Added `getFlexFeatures()` method to return all features that `FlexObject` or `FlexCollection` implements
* Added `FlexStorage::getMetaData()` to get updated object meta information for listed keys
* `FlexDirectory::getObject()` can now be called without any parameters to create a new object
* Flex Directory: Implemented customizable configuration per flex type
* **DEPRECATED** `FlexDirectory::update()` and `FlexDirectory::remove()`
* **BC BREAK** Moved all Flex type classes under `Grav\Common\Flex`
* **BC BREAK** `FlexStorageInterface::getStoragePath()` and `getMediaPath()` can now return null
* **BC BREAK** `FlexStorageIngerface::getMetaData()` now has second optional argument
* **BC BREAK** Flex objects no longer return temporary key if they do not have one; empty key is returned instead
* **BC BREAK** Added reload argument to `FlexStorageInterface::getMetaData()`
* You can add `edit_list.html.twig` file to a form field in order to customize look in the listing view
### Templating
* Added support for Twig 2.12 (using Twig 1.43)
* Added a new `{% cache %}` Twig tag eliminating need for `twigcache` extension.
* Added `array_diff()` twig function
* Added `template_from_string()` twig function
* Added a new `svg_image()` twig function to make it easier to 'include' SVG source in Twig
* Improved `url()` twig function to take third parameter (`true`) to return URL on non-existing file instead of returning false
* Improved `|array` twig filter to work with iterators and objects with `toArray()` method
* Improved `authorize()` twig function to work better with nested rule parameters
* Improved `|yaml_serialize` twig filter: added support for `JsonSerializable` objects and other array-like objects
* Added `themes` to cached blueprints and configuration
* Added default templates for `external.html.twig`, `default.html.twig`, and `modular.html.twig`
### Multi-language
* Improved language support for `Route` class
* Translations: rename MODULAR to MODULE everywhere
* Added `Language::getPageExtensions()` to get full list of supported page language extensions
* **BC BREAK** Fixed `Language::getFallbackPageExtensions()` to fall back only to default language instead of going through all languages
### Blueprints
* Added `flatten_array` filter to form field validation
* Added support for `security@: or: [admin.super, admin.pages]` in blueprints (nested AND/OR mode support)
* Blueprint validation: Added `validate: value_type: bool|int|float|string|trim` to `array` to filter all the values inside the array
* If your plugins has blueprints folder, initializing it in the event will be too late. Do this instead:
```php
class MyPlugin extends Plugin
{
/** @var array */
public $features = [
'blueprints' => 0, // Use priority 0
];
}
```
### Events
* Use `Symfony EventDispatcher` directly instead of `rockettheme/toolbox` wrapper.
* Added `$grav->dispatchEvent()` method for PSR-14 events
* Added `PluginsLoadedEvent` which triggers after plugins have been loaded but not yet initialized
* Added `SessionStartEvent` which triggers when session is started
* Added `FlexRegisterEvent` which triggers when `$grav['flex']` is being accessed the first time
* Added `PermissionsRegisterEvent` which triggers when `$grav['permissions']` is being accessed the first time
* Added `onAfterCacheClear` event
* Check `onAdminTwigTemplatePaths` event, it should NOT be:
```php
public function onAdminTwigTemplatePaths($event)
{
// This code breaks all the other plugins in admin, including Flex Objects
$event['paths'] = [__DIR__ . '/admin/themes/grav/templates'];
}
```
but:
```php
public function onAdminTwigTemplatePaths($event)
{
// Add plugin template path for admin.
$paths = $event['paths'];
$paths[] = __DIR__ . '/admin/themes/grav/templates';
$event['paths'] = $paths;
}
```
### Misc
* Added `Utils::isAssoc()` and `Utils::isNegative()` helper methods
* Added `Utils::simpleTemplate()` method for very simple variable templating
* Added `Utils::fullPath()` to get the full path to a file be it stream, relative, etc.
* Support customizable null character replacement in `CSVFormatter::decode()`
* Added new `Security::sanitizeSVG()` function
* Added `$grav->close()` method to properly terminate the request with a response
* Added `Folder::countChildren()` method to determine if a folder has child folders
* Support symlinks when saving `File`
* Added `Route::getBase()` method
* **BC BREAK** Make `Route` objects immutable. This means that you need to do: `{% set route = route.withExtension('.html') %}` (for all `withX` methods) to keep the updated version.
* Better `Content-Encoding` handling in Apache when content compression is disabled
* Added a `Uri::getAllHeaders()` compatibility function
### CLI
* **BC BREAK** Many plugins initialize Grav in a wrong way, it is not safe to initialize plugins and theme by yourself
* Following calls require Grav 1.6.21 or later, so it is recommended to set Grav dependency to that version
* Inside `serve()` method:
* Call `$this->setLanguage($langCode);` before doing anything else if you want to set the language (or use default)
* Call one of following:
* `$this->initializeGrav();` Already called if you're in `bin/plugin`, otherwise you may need to call this one
* `$this->initializePlugins();` This initializes grav, plugins (up to `onPluginsInitialized`)
* `$this->initializeThemes();` This initializes grav, plugins and theme
* `$this->initializePages();` This initializes grav, plugins, theme and everything needed by pages
* It is a good idea to prefix your CLI command classes with your plugin name, otherwise there may be naming conflicts (we already have some!)
### Composer dependencies
* Updated Symfony Components to 4.4, please update any deprecated features in your code
* **BC BREAK** Please run `bin/grav yamllinter -f user://` to find any YAML parsing errors in your site (including your plugins and themes).
## PLUGINS
### Admin
* Added `Content Editor` option to user account blueprint
* **BC BREAK** Admin will not initialize frontend pages anymore, this has been done to greatly speed up Admin plugin.
Please call `$grav['admin']->enablePages()` or `{% do admin.enablePages() %}` if you need to access frontend pages. This call can be safely made multiple times.
If you're using `Flex Pages`, please use Flex Directory instead, it will make your code so much faster.
* Admin now uses Flex for editing `Accounts` and `Pages`. If your plugin hooks into either of those, please make sure they still work.
* Admin cache is enabled by default, make sure your plugin clears cache when needed. Please avoid clearing all cache!
### Shortcode Core
* **DEPRECATED** Every shortcode needs to have `init()` method, classes without it will stop working in the future.

34
bin/gpm
View File

@@ -2,8 +2,8 @@
<?php
use Grav\Common\Composer;
use Symfony\Component\Console\Application;
use Grav\Common\Grav;
use Grav\Console\Application\GpmApplication;
\define('GRAV_CLI', true);
\define('GRAV_REQUEST_TIME', microtime(true));
@@ -43,37 +43,7 @@ if (!function_exists('curl_version')) {
exit('FATAL: GPM requires PHP Curl module to be installed');
}
$climate = new League\CLImate\CLImate;
$climate->arguments->add([
'environment' => [
'prefix' => 'e',
'longPrefix' => 'env',
'description' => 'Configuration Environment',
'defaultValue' => 'localhost'
]
]);
$climate->arguments->parse();
// Set up environment based on params.
$environment = $climate->arguments->get('environment');
$grav = Grav::instance(array('loader' => $autoload));
$grav->setup($environment);
$grav['config']->init();
$grav['uri']->init();
$grav['accounts'];
$app = new Application('Grav Package Manager', GRAV_VERSION);
$app->addCommands(array(
new \Grav\Console\Gpm\IndexCommand(),
new \Grav\Console\Gpm\VersionCommand(),
new \Grav\Console\Gpm\InfoCommand(),
new \Grav\Console\Gpm\InstallCommand(),
new \Grav\Console\Gpm\UninstallCommand(),
new \Grav\Console\Gpm\UpdateCommand(),
new \Grav\Console\Gpm\SelfupgradeCommand(),
new \Grav\Console\Gpm\DirectInstallCommand(),
));
$app = new GpmApplication('Grav Package Manager', GRAV_VERSION);
$app->run();

View File

@@ -3,8 +3,7 @@
use Grav\Common\Composer;
use Grav\Common\Grav;
use Grav\Console\Cli;
use Symfony\Component\Console\Application;
use Grav\Console\Application\GravApplication;
\define('GRAV_CLI', true);
\define('GRAV_REQUEST_TIME', microtime(true));
@@ -36,41 +35,11 @@ if (!\extension_loaded('mbstring')) {
@ini_set('default_charset', 'UTF-8');
mb_internal_encoding('UTF-8');
$climate = new League\CLImate\CLImate;
$climate->arguments->add([
'environment' => [
'prefix' => 'e',
'longPrefix' => 'env',
'description' => 'Configuration Environment',
'defaultValue' => 'localhost'
]
]);
$climate->arguments->parse();
// Set up environment based on params.
$environment = $climate->arguments->get('environment');
$grav = Grav::instance(array('loader' => $autoload));
$grav->setup($environment);
if (!file_exists(GRAV_ROOT . '/index.php')) {
exit('FATAL: Must be run from ROOT directory of Grav!');
}
$app = new Application('Grav CLI Application', GRAV_VERSION);
$app->addCommands(array(
new Cli\InstallCommand(),
new Cli\ComposerCommand(),
new Cli\SandboxCommand(),
new Cli\CleanCommand(),
new Cli\ClearCacheCommand(),
new Cli\BackupCommand(),
new Cli\NewProjectCommand(),
new Cli\SchedulerCommand(),
new Cli\SecurityCommand(),
new Cli\LogViewerCommand(),
new Cli\YamlLinterCommand(),
new Cli\ServerCommand(),
new Cli\PageSystemValidatorCommand(),
));
$app = new GravApplication('Grav CLI Application', GRAV_VERSION);
$app->run();

View File

@@ -2,12 +2,8 @@
<?php
use Grav\Common\Composer;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Grav\Common\Grav;
use Grav\Common\Filesystem\Folder;
use Grav\Console\Application\PluginApplication;
\define('GRAV_CLI', true);
\define('GRAV_REQUEST_TIME', microtime(true));
@@ -43,112 +39,8 @@ if (!file_exists(GRAV_ROOT . '/index.php')) {
exit('FATAL: Must be run from ROOT directory of Grav!');
}
$climate = new League\CLImate\CLImate;
$climate->arguments->add([
'environment' => [
'prefix' => 'e',
'longPrefix' => 'env',
'description' => 'Configuration Environment',
'defaultValue' => 'localhost'
]
]);
$climate->arguments->parse();
$environment = $climate->arguments->get('environment');
// Bootstrap Grav container.
$grav = Grav::instance(array('loader' => $autoload));
$grav->setup($environment);
$grav->initializeCli();
$app = new Application('Grav Plugins Commands', GRAV_VERSION);
$pattern = '([A-Z]\w+Command\.php)';
// get arguments and strip the application name
if (null === $argv) {
$argv = $_SERVER['argv'];
}
$bin = array_shift($argv);
$name = array_shift($argv);
$argv = array_merge([$bin], $argv);
$input = new ArgvInput($argv);
/** @var \Grav\Common\Data\Data $plugin */
$plugin = $grav['plugins']->get($name);
$output = new ConsoleOutput();
$output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold')));
$output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold')));
if (!$name) {
$output->writeln('');
$output->writeln('<red>Usage:</red>');
$output->writeln(" {$bin} [slug] [command] [arguments]");
$output->writeln('');
$output->writeln('<red>Example:</red>');
$output->writeln(" {$bin} error log -l 1 --trace");
$list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 2]);
$total = 0;
if (count($list)) {
$available = [];
$output->writeln('');
$output->writeln('<red>Plugins with CLI available:</red>');
foreach ($list as $index => $entry) {
$split = explode('/', $entry);
$entry = array_shift($split);
$index = str_pad($index++ + 1, 2, '0', STR_PAD_LEFT);
$total = str_pad($total++ + 1, 2, '0', STR_PAD_LEFT);
if (\in_array($entry, $available, true)) {
$total--;
continue;
}
$available[] = $entry;
$commands_count = $index - $total + 1;
$output->writeln(' ' . $total . '. <red>' . str_pad($entry, 15) . "</red> <white>{$bin} {$entry} list</white>");
}
}
exit;
}
if (null === $plugin) {
$output->writeln('');
$output->writeln("<red>$name plugin not found</red>");
die;
}
if (!$plugin->enabled) {
$output->writeln('');
$output->writeln("<red>$name not enabled</red>");
die;
}
if ($plugin === null) {
$output->writeln("<red>Grav Plugin <white>'{$name}'</white> is not installed</red>");
exit;
}
$path = 'plugins://' . $name . '/cli';
try {
$commands = Folder::all($path, ['compare' => 'Filename', 'pattern' => '/' . $pattern . '$/usm', 'levels' => 1]);
} catch (\RuntimeException $e) {
$output->writeln("<red>No Console Commands for <white>'{$name}'</white> where found in <white>'{$path}'</white></red>");
exit;
}
foreach ($commands as $command_path) {
$full_path = $grav['locator']->findResource("plugins://{$name}/cli/{$command_path}");
require_once $full_path;
$command_class = 'Grav\Plugin\Console\\' . preg_replace('/.php$/', '', $command_path);
$command = new $command_class();
$app->add($command);
}
$app->run($input);
$app = new PluginApplication('Grav Plugins Commands', GRAV_VERSION);
$app->run();

View File

@@ -32,7 +32,7 @@
"erusev/parsedown": "^1.7",
"erusev/parsedown-extra": "~0.8",
"symfony/contracts": "~1.1",
"symfony/yaml": "4.4.16",
"symfony/yaml": "~4.4",
"symfony/console": "~4.4",
"symfony/event-dispatcher": "~4.4",
"symfony/var-dumper": "~4.4",
@@ -70,6 +70,10 @@
"codeception/module-asserts": "^1.3",
"codeception/module-phpbrowser": "^1.0"
},
"provide": {
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*"
},
"suggest": {
"ext-mbstring": "Recommended for better performance",
"ext-iconv": "Recommended for better performance",
@@ -115,8 +119,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 3 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src",
"phpstan-framework": "vendor/bin/phpstan analyse -l 8 -c ./tests/phpstan/phpstan.neon --memory-limit=480M system/src/Grav/Framework system/src/Grav/Events system/src/Grav/Installer system/src/Grav/Common/Assets system/src/Grav/Common/Backup system/src/Grav/Common/Config system/src/Grav/Common/Data system/src/Grav/Common/Errors system/src/Grav/Common/File system/src/Grav/Common/Helpers system/src/Grav/Common/Language system/src/Grav/Common/Markdown system/src/Grav/Common/Form system/src/Grav/Common/User",
"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-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"

552
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
{
{
"version": 2,
"builds": [{ "src": "*.php", "use": "@now/php" }]
}

View File

@@ -12,4 +12,5 @@ Allow: /user/themes/
Allow: /user/images/
Allow: /
Allow: *.css$
Allow: *.js$
Allow: *.js$
Allow: /system/*.js$

View File

@@ -1,16 +0,0 @@
schemes:
image:
type: Stream
paths:
- user://images
- system://images
page:
type: ReadOnlyStream
paths:
- user://pages
account:
type: ReadOnlyStream
paths:
- user://accounts

View File

@@ -73,7 +73,7 @@ pages:
expires: 604800 # Page expires time in seconds (604800 seconds = 7 days)
cache_control: # Can be blank for no setting, or a valid `cache-control` text value
last_modified: false # Set the last modified date header based on file modification timestamp
etag: false # Set the etag header tag
etag: true # Set the etag header tag
vary_accept_encoding: false # Add `Vary: Accept-Encoding` header
redirect_default_route: false # Automatically redirect to a page's default route
redirect_default_code: 302 # Default code to use for redirects

View File

@@ -8,9 +8,9 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.7.0-rc.20');
define('GRAV_VERSION', '1.7.0');
define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
define('GRAV_TESTING', true);
define('GRAV_TESTING', false);
// PHP minimum requirement
if (!defined('GRAV_PHP_MIN')) {

View File

@@ -125,6 +125,8 @@ GRAV:
- 'Freitag'
- 'Samstag'
- 'Sonntag'
YES: 'Ja'
NO: 'Nein'
CRON:
EVERY: jede
EVERY_HOUR: jede Stunde

View File

@@ -9,6 +9,7 @@
namespace Grav\Common;
use Closure;
use Grav\Common\Assets\Pipeline;
use Grav\Common\Assets\Traits\LegacyAssetsTrait;
use Grav\Common\Assets\Traits\TestingAssetsTrait;
@@ -81,7 +82,7 @@ class Assets extends PropertyObject
/** @var array */
protected $pipeline_options = [];
/** @var \Closure|string */
/** @var Closure|string */
protected $fetch_command;
/** @var string */
protected $autoload;

View File

@@ -15,6 +15,8 @@ use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Object\PropertyObject;
use MatthiasMullie\Minify\CSS;
use MatthiasMullie\Minify\JS;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use function array_key_exists;
@@ -131,7 +133,7 @@ class Pipeline extends PropertyObject
// Minify if required
if ($this->shouldMinify('css')) {
$minifier = new \MatthiasMullie\Minify\CSS();
$minifier = new CSS();
$minifier->add($buffer);
$buffer = $minifier->minify();
}
@@ -194,7 +196,7 @@ class Pipeline extends PropertyObject
// Minify if required
if ($this->shouldMinify('js')) {
$minifier = new \MatthiasMullie\Minify\JS();
$minifier = new JS();
$minifier->add($buffer);
$buffer = $minifier->minify();
}

View File

@@ -12,6 +12,7 @@ namespace Grav\Common\Assets\Traits;
use Closure;
use Grav\Common\Grav;
use Grav\Common\Utils;
use function dirname;
use function in_array;
use function is_array;
@@ -80,7 +81,7 @@ trait AssetUtilsTrait
if (0 === strpos($link, '//')) {
$link = 'http:' . $link;
}
$relative_dir = \dirname($relative_path);
$relative_dir = dirname($relative_path);
} else {
// Fix to remove relative dir if grav is in one
if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
@@ -88,7 +89,7 @@ trait AssetUtilsTrait
$relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
}
$relative_dir = \dirname($relative_path);
$relative_dir = dirname($relative_path);
$link = ROOT_DIR . $relative_path;
}

View File

@@ -9,6 +9,7 @@
namespace Grav\Common;
use InvalidArgumentException;
use function donatj\UserAgent\parse_user_agent;
/**
@@ -26,7 +27,7 @@ class Browser
{
try {
$this->useragent = parse_user_agent();
} catch (\InvalidArgumentException $e) {
} catch (InvalidArgumentException $e) {
$this->useragent = parse_user_agent("Mozilla/5.0 (compatible; Unknown;)");
}
}

View File

@@ -10,6 +10,7 @@
namespace Grav\Common\Config;
use Grav\Common\File\CompiledYamlFile;
use function is_callable;
/**
* Class CompiledConfig
@@ -68,7 +69,7 @@ class CompiledConfig extends CompiledBase
*/
protected function createObject(array $data = [])
{
if ($this->withDefaults && empty($data) && \is_callable($this->callable)) {
if ($this->withDefaults && empty($data) && is_callable($this->callable)) {
$blueprints = $this->callable;
$data = $blueprints()->getDefaults();
}

View File

@@ -14,6 +14,7 @@ use Grav\Common\Grav;
use Grav\Common\Data\Data;
use Grav\Common\Service\ConfigServiceProvider;
use Grav\Common\Utils;
use function is_array;
/**
* Class Config
@@ -130,7 +131,7 @@ class Config extends Data
{
$setup = Grav::instance()['setup']->toArray();
foreach ($setup as $key => $value) {
if ($key === 'streams' || !\is_array($value)) {
if ($key === 'streams' || !is_array($value)) {
// Optimized as streams and simple values are fully defined in setup.
$this->items[$key] = $value;
} else {

View File

@@ -37,7 +37,7 @@ class Setup extends Data
];
/**
* @var string Current environment normalized to lower case.
* @var string|null Current environment normalized to lower case.
*/
public static $environment;

View File

@@ -23,6 +23,7 @@ use function is_array;
use function is_int;
use function is_object;
use function is_string;
use function strlen;
/**
* Class Blueprint
@@ -371,6 +372,10 @@ class Blueprint extends BlueprintForm
$locator = Grav::instance()['locator'];
if (is_string($path) && !$locator->isStream($path)) {
if (is_file($path)) {
return [$path];
}
// Find path overrides.
if (null === $context) {
$paths = (array) ($this->overrides[$path] ?? null);
@@ -383,7 +388,7 @@ class Blueprint extends BlueprintForm
$context = $this->context;
}
if ($context && $context[\strlen($context)-1] !== '/') {
if ($context && $context[strlen($context)-1] !== '/') {
$context .= '/';
}

View File

@@ -85,7 +85,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
*/
public function processForm(array $data, array $toggles = [])
{
return $this->processFormRecursive($data, $toggles, $this->nested);
return $this->processFormRecursive($data, $toggles, $this->nested) ?? [];
}
/**
@@ -254,7 +254,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
/**
* @param array $nested
* @param string $parent
* @return void
* @return bool
*/
protected function buildIgnoreNested(array $nested, $parent = '')
{
@@ -272,6 +272,8 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
$key = trim($parent, '.');
$this->items[$key]['validate']['ignore'] = true;
}
return $ignore;
}
/**

View File

@@ -9,7 +9,9 @@
namespace Grav\Common\Data;
use ArrayAccess;
use Exception;
use JsonSerializable;
use RocketTheme\Toolbox\ArrayTraits\Countable;
use RocketTheme\Toolbox\ArrayTraits\Export;
use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
@@ -25,7 +27,7 @@ use function is_object;
* Class Data
* @package Grav\Common\Data
*/
class Data implements DataInterface, \ArrayAccess, \Countable, \JsonSerializable, ExportInterface
class Data implements DataInterface, ArrayAccess, \Countable, JsonSerializable, ExportInterface
{
use NestedArrayAccessWithGetters, Countable, Export;

View File

@@ -27,6 +27,7 @@ use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function strlen;
/**
* Class Validation
@@ -63,7 +64,7 @@ class Validation
$name = ucfirst($field['label'] ?? $field['name']);
$message = (string) isset($field['validate']['message'])
? $language->translate($field['validate']['message'])
: $language->translate('GRAV.FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"';
: $language->translate('GRAV.FORM.INVALID_INPUT') . ' "' . $language->translate($name) . '"';
// Validate type with fallback type text.
@@ -238,16 +239,16 @@ class Validation
$value = trim($value);
}
if (isset($params['min']) && \strlen($value) < $params['min']) {
if (isset($params['min']) && strlen($value) < $params['min']) {
return false;
}
if (isset($params['max']) && \strlen($value) > $params['max']) {
if (isset($params['max']) && strlen($value) > $params['max']) {
return false;
}
$min = $params['min'] ?? 0;
if (isset($params['step']) && (\strlen($value) - $min) % $params['step'] === 0) {
if (isset($params['step']) && (strlen($value) - $min) % $params['step'] === 0) {
return false;
}

View File

@@ -10,12 +10,13 @@
namespace Grav\Common\Data;
use Grav\Common\Grav;
use RuntimeException;
/**
* Class ValidationException
* @package Grav\Common\Data
*/
class ValidationException extends \RuntimeException
class ValidationException extends RuntimeException
{
/** @var array */
protected $messages = [];

View File

@@ -36,12 +36,14 @@ use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionObject;
use SplFileInfo;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Throwable;
use Twig\Environment;
use Twig\Template;
use Twig\TemplateWrapper;
use function array_slice;
use function call_user_func;
use function count;
use function define;
use function defined;
@@ -743,18 +745,19 @@ class Debugger
}
if ($this->clockwork) {
$context = $isString;
if (!is_scalar($message)) {
$isString = $message;
$message = '';
$context = $message;
$message = gettype($context);
}
if (is_bool($isString)) {
$isString = [];
if (is_bool($context)) {
$context = [];
} elseif (!is_array($context)) {
$type = gettype($context);
$context = [$type => $context];
}
if (!is_array($isString)) {
$type = gettype($isString);
$isString = [$type => $isString];
}
$this->clockwork->log($label, $message, $isString);
$this->clockwork->log($label, $message, $context);
}
}
@@ -836,7 +839,7 @@ class Debugger
{
if ($errno !== E_USER_DEPRECATED && $errno !== E_DEPRECATED) {
if ($this->errorHandler) {
return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
return call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
}
return true;
@@ -869,7 +872,7 @@ class Debugger
foreach ($backtrace as $current) {
if (isset($current['args'])) {
foreach ($current['args'] as $arg) {
if ($arg instanceof \SplFileInfo) {
if ($arg instanceof SplFileInfo) {
$arg = $arg->getPathname();
}
if (is_string($arg) && preg_match('/.+\.(yaml|md)$/i', $arg)) {

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Errors;
use ErrorException;
use InvalidArgumentException;
use RuntimeException;
use Whoops\Handler\Handler;
@@ -49,7 +50,7 @@ class SimplePageHandler extends Handler
}
$message = $inspector->getException()->getMessage();
if ($inspector->getException() instanceof \ErrorException) {
if ($inspector->getException() instanceof ErrorException) {
$code = Misc::translateErrorCode($code);
}

View File

@@ -18,6 +18,8 @@ use Grav\Framework\Flex\FlexCollection;
/**
* Class GenericCollection
* @package Grav\Common\Flex\Generic
*
* @extends FLexCollection<string,GenericObject>
*/
class GenericCollection extends FlexCollection
{

View File

@@ -18,6 +18,9 @@ use Grav\Framework\Flex\FlexIndex;
/**
* Class GenericIndex
* @package Grav\Common\Flex\Generic
*
* @extends FLexIndex<string,GenericObject,GenericCollection>
* @mixin GenericCollection
*/
class GenericIndex extends FlexIndex
{

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Grav\Common\Flex\Types\Pages;
use Exception;
use Grav\Common\Flex\Traits\FlexCollectionTrait;
use Grav\Common\Flex\Traits\FlexGravTrait;
use Grav\Common\Grav;
@@ -34,6 +35,8 @@ use function is_string;
* Class GravPageCollection
* @package Grav\Plugin\FlexObjects\Types\GravPages
*
* @extends FlexPageCollection<string,PageObject>
*
* Incompatibilities with Grav\Common\Page\Collection:
* $page = $collection->key() will not work at all
* $clone = clone $collection does not clone objects inside the collection, does it matter?
@@ -43,6 +46,12 @@ use function is_string;
* $collection->filter() incompatible method signature (takes closure instead of callable)
* $collection->prev() does not rewind the internal pointer
* AND most methods are immutable; they do not update the current collection, but return updated one
*
* @method static shuffle()
* @method static select(array $keys)
* @method static unselect(array $keys)
* @method static createFrom(array $elements, string $keyField = null)
* @method PageIndex getIndex()
*/
class PageCollection extends FlexPageCollection implements PageCollectionInterface
{
@@ -100,7 +109,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
}
/**
* @return PageInterface|FlexObjectInterface
* @return PageObject
*/
public function getRoot()
{
@@ -187,6 +196,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* Return previous item.
*
* @return PageInterface|false
* @phpstan-return PageObject|false
*/
public function prev()
{
@@ -201,6 +211,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* Return nth item.
* @param int $key
* @return PageInterface|bool
* @phpstan-return PageObject|false
*/
public function nth($key)
{
@@ -253,7 +264,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* @param string $by
* @param string $dir
* @param array|null $manual
* @param string|null $sort_flags
* @param int|null $sort_flags
* @return static
*/
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
@@ -422,7 +433,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* @param string|false $endDate
* @param string|null $field
* @return static
* @throws \Exception
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
{
@@ -761,7 +772,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
* Get the extended version of this Collection with each page keyed by route
*
* @return array
* @throws \Exception
* @throws Exception
*/
public function toExtendedArray(): array
{
@@ -771,6 +782,7 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
$entries[$object->route()] = $object->toArray();
}
}
return $entries;
}

View File

@@ -26,6 +26,7 @@ use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexStorageInterface;
use Grav\Framework\Flex\Pages\FlexPageIndex;
use InvalidArgumentException;
use RuntimeException;
use function is_array;
use function is_string;
@@ -34,6 +35,9 @@ use function is_string;
* Class GravPageObject
* @package Grav\Plugin\FlexObjects\Types\GravPages
*
* @extends FlexPageIndex<string,PageObject,PageCollection>
* @mixin PageCollection
*
* @method PageIndex withModules(bool $bool = true)
* @method PageIndex withPages(bool $bool = true)
* @method PageIndex withTranslation(bool $bool = true, string $languageCode = null, bool $fallback = null)
@@ -205,7 +209,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
*
* @param array $filters
* @param bool $recursive
* @return FlexCollectionInterface
* @return static
*/
public function filterBy(array $filters, bool $recursive = false)
{
@@ -261,7 +265,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* @param array $filters
* @return FlexCollectionInterface
* @return static
*/
protected function filterByParent(array $filters)
{
@@ -590,7 +594,6 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Add a single page to a collection
*
* @param PageInterface $page
*
* @return PageCollection
*/
public function addPage(PageInterface $page)
@@ -614,7 +617,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Merge another collection with the current collection
*
* @param PageCollectionInterface $collection
* @return $this
* @return PageCollection
*/
public function merge(PageCollectionInterface $collection)
{
@@ -626,7 +629,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Intersect another collection with the current collection
*
* @param PageCollectionInterface $collection
* @return $this
* @return PageCollection
*/
public function intersect(PageCollectionInterface $collection)
{
@@ -637,7 +640,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Split collection into array of smaller collections.
*
* @param int $size
* @return PageCollectionInterface[]
* @return PageCollection[]
*/
public function batch($size)
{
@@ -650,7 +653,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @param PageInterface|string|null $key
*
* @return $this
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
public function remove($key = null)
{
@@ -664,8 +667,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @param string $dir
* @param array $manual
* @param string $sort_flags
*
* @return PageCollectionInterface
* @return static
*/
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
{
@@ -679,7 +681,6 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Check to see if this item is the first in the collection.
*
* @param string $path
*
* @return bool True if item is first.
*/
public function isFirst($path): bool
@@ -695,7 +696,6 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Check to see if this item is the last in the collection.
*
* @param string $path
*
* @return bool True if item is last.
*/
public function isLast($path): bool
@@ -710,12 +710,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Gets the previous sibling based on current position.
*
* @param string $path
*
* @return PageInterface|null The previous item.
* @return PageObject|null The previous item.
*/
public function prevSibling($path)
{
/** @var PageInterface|null $result */
/** @var PageObject|null $result */
$result = $this->__call('prevSibling', [$path]);
return $result;
@@ -725,12 +724,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Gets the next sibling based on current position.
*
* @param string $path
*
* @return PageInterface|null The next item.
* @return PageObject|null The next item.
*/
public function nextSibling($path)
{
/** @var PageInterface|null $result */
/** @var PageObject|null $result */
$result = $this->__call('nextSibling', [$path]);
return $result;
@@ -741,12 +739,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
*
* @param string $path
* @param int $direction either -1 or +1
*
* @return PageInterface|false The sibling item.
* @return PageObject|false The sibling item.
*/
public function adjacentSibling($path, $direction = 1)
{
/** @var PageInterface|false $result */
/** @var PageObject|false $result */
$result = $this->__call('adjacentSibling', [$path, $direction]);
return $result;
@@ -756,7 +753,6 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Returns the item in the current position.
*
* @param string $path the path the item
*
* @return int|null The index of the current page, null if not found.
*/
public function currentPosition($path): ?int
@@ -776,13 +772,11 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* @param string $startDate
* @param bool $endDate
* @param string|null $field
*
* @return PageCollectionInterface
* @throws \Exception
* @return static
* @throws Exception
*/
public function dateRange($startDate, $endDate = false, $field = null)
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('dateRange', [$startDate, $endDate, $field]);
return $collection;
@@ -802,11 +796,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only visible pages
*
* @return PageCollectionInterface The collection with only visible pages
* @return static The collection with only visible pages
*/
public function visible()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('visible', []);
return $collection;
@@ -815,11 +808,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only non-visible pages
*
* @return PageCollectionInterface The collection with only non-visible pages
* @return static The collection with only non-visible pages
*/
public function nonVisible()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('nonVisible', []);
return $collection;
@@ -828,11 +820,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only non-modular pages
*
* @return PageCollectionInterface The collection with only non-modular pages
* @return static The collection with only non-modular pages
*/
public function pages()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('pages', []);
return $collection;
@@ -841,11 +832,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only modular pages
*
* @return PageCollectionInterface The collection with only modular pages
* @return static The collection with only modular pages
*/
public function modules()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('modules', []);
return $collection;
@@ -854,7 +844,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only modular pages
*
* @return PageCollectionInterface The collection with only modular pages
* @return static The collection with only modular pages
*/
public function modular()
{
@@ -864,7 +854,7 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only non-modular pages
*
* @return PageCollectionInterface The collection with only non-modular pages
* @return static The collection with only non-modular pages
*/
public function nonModular()
{
@@ -874,11 +864,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only published pages
*
* @return PageCollectionInterface The collection with only published pages
* @return static The collection with only published pages
*/
public function published()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('published', []);
return $collection;
@@ -887,11 +876,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only non-published pages
*
* @return PageCollectionInterface The collection with only non-published pages
* @return static The collection with only non-published pages
*/
public function nonPublished()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('nonPublished', []);
return $collection;
@@ -900,11 +888,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only routable pages
*
* @return PageCollectionInterface The collection with only routable pages
* @return static The collection with only routable pages
*/
public function routable()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('routable', []);
return $collection;
@@ -913,11 +900,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
/**
* Creates new collection with only non-routable pages
*
* @return PageCollectionInterface The collection with only non-routable pages
* @return static The collection with only non-routable pages
*/
public function nonRoutable()
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('nonRoutable', []);
return $collection;
@@ -927,12 +913,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Creates new collection with only pages of the specified type
*
* @param string $type
*
* @return PageCollectionInterface The collection
* @return static The collection
*/
public function ofType($type)
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('ofType', []);
return $collection;
@@ -942,12 +926,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Creates new collection with only pages of one of the specified types
*
* @param string[] $types
*
* @return PageCollectionInterface The collection
* @return static The collection
*/
public function ofOneOfTheseTypes($types)
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('ofOneOfTheseTypes', []);
return $collection;
@@ -957,12 +939,10 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
* Creates new collection with only pages of one of the specified access levels
*
* @param array $accessLevels
*
* @return PageCollectionInterface The collection
* @return static The collection
*/
public function ofOneOfTheseAccessLevels($accessLevels)
{
/** @var PageCollectionInterface $collection */
$collection = $this->__call('ofOneOfTheseAccessLevels', []);
return $collection;

View File

@@ -96,11 +96,16 @@ class PageObject extends FlexPageObject
/**
* @param string|array $query
* @return Route
* @return Route|null
*/
public function getRoute($query = []): Route
public function getRoute($query = []): ?Route
{
$route = RouteFactory::createFromString($this->route());
$route = $this->route();
if (null === $route) {
return null;
}
$route = RouteFactory::createFromString($route);
if ($lang = $route->getLanguage()) {
$grav = Grav::instance();
if (!$grav['config']->get('system.languages.include_default_lang')) {
@@ -295,7 +300,6 @@ class PageObject extends FlexPageObject
* You need to call $this->save() in order to perform the move.
*
* @param PageInterface $parent New parent page.
*
* @return $this
*/
public function move(PageInterface $parent)
@@ -316,6 +320,13 @@ 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;
$order = !$isMoved ? $this->order() : false;
$parent = $this->parent();
if (!$parent) {
throw new RuntimeException('Cannot reorder a page which has no parent');
@@ -327,13 +338,6 @@ class PageObject extends FlexPageObject
/** @var PageCollection|null $siblings */
$siblings = $siblings->getCollection()->withOrdered()->orderBy(['order' => 'ASC']);
$storageKey = $this->getMasterKey();
$filesystem = Filesystem::getInstance(false);
$oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
$newParentKey = $this->getProperty('parent_key');
$isMoved = $oldParentKey !== $newParentKey;
$order = !$isMoved ? $this->order() : false;
if ($storageKey !== null) {
if ($order !== false) {
// Add current page back to the list if it's ordered.
@@ -366,7 +370,7 @@ class PageObject extends FlexPageObject
$siblings->removeElement($this);
// If menu item was moved, just make it to be the last in order.
if ($isMoved && $order !== false) {
if ($isMoved && $this->order() !== false) {
$parentKey = $this->getProperty('parent_key');
$newParent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
$newSiblings = $newParent->children()->getCollection()->withOrdered();
@@ -439,8 +443,26 @@ class PageObject extends FlexPageObject
public function getLevelListing(array $options): array
{
$index = $this->getFlexDirectory()->getIndex();
if (!is_callable([$index, 'getLevelListing'])) {
return [];
}
return method_exists($index, 'getLevelListing') ? $index->getLevelListing($options) : [];
// Deal with relative paths.
$initial = $options['initial'] ?? null;
$var = $initial ? 'leaf_route' : 'route';
$route = $options[$var] ?? '';
if ($route !== '' && !str_starts_with($route, '/')) {
$filesystem = Filesystem::getInstance();
$route = "/{$this->getKey()}/{$route}";
$route = $filesystem->normalize($route);
$options[$var] = $route;
}
[$status, $message, $response,] = $index->getLevelListing($options);
return [$status, $message, $response, $options[$var] ?? null];
}
/**
@@ -591,8 +613,8 @@ class PageObject extends FlexPageObject
'storage_key' => $this->getStorageKey(),
'parent_key' => $this->getProperty('parent_key'),
'order' => $this->getProperty('order'),
'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder')),
'template' => preg_replace('|modular/|', '', $this->getProperty('template')),
'folder' => preg_replace('|^\d+\.|', '', $this->getProperty('folder') ?? ''),
'template' => preg_replace('|modular/|', '', $this->getProperty('template') ?? ''),
'lang' => $newLang
] + parent::prepareStorage();

View File

@@ -192,9 +192,6 @@ trait PageLegacyTrait
throw new InvalidArgumentException('Argument should be either header variable name or array of parameters');
}
if (!$pagination) {
$params['pagination'] = false;
}
$context = [
'pagination' => $pagination,
'self' => $this

View File

@@ -16,6 +16,7 @@ use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use SplFileInfo;
/**
* Implements PageTranslateInterface
@@ -75,7 +76,7 @@ trait PageTranslateTrait
// FIXME: use flex, also rawRoute() does not fully work?
$aPage = new Page();
$aPage->init(new \SplFileInfo($path), $languageExtension);
$aPage->init(new SplFileInfo($path), $languageExtension);
if ($onlyPublished && !$aPage->published()) {
continue;
}

View File

@@ -18,6 +18,8 @@ use Grav\Framework\Flex\FlexCollection;
/**
* Class UserGroupCollection
* @package Grav\Common\Flex\Types\UserGroups
*
* @extends FlexCollection<string,UserGroupObject>
*/
class UserGroupCollection extends FlexCollection
{

View File

@@ -19,7 +19,8 @@ use Grav\Framework\Flex\FlexIndex;
* Class GroupIndex
* @package Grav\Common\User\FlexUser
*
* @method bool|null authorize(string $action, string $scope = null)
* @extends FlexIndex<string,UserGroupObject,UserGroupCollection>
* @mixin UserGroupCollection
*/
class UserGroupIndex extends FlexIndex
{

View File

@@ -13,6 +13,7 @@ namespace Grav\Common\Flex\Types\Users\Traits;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\StaticImageMedium;
use function count;
/**
* Trait UserObjectLegacyTrait
@@ -87,6 +88,6 @@ trait UserObjectLegacyTrait
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED);
return \count($this->jsonSerialize());
return count($this->jsonSerialize());
}
}

View File

@@ -21,6 +21,8 @@ use function is_string;
/**
* Class UserCollection
* @package Grav\Common\Flex\Types\Users
*
* @extends FlexCollection<string,UserObject>
*/
class UserCollection extends FlexCollection implements UserCollectionInterface
{

View File

@@ -27,6 +27,9 @@ use function method_exists;
/**
* Class UserIndex
* @package Grav\Common\Flex\Types\Users
*
* @extends FlexIndex<string,UserObject,UserCollection>
* @mixin UserCollection
*/
class UserIndex extends FlexIndex
{

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Grav\Common\Flex\Types\Users;
use Countable;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
use Grav\Common\Flex\Traits\FlexGravTrait;
@@ -65,7 +66,7 @@ use function is_object;
* @property bool $authenticated
* @property bool $authorized
*/
class UserObject extends FlexObject implements UserInterface, \Countable
class UserObject extends FlexObject implements UserInterface, Countable
{
use FlexGravTrait;
use FlexObjectTrait;

View File

@@ -17,6 +17,7 @@ use Grav\Common\Iterator;
use Grav\Common\Utils;
use RocketTheme\Toolbox\File\YamlFile;
use RuntimeException;
use stdClass;
use function array_key_exists;
use function count;
use function in_array;
@@ -485,7 +486,7 @@ class GPM extends Iterator
*
* @param string $search Can be either the slug or the name
* @param bool $ignore_exception True if should not fire an exception (for use in Twig)
* @return Remote\Package|bool Package if found, FALSE if not
* @return Remote\Package|false Package if found, FALSE if not
*/
public function findPackage($search, $ignore_exception = false)
{
@@ -728,7 +729,7 @@ class GPM extends Iterator
$type = 'plugins';
}
$not_found = new \stdClass();
$not_found = new stdClass();
$not_found->name = $inflector::camelize($search);
$not_found->slug = $search;
$not_found->package_type = $type;

View File

@@ -12,6 +12,7 @@ namespace Grav\Common\GPM;
use DirectoryIterator;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use RuntimeException;
use ZipArchive;
use function count;
use function in_array;
@@ -274,7 +275,7 @@ class Installer
public static function copyInstall($source_path, $install_path)
{
if (empty($source_path)) {
throw new \RuntimeException("Directory $source_path is missing");
throw new RuntimeException("Directory $source_path is missing");
}
Folder::rcopy($source_path, $install_path);

View File

@@ -17,7 +17,9 @@ 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;
/**
@@ -40,7 +42,7 @@ class Response
* @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 \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
* @throws TransportExceptionInterface
*/
public static function get($uri = '', $overrides = [], $callback = null)
{
@@ -57,7 +59,7 @@ class Response
}
$config = Grav::instance()['config'];
$referer = \defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true);
$referer = defined('GRAV_CLI') ? 'grav_cli' : Grav::instance()['uri']->rootUrl(true);
$options = new HttpOptions();
// Set default Headers

View File

@@ -29,10 +29,26 @@ use Grav\Common\Processors\TasksProcessor;
use Grav\Common\Processors\ThemesProcessor;
use Grav\Common\Processors\TwigProcessor;
use Grav\Common\Scheduler\Scheduler;
use Grav\Common\Service\AccountsServiceProvider;
use Grav\Common\Service\AssetsServiceProvider;
use Grav\Common\Service\BackupsServiceProvider;
use Grav\Common\Service\ConfigServiceProvider;
use Grav\Common\Service\ErrorServiceProvider;
use Grav\Common\Service\FilesystemServiceProvider;
use Grav\Common\Service\FlexServiceProvider;
use Grav\Common\Service\InflectorServiceProvider;
use Grav\Common\Service\LoggerServiceProvider;
use Grav\Common\Service\OutputServiceProvider;
use Grav\Common\Service\PagesServiceProvider;
use Grav\Common\Service\RequestServiceProvider;
use Grav\Common\Service\SessionServiceProvider;
use Grav\Common\Service\StreamsServiceProvider;
use Grav\Common\Service\TaskServiceProvider;
use Grav\Common\Twig\Twig;
use Grav\Framework\DI\Container;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\RequestHandler;
use Grav\Framework\Session\Messages;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
@@ -40,6 +56,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use function array_key_exists;
use function call_user_func_array;
use function function_exists;
use function get_class;
use function in_array;
use function is_callable;
@@ -64,21 +81,21 @@ class Grav extends Container
* to the dependency injection container.
*/
protected static $diMap = [
'Grav\Common\Service\AccountsServiceProvider',
'Grav\Common\Service\AssetsServiceProvider',
'Grav\Common\Service\BackupsServiceProvider',
'Grav\Common\Service\ConfigServiceProvider',
'Grav\Common\Service\ErrorServiceProvider',
'Grav\Common\Service\FilesystemServiceProvider',
'Grav\Common\Service\FlexServiceProvider',
'Grav\Common\Service\InflectorServiceProvider',
'Grav\Common\Service\LoggerServiceProvider',
'Grav\Common\Service\OutputServiceProvider',
'Grav\Common\Service\PagesServiceProvider',
'Grav\Common\Service\RequestServiceProvider',
'Grav\Common\Service\SessionServiceProvider',
'Grav\Common\Service\StreamsServiceProvider',
'Grav\Common\Service\TaskServiceProvider',
AccountsServiceProvider::class,
AssetsServiceProvider::class,
BackupsServiceProvider::class,
ConfigServiceProvider::class,
ErrorServiceProvider::class,
FilesystemServiceProvider::class,
FlexServiceProvider::class,
InflectorServiceProvider::class,
LoggerServiceProvider::class,
OutputServiceProvider::class,
PagesServiceProvider::class,
RequestServiceProvider::class,
SessionServiceProvider::class,
StreamsServiceProvider::class,
TaskServiceProvider::class,
'browser' => Browser::class,
'cache' => Cache::class,
'events' => EventDispatcher::class,
@@ -145,6 +162,11 @@ class Grav extends Container
return self::$instance;
}
public function isSetup(): bool
{
return isset($this->initialized['setup']);
}
/**
* Setup Grav instance using specific environment.
*
@@ -252,7 +274,7 @@ class Grav extends Container
);
$default = static function () {
return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-cache, no-store, must-revalidate'], 'Not Found');
return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-store, max-age=0'], 'Not Found');
};
$collection = new RequestHandler($this->middleware, $default, $container);
@@ -260,17 +282,28 @@ class Grav extends Container
$response = $collection->handle($this['request']);
$body = $response->getBody();
/** @var Messages $messages */
$messages = $this['messages'];
// Prevent caching if session messages were displayed in the page.
$noCache = $messages->isCleared();
if ($noCache) {
$response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
}
// Handle ETag and If-None-Match headers.
if ($response->getHeaderLine('ETag') === '1') {
$etag = md5($body);
$response = $response->withHeader('ETag', $etag);
$response = $response->withHeader('ETag', '"' . $etag . '"');
if ($this['request']->getHeaderLine('If-None-Match') === $etag) {
$search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
if ($noCache === false && $search === $etag) {
$response = $response->withStatus(304);
$body = '';
}
}
// Echo page content.
$this->header($response);
echo $body;
@@ -312,17 +345,28 @@ class Grav extends Container
$body = $response->getBody();
/** @var Messages $messages */
$messages = $this['messages'];
// Prevent caching if session messages were displayed in the page.
$noCache = $messages->isCleared();
if ($noCache) {
$response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
}
// Handle ETag and If-None-Match headers.
if ($response->getHeaderLine('ETag') === '1') {
$etag = md5($body);
$response = $response->withHeader('ETag', $etag);
$response = $response->withHeader('ETag', '"' . $etag . '"');
if ($request->getHeaderLine('If-None-Match') === $etag) {
$search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
if ($noCache === false && $search === $etag) {
$response = $response->withStatus(304);
$body = '';
}
}
// Echo page content.
$this->header($response);
echo $body;
exit();
@@ -507,7 +551,7 @@ class Grav extends Container
public function shutdown()
{
// Prevent user abort allowing onShutdown event to run without interruptions.
if (\function_exists('ignore_user_abort')) {
if (function_exists('ignore_user_abort')) {
@ignore_user_abort(true);
}
@@ -523,7 +567,7 @@ class Grav extends Container
// the connection to the client open. This will make page loads to feel much faster.
// FastCGI allows us to flush all response data to the client and finish the request.
$success = \function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
$success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
if (!$success) {
// Unfortunately without FastCGI there is no way to force close the connection.
// We need to ask browser to close the connection for us.

View File

@@ -139,7 +139,7 @@ class LogViewer
}
return array(
'date' => \DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
'date' => DateTime::createFromFormat('Y-m-d H:i:s', $data['date']),
'logger' => $data['logger'],
'level' => $data['level'],
'message' => $data['message'],

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Helpers;
use Exception;
use Grav\Common\Grav;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
@@ -93,7 +94,7 @@ class YamlLinter
foreach ($iterator as $filepath => $file) {
try {
Yaml::parse(static::extractYaml($filepath));
} catch (\Exception $e) {
} catch (Exception $e) {
$lint_errors[str_replace(GRAV_ROOT, '', $filepath)] = $e->getMessage();
}
}

View File

@@ -13,6 +13,7 @@ use DateInterval;
use DateTime;
use Grav\Common\Language\Language;
use function in_array;
use function is_array;
use function strlen;
/**
@@ -20,15 +21,17 @@ use function strlen;
*/
class Inflector
{
/** @var array */
/** @var bool */
protected static $initialized = false;
/** @var array|null */
protected static $plural;
/** @var array */
/** @var array|null */
protected static $singular;
/** @var array */
/** @var array|null */
protected static $uncountable;
/** @var array */
/** @var array|null */
protected static $irregular;
/** @var array */
/** @var array|null */
protected static $ordinals;
/**
@@ -36,14 +39,17 @@ class Inflector
*/
public static function init()
{
if (empty(static::$plural)) {
if (!static::$initialized) {
static::$initialized = true;
/** @var Language $language */
$language = Grav::instance()['language'];
static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true);
static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true);
static::$uncountable = $language->translate('GRAV.INFLECTOR_UNCOUNTABLE', null, true);
static::$irregular = $language->translate('GRAV.INFLECTOR_IRREGULAR', null, true);
static::$ordinals = $language->translate('GRAV.INFLECTOR_ORDINALS', null, true);
if (!$language->isDebug()) {
static::$plural = $language->translate('GRAV.INFLECTOR_PLURALS', null, true);
static::$singular = $language->translate('GRAV.INFLECTOR_SINGULAR', null, true);
static::$uncountable = $language->translate('GRAV.INFLECTOR_UNCOUNTABLE', null, true);
static::$irregular = $language->translate('GRAV.INFLECTOR_IRREGULAR', null, true);
static::$ordinals = $language->translate('GRAV.INFLECTOR_ORDINALS', null, true);
}
}
}
@@ -64,21 +70,27 @@ class Inflector
$lowercased_word = strtolower($word);
foreach (static::$uncountable as $_uncountable) {
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
return $word;
if (is_array(static::$uncountable)) {
foreach (static::$uncountable as $_uncountable) {
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
return $word;
}
}
}
foreach (static::$irregular as $_plural => $_singular) {
if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) {
return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word);
if (is_array(static::$irregular)) {
foreach (static::$irregular as $_plural => $_singular) {
if (preg_match('/(' . $_plural . ')$/i', $word, $arr)) {
return preg_replace('/(' . $_plural . ')$/i', substr($arr[0], 0, 1) . substr($_singular, 1), $word);
}
}
}
foreach (static::$plural as $rule => $replacement) {
if (preg_match($rule, $word)) {
return preg_replace($rule, $replacement, $word);
if (is_array(static::$plural)) {
foreach (static::$plural as $rule => $replacement) {
if (preg_match($rule, $word)) {
return preg_replace($rule, $replacement, $word);
}
}
}
@@ -102,21 +114,28 @@ class Inflector
}
$lowercased_word = strtolower($word);
foreach (static::$uncountable as $_uncountable) {
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
return $word;
if (is_array(static::$uncountable)) {
foreach (static::$uncountable as $_uncountable) {
if (substr($lowercased_word, -1 * strlen($_uncountable)) === $_uncountable) {
return $word;
}
}
}
foreach (static::$irregular as $_plural => $_singular) {
if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) {
return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word);
if (is_array(static::$irregular)) {
foreach (static::$irregular as $_plural => $_singular) {
if (preg_match('/(' . $_singular . ')$/i', $word, $arr)) {
return preg_replace('/(' . $_singular . ')$/i', substr($arr[0], 0, 1) . substr($_plural, 1), $word);
}
}
}
foreach (static::$singular as $rule => $replacement) {
if (preg_match($rule, $word)) {
return preg_replace($rule, $replacement, $word);
if (is_array(static::$singular)) {
foreach (static::$singular as $rule => $replacement) {
if (preg_match($rule, $word)) {
return preg_replace($rule, $replacement, $word);
}
}
}
@@ -291,6 +310,10 @@ class Inflector
*/
public static function ordinalize($number)
{
if (!is_array(static::$ordinals)) {
return (string)$number;
}
static::init();
if (in_array($number % 100, range(11, 13), true)) {

View File

@@ -106,6 +106,16 @@ class Language
return $this->enabled;
}
/**
* Returns true if language debugging is turned on.
*
* @return bool
*/
public function isDebug(): bool
{
return !$this->config->get('system.languages.translations', true);
}
/**
* Gets the array of supported languages
*
@@ -488,7 +498,7 @@ class Language
$args = [];
}
if ($this->config->get('system.languages.translations', true)) {
if (!$this->isDebug()) {
if ($lookup && $this->enabled() && empty($languages)) {
$languages = $this->getTranslatedLanguages();
}
@@ -507,7 +517,7 @@ class Language
}
}
} elseif ($array_support) {
return [];
return [$lookup];
}
if ($html_out) {
@@ -528,18 +538,20 @@ class Language
*/
public function translateArray($key, $index, $languages = null, $html_out = false)
{
if ($this->config->get('system.languages.translations', true)) {
if ($this->enabled() && $key && empty($languages)) {
$languages = $this->getTranslatedLanguages();
}
if ($this->isDebug()) {
return $key . '[' . $index . ']';
}
$languages = $languages ?: ['en'];
if ($key && empty($languages) && $this->enabled()) {
$languages = $this->getTranslatedLanguages();
}
foreach ((array)$languages as $lang) {
$translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
if ($translation_array && array_key_exists($index, $translation_array)) {
return $translation_array[$index];
}
$languages = $languages ?: ['en'];
foreach ((array)$languages as $lang) {
$translation_array = (array)Grav::instance()['languages']->get($lang . '.' . $key, null);
if ($translation_array && array_key_exists($index, $translation_array)) {
return $translation_array[$index];
}
}
@@ -560,6 +572,10 @@ class Language
*/
public function getTranslation($lang, $key, $array_support = false)
{
if ($this->isDebug()) {
return $key;
}
$translation = Grav::instance()['languages']->get($lang . '.' . $key, null);
if (!$array_support && is_array($translation)) {
return (string)array_shift($translation);

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Media\Interfaces;
use ArrayAccess;
use Grav\Common\Data\Data;
/**
@@ -16,7 +17,7 @@ use Grav\Common\Data\Data;
*
* @property string $type
*/
interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObjectInterface, \ArrayAccess
interface MediaObjectInterface extends \Grav\Framework\Media\Interfaces\MediaObjectInterface, ArrayAccess
{
/**
* Create a copy of this media object

View File

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

View File

@@ -455,6 +455,48 @@ class Collection extends Iterator implements PageCollectionInterface
return $this;
}
/**
* Creates new collection with only translated pages
*
* @return Collection The collection with only published pages
* @internal
*/
public function translated()
{
$published = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
if ($page !== null && $page->translated()) {
$published[$path] = $slug;
}
}
$this->items = $published;
return $this;
}
/**
* Creates new collection with only untranslated pages
*
* @return Collection The collection with only non-published pages
* @internal
*/
public function nonTranslated()
{
$published = [];
foreach ($this->items as $path => $slug) {
$page = $this->pages->get($path);
if ($page !== null && !$page->translated()) {
$published[$path] = $slug;
}
}
$this->items = $published;
return $this;
}
/**
* Creates new collection with only published pages
*

View File

@@ -12,6 +12,7 @@ namespace Grav\Common\Page\Interfaces;
use ArrayAccess;
use Countable;
use Exception;
use InvalidArgumentException;
use Serializable;
use Traversable;
@@ -91,7 +92,7 @@ interface PageCollectionInterface extends Traversable, ArrayAccess, Countable, S
*
* @param PageInterface|string|null $key
* @return PageCollectionInterface
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
//public function remove($key = null);

View File

@@ -196,7 +196,7 @@ class ImageFile extends Image
}
try {
$exif = exif_read_data($filepath);
$exif = @exif_read_data($filepath);
} catch (Exception $e) {
Grav::instance()['log']->error($filepath . ' - ' . $e->getMessage());
return $this;

View File

@@ -27,9 +27,11 @@ use Grav\Common\Page\Traits\PageFormTrait;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Yaml;
use Grav\Framework\Flex\Flex;
use InvalidArgumentException;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile;
use RuntimeException;
use SplFileInfo;
use function dirname;
use function in_array;
@@ -51,6 +53,8 @@ class Page implements PageInterface
/** @var string|null Filename. Leave as null if page is folder. */
protected $name;
/** @var bool */
protected $initialized = false;
/** @var string */
protected $folder;
/** @var string */
@@ -181,6 +185,8 @@ class Page implements PageInterface
{
$config = Grav::instance()['config'];
$this->initialized = true;
// some extension logic
if (empty($extension)) {
$this->extension('.' . $file->getExtension());
@@ -730,13 +736,13 @@ class Page implements PageInterface
);
$twig_first = $this->header->twig_first ?? $config->get(
'system.pages.twig_first',
true
false
);
// never cache twig means it's always run after content
$never_cache_twig = $this->header->never_cache_twig ?? $config->get(
'system.pages.never_cache_twig',
false
true
);
// if no cached-content run everything
@@ -1046,6 +1052,15 @@ class Page implements PageInterface
return $this->raw_content;
}
/**
* @return bool
* @internal
*/
public function translated(): bool
{
return $this->initialized;
}
/**
* Get file object to the page.
*
@@ -1083,6 +1098,14 @@ class Page implements PageInterface
$this->doReorder($reorder);
}
// We need to signal Flex Pages about the change.
/** @var Flex|null $flex */
$flex = Grav::instance()['flex'] ?? null;
$directory = $flex ? $flex->getDirectory('pages') : null;
if (null !== $directory) {
$directory->clearCache();
}
$this->_original = null;
}
@@ -1104,10 +1127,10 @@ class Page implements PageInterface
$this->_action = 'move';
if ($this->route() === $parent->route()) {
throw new \RuntimeException('Failed: Cannot set page parent to self');
throw new RuntimeException('Failed: Cannot set page parent to self');
}
if (Utils::startsWith($parent->rawRoute(), $this->rawRoute())) {
throw new \RuntimeException('Failed: Cannot set page parent to a child of current page');
throw new RuntimeException('Failed: Cannot set page parent to a child of current page');
}
$this->parent($parent);
@@ -2587,9 +2610,7 @@ class Page implements PageInterface
throw new InvalidArgumentException('Argument should be either header variable name or array of parameters');
}
if (!$pagination) {
$params['pagination'] = false;
}
$params['filter'] = ($params['filter'] ?? []) + ['translated' => true];
$context = [
'pagination' => $pagination,
'self' => $this

View File

@@ -34,6 +34,7 @@ use Grav\Plugin\Admin;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
use SplFileInfo;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Whoops\Exception\ErrorException;
use Collator;
@@ -421,10 +422,10 @@ class Pages
}
$pagination = $params['pagination'] ?? $context['pagination'];
if ($pagination && !isset($params['page'])) {
if ($pagination && !isset($params['page'], $params['start'])) {
/** @var Uri $uri */
$uri = $this->grav['uri'];
$context['pagination_page'] = $uri->currentPage();
$context['current_page'] = $uri->currentPage();
}
$collection = $this->evaluate($params['items'], $context['self']);
@@ -464,6 +465,13 @@ class Pages
}
switch ($type) {
case 'translated':
if ($filter) {
$collection = $collection->translated();
} else {
$collection = $collection->nonTranslated();
}
break;
case 'published':
if ($filter) {
$collection = $collection->published();
@@ -537,18 +545,20 @@ class Pages
// New Custom event to handle things like pagination.
if ($context['event']) {
$this->grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection]));
$this->grav->fireEvent('onCollectionProcessed', new Event(['collection' => $collection, 'context' => $context]));
}
// Slice and dice the collection if pagination is required
if ($pagination) {
if ($context['pagination']) {
// Slice and dice the collection if pagination is required
$params = $collection->params();
$limit = (int)($params['limit'] ?? 0);
$start = !empty($params['pagination']) ? (($params['page'] ?? $context['pagination_page']) - 1) * $limit : 0;
$page = (int)($params['page'] ?? $context['current_page'] ?? 0);
$start = (int)($params['start'] ?? 0);
$start = $limit > 0 && $page > 0 ? ($page - 1) * $limit : max(0, $start);
if ($limit && $collection->count() > $limit) {
$collection->slice($start, $limit);
if ($start || ($limit && $collection->count() > $limit)) {
$collection->slice($start, $limit ?: null);
}
}
@@ -1344,7 +1354,7 @@ class Pages
/** @var PageInterface $page */
$page = $event['page'];
$page->init(new \SplFileInfo('plugin://admin/pages/admin/error.md'));
$page->init(new SplFileInfo('plugin://admin/pages/admin/error.md'));
$page->routable(true);
$header = $page->header();
$header->title = 'Please install missing plugin';

View File

@@ -9,6 +9,7 @@
namespace Grav\Common;
use ArrayAccess;
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Data;
use Grav\Common\Page\Interfaces\PageInterface;
@@ -25,7 +26,7 @@ use function is_string;
* Class Plugin
* @package Grav\Common
*/
class Plugin implements EventSubscriberInterface, \ArrayAccess
class Plugin implements EventSubscriberInterface, ArrayAccess
{
/** @var string */
public $name;
@@ -288,6 +289,8 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
/**
* Merge global and page configurations.
*
* WARNING: This method modifies page header!
*
* @param PageInterface $page The page to merge the configurations with the
* plugin settings.
* @param mixed $deep false = shallow|true = recursive|merge = recursive+unique

View File

@@ -30,6 +30,7 @@ class Plugins extends Iterator
/** @var array */
public $formFieldTypes;
/** @var bool */
private $plugins_initialized = false;
/**
@@ -172,7 +173,7 @@ class Plugins extends Iterator
/**
* Return list of all plugin data with their blueprints.
*
* @return array
* @return array<string,Data>
*/
public static function all()
{

View File

@@ -193,6 +193,39 @@ class InitializeProcessor extends ProcessorBase
}
}
// Override configuration using the environment.
$prefix = 'GRAV_CONFIG';
$env = getenv($prefix);
if ($env) {
$cPrefix = $prefix . '__';
$aPrefix = $prefix . '_ALIAS__';
$cLen = strlen($cPrefix);
$aLen = strlen($aPrefix);
$keys = $aliases = [];
$env = $_ENV + $_SERVER;
foreach ($env as $key => $value) {
if (!str_starts_with($key, $prefix)) {
continue;
}
if (str_starts_with($key, $cPrefix)) {
$key = str_replace('__', '.', substr($key, $cLen));
$keys[$key] = $value;
} elseif (str_starts_with($key, $aPrefix)) {
$key = substr($key, $aLen);
$aliases[$key] = $value;
}
}
$list = [];
foreach ($keys as $key => $value) {
foreach ($aliases as $alias => $real) {
$key = str_replace($alias, $real, $key);
}
$list[$key] = $value;
$config->set($key, $value);
}
}
$this->stopTimer('_init_config');
return $config;

View File

@@ -14,6 +14,7 @@ use RocketTheme\Toolbox\Event\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
/**
* Class PagesProcessor
@@ -55,7 +56,7 @@ class PagesProcessor extends ProcessorBase
unset($this->container['page']);
$this->container['page'] = $page = $event->page;
} else {
throw new \RuntimeException('Page Not Found', 404);
throw new RuntimeException('Page Not Found', 404);
}
$this->addMessage("Routed to page {$page->rawRoute()} (type: {$page->template()}) [Not Found fallback]");

View File

@@ -46,6 +46,7 @@ namespace Grav\Common\Scheduler;
* // bool(true)
*/
use DateInterval;
use DateTime;
use RuntimeException;
use function count;
@@ -473,9 +474,9 @@ class Cron
}
$date = $this->parseDate($date, $min, $hour, $day, $month, $weekday);
$interval = new \DateInterval('PT1M'); // 1 min
$interval = new DateInterval('PT1M'); // 1 min
if ($minuteBefore !== 0) {
$date->sub(new \DateInterval('PT' . abs($minuteBefore) . 'M'));
$date->sub(new DateInterval('PT' . abs($minuteBefore) . 'M'));
}
$n = $minuteAfter - $minuteBefore + 1;
for ($i = 0; $i < $n; $i++) {

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Scheduler;
use Closure;
use Cron\CronExpression;
use DateTime;
use Grav\Common\Grav;
@@ -109,7 +110,7 @@ class Job
/**
* Get the command
*
* @return \Closure|string
* @return Closure|string
*/
public function getCommand()
{
@@ -336,7 +337,7 @@ class Job
if (is_callable($this->command)) {
$this->output = $this->exec();
} else {
$args = \is_string($this->args) ? explode(' ', $this->args) : $this->args;
$args = is_string($this->args) ? explode(' ', $this->args) : $this->args;
$command = array_merge([$this->command], $args);
$process = new Process($command);
@@ -446,6 +447,7 @@ class Job
$return_data = call_user_func_array($this->command, $this->args);
$this->successful = true;
} catch (RuntimeException $e) {
$return_data = $e->getMessage();
$this->successful = false;
}
$this->output = ob_get_clean() . (is_string($return_data) ? $return_data : '');

View File

@@ -13,6 +13,7 @@ use DateTime;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Utils;
use InvalidArgumentException;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
use RocketTheme\Toolbox\File\YamlFile;
@@ -184,9 +185,10 @@ class Scheduler
/**
* Run the scheduler.
*
* @param DateTime|null $runTime Optional, run at specific moment
* @param DateTime|null $runTime Optional, run at specific moment
* @param bool $force force run even if not due
*/
public function run(DateTime $runTime = null)
public function run(DateTime $runTime = null, $force = false)
{
$this->loadSavedJobs();
@@ -199,7 +201,7 @@ class Scheduler
// Star processing jobs
foreach ($alljobs as $job) {
if ($job->isDue($runTime)) {
if ($job->isDue($runTime) || $force) {
$job->run();
$this->jobs_run[] = $job;
}
@@ -247,7 +249,7 @@ class Scheduler
case 'array':
return $this->output_schedule;
default:
throw new \InvalidArgumentException('Invalid output type');
throw new InvalidArgumentException('Invalid output type');
}
}

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Service;
use DirectoryIterator;
use Grav\Common\Config\CompiledBlueprints;
use Grav\Common\Config\CompiledConfig;
use Grav\Common\Config\CompiledLanguages;
@@ -169,9 +170,9 @@ class ConfigServiceProvider implements ServiceProviderInterface
$paths = [];
foreach ($plugins as $path) {
$iterator = new \DirectoryIterator($path);
$iterator = new DirectoryIterator($path);
/** @var \DirectoryIterator $directory */
/** @var DirectoryIterator $directory */
foreach ($iterator as $directory) {
if (!$directory->isDir() || $directory->isDot()) {
continue;

View File

@@ -70,7 +70,8 @@ class PagesServiceProvider implements ServiceProviderInterface
}
if ($config->get('system.force_ssl')) {
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
$scheme = $uri->scheme(true);
if ($scheme !== 'https') {
$url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$grav->redirect($url);
}

View File

@@ -14,9 +14,9 @@ use Grav\Common\Debugger;
use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Framework\Session\Messages;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\Session\Message;
/**
* Class SessionServiceProvider
@@ -113,14 +113,14 @@ class SessionServiceProvider implements ServiceProviderInterface
$debugger = $c['debugger'];
$debugger->addMessage('Inactive session: session messages may disappear', 'warming');
return new Message;
return new Messages();
}
/** @var Session $session */
$session = $c['session'];
if (!isset($session->messages)) {
$session->messages = new Message;
if (!$session->messages instanceof Messages) {
$session->messages = new Messages();
}
return $session->messages;

View File

@@ -11,6 +11,7 @@ namespace Grav\Common;
use Grav\Common\Form\FormFlash;
use Grav\Events\SessionStartEvent;
use Grav\Plugin\Form\Forms;
use function is_string;
/**
@@ -126,7 +127,7 @@ class Session extends \Grav\Framework\Session\Session
/** @var Uri $uri */
$uri = $grav['uri'];
/** @var Grav\Plugin\Form\Forms|null $form */
/** @var Forms|null $form */
$form = $grav['forms']->getActiveForm();
$sessionField = base64_encode($uri->url);

View File

@@ -130,12 +130,12 @@ class TwigExtension extends AbstractExtension implements GlobalsInterface
new TwigFilter('pad', [$this, 'padFilter']),
new TwigFilter('regex_replace', [$this, 'regexReplace']),
new TwigFilter('safe_email', [$this, 'safeEmailFilter'], ['is_safe' => ['html']]),
new TwigFilter('safe_truncate', ['\Grav\Common\Utils', 'safeTruncate']),
new TwigFilter('safe_truncate_html', ['\Grav\Common\Utils', 'safeTruncateHTML']),
new TwigFilter('safe_truncate', [Utils::class, 'safeTruncate']),
new TwigFilter('safe_truncate_html', [Utils::class, 'safeTruncateHTML']),
new TwigFilter('sort_by_key', [$this, 'sortByKeyFilter']),
new TwigFilter('starts_with', [$this, 'startsWithFilter']),
new TwigFilter('truncate', ['\Grav\Common\Utils', 'truncate']),
new TwigFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']),
new TwigFilter('truncate', [Utils::class, 'truncate']),
new TwigFilter('truncate_html', [Utils::class, 'truncateHTML']),
new TwigFilter('json_decode', [$this, 'jsonDecodeFilter']),
new TwigFilter('array_unique', 'array_unique'),
new TwigFilter('basename', 'basename'),
@@ -531,7 +531,7 @@ class TwigExtension extends AbstractExtension implements GlobalsInterface
public function nicetimeFunc($date, $long_strings = true, $show_tense = true)
{
if (empty($date)) {
return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED', null, true);
return $this->grav['language']->translate('GRAV.NICETIME.NO_DATE_PROVIDED');
}
if ($long_strings) {
@@ -571,19 +571,19 @@ class TwigExtension extends AbstractExtension implements GlobalsInterface
// check validity of date
if (empty($unix_date)) {
return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE', null, true);
return $this->grav['language']->translate('GRAV.NICETIME.BAD_DATE');
}
// is it future date or past date
if ($now > $unix_date) {
$difference = $now - $unix_date;
$tense = $this->grav['language']->translate('GRAV.NICETIME.AGO', null, true);
$tense = $this->grav['language']->translate('GRAV.NICETIME.AGO');
} elseif ($now == $unix_date) {
$difference = $now - $unix_date;
$tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW', null, false);
$tense = $this->grav['language']->translate('GRAV.NICETIME.JUST_NOW');
} else {
$difference = $unix_date - $now;
$tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW', null, true);
$tense = $this->grav['language']->translate('GRAV.NICETIME.FROM_NOW');
}
for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) {
@@ -606,7 +606,7 @@ class TwigExtension extends AbstractExtension implements GlobalsInterface
}
}
$periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j], null, true);
$periods[$j] = $this->grav['language']->translate('GRAV.'.$periods[$j]);
if ($now == $unix_date) {
return $tense;
@@ -892,6 +892,7 @@ class TwigExtension extends AbstractExtension implements GlobalsInterface
$loader = new FilesystemLoader('.');
$env = new Environment($loader);
$env->addExtension($this);
$template = $env->createTemplate($twig);

View File

@@ -165,7 +165,6 @@ class Uri
// Handle custom base
$custom_base = rtrim($grav['config']->get('system.custom_base_url'), '/');
if ($custom_base) {
$custom_parts = parse_url($custom_base);
if ($custom_parts === false) {
@@ -175,6 +174,10 @@ class Uri
$this->root_path = isset($custom_parts['path']) ? rtrim($custom_parts['path'], '/') : '';
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;
}
$this->root = $custom_base;
} else {
$this->root = $this->base . $this->root_path;

View File

@@ -22,6 +22,7 @@ use Grav\Common\User\Authentication;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\Traits\UserTrait;
use Grav\Framework\Flex\Flex;
use function is_array;
/**
* Class User
@@ -141,10 +142,11 @@ class User extends Data implements UserInterface
$file->save($data);
// We need to signal Flex Users about the change.
/** @var Flex|null $flex */
$flex = Grav::instance()['flex'] ?? null;
$users = $flex ? $flex->getDirectory('user-accounts') : null;
if ($users) {
if (null !== $users) {
$users->clearCache();
}
}
@@ -303,7 +305,7 @@ class User extends Data implements UserInterface
protected function getAvatarFile(): ?string
{
$avatars = $this->get('avatar');
if (\is_array($avatars) && $avatars) {
if (is_array($avatars) && $avatars) {
$avatar = array_shift($avatars);
return $avatar['path'] ?? null;
}

View File

@@ -17,6 +17,7 @@ use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
use function count;
use function in_array;
use function is_string;

View File

@@ -30,6 +30,7 @@ use function extension_loaded;
use function function_exists;
use function in_array;
use function is_array;
use function is_callable;
use function is_string;
use function strlen;

View File

@@ -0,0 +1,106 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Common\Grav;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Class GpmApplication
* @package Grav\Console\Application
*/
class Application extends \Symfony\Component\Console\Application
{
/** @var string|null */
protected $environment;
/** @var string|null */
protected $language;
/** @var bool */
protected $initialized = false;
/**
* @param InputInterface $input
* @return string|null
*/
public function getCommandName(InputInterface $input): ?string
{
$this->environment = $input->getOption('env');
$this->language = $input->getOption('lang') ?? $this->language;
$this->init();
return parent::getCommandName($input);
}
/**
* @return void
*/
protected function init(): void
{
if ($this->initialized) {
return;
}
$this->initialized = true;
$grav = Grav::instance();
$grav->setup($this->environment);
}
/**
* Add global a --env option.
*
* @return InputDefinition
*/
protected function getDefaultInputDefinition(): InputDefinition
{
$inputDefinition = parent::getDefaultInputDefinition();
$inputDefinition->addOption(
new InputOption(
'env',
null,
InputOption::VALUE_OPTIONAL,
'Use environment configuration (defaults to localhost)'
)
);
$inputDefinition->addOption(
new InputOption(
'lang',
null,
InputOption::VALUE_OPTIONAL,
'Language to be used (defaults to en)'
)
);
return $inputDefinition;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return void
*/
protected function configureIO(InputInterface $input, OutputInterface $output)
{
$formatter = $output->getFormatter();
$formatter->setStyle('normal', new OutputFormatterStyle('white'));
$formatter->setStyle('yellow', new OutputFormatterStyle('yellow', null, ['bold']));
$formatter->setStyle('red', new OutputFormatterStyle('red', null, ['bold']));
$formatter->setStyle('cyan', new OutputFormatterStyle('cyan', null, ['bold']));
$formatter->setStyle('green', new OutputFormatterStyle('green', null, ['bold']));
$formatter->setStyle('magenta', new OutputFormatterStyle('magenta', null, ['bold']));
$formatter->setStyle('white', new OutputFormatterStyle('white', null, ['bold']));
parent::configureIO($input, $output);
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application\CommandLoader;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* Class GpmApplication
* @package Grav\Console\Application
*/
class PluginCommandLoader implements CommandLoaderInterface
{
/** @var array */
private $commands;
/**
* PluginCommandLoader constructor.
*
* @param string $name
*/
public function __construct(string $name)
{
$this->commands = [];
try {
$path = "plugins://{$name}/cli";
$pattern = '([A-Z]\w+Command\.php)';
$commands = is_dir($path) ? Folder::all($path, ['compare' => 'Filename', 'pattern' => '/' . $pattern . '$/usm', 'levels' => 1]) : [];
} catch (RuntimeException $e) {
throw new RuntimeException("Failed to load console commands for plugin {$name}");
}
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
foreach ($commands as $command_path) {
$full_path = $locator->findResource("plugins://{$name}/cli/{$command_path}");
require_once $full_path;
$command_class = 'Grav\Plugin\Console\\' . preg_replace('/.php$/', '', $command_path);
if (class_exists($command_class)) {
$command = new $command_class();
if ($command instanceof Command) {
$this->commands[$command->getName()] = $command;
}
}
}
}
/**
* @param string $name
* @return Command
*/
public function get($name): Command
{
$command = $this->commands[$name] ?? null;
if (null === $command) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
}
return $command;
}
/**
* @param string $name
* @return bool
*/
public function has($name): bool
{
return isset($this->commands[$name]);
}
/**
* @return string[]
*/
public function getNames(): array
{
return array_keys($this->commands);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Console\Gpm\DirectInstallCommand;
use Grav\Console\Gpm\IndexCommand;
use Grav\Console\Gpm\InfoCommand;
use Grav\Console\Gpm\InstallCommand;
use Grav\Console\Gpm\SelfupgradeCommand;
use Grav\Console\Gpm\UninstallCommand;
use Grav\Console\Gpm\UpdateCommand;
use Grav\Console\Gpm\VersionCommand;
/**
* Class GpmApplication
* @package Grav\Console\Application
*/
class GpmApplication extends Application
{
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);
$this->addCommands([
new IndexCommand(),
new VersionCommand(),
new InfoCommand(),
new InstallCommand(),
new UninstallCommand(),
new UpdateCommand(),
new SelfupgradeCommand(),
new DirectInstallCommand(),
]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Console\Cli\BackupCommand;
use Grav\Console\Cli\CleanCommand;
use Grav\Console\Cli\ClearCacheCommand;
use Grav\Console\Cli\ComposerCommand;
use Grav\Console\Cli\InstallCommand;
use Grav\Console\Cli\LogViewerCommand;
use Grav\Console\Cli\NewProjectCommand;
use Grav\Console\Cli\PageSystemValidatorCommand;
use Grav\Console\Cli\SandboxCommand;
use Grav\Console\Cli\SchedulerCommand;
use Grav\Console\Cli\SecurityCommand;
use Grav\Console\Cli\ServerCommand;
use Grav\Console\Cli\YamlLinterCommand;
/**
* Class GravApplication
* @package Grav\Console\Application
*/
class GravApplication extends Application
{
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);
$this->addCommands([
new InstallCommand(),
new ComposerCommand(),
new SandboxCommand(),
new CleanCommand(),
new ClearCacheCommand(),
new BackupCommand(),
new NewProjectCommand(),
new SchedulerCommand(),
new SecurityCommand(),
new LogViewerCommand(),
new YamlLinterCommand(),
new ServerCommand(),
new PageSystemValidatorCommand(),
]);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* @package Grav\Console
*
* @copyright Copyright (C) 2015 - 2020 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Application;
use Grav\Common\Grav;
use Grav\Common\Plugins;
use Grav\Console\Application\CommandLoader\PluginCommandLoader;
use Grav\Console\Plugin\PluginListCommand;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/**
* Class PluginApplication
* @package Grav\Console\Application
*/
class PluginApplication extends Application
{
/** @var string|null */
protected $pluginName;
/**
* PluginApplication constructor.
* @param string $name
* @param string $version
*/
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
{
parent::__construct($name, $version);
$this->addCommands([
new PluginListCommand(),
]);
}
/**
* @param string $pluginName
* @return void
*/
public function setPluginName(string $pluginName): void
{
$this->pluginName = $pluginName;
}
/**
* @return string
*/
public function getPluginName(): string
{
return $this->pluginName;
}
/**
* @param InputInterface|null $input
* @param OutputInterface|null $output
* @return int
* @throws Throwable
*/
public function run(InputInterface $input = null, OutputInterface $output = null): int
{
if (null === $input) {
$argv = $_SERVER['argv'] ?? [];
$bin = array_shift($argv);
$this->pluginName = array_shift($argv);
$argv = array_merge([$bin], $argv);
$input = new ArgvInput($argv);
}
return parent::run($input, $output);
}
/**
* @return void
*/
protected function init(): void
{
if ($this->initialized) {
return;
}
parent::init();
if (null === $this->pluginName) {
$this->setDefaultCommand('plugins:list');
return;
}
$grav = Grav::instance();
$grav->initializeCli();
/** @var Plugins $plugins */
$plugins = $grav['plugins'];
$plugin = $this->pluginName ? $plugins::get($this->pluginName) : null;
if (null === $plugin) {
throw new NamespaceNotFoundException("Plugin \"{$this->pluginName}\" is not installed.");
}
if (!$plugin->enabled) {
throw new NamespaceNotFoundException("Plugin \"{$this->pluginName}\" is not enabled.");
}
$this->setCommandLoader(new PluginCommandLoader($this->pluginName));
}
}

View File

@@ -11,22 +11,21 @@ namespace Grav\Console\Cli;
use Grav\Common\Backup\Backups;
use Grav\Common\Grav;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use ZipArchive;
use function count;
/**
* Class BackupCommand
* @package Grav\Console\Cli
*/
class BackupCommand extends ConsoleCommand
class BackupCommand extends GravCommand
{
/** @var string $source */
protected $source;
/** @var ProgressBar $progress */
protected $progress;
@@ -55,40 +54,43 @@ class BackupCommand extends ConsoleCommand
{
$this->initializeGrav();
$this->progress = new ProgressBar($this->output);
$this->progress->setFormat('Archiving <cyan>%current%</cyan> files [<green>%bar%</green>] <white>%percent:3s%%</white> %elapsed:6s% <yellow>%message%</yellow>');
$this->progress->setBarWidth(100);
$input = $this->getInput();
$io = $this->getIO();
$io = new SymfonyStyle($this->input, $this->output);
$io->title('Grav Backup');
if (!class_exists(\ZipArchive::class)) {
if (!class_exists(ZipArchive::class)) {
$io->error('php-zip extension needs to be enabled!');
return 1;
}
ProgressBar::setFormatDefinition('zip', 'Archiving <cyan>%current%</cyan> files [<green>%bar%</green>] <white>%percent:3s%%</white> %elapsed:6s% <yellow>%message%</yellow>');
$this->progress = $io->createProgressBar();
$this->progress->setFormat('zip');
$this->progress->setBarWidth(100);
/** @var Backups $backups */
$backups = Grav::instance()['backups'];
$backups_list = $backups->getBackupProfiles();
$backups_list = $backups::getBackupProfiles();
$backups_names = $backups->getBackupNames();
$id = null;
$inline_id = $this->input->getArgument('id');
$inline_id = $input->getArgument('id');
if (null !== $inline_id && is_numeric($inline_id)) {
$id = $inline_id;
}
if (null === $id) {
if (count($backups_list) > 1) {
$helper = $this->getHelper('question');
$question = new ChoiceQuestion(
'Choose a backup?',
$backups_names,
0
);
$question->setErrorMessage('Option %s is invalid.');
$backup_name = $helper->ask($this->input, $this->output, $question);
$backup_name = $io->askQuestion($question);
$id = array_search($backup_name, $backups_names, true);
$io->newLine();
@@ -98,7 +100,7 @@ class BackupCommand extends ConsoleCommand
}
}
$backup = $backups->backup($id, [$this, 'outputProgress']);
$backup = $backups::backup($id, function($args) { $this->outputProgress($args); });
$io->newline(2);
$io->success('Backup Successfully Created: ' . $backup);
@@ -110,7 +112,7 @@ class BackupCommand extends ConsoleCommand
* @param array $args
* @return void
*/
public function outputProgress($args): void
public function outputProgress(array $args): void
{
switch ($args['type']) {
case 'count':

View File

@@ -14,6 +14,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Class CleanCommand
@@ -21,11 +22,10 @@ use Symfony\Component\Console\Formatter\OutputFormatterStyle;
*/
class CleanCommand extends Command
{
/* @var InputInterface $output */
/** @var InputInterface */
protected $input;
/* @var OutputInterface $output */
protected $output;
/** @var SymfonyStyle */
protected $io;
/** @var array */
protected $paths_to_remove = [
@@ -284,33 +284,40 @@ class CleanCommand extends Command
{
$this->setupConsole($input, $output);
$this->cleanPaths();
return 0;
return $this->cleanPaths() ? 0 : 1;
}
/**
* @return void
* @return bool
*/
private function cleanPaths(): void
private function cleanPaths(): bool
{
$this->output->writeln('');
$this->output->writeln('<red>DELETING</red>');
$success = true;
$this->io->writeln('');
$this->io->writeln('<red>DELETING</red>');
$anything = false;
foreach ($this->paths_to_remove as $path) {
$path = ROOT_DIR . $path;
if (is_dir($path) && @Folder::delete($path)) {
$anything = true;
$this->output->writeln('<red>dir: </red>' . $path);
} elseif (is_file($path) && @unlink($path)) {
$anything = true;
$this->output->writeln('<red>file: </red>' . $path);
$path = GRAV_ROOT . $path;
try {
if (is_dir($path) && Folder::delete($path)) {
$anything = true;
$this->io->writeln('<red>dir: </red>' . $path);
} elseif (is_file($path) && @unlink($path)) {
$anything = true;
$this->io->writeln('<red>file: </red>' . $path);
}
} catch (\Exception $e) {
$success = false;
$this->io->error(sprintf('Failed to delete %s: %s', $path, $e->getMessage()));
}
}
if (!$anything) {
$this->output->writeln('');
$this->output->writeln('<green>Nothing to clean...</green>');
$this->io->writeln('');
$this->io->writeln('<green>Nothing to clean...</green>');
}
return $success;
}
/**
@@ -323,14 +330,14 @@ class CleanCommand extends Command
public function setupConsole(InputInterface $input, OutputInterface $output): void
{
$this->input = $input;
$this->output = $output;
$this->io = new SymfonyStyle($input, $output);
$this->output->getFormatter()->setStyle('normal', new OutputFormatterStyle('white'));
$this->output->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, ['bold']));
$this->output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, ['bold']));
$this->output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, ['bold']));
$this->output->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, ['bold']));
$this->output->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, ['bold']));
$this->output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, ['bold']));
$this->io->getFormatter()->setStyle('normal', new OutputFormatterStyle('white'));
$this->io->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, ['bold']));
$this->io->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, ['bold']));
$this->io->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, ['bold']));
$this->io->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, ['bold']));
$this->io->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, ['bold']));
$this->io->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, ['bold']));
}
}

View File

@@ -10,14 +10,14 @@
namespace Grav\Console\Cli;
use Grav\Common\Cache;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ClearCacheCommand
* @package Grav\Console\Cli
*/
class ClearCacheCommand extends ConsoleCommand
class ClearCacheCommand extends GravCommand
{
/**
* @return void
@@ -57,37 +57,39 @@ class ClearCacheCommand extends ConsoleCommand
*/
private function cleanPaths(): void
{
$this->output->writeln('');
$input = $this->getInput();
$io = $this->getIO();
$io->newLine();
if ($this->input->getOption('purge')) {
$this->output->writeln('<magenta>Purging old cache</magenta>');
$this->output->writeln('');
if ($input->getOption('purge')) {
$io->writeln('<magenta>Purging old cache</magenta>');
$io->newLine();
$msg = Cache::purgeJob();
$this->output->writeln($msg);
$io->writeln($msg);
} else {
$this->output->writeln('<magenta>Clearing cache</magenta>');
$this->output->writeln('');
$io->writeln('<magenta>Clearing cache</magenta>');
$io->newLine();
if ($this->input->getOption('all')) {
if ($input->getOption('all')) {
$remove = 'all';
} elseif ($this->input->getOption('assets-only')) {
} elseif ($input->getOption('assets-only')) {
$remove = 'assets-only';
} elseif ($this->input->getOption('images-only')) {
} elseif ($input->getOption('images-only')) {
$remove = 'images-only';
} elseif ($this->input->getOption('cache-only')) {
} elseif ($input->getOption('cache-only')) {
$remove = 'cache-only';
} elseif ($this->input->getOption('tmp-only')) {
} elseif ($input->getOption('tmp-only')) {
$remove = 'tmp-only';
} elseif ($this->input->getOption('invalidate')) {
} elseif ($input->getOption('invalidate')) {
$remove = 'invalidate';
} else {
$remove = 'standard';
}
foreach (Cache::clearCache($remove) as $result) {
$this->output->writeln($result);
$io->writeln($result);
}
}
}

View File

@@ -9,14 +9,14 @@
namespace Grav\Console\Cli;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ComposerCommand
* @package Grav\Console\Cli
*/
class ComposerCommand extends ConsoleCommand
class ComposerCommand extends GravCommand
{
/**
* @return void
@@ -46,15 +46,18 @@ class ComposerCommand extends ConsoleCommand
*/
protected function serve(): int
{
$action = $this->input->getOption('install') ? 'install' : ($this->input->getOption('update') ? 'update' : 'install');
$input = $this->getInput();
$io = $this->getIO();
if ($this->input->getOption('install')) {
$action = $input->getOption('install') ? 'install' : ($input->getOption('update') ? 'update' : 'install');
if ($input->getOption('install')) {
$action = 'install';
}
// Updates composer first
$this->output->writeln("\nInstalling vendor dependencies");
$this->output->writeln($this->composerUpdate(GRAV_ROOT, $action));
$io->writeln("\nInstalling vendor dependencies");
$io->writeln($this->composerUpdate(GRAV_ROOT, $action));
return 0;
}

View File

@@ -9,7 +9,7 @@
namespace Grav\Console\Cli;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@@ -18,14 +18,12 @@ use Symfony\Component\Console\Input\InputOption;
* Class InstallCommand
* @package Grav\Console\Cli
*/
class InstallCommand extends ConsoleCommand
class InstallCommand extends GravCommand
{
/** @var array */
protected $config;
/** @var string */
protected $destination;
/** @var string */
protected $user_path;
@@ -56,14 +54,17 @@ class InstallCommand extends ConsoleCommand
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$dependencies_file = '.dependencies';
$this->destination = $this->input->getArgument('destination') ?: ROOT_DIR;
$this->destination = $input->getArgument('destination') ?: ROOT_DIR;
// fix trailing slash
$this->destination = rtrim($this->destination, DS) . DS;
$this->user_path = $this->destination . USER_PATH;
if ($local_config_file = $this->loadLocalConfig()) {
$this->output->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
$io->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
}
// Look for dependencies file in ROOT and USER dir
@@ -72,11 +73,11 @@ class InstallCommand extends ConsoleCommand
} elseif (file_exists($this->destination . $dependencies_file)) {
$file = YamlFile::instance($this->destination . $dependencies_file);
} else {
$this->output->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
if ($this->input->getArgument('destination')) {
$this->output->writeln('<yellow>HINT</yellow> <info>Are you trying to install a plugin or a theme? Make sure you use <cyan>bin/gpm install <something></cyan>, not <cyan>bin/grav install</cyan>. This command is only used to install Grav skeletons.');
$io->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
if ($input->getArgument('destination')) {
$io->writeln('<yellow>HINT</yellow> <info>Are you trying to install a plugin or a theme? Make sure you use <cyan>bin/gpm install <something></cyan>, not <cyan>bin/grav install</cyan>. This command is only used to install Grav skeletons.');
} else {
$this->output->writeln('<yellow>HINT</yellow> <info>Are you trying to install Grav? Grav is already installed. You need to run this command only if you download a skeleton from GitHub directly.');
$io->writeln('<yellow>HINT</yellow> <info>Are you trying to install Grav? Grav is already installed. You need to run this command only if you download a skeleton from GitHub directly.');
}
return 1;
@@ -87,16 +88,15 @@ class InstallCommand extends ConsoleCommand
// If no config, fail.
if (!$this->config) {
$this->output->writeln('<red>ERROR</red> invalid YAML in ' . $dependencies_file);
$io->writeln('<red>ERROR</red> invalid YAML in ' . $dependencies_file);
return 1;
}
$error = 0;
if (!$this->input->getOption('symlink')) {
if (!$input->getOption('symlink')) {
// Updates composer first
$this->output->writeln("\nInstalling vendor dependencies");
$this->output->writeln($this->composerUpdate(GRAV_ROOT, 'install'));
$io->writeln("\nInstalling vendor dependencies");
$io->writeln($this->composerUpdate(GRAV_ROOT, 'install'));
$error = $this->gitclone();
} else {
@@ -113,10 +113,12 @@ class InstallCommand extends ConsoleCommand
*/
private function gitclone(): int
{
$this->output->writeln('');
$this->output->writeln('<green>Cloning Bits</green>');
$this->output->writeln('============');
$this->output->writeln('');
$io = $this->getIO();
$io->newLine();
$io->writeln('<green>Cloning Bits</green>');
$io->writeln('============');
$io->newLine();
$error = 0;
$this->destination = rtrim($this->destination, DS);
@@ -126,16 +128,16 @@ class InstallCommand extends ConsoleCommand
exec('cd "' . $this->destination . '" && git clone -b ' . $data['branch'] . ' --depth 1 ' . $data['url'] . ' ' . $data['path'], $output, $return);
if (!$return) {
$this->output->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
$io->writeln('<green>SUCCESS</green> cloned <magenta>' . $data['url'] . '</magenta> -> <cyan>' . $path . '</cyan>');
} else {
$this->output->writeln('<red>ERROR</red> cloning <magenta>' . $data['url']);
$io->writeln('<red>ERROR</red> cloning <magenta>' . $data['url']);
$error = 1;
}
$this->output->writeln('');
$io->newLine();
} else {
$this->output->writeln('<yellow>' . $path . ' already exists, skipping...</yellow>');
$this->output->writeln('');
$io->writeln('<yellow>' . $path . ' already exists, skipping...</yellow>');
$io->newLine();
}
}
@@ -149,14 +151,16 @@ class InstallCommand extends ConsoleCommand
*/
private function symlink(): int
{
$this->output->writeln('');
$this->output->writeln('<green>Symlinking Bits</green>');
$this->output->writeln('===============');
$this->output->writeln('');
$io = $this->getIO();
$io->newLine();
$io->writeln('<green>Symlinking Bits</green>');
$io->writeln('===============');
$io->newLine();
if (!$this->local_config) {
$this->output->writeln('<red>No local configuration available, aborting...</red>');
$this->output->writeln('');
$io->writeln('<red>No local configuration available, aborting...</red>');
$io->newLine();
return 1;
}
@@ -168,8 +172,8 @@ class InstallCommand extends ConsoleCommand
$src = $data['src'] ?? null;
$path = $data['path'] ?? null;
if (!isset($scm, $src, $path)) {
$this->output->writeln("<red>Dependency '$name' has broken configuration, skipping...</red>");
$this->output->writeln('');
$io->writeln("<red>Dependency '$name' has broken configuration, skipping...</red>");
$io->newLine();
$error = 1;
continue;
@@ -188,20 +192,20 @@ class InstallCommand extends ConsoleCommand
}
if (is_link($to) && !is_file("{$to}/{$name}.yaml")) {
$this->output->writeln('<yellow>Removed broken symlink '. $path .'</yellow>');
$io->writeln('<yellow>Removed broken symlink '. $path .'</yellow>');
unlink($to);
}
if (null === $from) {
$this->output->writeln('<red>source for ' . $src . ' does not exists, skipping...</red>');
$this->output->writeln('');
$io->writeln('<red>source for ' . $src . ' does not exists, skipping...</red>');
$io->newLine();
$error = 1;
} elseif (!file_exists($to)) {
symlink($from, $to);
$this->output->writeln('<green>SUCCESS</green> symlinked <magenta>' . $src . '</magenta> -> <cyan>' . $path . '</cyan>');
$this->output->writeln('');
$io->writeln('<green>SUCCESS</green> symlinked <magenta>' . $src . '</magenta> -> <cyan>' . $path . '</cyan>');
$io->newLine();
} else {
$this->output->writeln('<yellow>destination: ' . $path . ' already exists, skipping...</yellow>');
$this->output->writeln('');
$io->writeln('<yellow>destination: ' . $path . ' already exists, skipping...</yellow>');
$io->newLine();
}
}

View File

@@ -12,15 +12,14 @@ namespace Grav\Console\Cli;
use DateTime;
use Grav\Common\Grav;
use Grav\Common\Helpers\LogViewer;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Class LogViewerCommand
* @package Grav\Console\Cli
*/
class LogViewerCommand extends ConsoleCommand
class LogViewerCommand extends GravCommand
{
/**
* @return void
@@ -50,11 +49,12 @@ class LogViewerCommand extends ConsoleCommand
*/
protected function serve(): int
{
$file = $this->input->getOption('file') ?? 'grav.log';
$lines = $this->input->getOption('lines') ?? 20;
$verbose = $this->input->getOption('verbose') ?? false;
$input = $this->getInput();
$io = $this->getIO();
$io = new SymfonyStyle($this->input, $this->output);
$file = $input->getOption('file') ?? 'grav.log';
$lines = $input->getOption('lines') ?? 20;
$verbose = $input->getOption('verbose') ?? false;
$io->title('Log Viewer');

View File

@@ -9,7 +9,7 @@
namespace Grav\Console\Cli;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@@ -18,7 +18,7 @@ use Symfony\Component\Console\Input\InputOption;
* Class NewProjectCommand
* @package Grav\Console\Cli
*/
class NewProjectCommand extends ConsoleCommand
class NewProjectCommand extends GravCommand
{
/**
* @return void
@@ -48,6 +48,8 @@ class NewProjectCommand extends ConsoleCommand
*/
protected function serve(): int
{
$io = $this->getIO();
$sandboxCommand = $this->getApplication()->find('sandbox');
$installCommand = $this->getApplication()->find('install');
@@ -63,9 +65,9 @@ class NewProjectCommand extends ConsoleCommand
'-s' => $this->input->getOption('symlink')
]);
$error = $sandboxCommand->run($sandboxArguments, $this->output);
$error = $sandboxCommand->run($sandboxArguments, $io);
if ($error === 0) {
$error = $installCommand->run($installArguments, $this->output);
$error = $installCommand->run($installArguments, $io);
}
return $error;

View File

@@ -14,7 +14,7 @@ use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Console\Input\InputOption;
use function in_array;
@@ -24,7 +24,7 @@ use function is_object;
* Class PageSystemValidatorCommand
* @package Grav\Console\Cli
*/
class PageSystemValidatorCommand extends ConsoleCommand
class PageSystemValidatorCommand extends GravCommand
{
/** @var array */
protected $tests = [
@@ -149,10 +149,13 @@ class PageSystemValidatorCommand extends ConsoleCommand
*/
protected function serve(): int
{
$input = $this->getInput();
$io = $this->getIO();
$this->setLanguage('en');
$this->initializePages();
$this->output->writeln('');
$io->newLine();
$this->grav = $grav = Grav::instance();
@@ -161,27 +164,27 @@ class PageSystemValidatorCommand extends ConsoleCommand
/** @var Config $config */
$config = $grav['config'];
if ($this->input->getOption('record')) {
$this->output->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
if ($input->getOption('record')) {
$io->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
$this->output->writeln('<magenta>Record tests</magenta>');
$this->output->writeln('');
$io->writeln('<magenta>Record tests</magenta>');
$io->newLine();
$results = $this->record();
$file = $this->getFile('pages-old');
$file->save($results);
$this->output->writeln('Recorded tests to ' . $file->filename());
} elseif ($this->input->getOption('check')) {
$this->output->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
$io->writeln('Recorded tests to ' . $file->filename());
} elseif ($input->getOption('check')) {
$io->writeln('Pages: ' . $config->get('system.pages.type', 'page'));
$this->output->writeln('<magenta>Run tests</magenta>');
$this->output->writeln('');
$io->writeln('<magenta>Run tests</magenta>');
$io->newLine();
$new = $this->record();
$file = $this->getFile('pages-new');
$file->save($new);
$this->output->writeln('Recorded tests to ' . $file->filename());
$io->writeln('Recorded tests to ' . $file->filename());
$file = $this->getFile('pages-old');
$old = $file->content();
@@ -189,11 +192,11 @@ class PageSystemValidatorCommand extends ConsoleCommand
$results = $this->check($old, $new);
$file = $this->getFile('diff');
$file->save($results);
$this->output->writeln('Recorded results to ' . $file->filename());
$io->writeln('Recorded results to ' . $file->filename());
} else {
$this->output->writeln('<green>page-system-validator [-r|--record] [-c|--check]</green>');
$io->writeln('<green>page-system-validator [-r|--record] [-c|--check]</green>');
}
$this->output->writeln('');
$io->newLine();
return 0;
}
@@ -201,8 +204,10 @@ class PageSystemValidatorCommand extends ConsoleCommand
/**
* @return array
*/
private function record()
private function record(): array
{
$io = $this->getIO();
/** @var Pages $pages */
$pages = $this->grav['pages'];
$all = $pages->all();
@@ -211,7 +216,7 @@ class PageSystemValidatorCommand extends ConsoleCommand
$results[''] = $this->recordRow($pages->root());
foreach ($all as $path => $page) {
if (null === $page) {
$this->output->writeln('<red>Error on page ' . $path . '</red>');
$io->writeln('<red>Error on page ' . $path . '</red>');
continue;
}
@@ -225,7 +230,7 @@ class PageSystemValidatorCommand extends ConsoleCommand
* @param PageInterface $page
* @return array
*/
private function recordRow(PageInterface $page)
private function recordRow(PageInterface $page): array
{
$results = [];
@@ -287,7 +292,7 @@ class PageSystemValidatorCommand extends ConsoleCommand
* @param string $name
* @return CompiledYamlFile
*/
private function getFile(string $name)
private function getFile(string $name): CompiledYamlFile
{
return CompiledYamlFile::instance('cache://tests/' . $name . '.yaml');
}

View File

@@ -9,8 +9,8 @@
namespace Grav\Console\Cli;
use Grav\Console\ConsoleCommand;
use Grav\Common\Filesystem\Folder;
use Grav\Console\GravCommand;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@@ -20,7 +20,7 @@ use function count;
* Class SandboxCommand
* @package Grav\Console\Cli
*/
class SandboxCommand extends ConsoleCommand
class SandboxCommand extends GravCommand
{
/** @var array */
protected $directories = [
@@ -100,7 +100,9 @@ class SandboxCommand extends ConsoleCommand
*/
protected function serve(): int
{
$this->destination = $this->input->getArgument('destination');
$input = $this->getInput();
$this->destination = $input->getArgument('destination');
// Create Some core stuff if it doesn't exist
$error = $this->createDirectories();
@@ -109,7 +111,7 @@ class SandboxCommand extends ConsoleCommand
}
// Copy files or create symlinks
$error = $this->input->getOption('symlink') ? $this->symlink() : $this->copy();
$error = $input->getOption('symlink') ? $this->symlink() : $this->copy();
if ($error) {
return $error;
}
@@ -137,8 +139,10 @@ class SandboxCommand extends ConsoleCommand
*/
private function createDirectories(): int
{
$this->output->writeln('');
$this->output->writeln('<comment>Creating Directories</comment>');
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Creating Directories</comment>');
$dirs_created = false;
if (!file_exists($this->destination)) {
@@ -148,13 +152,13 @@ class SandboxCommand extends ConsoleCommand
foreach ($this->directories as $dir) {
if (!file_exists($this->destination . $dir)) {
$dirs_created = true;
$this->output->writeln(' <cyan>' . $dir . '</cyan>');
$io->writeln(' <cyan>' . $dir . '</cyan>');
Folder::create($this->destination . $dir);
}
}
if (!$dirs_created) {
$this->output->writeln(' <red>Directories already exist</red>');
$io->writeln(' <red>Directories already exist</red>');
}
return 0;
@@ -165,8 +169,10 @@ class SandboxCommand extends ConsoleCommand
*/
private function copy(): int
{
$this->output->writeln('');
$this->output->writeln('<comment>Copying Files</comment>');
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Copying Files</comment>');
foreach ($this->mappings as $source => $target) {
@@ -177,7 +183,7 @@ class SandboxCommand extends ConsoleCommand
$from = $this->source . $source;
$to = $this->destination . $target;
$this->output->writeln(' <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
$io->writeln(' <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
@Folder::rcopy($from, $to);
}
@@ -189,8 +195,10 @@ class SandboxCommand extends ConsoleCommand
*/
private function symlink(): int
{
$this->output->writeln('');
$this->output->writeln('<comment>Resetting Symbolic Links</comment>');
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Resetting Symbolic Links</comment>');
foreach ($this->mappings as $source => $target) {
@@ -201,7 +209,7 @@ class SandboxCommand extends ConsoleCommand
$from = $this->source . $source;
$to = $this->destination . $target;
$this->output->writeln(' <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
$io->writeln(' <cyan>' . $source . '</cyan> <comment>-></comment> ' . $to);
if (is_dir($to)) {
@Folder::delete($to);
@@ -219,8 +227,10 @@ class SandboxCommand extends ConsoleCommand
*/
private function pages(): int
{
$this->output->writeln('');
$this->output->writeln('<comment>Pages Initializing</comment>');
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Pages Initializing</comment>');
// get pages files and initialize if no pages exist
$pages_dir = $this->destination . '/user/pages';
@@ -229,7 +239,7 @@ class SandboxCommand extends ConsoleCommand
if (count($pages_files) === 0) {
$destination = $this->source . '/user/pages';
Folder::rcopy($destination, $pages_dir);
$this->output->writeln(' <cyan>' . $destination . '</cyan> <comment>-></comment> Created');
$io->writeln(' <cyan>' . $destination . '</cyan> <comment>-></comment> Created');
}
return 0;
@@ -244,8 +254,9 @@ class SandboxCommand extends ConsoleCommand
return 1;
}
$this->output->writeln('');
$this->output->writeln('<comment>File Initializing</comment>');
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>File Initializing</comment>');
$files_init = false;
// Copy files if they do not exist
@@ -260,12 +271,12 @@ class SandboxCommand extends ConsoleCommand
if (!file_exists($to)) {
$files_init = true;
copy($from, $to);
$this->output->writeln(' <cyan>' . $target . '</cyan> <comment>-></comment> Created');
$io->writeln(' <cyan>' . $target . '</cyan> <comment>-></comment> Created');
}
}
if (!$files_init) {
$this->output->writeln(' <red>Files already exist</red>');
$io->writeln(' <red>Files already exist</red>');
}
return 0;
@@ -276,8 +287,9 @@ class SandboxCommand extends ConsoleCommand
*/
private function perms(): int
{
$this->output->writeln('');
$this->output->writeln('<comment>Permissions Initializing</comment>');
$io = $this->getIO();
$io->newLine();
$io->writeln('<comment>Permissions Initializing</comment>');
$dir_perms = 0755;
@@ -285,10 +297,10 @@ class SandboxCommand extends ConsoleCommand
foreach ($binaries as $bin) {
chmod($bin, $dir_perms);
$this->output->writeln(' <cyan>bin/' . basename($bin) . '</cyan> permissions reset to ' . decoct($dir_perms));
$io->writeln(' <cyan>bin/' . basename($bin) . '</cyan> permissions reset to ' . decoct($dir_perms));
}
$this->output->writeln("");
$io->newLine();
return 0;
}
@@ -299,29 +311,30 @@ class SandboxCommand extends ConsoleCommand
private function check(): bool
{
$success = true;
$io = $this->getIO();
if (!file_exists($this->destination)) {
$this->output->writeln(' file: <red>' . $this->destination . '</red> does not exist!');
$io->writeln(' file: <red>' . $this->destination . '</red> does not exist!');
$success = false;
}
foreach ($this->directories as $dir) {
if (!file_exists($this->destination . $dir)) {
$this->output->writeln(' directory: <red>' . $dir . '</red> does not exist!');
$io->writeln(' directory: <red>' . $dir . '</red> does not exist!');
$success = false;
}
}
foreach ($this->mappings as $target => $link) {
if (!file_exists($this->destination . $target)) {
$this->output->writeln(' mappings: <red>' . $target . '</red> does not exist!');
$io->writeln(' mappings: <red>' . $target . '</red> does not exist!');
$success = false;
}
}
if (!$success) {
$this->output->writeln('');
$this->output->writeln('<comment>install should be run with --symlink|--s to symlink first</comment>');
$io->newLine();
$io->writeln('<comment>install should be run with --symlink|--s to symlink first</comment>');
}
return $success;

View File

@@ -13,18 +13,17 @@ use Cron\CronExpression;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Common\Scheduler\Scheduler;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
use function is_null;
/**
* Class SchedulerCommand
* @package Grav\Console\Cli
*/
class SchedulerCommand extends ConsoleCommand
class SchedulerCommand extends GravCommand
{
/**
* @return void
@@ -54,8 +53,9 @@ class SchedulerCommand extends ConsoleCommand
->addOption(
'run',
'r',
InputOption::VALUE_REQUIRED,
'Force run a job with a specific Job ID'
InputOption::VALUE_OPTIONAL,
'Force run all jobs or a specific job if you specify a specific Job ID',
false
)
->setDescription('Run the Grav Scheduler. Best when integrated with system cron')
->setHelp("Running without any options will force the Scheduler to run through it's jobs and process them");
@@ -70,6 +70,8 @@ class SchedulerCommand extends ConsoleCommand
$grav = Grav::instance();
$grav['backups']->init();
$this->initializePages();
$this->initializeThemes();
/** @var Scheduler $scheduler */
$scheduler = $grav['scheduler'];
@@ -77,17 +79,20 @@ class SchedulerCommand extends ConsoleCommand
$this->setHelp('foo');
$io = new SymfonyStyle($this->input, $this->output);
$input = $this->getInput();
$io = $this->getIO();
$error = 0;
if ($this->input->getOption('jobs')) {
$run = $input->getOption('run');
if ($input->getOption('jobs')) {
// Show jobs list
$jobs = $scheduler->getAllJobs();
$job_states = (array)$scheduler->getJobStates()->content();
$rows = [];
$table = new Table($this->output);
$table = new Table($io);
$table->setStyle('box');
$headers = ['Job ID', 'Command', 'Run At', 'Status', 'Last Run', 'State'];
@@ -121,13 +126,13 @@ class SchedulerCommand extends ConsoleCommand
$io->newLine();
$io->note('For error details run "bin/grav scheduler -d"');
$io->newLine();
} elseif ($this->input->getOption('details')) {
} elseif ($input->getOption('details')) {
$jobs = $scheduler->getAllJobs();
$job_states = (array)$scheduler->getJobStates()->content();
$io->title('Job Details');
$table = new Table($this->output);
$table = new Table($io);
$table->setStyle('box');
$table->setHeaders(['Job ID', 'Last Run', 'Next Run', 'Errors']);
$rows = [];
@@ -159,10 +164,10 @@ class SchedulerCommand extends ConsoleCommand
$table->setRows($rows);
$table->render();
} elseif ($jobid = $this->input->getOption('run')) {
$io->title('Force Run Job: ' . $jobid);
} elseif ($run !== false && $run !== null) {
$io->title('Force Run Job: ' . $run);
$job = $scheduler->getJob($jobid);
$job = $scheduler->getJob($run);
if ($job) {
$job->inForeground()->run();
@@ -177,13 +182,13 @@ class SchedulerCommand extends ConsoleCommand
$output = $job->getOutput();
if ($output) {
$this->output->write($output);
$io->write($output);
}
} else {
$error = 1;
$io->error('Could not find a job with id: ' . $jobid);
$io->error('Could not find a job with id: ' . $run);
}
} elseif ($this->input->getOption('install')) {
} elseif ($input->getOption('install')) {
$io->title('Install Scheduler');
$verb = 'install';
@@ -206,9 +211,10 @@ class SchedulerCommand extends ConsoleCommand
}
} else {
// Run scheduler
$scheduler->run();
$force = $run === null;
$scheduler->run(null, $force);
if ($this->input->getOption('verbose')) {
if ($input->getOption('verbose')) {
$io->title('Running Scheduled Jobs');
$io->text($scheduler->getVerboseOutput());
}

View File

@@ -11,16 +11,15 @@ namespace Grav\Console\Cli;
use Grav\Common\Grav;
use Grav\Common\Security;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Style\SymfonyStyle;
use function count;
/**
* Class SecurityCommand
* @package Grav\Console\Cli
*/
class SecurityCommand extends ConsoleCommand
class SecurityCommand extends GravCommand
{
/** @var ProgressBar $progress */
protected $progress;
@@ -43,19 +42,19 @@ class SecurityCommand extends ConsoleCommand
{
$this->initializePages();
$io = $this->getIO();
/** @var Grav $grav */
$grav = Grav::instance();
$this->progress = new ProgressBar($this->output, count($grav['pages']->routes()) - 1);
$this->progress = $io->createProgressBar(count($grav['pages']->routes()) - 1);
$this->progress->setFormat('Scanning <cyan>%current%</cyan> pages [<green>%bar%</green>] <white>%percent:3s%%</white> %elapsed:6s%');
$this->progress->setBarWidth(100);
$io = new SymfonyStyle($this->input, $this->output);
$io->title('Grav Security Check');
$io->newline(2);
$output = Security::detectXssFromPages($grav['pages'], false, [$this, 'outputProgress']);
$io->newline(2);
$error = 0;
if (!empty($output)) {
$counter = 1;
@@ -82,7 +81,7 @@ class SecurityCommand extends ConsoleCommand
* @param array $args
* @return void
*/
public function outputProgress($args): void
public function outputProgress(array $args): void
{
switch ($args['type']) {
case 'count':

View File

@@ -10,9 +10,8 @@
namespace Grav\Console\Cli;
use Grav\Common\Utils;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
@@ -20,7 +19,7 @@ use Symfony\Component\Process\Process;
* Class ServerCommand
* @package Grav\Console\Cli
*/
class ServerCommand extends ConsoleCommand
class ServerCommand extends GravCommand
{
const SYMFONY_SERVER = 'Symfony Server';
const PHP_SERVER = 'Built-in PHP Server';
@@ -29,8 +28,6 @@ class ServerCommand extends ConsoleCommand
protected $ip;
/** @var int */
protected $port;
/** @var SymfonyStyle */
protected $io;
/**
* @return void
@@ -51,7 +48,8 @@ class ServerCommand extends ConsoleCommand
*/
protected function serve(): int
{
$io = $this->io = new SymfonyStyle($this->input, $this->output);
$input = $this->getInput();
$io = $this->getIO();
$io->title('Grav Web Server');
@@ -59,15 +57,15 @@ class ServerCommand extends ConsoleCommand
ini_set('cli_server.color', 'on');
// Options
$force_symfony = $this->input->getOption('symfony');
$force_php = $this->input->getOption('php');
$force_symfony = $input->getOption('symfony');
$force_php = $input->getOption('php');
// Find PHP
$executableFinder = new PhpExecutableFinder();
$php = $executableFinder->find(false);
$this->ip = '127.0.0.1';
$this->port = (int)($this->input->getOption('port') ?? 8000);
$this->port = (int)($input->getOption('port') ?? 8000);
// Get an open port
while (!$this->portAvailable($this->ip, $this->port)) {
@@ -92,16 +90,12 @@ class ServerCommand extends ConsoleCommand
$error = 0;
foreach ($commands as $name => $command) {
$process = $this->runProcess($name, $command);
if (!$process) {
$io->note('Starting ' . $name . '...');
}
// Should only get here if there's an error running
if (!$process->isRunning() && (
($name === self::SYMFONY_SERVER && $force_symfony) ||
($name === self::PHP_SERVER)
)) {
if (!$process->isRunning() && (($name === self::SYMFONY_SERVER && $force_symfony) || ($name === self::PHP_SERVER))) {
$error = 1;
$io->error('Could not start ' . $name);
}
@@ -115,23 +109,25 @@ class ServerCommand extends ConsoleCommand
* @param array $cmd
* @return Process
*/
protected function runProcess($name, $cmd)
protected function runProcess(string $name, array $cmd): Process
{
$io = $this->getIO();
$process = new Process($cmd);
$process->setTimeout(0);
$process->start();
if ($name === self::SYMFONY_SERVER && Utils::contains($process->getErrorOutput(), 'symfony: not found')) {
$this->io->error('The symfony binary could not be found, please install the CLI tools: https://symfony.com/download');
$this->io->warning('Falling back to PHP web server...');
$io->error('The symfony binary could not be found, please install the CLI tools: https://symfony.com/download');
$io->warning('Falling back to PHP web server...');
}
if ($name === self::PHP_SERVER) {
$this->io->success('Built-in PHP web server listening on http://' . $this->ip . ':' . $this->port . ' (PHP v' . PHP_VERSION . ')');
$io->success('Built-in PHP web server listening on http://' . $this->ip . ':' . $this->port . ' (PHP v' . PHP_VERSION . ')');
}
$process->wait(function ($type, $buffer) {
$this->output->write($buffer);
$this->getIO()->write($buffer);
});
return $process;
@@ -144,7 +140,7 @@ class ServerCommand extends ConsoleCommand
* @param int $port
* @return bool
*/
protected function portAvailable($ip, $port): bool
protected function portAvailable(string $ip, int $port): bool
{
$fp = @fsockopen($ip, $port, $errno, $errstr, 0.1);
if (!$fp) {

View File

@@ -10,7 +10,7 @@
namespace Grav\Console\Cli;
use Grav\Common\Helpers\YamlLinter;
use Grav\Console\ConsoleCommand;
use Grav\Console\GravCommand;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -18,7 +18,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
* Class YamlLinterCommand
* @package Grav\Console\Cli
*/
class YamlLinterCommand extends ConsoleCommand
class YamlLinterCommand extends GravCommand
{
/**
* @return void
@@ -27,12 +27,6 @@ class YamlLinterCommand extends ConsoleCommand
{
$this
->setName('yamllinter')
->addOption(
'env',
'e',
InputOption::VALUE_OPTIONAL,
'The environment to trigger a specific configuration. For example: localhost, mysite.dev, www.mysite.com'
)
->addOption(
'all',
'a',
@@ -54,12 +48,13 @@ class YamlLinterCommand extends ConsoleCommand
*/
protected function serve(): int
{
$io = new SymfonyStyle($this->input, $this->output);
$input = $this->getInput();
$io = $this->getIO();
$io->title('Yaml Linter');
$error = 0;
if ($this->input->getOption('all')) {
if ($input->getOption('all')) {
$io->section('All');
$errors = YamlLinter::lint('');
@@ -69,7 +64,7 @@ class YamlLinterCommand extends ConsoleCommand
$error = 1;
$this->displayErrors($errors, $io);
}
} elseif ($folder = $this->input->getOption('folder')) {
} elseif ($folder = $input->getOption('folder')) {
$io->section($folder);
$errors = YamlLinter::lint($folder);
@@ -119,7 +114,7 @@ class YamlLinterCommand extends ConsoleCommand
* @param SymfonyStyle $io
* @return void
*/
protected function displayErrors($errors, SymfonyStyle $io): void
protected function displayErrors(array $errors, SymfonyStyle $io): void
{
$io->error('YAML Linting issues found...');
foreach ($errors as $path => $error) {

View File

@@ -9,10 +9,6 @@
namespace Grav\Console;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Processors\InitializeProcessor;
use RocketTheme\Toolbox\Event\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -25,18 +21,10 @@ class ConsoleCommand extends Command
{
use ConsoleTrait;
/** @var bool */
private $plugins_initialized = false;
/** @var bool */
private $themes_initialized = false;
/** @var bool */
private $pages_initialized = false;
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int|null|void
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
@@ -55,147 +43,4 @@ class ConsoleCommand extends Command
// Return error.
return 1;
}
/**
* Initialize Grav.
*
* - Load configuration
* - Initialize logger
* - Disable debugger
* - Set timezone, locale
* - Load plugins (call PluginsLoadedEvent)
* - Set Pages and Users type to be used in the site
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeGrav()
{
InitializeProcessor::initializeCli(Grav::instance());
return $this;
}
/**
* Set language to be used in CLI.
*
* @param string|null $code
* @return $this
*/
final protected function setLanguage(string $code = null)
{
$this->initializeGrav();
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
if ($language->enabled()) {
if ($code && $language->validate($code)) {
$language->setActive($code);
} else {
$language->setActive($language->getDefault());
}
}
return $this;
}
/**
* Properly initialize plugins.
*
* - call $this->initializeGrav()
* - call onPluginsInitialized event
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePlugins()
{
if (!$this->plugins_initialized) {
$this->plugins_initialized = true;
$this->initializeGrav();
// Initialize plugins.
$grav = Grav::instance();
$grav['plugins']->init();
$grav->fireEvent('onPluginsInitialized');
}
return $this;
}
/**
* Properly initialize themes.
*
* - call $this->initializePlugins()
* - initialize theme (call onThemeInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeThemes()
{
if (!$this->themes_initialized) {
$this->themes_initialized = true;
$this->initializePlugins();
// Initialize themes.
$grav = Grav::instance();
$grav['themes']->init();
}
return $this;
}
/**
* Properly initialize pages.
*
* - call $this->initializeThemes()
* - initialize assets (call onAssetsInitialized event)
* - initialize twig (calls the twig events)
* - initialize pages (calls onPagesInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePages()
{
if (!$this->pages_initialized) {
$this->pages_initialized = true;
$this->initializeThemes();
$grav = Grav::instance();
// Initialize assets.
$grav['assets']->init();
$grav->fireEvent('onAssetsInitialized');
// Initialize twig.
$grav['twig']->init();
// Initialize pages.
$pages = $grav['pages'];
$pages->init();
$grav->fireEvent('onPagesInitialized', new Event(['pages' => $pages]));
}
return $this;
}
/**
* @return void
*/
protected function displayGPMRelease()
{
$this->output->writeln('');
$this->output->writeln('GPM Releases Configuration: <yellow>' . ucfirst(Grav::instance()['config']->get('system.gpm.releases')) . '</yellow>');
$this->output->writeln('');
}
}

View File

@@ -13,12 +13,16 @@ use Exception;
use Grav\Common\Cache;
use Grav\Common\Grav;
use Grav\Common\Composer;
use Grav\Common\Language\Language;
use Grav\Common\Processors\InitializeProcessor;
use Grav\Console\Cli\ClearCacheCommand;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Trait ConsoleTrait
@@ -28,16 +32,20 @@ trait ConsoleTrait
{
/** @var string */
protected $argv;
/* @var InputInterface $output */
/** @var InputInterface */
protected $input;
/* @var OutputInterface $output */
/** @var SymfonyStyle */
protected $output;
/** @var array */
protected $local_config;
/** @var bool */
private $plugins_initialized = false;
/** @var bool */
private $themes_initialized = false;
/** @var bool */
private $pages_initialized = false;
/**
* Set colors style definition for the formatter.
*
@@ -47,21 +55,195 @@ trait ConsoleTrait
*/
public function setupConsole(InputInterface $input, OutputInterface $output)
{
// Initialize cache with CLI compatibility
Grav::instance()['config']->set('system.cache.cli_compatibility', true);
Grav::instance()['cache'];
$this->argv = $_SERVER['argv'][0];
$this->input = $input;
$this->output = $output;
$this->input = $input;
$this->output = new SymfonyStyle($input, $output);
$this->output->getFormatter()->setStyle('normal', new OutputFormatterStyle('white'));
$this->output->getFormatter()->setStyle('yellow', new OutputFormatterStyle('yellow', null, array('bold')));
$this->output->getFormatter()->setStyle('red', new OutputFormatterStyle('red', null, array('bold')));
$this->output->getFormatter()->setStyle('cyan', new OutputFormatterStyle('cyan', null, array('bold')));
$this->output->getFormatter()->setStyle('green', new OutputFormatterStyle('green', null, array('bold')));
$this->output->getFormatter()->setStyle('magenta', new OutputFormatterStyle('magenta', null, array('bold')));
$this->output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', null, array('bold')));
$this->setupGrav();
}
public function getInput(): InputInterface
{
return $this->input;
}
/**
* @return SymfonyStyle
*/
public function getIO(): SymfonyStyle
{
return $this->output;
}
/**
* Adds an option.
*
* @param string $name The option name
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
* @param string $description A description text
* @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE)
* @return $this
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null)
{
if ($name !== 'env' && $name !== 'lang') {
parent::addOption($name, $shortcut, $mode, $description, $default);
}
return $this;
}
/**
* @return void
*/
final protected function setupGrav(): void
{
try {
$language = $this->input->getOption('lang');
if ($language) {
// Set used language.
$this->setLanguage($language);
}
} catch (InvalidArgumentException $e) {}
// Initialize cache with CLI compatibility
$grav = Grav::instance();
$grav['config']->set('system.cache.cli_compatibility', true);
}
/**
* Initialize Grav.
*
* - Load configuration
* - Initialize logger
* - Disable debugger
* - Set timezone, locale
* - Load plugins (call PluginsLoadedEvent)
* - Set Pages and Users type to be used in the site
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeGrav()
{
InitializeProcessor::initializeCli(Grav::instance());
return $this;
}
/**
* Set language to be used in CLI.
*
* @param string|null $code
* @return $this
*/
final protected function setLanguage(string $code = null)
{
$this->initializeGrav();
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
if ($language->enabled()) {
if ($code && $language->validate($code)) {
$language->setActive($code);
} else {
$language->setActive($language->getDefault());
}
}
return $this;
}
/**
* Properly initialize plugins.
*
* - call $this->initializeGrav()
* - call onPluginsInitialized event
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePlugins()
{
if (!$this->plugins_initialized) {
$this->plugins_initialized = true;
$this->initializeGrav();
// Initialize plugins.
$grav = Grav::instance();
$grav['plugins']->init();
$grav->fireEvent('onPluginsInitialized');
}
return $this;
}
/**
* Properly initialize themes.
*
* - call $this->initializePlugins()
* - initialize theme (call onThemeInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializeThemes()
{
if (!$this->themes_initialized) {
$this->themes_initialized = true;
$this->initializePlugins();
// Initialize themes.
$grav = Grav::instance();
$grav['themes']->init();
}
return $this;
}
/**
* Properly initialize pages.
*
* - call $this->initializeThemes()
* - initialize assets (call onAssetsInitialized event)
* - initialize twig (calls the twig events)
* - initialize pages (calls onPagesInitialized event)
*
* Safe to be called multiple times.
*
* @return $this
*/
final protected function initializePages()
{
if (!$this->pages_initialized) {
$this->pages_initialized = true;
$this->initializeThemes();
$grav = Grav::instance();
// Initialize assets.
$grav['assets']->init();
$grav->fireEvent('onAssetsInitialized');
// Initialize twig.
$grav['twig']->init();
// Initialize pages.
$pages = $grav['pages'];
$pages->init();
$grav->fireEvent('onPagesInitialized', new Event(['pages' => $pages]));
}
return $this;
}
/**
@@ -70,27 +252,29 @@ trait ConsoleTrait
*/
public function isGravInstance($path)
{
$io = $this->getIO();
if (!file_exists($path)) {
$this->output->writeln('');
$this->output->writeln("<red>ERROR</red>: Destination doesn't exist:");
$this->output->writeln(" <white>$path</white>");
$this->output->writeln('');
$io->writeln('');
$io->writeln("<red>ERROR</red>: Destination doesn't exist:");
$io->writeln(" <white>$path</white>");
$io->writeln('');
exit;
}
if (!is_dir($path)) {
$this->output->writeln('');
$this->output->writeln("<red>ERROR</red>: Destination chosen to install is not a directory:");
$this->output->writeln(" <white>$path</white>");
$this->output->writeln('');
$io->writeln('');
$io->writeln("<red>ERROR</red>: Destination chosen to install is not a directory:");
$io->writeln(" <white>$path</white>");
$io->writeln('');
exit;
}
if (!file_exists($path . DS . 'index.php') || !file_exists($path . DS . '.dependencies') || !file_exists($path . DS . 'system' . DS . 'config' . DS . 'system.yaml')) {
$this->output->writeln('');
$this->output->writeln('<red>ERROR</red>: Destination chosen to install does not appear to be a Grav instance:');
$this->output->writeln(" <white>$path</white>");
$this->output->writeln('');
$io->writeln('');
$io->writeln('<red>ERROR</red>: Destination chosen to install does not appear to be a Grav instance:');
$io->writeln(" <white>$path</white>");
$io->writeln('');
exit;
}
}
@@ -109,7 +293,6 @@ trait ConsoleTrait
/**
* @param array $all
*
* @return int
* @throws Exception
*/

View File

@@ -16,12 +16,12 @@ use Grav\Common\Filesystem\Folder;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Console\ConsoleCommand;
use Grav\Console\GpmCommand;
use RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use ZipArchive;
use function is_array;
use function is_callable;
@@ -29,11 +29,10 @@ use function is_callable;
* Class DirectInstallCommand
* @package Grav\Console\Gpm
*/
class DirectInstallCommand extends ConsoleCommand
class DirectInstallCommand extends GpmCommand
{
/** @var string */
protected $all_yes;
/** @var string */
protected $destination;
@@ -72,8 +71,9 @@ class DirectInstallCommand extends ConsoleCommand
*/
protected function serve(): int
{
if (!class_exists(\ZipArchive::class)) {
$io = new SymfonyStyle($this->input, $this->output);
$input = $this->getInput();
$io = $this->getIO();
if (!class_exists(ZipArchive::class)) {
$io->title('Direct Install');
$io->error('php-zip extension needs to be enabled!');
@@ -81,90 +81,89 @@ class DirectInstallCommand extends ConsoleCommand
}
// Making sure the destination is usable
$this->destination = realpath($this->input->getOption('destination'));
$this->destination = realpath($input->getOption('destination'));
if (!Installer::isGravInstance($this->destination) ||
!Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK])
) {
$this->output->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
$io->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
return 1;
}
$this->all_yes = $this->input->getOption('all-yes');
$this->all_yes = $input->getOption('all-yes');
$package_file = $this->input->getArgument('package-file');
$package_file = $input->getArgument('package-file');
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion("Are you sure you want to direct-install <cyan>{$package_file}</cyan> [y|N] ", false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if (!$answer) {
$this->output->writeln('exiting...');
$this->output->writeln('');
$io->writeln('exiting...');
$io->newLine();
return 1;
}
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$tmp_zip = $tmp_dir . '/Grav-' . uniqid();
$tmp_zip = $tmp_dir . uniqid('/Grav-', false);
$this->output->writeln('');
$this->output->writeln("Preparing to install <cyan>{$package_file}</cyan>");
$io->newLine();
$io->writeln("Preparing to install <cyan>{$package_file}</cyan>");
if (Response::isRemote($package_file)) {
$this->output->write(' |- Downloading package... 0%');
$io->write(' |- Downloading package... 0%');
try {
$zip = GPM::downloadPackage($package_file, $tmp_zip);
} catch (RuntimeException $e) {
$this->output->writeln('');
$this->output->writeln(" `- <red>ERROR: {$e->getMessage()}</red>");
$this->output->writeln('');
$io->newLine();
$io->writeln(" `- <red>ERROR: {$e->getMessage()}</red>");
$io->newLine();
return 1;
}
if ($zip) {
$this->output->write("\x0D");
$this->output->write(' |- Downloading package... 100%');
$this->output->writeln('');
$io->write("\x0D");
$io->write(' |- Downloading package... 100%');
$io->newLine();
}
} else {
$this->output->write(' |- Copying package... 0%');
$io->write(' |- Copying package... 0%');
$zip = GPM::copyPackage($package_file, $tmp_zip);
if ($zip) {
$this->output->write("\x0D");
$this->output->write(' |- Copying package... 100%');
$this->output->writeln('');
$io->write("\x0D");
$io->write(' |- Copying package... 100%');
$io->newLine();
}
}
if (file_exists($zip)) {
$tmp_source = $tmp_dir . '/Grav-' . uniqid();
$tmp_source = $tmp_dir . uniqid('/Grav-', false);
$this->output->write(' |- Extracting package... ');
$io->write(' |- Extracting package... ');
$extracted = Installer::unZip($zip, $tmp_source);
if (!$extracted) {
$this->output->write("\x0D");
$this->output->writeln(' |- Extracting package... <red>failed</red>');
$io->write("\x0D");
$io->writeln(' |- Extracting package... <red>failed</red>');
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$this->output->write("\x0D");
$this->output->writeln(' |- Extracting package... <green>ok</green>');
$io->write("\x0D");
$io->writeln(' |- Extracting package... <green>ok</green>');
$type = GPM::getPackageType($extracted);
if (!$type) {
$this->output->writeln(" '- <red>ERROR: Not a valid Grav package</red>");
$this->output->writeln('');
$io->writeln(" '- <red>ERROR: Not a valid Grav package</red>");
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
@@ -187,14 +186,14 @@ class DirectInstallCommand extends ConsoleCommand
$dependencies[] = $dependency;
}
}
$this->output->writeln(' |- Dependencies found... <cyan>[' . implode(',', $dependencies) . ']</cyan>');
$io->writeln(' |- Dependencies found... <cyan>[' . implode(',', $dependencies) . ']</cyan>');
$question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if (!$answer) {
$this->output->writeln('exiting...');
$this->output->writeln('');
$io->writeln('exiting...');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
@@ -204,31 +203,31 @@ class DirectInstallCommand extends ConsoleCommand
}
if ($type === 'grav') {
$this->output->write(' |- Checking destination... ');
$io->write(' |- Checking destination... ');
Installer::isValidDestination(GRAV_ROOT . '/system');
if (Installer::IS_LINK === Installer::lastErrorCode()) {
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
$this->output->writeln(" '- <red>ERROR: symlinks found...</red> <yellow>" . GRAV_ROOT . '</yellow>');
$this->output->writeln('');
$io->write("\x0D");
$io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
$io->writeln(" '- <red>ERROR: symlinks found...</red> <yellow>" . GRAV_ROOT . '</yellow>');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... <green>ok</green>');
$io->write("\x0D");
$io->writeln(' |- Checking destination... <green>ok</green>');
$this->output->write(' |- Installing package... ');
$io->write(' |- Installing package... ');
$this->upgradeGrav($zip, $extracted);
} else {
$name = GPM::getPackageName($extracted);
if (!$name) {
$this->output->writeln('<red>ERROR: Name could not be determined.</red> Please specify with --name|-n');
$this->output->writeln('');
$io->writeln('<red>ERROR: Name could not be determined.</red> Please specify with --name|-n');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
@@ -238,24 +237,24 @@ class DirectInstallCommand extends ConsoleCommand
$install_path = GPM::getInstallPath($type, $name);
$is_update = file_exists($install_path);
$this->output->write(' |- Checking destination... ');
$io->write(' |- Checking destination... ');
Installer::isValidDestination(GRAV_ROOT . DS . $install_path);
if (Installer::lastErrorCode() === Installer::IS_LINK) {
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
$this->output->writeln(" '- <red>ERROR: symlink found...</red> <yellow>" . GRAV_ROOT . DS . $install_path . '</yellow>');
$this->output->writeln('');
$io->write("\x0D");
$io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
$io->writeln(" '- <red>ERROR: symlink found...</red> <yellow>" . GRAV_ROOT . DS . $install_path . '</yellow>');
$io->newLine();
Folder::delete($tmp_source);
Folder::delete($tmp_zip);
return 1;
}
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... <green>ok</green>');
$io->write("\x0D");
$io->writeln(' |- Checking destination... <green>ok</green>');
$this->output->write(' |- Installing package... ');
$io->write(' |- Installing package... ');
Installer::install(
$zip,
@@ -271,18 +270,18 @@ class DirectInstallCommand extends ConsoleCommand
Folder::delete($tmp_source);
$this->output->write("\x0D");
$io->write("\x0D");
if (Installer::lastErrorCode()) {
$this->output->writeln(" '- <red>" . Installer::lastErrorMsg() . '</red>');
$this->output->writeln('');
$io->writeln(" '- <red>" . Installer::lastErrorMsg() . '</red>');
$io->newLine();
} else {
$this->output->writeln(' |- Installing package... <green>ok</green>');
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
$io->writeln(' |- Installing package... <green>ok</green>');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
} else {
$this->output->writeln(" '- <red>ERROR: ZIP package could not be found</red>");
$io->writeln(" '- <red>ERROR: ZIP package could not be found</red>");
Folder::delete($tmp_zip);
return 1;
@@ -299,9 +298,10 @@ class DirectInstallCommand extends ConsoleCommand
/**
* @param string $zip
* @param string $folder
* @param false $keepFolder
* @param bool $keepFolder
* @return void
*/
private function upgradeGrav($zip, $folder, $keepFolder = false)
private function upgradeGrav(string $zip, string $folder, bool $keepFolder = false): void
{
static $ignores = [
'backup',

View File

@@ -13,7 +13,7 @@ use Grav\Common\GPM\Remote\Package;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Remote\Packages;
use Grav\Common\Utils;
use Grav\Console\ConsoleCommand;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -23,14 +23,12 @@ use function count;
* Class IndexCommand
* @package Grav\Console\Gpm
*/
class IndexCommand extends ConsoleCommand
class IndexCommand extends GpmCommand
{
/** @var Packages */
protected $data;
/** @var GPM */
protected $gpm;
/** @var array */
protected $options;
@@ -100,20 +98,21 @@ class IndexCommand extends ConsoleCommand
*/
protected function serve(): int
{
$this->options = $this->input->getOptions();
$input = $this->getInput();
$this->options = $input->getOptions();
$this->gpm = new GPM($this->options['force']);
$this->displayGPMRelease();
$this->data = $this->gpm->getRepository();
$data = $this->filter($this->data);
$io = new SymfonyStyle($this->input, $this->output);
$io = $this->getIO();
if (count($data) === 0) {
$io->writeln('No data was found in the GPM repository stored locally.');
$io->writeln('Please try clearing cache and running the <green>bin/gpm index -f</green> command again');
$io->writeln('If this doesn\'t work try tweaking your GPM system settings.');
$io->writeln('');
$io->newLine();
$io->writeln('For more help go to:');
$io->writeln(' -> <yellow>https://learn.getgrav.org/troubleshooting/common-problems#cannot-connect-to-the-gpm</yellow>');
@@ -126,8 +125,8 @@ class IndexCommand extends ConsoleCommand
$packages = $this->sort($packages);
if (!empty($packages)) {
$section = $this->output->section('Packages table');
$table = new Table($section);
$io->section('Packages table');
$table = new Table($io);
$table->setHeaders(['Count', 'Name', 'Slug', 'Version', 'Installed']);
$index = 0;
@@ -146,25 +145,24 @@ class IndexCommand extends ConsoleCommand
$table->render();
}
$io->writeln('');
$io->newLine();
}
$io->writeln('You can either get more informations about a package by typing:');
$io->writeln(" <green>{$this->argv} info <cyan><package></cyan></green>");
$io->writeln('');
$io->newLine();
$io->writeln('Or you can install a package by typing:');
$io->writeln(" <green>{$this->argv} install <cyan><package></cyan></green>");
$io->writeln('');
$io->newLine();
return 0;
}
/**
* @param Package $package
*
* @return string
*/
private function version($package)
private function version(Package $package): string
{
$list = $this->gpm->{'getUpdatable' . ucfirst($package->package_type)}();
$package = $list[$package->slug] ?? $package;
@@ -178,19 +176,14 @@ class IndexCommand extends ConsoleCommand
return "v<green>{$version}</green>";
}
if ($updatable) {
return "v<red>{$package->version}</red> <cyan>-></cyan> v<green>{$package->available}</green>";
}
return '';
return "v<red>{$package->version}</red> <cyan>-></cyan> v<green>{$package->available}</green>";
}
/**
* @param Package $package
*
* @return string
*/
private function installed($package)
private function installed(Package $package): string
{
$package = $list[$package->slug] ?? $package;
$type = ucfirst(preg_replace('/s$/', '', $package->package_type));
@@ -202,10 +195,9 @@ class IndexCommand extends ConsoleCommand
/**
* @param Packages $data
*
* @return Packages
*/
public function filter($data)
public function filter(Packages $data): Packages
{
// filtering and sorting
if ($this->options['plugins-only']) {
@@ -260,14 +252,13 @@ class IndexCommand extends ConsoleCommand
* @param Packages $packages
* @return Packages
*/
public function sort($packages)
public function sort(Packages $packages): Packages
{
foreach ($this->options['sort'] as $key) {
$packages = $packages->sort(function ($a, $b) use ($key) {
switch ($key) {
case 'author':
return strcmp($a->{$key}['name'], $b->{$key}['name']);
break;
default:
return strcmp($a->$key, $b->$key);
}

View File

@@ -10,23 +10,22 @@
namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Console\ConsoleCommand;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use function strlen;
/**
* Class InfoCommand
* @package Grav\Console\Gpm
*/
class InfoCommand extends ConsoleCommand
class InfoCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var GPM */
protected $gpm;
/** @var string */
protected $all_yes;
@@ -63,37 +62,40 @@ class InfoCommand extends ConsoleCommand
*/
protected function serve(): int
{
$this->gpm = new GPM($this->input->getOption('force'));
$input = $this->getInput();
$io = $this->getIO();
$this->all_yes = $this->input->getOption('all-yes');
$this->gpm = new GPM($input->getOption('force'));
$this->all_yes = $input->getOption('all-yes');
$this->displayGPMRelease();
$foundPackage = $this->gpm->findPackage($this->input->getArgument('package'));
$foundPackage = $this->gpm->findPackage($input->getArgument('package'));
if (!$foundPackage) {
$this->output->writeln("The package <cyan>'{$this->input->getArgument('package')}'</cyan> was not found in the Grav repository.");
$this->output->writeln('');
$this->output->writeln('You can list all the available packages by typing:');
$this->output->writeln(" <green>{$this->argv} index</green>");
$this->output->writeln('');
$io->writeln("The package <cyan>'{$input->getArgument('package')}'</cyan> was not found in the Grav repository.");
$io->newLine();
$io->writeln('You can list all the available packages by typing:');
$io->writeln(" <green>{$this->argv} index</green>");
$io->newLine();
return 1;
}
$this->output->writeln("Found package <cyan>'{$this->input->getArgument('package')}'</cyan> under the '<green>" . ucfirst($foundPackage->package_type) . "</green>' section");
$this->output->writeln('');
$this->output->writeln("<cyan>{$foundPackage->name}</cyan> [{$foundPackage->slug}]");
$this->output->writeln(str_repeat('-', \strlen($foundPackage->name) + \strlen($foundPackage->slug) + 3));
$this->output->writeln('<white>' . strip_tags($foundPackage->description_plain) . '</white>');
$this->output->writeln('');
$io->writeln("Found package <cyan>'{$input->getArgument('package')}'</cyan> under the '<green>" . ucfirst($foundPackage->package_type) . "</green>' section");
$io->newLine();
$io->writeln("<cyan>{$foundPackage->name}</cyan> [{$foundPackage->slug}]");
$io->writeln(str_repeat('-', strlen($foundPackage->name) + strlen($foundPackage->slug) + 3));
$io->writeln('<white>' . strip_tags($foundPackage->description_plain) . '</white>');
$io->newLine();
$packageURL = '';
if (isset($foundPackage->author['url'])) {
$packageURL = '<' . $foundPackage->author['url'] . '>';
}
$this->output->writeln('<green>' . str_pad(
$io->writeln('<green>' . str_pad(
'Author',
12
) . ':</green> ' . $foundPackage->author['name'] . ' <' . $foundPackage->author['email'] . '> ' . $packageURL);
@@ -125,7 +127,7 @@ class InfoCommand extends ConsoleCommand
}
$name = str_pad($name, 12);
$this->output->writeln("<green>{$name}:</green> {$data}");
$io->writeln("<green>{$name}:</green> {$data}");
}
}
@@ -136,54 +138,53 @@ class InfoCommand extends ConsoleCommand
// display current version if installed and different
if ($installed && $updatable) {
$local = $this->gpm->{'getInstalled'. $type}($foundPackage->slug);
$this->output->writeln('');
$this->output->writeln("Currently installed version: <magenta>{$local->version}</magenta>");
$this->output->writeln('');
$io->newLine();
$io->writeln("Currently installed version: <magenta>{$local->version}</magenta>");
$io->newLine();
}
// display changelog information
$questionHelper = $this->getHelper('question');
$question = new ConfirmationQuestion(
'Would you like to read the changelog? [y|N] ',
false
);
$answer = $this->all_yes ? true : $questionHelper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if ($answer) {
$changelog = $foundPackage->changelog;
$this->output->writeln('');
$io->newLine();
foreach ($changelog as $version => $log) {
$title = $version . ' [' . $log['date'] . ']';
$content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', static function ($match) {
return "\n" . ucfirst($match[1]) . ':';
}, $log['content']);
$this->output->writeln("<cyan>{$title}</cyan>");
$this->output->writeln(str_repeat('-', \strlen($title)));
$this->output->writeln($content);
$this->output->writeln('');
$io->writeln("<cyan>{$title}</cyan>");
$io->writeln(str_repeat('-', strlen($title)));
$io->writeln($content);
$io->newLine();
$question = new ConfirmationQuestion('Press [ENTER] to continue or [q] to quit ', true);
$answer = $this->all_yes ? false : $questionHelper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? false : $io->askQuestion($question);
if (!$answer) {
break;
}
$this->output->writeln('');
$io->newLine();
}
}
$this->output->writeln('');
$io->newLine();
if ($installed && $updatable) {
$this->output->writeln('You can update this package by typing:');
$this->output->writeln(" <green>{$this->argv} update</green> <cyan>{$foundPackage->slug}</cyan>");
$io->writeln('You can update this package by typing:');
$io->writeln(" <green>{$this->argv} update</green> <cyan>{$foundPackage->slug}</cyan>");
} else {
$this->output->writeln("You can install this package by typing:");
$this->output->writeln(" <green>{$this->argv} install</green> <cyan>{$foundPackage->slug}</cyan>");
$io->writeln('You can install this package by typing:');
$io->writeln(" <green>{$this->argv} install</green> <cyan>{$foundPackage->slug}</cyan>");
}
$this->output->writeln('');
$io->newLine();
return 0;
}

View File

@@ -18,43 +18,37 @@ use Grav\Common\GPM\Response;
use Grav\Common\GPM\Remote\Package;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Console\ConsoleCommand;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use ZipArchive;
use function array_key_exists;
use function count;
use function define;
\define('GIT_REGEX', '/http[s]?:\/\/(?:.*@)?(github|bitbucket)(?:.org|.com)\/.*\/(.*)/');
define('GIT_REGEX', '/http[s]?:\/\/(?:.*@)?(github|bitbucket)(?:.org|.com)\/.*\/(.*)/');
/**
* Class InstallCommand
* @package Grav\Console\Gpm
*/
class InstallCommand extends ConsoleCommand
class InstallCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var GPM */
protected $gpm;
/** @var string */
protected $destination;
/** @var string */
protected $file;
/** @var string */
protected $tmp;
/** @var bool */
protected $use_symlinks;
/** @var array */
protected $demo_processing = [];
/** @var string */
protected $all_yes;
@@ -98,7 +92,7 @@ class InstallCommand extends ConsoleCommand
*
* @param GPM $gpm
*/
public function setGpm(GPM $gpm)
public function setGpm(GPM $gpm): void
{
$this->gpm = $gpm;
}
@@ -108,45 +102,47 @@ class InstallCommand extends ConsoleCommand
*/
protected function serve(): int
{
if (!class_exists(\ZipArchive::class)) {
$io = new SymfonyStyle($this->input, $this->output);
$input = $this->getInput();
$io = $this->getIO();
if (!class_exists(ZipArchive::class)) {
$io->title('GPM Install');
$io->error('php-zip extension needs to be enabled!');
return 1;
}
$this->gpm = new GPM($this->input->getOption('force'));
$this->gpm = new GPM($input->getOption('force'));
$this->all_yes = $this->input->getOption('all-yes');
$this->all_yes = $input->getOption('all-yes');
$this->displayGPMRelease();
$this->destination = realpath($this->input->getOption('destination'));
$this->destination = realpath($input->getOption('destination'));
$packages = array_map('strtolower', $this->input->getArgument('package'));
$packages = array_map('strtolower', $input->getArgument('package'));
$this->data = $this->gpm->findPackages($packages);
$this->loadLocalConfig();
if (!Installer::isGravInstance($this->destination) ||
!Installer::isValidDestination($this->destination, [Installer::EXISTS, Installer::IS_LINK])
) {
$this->output->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
$io->writeln('<red>ERROR</red>: ' . Installer::lastErrorMsg());
return 1;
}
$this->output->writeln('');
$io->newLine();
if (!$this->data['total']) {
$this->output->writeln('Nothing to install.');
$this->output->writeln('');
$io->writeln('Nothing to install.');
$io->newLine();
return 0;
}
if (count($this->data['not_found'])) {
$this->output->writeln('These packages were not found on Grav: <red>' . implode(
$io->writeln('These packages were not found on Grav: <red>' . implode(
'</red>, <red>',
array_keys($this->data['not_found'])
) . '</red>');
@@ -157,24 +153,23 @@ class InstallCommand extends ConsoleCommand
if (null !== $this->local_config) {
// Symlinks available, ask if Grav should use them
$this->use_symlinks = false;
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Should Grav use the symlinks if available? [y|N] ', false);
$answer = $this->all_yes ? false : $helper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? false : $io->askQuestion($question);
if ($answer) {
$this->use_symlinks = true;
}
}
$this->output->writeln('');
$io->newLine();
try {
$dependencies = $this->gpm->getDependencies($packages);
} catch (Exception $e) {
//Error out if there are incompatible packages requirements and tell which ones, and what to do
//Error out if there is any error in parsing the dependencies and their versions, and tell which one is broken
$this->output->writeln("<red>{$e->getMessage()}</red>");
$io->writeln("<red>{$e->getMessage()}</red>");
return 1;
}
@@ -185,13 +180,13 @@ class InstallCommand extends ConsoleCommand
$this->installDependencies($dependencies, 'update', 'The following dependencies need to be updated...');
$this->installDependencies($dependencies, 'ignore', "The following dependencies can be updated as there is a newer version, but it's not mandatory...", false);
} catch (Exception $e) {
$this->output->writeln('<red>Installation aborted</red>');
$io->writeln('<red>Installation aborted</red>');
return 1;
}
$this->output->writeln('<green>Dependencies are OK</green>');
$this->output->writeln('');
$io->writeln('<green>Dependencies are OK</green>');
$io->newLine();
}
@@ -199,7 +194,7 @@ class InstallCommand extends ConsoleCommand
foreach ($this->data as $data) {
foreach ($data as $package_name => $package) {
if (array_key_exists($package_name, $dependencies)) {
$this->output->writeln("<green>Package {$package_name} already installed as dependency</green>");
$io->writeln("<green>Package {$package_name} already installed as dependency</green>");
} else {
$is_valid_destination = Installer::isValidDestination($this->destination . DS . $package->install_path);
if ($is_valid_destination || Installer::lastErrorCode() == Installer::NOT_FOUND) {
@@ -210,25 +205,24 @@ class InstallCommand extends ConsoleCommand
$this->askConfirmationIfMajorVersionUpdated($package);
$this->gpm->checkNoOtherPackageNeedsThisDependencyInALowerVersion($package->slug, $package->available, array_keys($data));
} catch (Exception $e) {
$this->output->writeln("<red>{$e->getMessage()}</red>");
$io->writeln("<red>{$e->getMessage()}</red>");
return 1;
}
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion("The package <cyan>{$package_name}</cyan> is already installed, overwrite? [y|N] ", false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if ($answer) {
$is_update = true;
$this->processPackage($package, $is_update);
} else {
$this->output->writeln("<yellow>Package {$package_name} not overwritten</yellow>");
$io->writeln("<yellow>Package {$package_name} not overwritten</yellow>");
}
} else {
if (Installer::lastErrorCode() == Installer::IS_LINK) {
$this->output->writeln("<red>Cannot overwrite existing symlink for </red><cyan>{$package_name}</cyan>");
$this->output->writeln('');
$io->writeln("<red>Cannot overwrite existing symlink for </red><cyan>{$package_name}</cyan>");
$io->newLine();
}
}
}
@@ -254,9 +248,9 @@ class InstallCommand extends ConsoleCommand
* @param Package $package
* @return void
*/
public function askConfirmationIfMajorVersionUpdated($package): void
public function askConfirmationIfMajorVersionUpdated(Package $package): void
{
$helper = $this->getHelper('question');
$io = $this->getIO();
$package_name = $package->name;
$new_version = $package->available ?: $this->gpm->getLatestVersionOfPackage($package->slug);
$old_version = $package->version;
@@ -265,14 +259,14 @@ class InstallCommand extends ConsoleCommand
if ($major_version_changed) {
if ($this->all_yes) {
$this->output->writeln("The package <cyan>{$package_name}</cyan> will be updated to a new major version <green>{$new_version}</green>, from <magenta>{$old_version}</magenta>");
$io->writeln("The package <cyan>{$package_name}</cyan> will be updated to a new major version <green>{$new_version}</green>, from <magenta>{$old_version}</magenta>");
return;
}
$question = new ConfirmationQuestion("The package <cyan>{$package_name}</cyan> will be updated to a new major version <green>{$new_version}</green>, from <magenta>{$old_version}</magenta>. Be sure to read what changed with the new major release. Continue? [y|N] ", false);
if (!$helper->ask($this->input, $this->output, $question)) {
$this->output->writeln("<yellow>Package {$package_name} not updated</yellow>");
if (!$io->askQuestion($question)) {
$io->writeln("<yellow>Package {$package_name} not updated</yellow>");
exit;
}
}
@@ -290,21 +284,20 @@ class InstallCommand extends ConsoleCommand
* @return void
* @throws Exception
*/
public function installDependencies($dependencies, $type, $message, $required = true): void
public function installDependencies(array $dependencies, string $type, string $message, bool $required = true): void
{
$io = $this->getIO();
$packages = array_filter($dependencies, static function ($action) use ($type) {
return $action === $type;
});
if (count($packages) > 0) {
$this->output->writeln($message);
$io->writeln($message);
foreach ($packages as $dependencyName => $dependencyVersion) {
$this->output->writeln(" |- Package <cyan>{$dependencyName}</cyan>");
$io->writeln(" |- Package <cyan>{$dependencyName}</cyan>");
}
$this->output->writeln('');
$helper = $this->getHelper('question');
$io->newLine();
if ($type === 'install') {
$questionAction = 'Install';
@@ -325,14 +318,14 @@ class InstallCommand extends ConsoleCommand
}
$question = new ConfirmationQuestion("${questionAction} {$questionArticle} {$questionNoun}? [Y|n] ", true);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if ($answer) {
foreach ($packages as $dependencyName => $dependencyVersion) {
$package = $this->gpm->findPackage($dependencyName);
$this->processPackage($package, $type === 'update');
}
$this->output->writeln('');
$io->newLine();
} elseif ($required) {
throw new Exception();
}
@@ -344,11 +337,13 @@ class InstallCommand extends ConsoleCommand
* @param bool $is_update True if the package is an update
* @return void
*/
private function processPackage($package, $is_update = false): void
private function processPackage(?Package $package, bool $is_update = false): void
{
$io = $this->getIO();
if (!$package) {
$this->output->writeln('<red>Package not found on the GPM!</red>');
$this->output->writeln('');
$io->writeln('<red>Package not found on the GPM!</red>');
$io->newLine();
return;
}
@@ -370,7 +365,7 @@ class InstallCommand extends ConsoleCommand
* @param Package $package
* @return void
*/
private function processDemo($package): void
private function processDemo(Package $package): void
{
$demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
if (file_exists($demo_dir)) {
@@ -384,8 +379,9 @@ class InstallCommand extends ConsoleCommand
* @param Package $package
* @return void
*/
private function installDemoContent($package): void
private function installDemoContent(Package $package): void
{
$io = $this->getIO();
$demo_dir = $this->destination . DS . $package->install_path . DS . '_demo';
if (file_exists($demo_dir)) {
@@ -393,15 +389,15 @@ class InstallCommand extends ConsoleCommand
$pages_dir = $dest_dir . DS . 'pages';
// Demo content exists, prompt to install it.
$this->output->writeln("<white>Attention: </white><cyan>{$package->name}</cyan> contains demo content");
$helper = $this->getHelper('question');
$io->writeln("<white>Attention: </white><cyan>{$package->name}</cyan> contains demo content");
$question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
$answer = $helper->ask($this->input, $this->output, $question);
$answer = $io->askQuestion($question);
if (!$answer) {
$this->output->writeln(" '- <red>Skipped!</red> ");
$this->output->writeln('');
$io->writeln(" '- <red>Skipped!</red> ");
$io->newLine();
return;
}
@@ -410,11 +406,11 @@ class InstallCommand extends ConsoleCommand
if (file_exists($demo_dir . DS . 'pages')) {
$pages_backup = 'pages.' . date('m-d-Y-H-i-s');
$question = new ConfirmationQuestion('This will backup your current `user/pages` folder to `user/' . $pages_backup . '`, continue? [y|N]', false);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? true : $io->askQuestion($question);
if (!$answer) {
$this->output->writeln(" '- <red>Skipped!</red> ");
$this->output->writeln('');
$io->writeln(" '- <red>Skipped!</red> ");
$io->newLine();
return;
}
@@ -422,18 +418,18 @@ class InstallCommand extends ConsoleCommand
// backup current pages folder
if (file_exists($dest_dir)) {
if (rename($pages_dir, $dest_dir . DS . $pages_backup)) {
$this->output->writeln(' |- Backing up pages... <green>ok</green>');
$io->writeln(' |- Backing up pages... <green>ok</green>');
} else {
$this->output->writeln(' |- Backing up pages... <red>failed</red>');
$io->writeln(' |- Backing up pages... <red>failed</red>');
}
}
}
// Confirmation received, copy over the data
$this->output->writeln(' |- Installing demo content... <green>ok</green> ');
$io->writeln(' |- Installing demo content... <green>ok</green> ');
Folder::rcopy($demo_dir, $dest_dir);
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
}
@@ -441,7 +437,7 @@ class InstallCommand extends ConsoleCommand
* @param Package $package
* @return array|false
*/
private function getGitRegexMatches($package)
private function getGitRegexMatches(Package $package)
{
if (isset($package->repository)) {
$repository = $package->repository;
@@ -458,7 +454,7 @@ class InstallCommand extends ConsoleCommand
* @param Package $package
* @return string|false
*/
private function getSymlinkSource($package)
private function getSymlinkSource(Package $package)
{
$matches = $this->getGitRegexMatches($package);
@@ -482,84 +478,88 @@ class InstallCommand extends ConsoleCommand
}
/**
* @param Package $package
* @param Package $package
* @return void
*/
private function processSymlink($package): void
private function processSymlink(Package $package): void
{
$io = $this->getIO();
exec('cd ' . $this->destination);
$to = $this->destination . DS . $package->install_path;
$from = $this->getSymlinkSource($package);
$this->output->writeln("Preparing to Symlink <cyan>{$package->name}</cyan>");
$this->output->write(' |- Checking source... ');
$io->writeln("Preparing to Symlink <cyan>{$package->name}</cyan>");
$io->write(' |- Checking source... ');
if (file_exists($from)) {
$this->output->writeln('<green>ok</green>');
$io->writeln('<green>ok</green>');
$this->output->write(' |- Checking destination... ');
$io->write(' |- Checking destination... ');
$checks = $this->checkDestination($package);
if (!$checks) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
} elseif (file_exists($to)) {
$this->output->writeln(" '- <red>Symlink cannot overwrite an existing package, please remove first</red>");
$this->output->writeln('');
$io->writeln(" '- <red>Symlink cannot overwrite an existing package, please remove first</red>");
$io->newLine();
} else {
symlink($from, $to);
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Symlinking package... <green>ok</green> ');
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
$io->writeln(' |- Symlinking package... <green>ok</green> ');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
return;
}
$this->output->writeln('<red>not found!</red>');
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$io->writeln('<red>not found!</red>');
$io->writeln(" '- <red>Installation failed or aborted.</red>");
}
/**
* @param Package $package
* @param bool $is_update
* @param Package $package
* @param bool $is_update
* @return bool
*/
private function processGpm($package, $is_update = false)
private function processGpm(Package $package, bool $is_update = false)
{
$io = $this->getIO();
$version = $package->available ?? $package->version;
$license = Licenses::get($package->slug);
$this->output->writeln("Preparing to install <cyan>{$package->name}</cyan> [v{$version}]");
$io->writeln("Preparing to install <cyan>{$package->name}</cyan> [v{$version}]");
$this->output->write(' |- Downloading package... 0%');
$io->write(' |- Downloading package... 0%');
$this->file = $this->downloadPackage($package, $license);
if (!$this->file) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
return false;
}
$this->output->write(' |- Checking destination... ');
$io->write(' |- Checking destination... ');
$checks = $this->checkDestination($package);
if (!$checks) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
} else {
$this->output->write(' |- Installing package... ');
$io->write(' |- Installing package... ');
$installation = $this->installPackage($package, $is_update);
if (!$installation) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
} else {
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
return true;
}
@@ -570,11 +570,13 @@ class InstallCommand extends ConsoleCommand
/**
* @param Package $package
* @param string|null $license
* @param string|null $license
* @return string|null
*/
private function downloadPackage($package, $license = null)
private function downloadPackage(Package $package, string $license = null)
{
$io = $this->getIO();
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/Grav-' . uniqid();
$filename = $package->slug . basename($package->zipball_url);
@@ -599,19 +601,19 @@ class InstallCommand extends ConsoleCommand
$output = Response::get($package->zipball_url . $query, [], [$this, 'progress']);
} catch (Exception $e) {
$error = str_replace("\n", "\n | '- ", $e->getMessage());
$this->output->write("\x0D");
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Downloading package... <red>error</red> ');
$this->output->writeln(" | '- " . $error);
$io->writeln(' |- Downloading package... <red>error</red> ');
$io->writeln(" | '- " . $error);
return null;
}
Folder::create($this->tmp);
$this->output->write("\x0D");
$this->output->write(' |- Downloading package... 100%');
$this->output->writeln('');
$io->write("\x0D");
$io->write(' |- Downloading package... 100%');
$io->newLine();
file_put_contents($this->tmp . DS . $filename, $output);
@@ -619,21 +621,21 @@ class InstallCommand extends ConsoleCommand
}
/**
* @param Package $package
* @param Package $package
* @return bool
*/
private function checkDestination($package): bool
private function checkDestination(Package $package): bool
{
$question_helper = $this->getHelper('question');
$io = $this->getIO();
Installer::isValidDestination($this->destination . DS . $package->install_path);
if (Installer::lastErrorCode() === Installer::IS_LINK) {
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
$io->write("\x0D");
$io->writeln(' |- Checking destination... <yellow>symbolic link</yellow>');
if ($this->all_yes) {
$this->output->writeln(" | '- <yellow>Skipped automatically.</yellow>");
$io->writeln(" | '- <yellow>Skipped automatically.</yellow>");
return false;
}
@@ -642,10 +644,10 @@ class InstallCommand extends ConsoleCommand
" | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ",
false
);
$answer = $question_helper->ask($this->input, $this->output, $question);
$answer = $io->askQuestion($question);
if (!$answer) {
$this->output->writeln(" | '- <red>You decided to not delete the symlink automatically.</red>");
$io->writeln(" | '- <red>You decided to not delete the symlink automatically.</red>");
return false;
}
@@ -653,8 +655,8 @@ class InstallCommand extends ConsoleCommand
unlink($this->destination . DS . $package->install_path);
}
$this->output->write("\x0D");
$this->output->writeln(' |- Checking destination... <green>ok</green>');
$io->write("\x0D");
$io->writeln(' |- Checking destination... <green>ok</green>');
return true;
}
@@ -663,11 +665,13 @@ class InstallCommand extends ConsoleCommand
* Install a package
*
* @param Package $package
* @param bool $is_update True if it's an update. False if it's an install
* @param bool $is_update True if it's an update. False if it's an install
* @return bool
*/
private function installPackage($package, $is_update = false)
private function installPackage(Package $package, bool $is_update = false): bool
{
$io = $this->getIO();
$type = $package->package_type;
Installer::install($this->file, $this->destination, ['install_path' => $package->install_path, 'theme' => $type === 'themes', 'is_update' => $is_update]);
@@ -675,24 +679,24 @@ class InstallCommand extends ConsoleCommand
Folder::delete($this->tmp);
if ($error_code) {
$this->output->write("\x0D");
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Installing package... <red>error</red> ');
$this->output->writeln(" | '- " . Installer::lastErrorMsg());
$io->writeln(' |- Installing package... <red>error</red> ');
$io->writeln(" | '- " . Installer::lastErrorMsg());
return false;
}
$message = Installer::getMessage();
if ($message) {
$this->output->write("\x0D");
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(" |- {$message}");
$io->writeln(" |- {$message}");
}
$this->output->write("\x0D");
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Installing package... <green>ok</green> ');
$io->writeln(' |- Installing package... <green>ok</green> ');
return true;
}
@@ -701,10 +705,12 @@ class InstallCommand extends ConsoleCommand
* @param array $progress
* @return void
*/
public function progress($progress): void
public function progress(array $progress): void
{
$this->output->write("\x0D");
$this->output->write(' |- Downloading package... ' . str_pad(
$io = $this->getIO();
$io->write("\x0D");
$io->write(' |- Downloading package... ' . str_pad(
$progress['percent'],
5,
' ',

View File

@@ -16,11 +16,11 @@ use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Common\GPM\Upgrader;
use Grav\Common\Grav;
use Grav\Console\ConsoleCommand;
use Grav\Console\GpmCommand;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use ZipArchive;
use function is_callable;
use function strlen;
@@ -28,29 +28,23 @@ use function strlen;
* Class SelfupgradeCommand
* @package Grav\Console\Gpm
*/
class SelfupgradeCommand extends ConsoleCommand
class SelfupgradeCommand extends GpmCommand
{
/** @var array */
protected $data;
/** @var string */
protected $file;
/** @var array */
protected $types = ['plugins', 'themes'];
/** @var string */
private $tmp;
/** @var Upgrader */
private $upgrader;
/** @var string */
protected $all_yes;
/** @var string */
protected $overwrite;
/** @var int */
protected $timeout;
@@ -96,18 +90,20 @@ class SelfupgradeCommand extends ConsoleCommand
*/
protected function serve(): int
{
if (!class_exists(\ZipArchive::class)) {
$io = new SymfonyStyle($this->input, $this->output);
$input = $this->getInput();
$io = $this->getIO();
if (!class_exists(ZipArchive::class)) {
$io->title('GPM Self Upgrade');
$io->error('php-zip extension needs to be enabled!');
return 1;
}
$this->upgrader = new Upgrader($this->input->getOption('force'));
$this->all_yes = $this->input->getOption('all-yes');
$this->overwrite = $this->input->getOption('overwrite');
$this->timeout = (int) $this->input->getOption('timeout');
$this->upgrader = new Upgrader($input->getOption('force'));
$this->all_yes = $input->getOption('all-yes');
$this->overwrite = $input->getOption('overwrite');
$this->timeout = (int) $input->getOption('timeout');
$this->displayGPMRelease();
@@ -118,28 +114,28 @@ class SelfupgradeCommand extends ConsoleCommand
$release = strftime('%c', strtotime($this->upgrader->getReleaseDate()));
if (!$this->upgrader->meetsRequirements()) {
$this->output->writeln('<red>ATTENTION:</red>');
$this->output->writeln(' Grav has increased the minimum PHP requirement.');
$this->output->writeln(' You are currently running PHP <red>' . phpversion() . '</red>, but PHP <green>' . $this->upgrader->minPHPVersion() . '</green> is required.');
$this->output->writeln(' Additional information: <white>http://getgrav.org/blog/changing-php-requirements</white>');
$this->output->writeln('');
$this->output->writeln('Selfupgrade aborted.');
$this->output->writeln('');
$io->writeln('<red>ATTENTION:</red>');
$io->writeln(' Grav has increased the minimum PHP requirement.');
$io->writeln(' You are currently running PHP <red>' . phpversion() . '</red>, but PHP <green>' . $this->upgrader->minPHPVersion() . '</green> is required.');
$io->writeln(' Additional information: <white>http://getgrav.org/blog/changing-php-requirements</white>');
$io->newLine();
$io->writeln('Selfupgrade aborted.');
$io->newLine();
return 1;
}
if (!$this->overwrite && !$this->upgrader->isUpgradable()) {
$this->output->writeln("You are already running the latest version of Grav (v{$local}) released on {$release}");
$io->writeln("You are already running the latest version of Grav (v{$local}) released on {$release}");
return 0;
}
Installer::isValidDestination(GRAV_ROOT . '/system');
if (Installer::IS_LINK === Installer::lastErrorCode()) {
$this->output->writeln('<red>ATTENTION:</red> Grav is symlinked, cannot upgrade, aborting...');
$this->output->writeln('');
$this->output->writeln("You are currently running a symbolically linked Grav v{$local}. Latest available is v{$remote}.");
$io->writeln('<red>ATTENTION:</red> Grav is symlinked, cannot upgrade, aborting...');
$io->newLine();
$io->writeln("You are currently running a symbolically linked Grav v{$local}. Latest available is v{$remote}.");
return 1;
}
@@ -147,65 +143,63 @@ class SelfupgradeCommand extends ConsoleCommand
// not used but preloaded just in case!
new ArrayInput([]);
$questionHelper = $this->getHelper('question');
$this->output->writeln("Grav v<cyan>{$remote}</cyan> is now available [release date: {$release}].");
$this->output->writeln('You are currently using v<cyan>' . GRAV_VERSION . '</cyan>.');
$io->writeln("Grav v<cyan>{$remote}</cyan> is now available [release date: {$release}].");
$io->writeln('You are currently using v<cyan>' . GRAV_VERSION . '</cyan>.');
if (!$this->all_yes) {
$question = new ConfirmationQuestion(
'Would you like to read the changelog before proceeding? [y|N] ',
false
);
$answer = $questionHelper->ask($this->input, $this->output, $question);
$answer = $io->askQuestion($question);
if ($answer) {
$changelog = $this->upgrader->getChangelog(GRAV_VERSION);
$this->output->writeln('');
$io->newLine();
foreach ($changelog as $version => $log) {
$title = $version . ' [' . $log['date'] . ']';
$content = preg_replace_callback('/\d\.\s\[\]\(#(.*)\)/', static function ($match) {
return "\n" . ucfirst($match[1]) . ':';
}, $log['content']);
$this->output->writeln($title);
$this->output->writeln(str_repeat('-', strlen($title)));
$this->output->writeln($content);
$this->output->writeln('');
$io->writeln($title);
$io->writeln(str_repeat('-', strlen($title)));
$io->writeln($content);
$io->newLine();
}
$question = new ConfirmationQuestion('Press [ENTER] to continue.', true);
$questionHelper->ask($this->input, $this->output, $question);
$io->askQuestion($question);
}
$question = new ConfirmationQuestion('Would you like to upgrade now? [y|N] ', false);
$answer = $questionHelper->ask($this->input, $this->output, $question);
$answer = $io->askQuestion($question);
if (!$answer) {
$this->output->writeln('Aborting...');
$io->writeln('Aborting...');
return 1;
}
}
$this->output->writeln('');
$this->output->writeln("Preparing to upgrade to v<cyan>{$remote}</cyan>..");
$io->newLine();
$io->writeln("Preparing to upgrade to v<cyan>{$remote}</cyan>..");
$this->output->write(" |- Downloading upgrade [{$this->formatBytes($update['size'])}]... 0%");
$io->write(" |- Downloading upgrade [{$this->formatBytes($update['size'])}]... 0%");
$this->file = $this->download($update);
$this->output->write(' |- Installing upgrade... ');
$io->write(' |- Installing upgrade... ');
$installation = $this->upgrade();
$error = 0;
if (!$installation) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
$io->writeln(" '- <red>Installation failed or aborted.</red>");
$io->newLine();
$error = 1;
} else {
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
$io->writeln(" '- <green>Success!</green> ");
$io->newLine();
}
// clear cache after successful upgrade
@@ -218,8 +212,10 @@ class SelfupgradeCommand extends ConsoleCommand
* @param array $package
* @return string
*/
private function download($package)
private function download(array $package): string
{
$io = $this->getIO();
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/Grav-' . uniqid('', false);
$options = [
@@ -235,9 +231,9 @@ class SelfupgradeCommand extends ConsoleCommand
Folder::create($this->tmp);
$this->output->write("\x0D");
$this->output->write(" |- Downloading upgrade [{$this->formatBytes($package['size'])}]... 100%");
$this->output->writeln('');
$io->write("\x0D");
$io->write(" |- Downloading upgrade [{$this->formatBytes($package['size'])}]... 100%");
$io->newLine();
file_put_contents($this->tmp . DS . $package['name'], $output);
@@ -247,8 +243,10 @@ class SelfupgradeCommand extends ConsoleCommand
/**
* @return bool
*/
private function upgrade()
private function upgrade(): bool
{
$io = $this->getIO();
if ($this->file) {
$folder = Installer::unZip($this->file, $this->tmp . '/zip');
} else {
@@ -264,17 +262,17 @@ class SelfupgradeCommand extends ConsoleCommand
}
if ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)) {
$this->output->write("\x0D");
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Installing upgrade... <red>error</red> ');
$this->output->writeln(" | '- " . Installer::lastErrorMsg());
$io->writeln(' |- Installing upgrade... <red>error</red> ');
$io->writeln(" | '- " . Installer::lastErrorMsg());
return false;
}
$this->output->write("\x0D");
$io->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(' |- Installing upgrade... <green>ok</green> ');
$io->writeln(' |- Installing upgrade... <green>ok</green> ');
return true;
}
@@ -283,10 +281,12 @@ class SelfupgradeCommand extends ConsoleCommand
* @param array $progress
* @return void
*/
public function progress($progress): void
public function progress(array $progress): void
{
$this->output->write("\x0D");
$this->output->write(" |- Downloading upgrade [{$this->formatBytes($progress['filesize']) }]... " . str_pad(
$io = $this->getIO();
$io->write("\x0D");
$io->write(" |- Downloading upgrade [{$this->formatBytes($progress['filesize']) }]... " . str_pad(
$progress['percent'],
5,
' ',
@@ -299,7 +299,7 @@ class SelfupgradeCommand extends ConsoleCommand
* @param int $precision
* @return string
*/
public function formatBytes($size, $precision = 2)
public function formatBytes($size, int $precision = 2): string
{
$base = log($size) / log(1024);
$suffixes = array('', 'k', 'M', 'G', 'T');
@@ -310,9 +310,10 @@ class SelfupgradeCommand extends ConsoleCommand
/**
* @param string $zip
* @param string $folder
* @param false $keepFolder
* @param bool $keepFolder
* @return void
*/
private function upgradeGrav($zip, $folder, $keepFolder = false)
private function upgradeGrav(string $zip, string $folder, bool $keepFolder = false): void
{
static $ignores = [
'backup',

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