Compare commits

..

335 Commits

Author SHA1 Message Date
Andy Miller
645285ca5c Merge branch 'release/1.1.14' 2017-01-18 15:13:13 -07:00
Andy Miller
c7fd01a644 Prepare for release 2017-01-18 15:12:14 -07:00
Andy Miller
61c0c31992 Fix to allow you to enable content-encoding: identity. Set to ‘none’ by default. #548 2017-01-18 15:02:53 -07:00
Matias Griese
e2ee02a71d Fixed page.collection() returning array and not Collection object when header variable did not exist 2017-01-18 10:42:13 +02:00
Andy Miller
4e283322ea Merge branch 'release/1.1.13' into develop 2017-01-17 13:20:08 -07:00
Andy Miller
b5b452e585 Merge branch 'release/1.1.13' 2017-01-17 13:20:07 -07:00
Andy Miller
138abdcab1 Prepare for release 2017-01-17 13:19:19 -07:00
Andy Miller
bf661c4355 Vendor updates 2017-01-17 12:06:59 -07:00
Flavio Copes
085ca323e2 Fix #1254 issue in trying to process broken symlink 2017-01-17 14:25:42 +01:00
Matias Griese
c22b28f312 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	CHANGELOG.md
2017-01-13 12:28:23 +02:00
Matias Griese
9b5ef4c263 Fixed all $_GET parameters missing in Nginx (please update your nginx.conf) [#1245](https://github.com/getgrav/grav/issues/1245) 2017-01-13 12:27:21 +02:00
Andy Miller
3cffe74965 Updated changelog 2017-01-12 11:55:10 -07:00
Andy Miller
759ba5143f Added ability to never cache twig. This makes it possible to cache content, but always process twig. Useful for regular but especially modular pages. (#1244) 2017-01-12 11:51:12 -07:00
Andy Miller
9ebff2287c Fixed an error in the ‘file’ type description 2017-01-12 10:59:05 -07:00
Andy Miller
e492fbde21 Hash added to blueprints 2017-01-12 06:40:51 -07:00
Andy Miller
7255556819 Fixed issue with multi-lang site caching non-lang specific pages for all langs #1211 2017-01-11 16:03:51 -07:00
Matias Griese
c718b8f32a Fixed broken hash method on page modifications detection 2017-01-09 21:23:11 +02:00
Andy Miller
07b66dd5d0 Vendor updates 2017-01-06 15:51:14 -07:00
Andy Miller
2e4686fada Updated changelog 2017-01-05 16:04:50 -07:00
ka7
e2544feeaf spelling fixes (#1236) 2017-01-05 16:02:45 -07:00
Andy Miller
970bf77492 Feature/theme dev improvements (#1232)
* Initial improvements to help theme development

* Added default language to site
2017-01-05 16:02:23 -07:00
Flavio Copes
0145f454b7 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	CHANGELOG.md
2017-01-05 16:25:01 +01:00
Flavio Copes
e984d9b68f Lint 2017-01-05 16:08:37 +01:00
Flavio Copes
1cd6773ded Fix renaming the folder name if the page, in the default language, had a custom slug set in its header 2017-01-05 16:08:25 +01:00
Matias Griese
0445aa707b Fixed issue with Content-Encoding: none. It should really be Content-Encoding: identity instead 2017-01-05 12:03:22 +02:00
Flavio Copes
cc96d160a4 Try rename before doing copy&delete 2017-01-04 16:43:56 +01:00
Flavio Copes
4331ab374e Fix issue with pages folders validation not accepting uppercase letters 2017-01-04 16:35:38 +01:00
Flavio Copes
61005360a5 Add a better fix for #635. Overwrites https://github.com/getgrav/grav/pull/1214 2017-01-03 21:39:57 +01:00
Flavio Copes
335c44385a Fixes #635 use mv instead of rename as that does not support cross volume operations (#1214)
* Fixes #635 use mv instead of rename as that does not support cross volume operations

* Handle case exec is disabled

* Handle case is windows, where mv does not work

* Move isWindows check to Utils
2017-01-03 09:28:25 +01:00
Nicolas Lœuillet
88e9ad3df2 Updated gitignore for user/config/security.yaml (#1231) 2017-01-02 20:51:33 -07:00
Andy Miller
2b19414598 Merge branch 'release/1.1.12' 2016-12-26 09:50:32 -07:00
Andy Miller
b00cd00259 Merge branch 'release/1.1.12' into develop 2016-12-26 09:50:32 -07:00
Andy Miller
6097431021 Prepare for release 2016-12-26 09:50:18 -07:00
Andy Miller
55a9356681 Fix for #1227 issue - Admin JSON calls throwing errors with debugger on 2016-12-26 09:47:51 -07:00
Flavio Copes
8aee946682 Fix changelog 2016-12-23 16:12:21 +01:00
Andy Miller
523d3a331a Merge branch 'release/1.1.11' into develop 2016-12-22 11:43:10 -07:00
Andy Miller
397107b611 Merge branch 'release/1.1.11' 2016-12-22 11:43:09 -07:00
Andy Miller
342af3deba Prepare for release 2016-12-22 11:42:56 -07:00
Flavio Copes
dc92498cd0 Fix #900 issue with modular pages folders validation 2016-12-22 17:36:53 +01:00
Andy Miller
364209a27d Fixed type on template not found 2016-12-21 15:16:30 -07:00
Andy Miller
46d741a2ed Merge branch 'release/1.1.10' 2016-12-21 13:06:53 -07:00
Andy Miller
b5be9ee3f0 Merge branch 'release/1.1.10' into develop 2016-12-21 13:06:53 -07:00
Andy Miller
e0f17a48d5 Prepare for release 2016-12-21 13:06:42 -07:00
Andy Miller
fa27856bc0 Reworked changelog 2016-12-21 12:50:01 -07:00
Andy Miller
befaf5d387 Improved theme/plugin detect logic 2016-12-21 12:35:58 -07:00
Flavio Copes
9571e992d9 Improve changelog line 2016-12-20 18:05:04 +01:00
Flavio Copes
e40bed5be2 Explicitly expose array_unique Twig filter 2016-12-20 17:37:32 +01:00
Flavio Copes
442249c3a1 Fix issue with Inflector when translation is disabled [https://github.com/getgrav/grav-plugin-simplesearch/issues/87](https://github.com/getgrav/grav-plugin-simplesearch/issues/87) 2016-12-19 21:07:43 +01:00
Flavio Copes
8b8d8bcc5b Fix https://github.com/getgrav/grav-plugin-admin/issues/891 Add pattern for frontend validation of folder slugs 2016-12-17 18:10:19 +01:00
Flavio Copes
d2152cb48e Changelog 2016-12-17 15:09:58 +01:00
Andy Miller
81fc0d47ac Reworked PHP CLI router PR #1218 (#1219) 2016-12-17 15:07:40 +01:00
ChrisGitIt
8450f77443 fixed array handling (#1208)
if the input array looks like this: 
array('valA','valB') (not a keyed array)

The return result before my change was:
array(array('valA'),array('valB'))

The return result after my change is:
array('valA','valB')
2016-12-16 12:09:23 -07:00
Flavio Copes
0ccc34d860 Improve detection of home path. Also allow ~/.grav on Windows, drop ConsoleTrait::isWindows() method, used just for that. (#1204)
* Improve detection of home path. Also allow ~/.grav on Windows, drop ConsoleTrait::isWindows() method, used just for that.

* Extract loadLocalConfig method to ConsoleTrait

* Fix issue with using Yaml::parse direcly on a filename, now deprecated

* Changelog
2016-12-16 19:10:00 +01:00
Flavio Copes
5b6452d89e Revert "Fix #635 use mv instead of rename as that does not support cross volume operations"
This reverts commit d61d260ef1.
2016-12-14 23:17:02 +01:00
Flavio Copes
d61d260ef1 Fix #635 use mv instead of rename as that does not support cross volume operations 2016-12-14 22:40:42 +01:00
Djamil Legato
1125b51f27 Fixed case where extracting a package would cause an error during rename 2016-12-13 18:45:34 -08:00
Andy Miller
bf552e22f1 Merge branch 'release/1.1.9' into develop 2016-12-13 13:06:16 -07:00
Andy Miller
b740142668 Merge branch 'release/1.1.9' 2016-12-13 13:06:15 -07:00
Andy Miller
d23f829559 prepare for release 2016-12-13 13:05:57 -07:00
Matias Griese
85bf215dc6 Changelog update 2016-12-12 09:37:02 +02:00
Matias Griese
9db04abd1c Add support for calling Media object as function to get medium by filename 2016-12-12 09:31:33 +02:00
Andy Miller
156f645576 Set Grav to be in 'testing' while in RC state 2016-12-11 18:22:54 -07:00
Flavio Copes
d3b654bdb0 Added checks before accessing admin reference during Page::blueprints() call 2016-12-10 18:34:13 +01:00
Andy Miller
7ed078ce31 unused 'use' statements 2016-12-08 16:10:44 -07:00
Andy Miller
3c6df48b8b Insight fixes 2016-12-08 15:49:45 -07:00
Matias Griese
833cd497bb YAML syntax fixes 2016-12-08 12:57:40 +02:00
Matias Griese
625f3d3a34 Better error handling in cache clear
Added new parameter `remove` for `onBeforeCacheClear` event
2016-12-08 12:57:18 +02:00
Andy Miller
380157f9cc prepare for release 2016-12-07 16:05:12 -07:00
Andy Miller
74c005d39c Added new onBeforeCacheClear event 2016-12-07 14:48:00 -07:00
Aaron Dalton
7b2716dab1 Added warning to command when new Grav version is available (#1194) 2016-12-07 14:16:00 -07:00
Djamil Legato
906c090bd4 Fixed issue with redirect of a page getting moved to a different location 2016-12-07 12:56:00 -08:00
Andy Miller
27ad9a24eb Updated vendor libraries 2016-12-07 11:51:56 -07:00
Matias Griese
83fdecbdd1 Added stream support for images (![Sepia Image](image://image.jpg?sepia))
Added stream support for links (`[Download PDF](user://data/pdf/my.pdf)`)
2016-12-07 19:51:06 +02:00
Flavio Copes
6c1a76b901 Use permissions field in group details. Fix saving permissions 2016-12-05 19:17:46 +01:00
Flavio Copes
4a5847784a Thanks to @hughbris for taxonomy filter issue 896fb8138b 2016-12-05 10:06:47 +01:00
Flavio Copes
896fb8138b Fix #1184 Fix issue with using a multiple taxonomy filter of which one had no results
If there are 2+ taxonomies required but one has 0 items, it was just
picking the ones corresponding to the other taxonomy, thus generating a
wrong result. Adding an empty array will later make array_intersect_key
generate an empty array if the operation is `and`
2016-12-03 13:47:25 +01:00
Andy Miller
678c445799 updated changelog 2016-12-02 10:27:39 -07:00
Fredrik Ekelund
36428e4735 Added ability to pass an array of explicit widths to ImageMedium#derivatives (#1133)
* Added ability to pass an array of explicit widths to ImageMedium#derivatives

Allows for more precise control than the min-width, max-width and
step parameters.

* ImageMedium#derivatives can now be called with an array from Markdown as well

Previously it was only possible from Twig code or PHP code
2016-12-02 10:21:59 -07:00
Vivien HAAG
10da784d53 Fix Page Collections problem with @page.modular (#1178) 2016-12-02 10:16:05 -07:00
Djamil Legato
c64cdb5dad Typos in changelog 2016-12-01 12:42:59 -08:00
Djamil Legato
52b68a0a1b Add range(int min, int max, int step) twig function to generate an array of numbers between min and max, inclusive 2016-12-01 12:40:21 -08:00
Flavio Copes
ee1742af1f Sync webserver-configs/htaccess.txt with .htaccess 2016-11-29 11:33:50 +01:00
Matias Griese
6315283a3a Fix publish date ordering 2016-11-29 12:29:16 +02:00
Flavio Copes
d8b3f215a2 Use new permissions field in user account 2016-11-28 19:00:25 +01:00
Flavio Copes
3838de1d97 If an array field has ignore_empty: true, only save options with a value 2016-11-28 18:59:41 +01:00
Flavio Copes
f6ddba52d8 Added alias selfupdate to the self-upgrade bin/gpm CLI command 2016-11-28 13:27:23 +01:00
Andy Miller
f7b35c3b79 Updated version 2016-11-26 20:22:33 -07:00
Flavio Copes
8dd65b709d Fix #946 issue with the sample nginx.conf, security section moved before PHP handler, and fixed backup folder uncorrectly named as backups 2016-11-25 22:32:14 +01:00
Flavio Copes
3064fe8ad9 Fix #713 Remove mappings missing if not cloned from github 2016-11-25 18:33:52 +01:00
Matias Griese
287a329a4d Added two new sort order options for pages: publish_date and unpublish_date 2016-11-24 15:03:51 +02:00
Flavio Copes
923b2469f9 Fix https://github.com/getgrav/grav-plugin-admin/issues/874 use updated class namespace 2016-11-24 13:50:15 +01:00
Flavio Copes
55bb4cf2fa Fix #1179 issue when we have a meta file without corresponding media
Also update changelog entries
2016-11-22 17:22:27 +01:00
Flavio Copes
5105be338a Decode single taxonomy params (#1164)
Allows to use commas in taxonomy terms
2016-11-19 11:32:54 -07:00
Amélie Turgeon
20e36c8a00 Add 2 new language values for French (#1174)
set the name and native name for these two.
(fr-FR and fr-CA)
2016-11-19 11:32:05 -07:00
Flavio Copes
9dd4f690a8 Allow to override sorting flags for page header-based or default ordering. (#1173)
If the `intl` PHP extension is loaded, only these flags are available:
https://secure.php.net/manual/en/collator.asort.php. Otherwise, you can
use the PHP standard sorting flags
(https://secure.php.net/manual/en/array.constants.php)
[#1169](https://github.com/getgrav/grav/issues/1169)
2016-11-19 11:29:28 -07:00
Matias Griese
9a15b5ebdc Multisite: Create image cache folder if it doesn't exist 2016-11-15 16:10:34 +02:00
Andy Miller
5c003d38be Ready for an RC.1 release 2016-11-09 10:58:13 -07:00
Andy Miller
95ab80b8f9 Added message service into core Grav 2016-11-09 10:48:36 -07:00
Andy Miller
079468c609 Added User::find() method 2016-11-08 11:17:01 -07:00
Andy Miller
7c98ca7134 Added Base32 encode/decode class 2016-11-07 12:19:04 -07:00
Matias Griese
fbf9c345b9 Fixed URI path in multi-site when query parameters were used in front page 2016-11-07 08:45:02 +02:00
Andy Miller
491e73eade Fixed CompiledJsonFile as it was not caching properly 2016-11-04 06:47:27 -06:00
Djamil Legato
e73773672b Removed internal docs reference for Folder::create and Folder::mkdir 2016-11-01 17:07:06 -07:00
Andy Miller
98d022ee49 Merge branch 'develop' of https://github.com/getgrav/grav into develop 2016-11-01 13:03:53 -06:00
Andy Miller
abca8ce433 CompiledJsonFile::content() should support $assoc 2016-11-01 13:03:43 -06:00
Flavio Copes
2dcd4aeaad Changelog 2016-11-01 13:54:06 +01:00
Fredrik Ekelund
58d4e3384e Use correct ratio when applying image filters to image alternatives (#1147)
Because of a previous change, the keys of the image alternatives array
is no longer the ratio, but rather the image width. We now make sure to
calculate the ratio correctly and use the appropriate one when applying
the image filters.
2016-11-01 13:52:50 +01:00
Andy Miller
76aed8a119 Added CompiledJsonFile 2016-10-28 13:49:11 -06:00
Flavio Copes
37b8ffb7d2 Fix #1135 return max available number of items when calling random() on a collection passing an int > available items 2016-10-27 21:04:25 +02:00
Flavio Copes
af53d79e5e Add Caddyfile for newer Caddy versions #1115 2016-10-25 18:58:52 +02:00
Flavio Copes
c3aa11abeb Changelog 2016-10-25 16:33:57 +02:00
Gilles van Eeden
8e1454b3ab functions added to taxonomy and twigextension (#1124)
* Update Taxonomy.php

* Update TwigExtension.php

* Update TwigExtension.php

Incorrect comment updated.
2016-10-25 16:32:18 +02:00
Flavio Copes
1c32f4eaee Changelog 2016-10-25 16:27:24 +02:00
Adam Roe
cd15b9197b Don't truncate HTML if content length is less than summary size (#1125), fixes #1114 2016-10-25 16:25:56 +02:00
Andy Miller
afc18236c2 Fixed issue with site redirects/routes not processing with extensions 2016-10-24 10:13:21 -06:00
Andy Miller
9bac4df02a Merge branch 'release/1.1.8' 2016-10-22 22:38:47 -06:00
Andy Miller
fb1c8eb80d Merge branch 'release/1.1.8' into develop 2016-10-22 22:38:47 -06:00
Andy Miller
19f3a24257 version update 2016-10-22 22:38:37 -06:00
Andy Miller
5bf95d8b87 Fixed an issues with unset SSL setting #1132 2016-10-22 22:36:58 -06:00
Andy Miller
7d7bb0d52a Merge branch 'release/1.1.7' 2016-10-22 20:48:51 -06:00
Andy Miller
0a459d256d Merge branch 'release/1.1.7' into develop 2016-10-22 20:48:51 -06:00
Andy Miller
6185edcc1b version update 2016-10-22 20:48:37 -06:00
Andy Miller
814c726323 Changelog updated 2016-10-22 20:47:58 -06:00
Fredrik Ekelund
db4c9c1844 ImageMedium#derivatives now works with image filters (#1107)
* ImageMedium->derivatives now works with image filters

Previously, using ImageMedium->derivatives would not work well in
combination with image filters or the other method of generating
srcset variants of images (by appending eg. "@2x" to their
filenames). This commit hopefully fixes that.

* Modified initialization of image alternatives

The biggest alternative will now become the base medium, and
alternatives will be filled out as necessary in a descending
manner.

* Fully reset image when derivatives method is called

Otherwise we get some funky results, with the possibility of two
different images being rendered between the full-width srcset
version and the original src version.

* Account for risk of original file not existing when generating image derivatives

* Fixed an issue where too many alternatives would be generated

When using naming conventions to generate image alternatives, this
patch would previously generate a “@1x” alternative if one didn’t
exist. That’s no longer the case

* Add an "@1x" alternative when an image lacks a base medium

When an image only has an alternative medium - ie. the only
version of the image ends in eg. "@3x", then we construct the
missing alternatives automatically. Previously, we would only do
this down till "@2x", meaning that the image that would have been
the base medium, had the image been manually resized, wasn't
created. This has now been fixed.

* Always make smallest image alternative the base medium

When an image lacks a base medium on disk (eg. the only existing
image is an @2x version), then we make a scaled down version the
base medium, which ensures that the smaller version is served up
in the src attribute in the HTML.

Also, don't reset the image alternatives when calling
ImageMedium#derivatives, instead only generate the image
alternatives that are missing.

* Set better prettynames for image derivatives

* Changed image derivatives prettynames to be width based

Instead of example2x.jpeg, we now have eg. example1280w.jpeg
2016-10-22 20:44:28 -06:00
Djamil Legato
a96820af36 Ensuring fallback to true for verify_peer, just in case 2016-10-20 11:01:11 -07:00
Djamil Legato
f2c6829cd9 Reverted change 2016-10-20 10:58:47 -07:00
Djamil Legato
790429e286 Only pass verify_peer settings to cURL and fopen if the setting is disabled 2016-10-20 10:54:05 -07:00
Andy Miller
b42366cad2 remote image test 2016-10-19 13:28:35 -06:00
Andy Miller
5069088501 Merge branch 'release/1.1.6' 2016-10-19 09:29:14 -06:00
Andy Miller
122db6330e Merge branch 'release/1.1.6' into develop 2016-10-19 09:29:14 -06:00
Andy Miller
cac93a73af version update 2016-10-19 09:29:00 -06:00
Andy Miller
f1692b20be vendor lib updates 2016-10-19 09:19:37 -06:00
Flavio Copes
855d4f73b9 Add ability to adjust images orientation using EXIF data (#555)
* Auto-adjust images orientation using EXIF data

* Fix composer.json

* Drop Excerpts edit

* Restore automatic orientation fix

* Revert "Restore automatic orientation fix"

This reverts commit 4b8af1fe72.

* Add auto orientation capability to images. Defaults to false to avoid forcing cache on every image

* Changelog
2016-10-19 06:46:07 -06:00
Andy Miller
92401de443 Fix for prettyname growing with operations 2016-10-14 17:51:13 -06:00
Andy Miller
31e358ca7c Merge branch 'feature/fixed_responsive_prettynames' into develop 2016-10-14 16:12:06 -06:00
Andy Miller
851dec76d2 Updated changelog 2016-10-14 16:11:49 -06:00
Andy Miller
19dfa4e011 Fixed responsive pretty names 2016-10-14 14:14:11 -06:00
Andy Miller
422735a1a2 Updated changelog 2016-10-12 18:06:26 -06:00
Benny
50c2ecbfdf ParsedownGravTrait addBlockType and addInlineType enhancements (#1062) 2016-10-12 18:04:32 -06:00
Andy Miller
5143941356 Tweaks for PR #1091 - using same 'display' setting and values 1,0,-1 for full backwards compatibility (even on save) 2016-10-12 17:46:56 -06:00
Andy Miller
05bd715d6c Tweaks to session split PR - #1096 2016-10-12 17:17:45 -06:00
Aaron Dalton
4f8ac36a9a Added 'system.errors.verbosity' that ideally would replace 'system.errors.display' eventually (#1091) 2016-10-12 17:10:58 -06:00
maxfrigge
6300ab8a03 feat(admin): add setting to split admin and front-end session (#1096) 2016-10-12 17:03:30 -06:00
Andy Miller
0cadb0cd90 Added onMediaLocate() event to add custom media locations for Excerpt support 2016-10-12 16:54:14 -06:00
Andy Miller
a9eb707d8b Added fast hash option for file checking. Checks all files and timestamps 2016-10-12 16:52:51 -06:00
Andy Miller
834505ee24 Composer update 2016-10-11 20:55:31 -06:00
Andy Miller
9a56bff1d4 Merge branch 'feature/breaking_out_link_and_image_logic' into develop 2016-10-11 15:56:35 -06:00
Andy Miller
4715ab7057 One more test 2016-10-11 15:53:31 -06:00
Andy Miller
4ed5f163ed Added an option to get just the route back from Uri::convertUrl() 2016-10-11 12:49:49 -06:00
Flavio Copes
e95c4db843 Avoid failing on files not listed in media.types, in which case fromFile returns null 2016-10-11 19:42:15 +02:00
Djamil Legato
efaf41c4e2 Reverted 21cd09e2a9 2016-10-11 09:58:09 -07:00
Djamil Legato
21cd09e2a9 Fixed jpeg file format support in Media 2016-10-10 14:58:42 -07:00
Andy Miller
8d84b94bc7 Some cleanup and a working test 2016-10-06 19:01:48 -06:00
Andy Miller
ce2b7d7175 Languages fixed again, was issue with type 2016-10-06 18:30:03 -06:00
Andy Miller
9a21792b27 Refactored into Excerpts helper but multilang broken 2016-10-06 18:05:56 -06:00
Andy Miller
110cd9535b Switched to stable toolbox 2016-10-06 16:51:02 -06:00
Andy Miller
a11e608463 Updated Vendor libs 2016-10-06 11:47:56 -06:00
Andy Miller
185acb4d2a updated some docblocks 2016-10-06 11:35:48 -06:00
Andy Miller
087ec7ebaf cleanup 2016-10-06 11:12:53 -06:00
Andy Miller
2a507ba994 cleanup 2016-10-06 11:11:03 -06:00
Andy Miller
1d852abad3 cleanup 2016-10-06 11:08:55 -06:00
Andy Miller
4762663507 cleanup 2016-10-06 11:03:36 -06:00
Andy Miller
e5524af557 cleanup 2016-10-06 10:58:27 -06:00
Andy Miller
5baec2dca5 Bit more flexible! 2016-10-05 21:49:09 -06:00
Andy Miller
5866379b92 some more helper stuff 2016-10-05 21:39:53 -06:00
Andy Miller
46a5567386 just dealing in excerpts for now 2016-10-05 20:57:00 -06:00
Andy Miller
4c6c9a722c initial attempt 2016-10-05 17:15:46 -06:00
Flavio Copes
bbcc627a70 Fix saving a group, use data 2016-10-04 16:41:07 +02:00
Andy Miller
0416956af8 Merge branch 'develop' of https://github.com/getgrav/grav into develop 2016-10-03 18:49:10 -06:00
Andy Miller
3a250d2744 cleaned up with regex constant 2016-10-03 18:48:43 -06:00
Flavio Copes
36e3b788a6 Add jQuery 3.x #1070 2016-10-03 17:18:00 +02:00
❤ rose shimada
c51a07c4e9 Adding xlsm to supported mime types (#1054)
Most of Excel family is there except for macro-capable xls in Office 2007. Tested and working to enable .xlsm file upload.
2016-10-03 16:56:59 +02:00
Flavio Copes
70a38d1d3a Avoid gitignoring any vendor folder in a Grav site subfolder (but still ignore the main vendor/ folder) 2016-10-03 16:22:03 +02:00
Flavio Copes
bc3943b386 Reformat 2016-10-03 16:03:01 +02:00
Andy Miller
c6f8fe259a Removed duplicate method Grav::mime() and used existing (renamed) Utils::getMimeByExtension(), also added Utils::getExtensionByMime() 2016-10-01 17:16:35 -06:00
Andy Miller
53baf47e58 Fixed an issue with inflectorFilter test 2016-09-30 19:11:25 -06:00
Andy Miller
29c6a94c92 Updated to Twig 1.25.0 and fixed an issue that resulted with streams in bin/plugin 2016-09-30 19:02:56 -06:00
Andy Miller
c0c77fff67 Added ability to override the twig template format (html, son, xml, etc) via page header #1067 2016-09-30 12:12:52 -06:00
Andy Miller
c3e74c2e09 Set Twig back to 1.24 until we can fix some issues 2016-09-30 09:02:49 -06:00
Andy Miller
9bd058e319 Added ability to set cache.enabled via plugin 2016-09-30 07:51:06 -06:00
Andy Miller
50ff5f0920 Don't support page folders that contain param_sep in the folder name - https://github.com/getgrav/grav-plugin-admin/issues/796 2016-09-29 17:03:28 -06:00
Andy Miller
d7dce7a6d7 Merge branch 'develop' of https://github.com/getgrav/grav into develop 2016-09-29 16:45:47 -06:00
Andy Miller
d1f87ca5d9 Updated vendor libraries 2016-09-29 16:45:36 -06:00
Andy Miller
956e5bd34f Fixed external page saving - https://github.com/getgrav/grav-plugin-admin/issues/789 2016-09-29 16:45:20 -06:00
Matias Griese
112b895d56 Add option not to set environment (CLI) 2016-09-29 11:48:04 +03:00
Djamil Legato
d824e8a934 Merge branch 'develop' of https://github.com/getgrav/grav into develop 2016-09-28 12:45:21 -07:00
Djamil Legato
3e02961c77 Ensure to always return an array from Licenses::get 2016-09-28 12:45:17 -07:00
Andy Miller
17b0dcc8fb updated changelog 2016-09-28 09:59:54 -06:00
Andy Miller
6f5b44be11 Added built-in check to see if in admin, and in correct route for the plugin 2016-09-28 09:57:54 -06:00
Djamil Legato
2938b08e3d Force Licenses slug to always be lowercase 2016-09-27 15:51:55 -07:00
Andy Miller
68c3287ad9 Refactored how file is processed 2016-09-27 13:22:17 -06:00
Djamil Legato
274fef2112 Validate license before adding it 2016-09-27 11:51:46 -07:00
Djamil Legato
0cc01d3355 Added new Licenses::validate method to validate the format of a License 2016-09-27 11:29:59 -07:00
Djamil Legato
ba6a32ad3f Better highlight for the troubleshooting link 2016-09-27 10:31:28 -07:00
Djamil Legato
a6ff929e22 If there is no repository data, display a nicer message with details on how to proceed to potentially fix it 2016-09-27 09:49:33 -07:00
Djamil Legato
840e27f20a Fixed missing check for Response when in CLI 2016-09-27 09:49:08 -07:00
Djamil Legato
007f4b8185 InstallCommand can now handle Licenses 2016-09-27 09:19:40 -07:00
Andy Miller
12659700af Tweaks to Licenses file 2016-09-26 18:06:55 -06:00
Andy Miller
37a65efd89 Fix for saving 2016-09-26 13:05:57 -06:00
Djamil Legato
4f7fb896cb Fixed missing $error_message 2016-09-24 14:54:28 -07:00
Djamil Legato
5d452578e2 Added new License class 2016-09-24 00:15:06 -07:00
Djamil Legato
08974738f1 Tweaked error output for Response 2016-09-23 16:11:48 -07:00
Djamil Legato
89070f0bbf More detailed error for cURL 2016-09-23 15:25:35 -07:00
Djamil Legato
18dff3f8e3 Updated changelog 2016-09-23 15:14:54 -07:00
Djamil Legato
4b43c39ff5 Response class now handles better unsuccessful requests such as 404 and 401 2016-09-23 15:09:04 -07:00
Djamil Legato
2eec82fb99 Fixed missing progress method in DirectInstall Command 2016-09-23 15:07:26 -07:00
Aaron Dalton
24ea511ad1 Added documented options to .htaccess to support detecting forwarded HTTPS (#1063)
support.
2016-09-23 15:36:40 -05:00
Benny
18463b958f Check for modular folder in Types::scanTemplates (#1061) 2016-09-23 12:46:08 -05:00
Djamil Legato
c4f71c9dda Fixed verify peer default value and issue with fopen 2016-09-22 17:59:52 -07:00
Bernhard Altendorfer
584f4efcb1 Added possiblity to connect to redis via a unix socket (#1055)
* Added possiblity to connect to redis via a unix socket

* Improved redis via socket config usage and added config option to blueprints

* Updated Changelog

* Improved redis via socket by adding a default value and a placeholder in the blueprint
2016-09-21 10:45:13 -06:00
Djamil Legato
afc7963644 SSL Verify Peer (#1053)
* GPM: SSL verify peer and method (auto|fopen|curl) are now settings

* Added Admin blueprints settings

* Fixed default verify_peer value

* Fixed lang references for verify_peer

* Minor fixes for improper comments default values
2016-09-19 11:21:16 -07:00
Djamil Legato
53f41d396e Another parsing error 2016-09-19 10:05:14 -07:00
Djamil Legato
f561f27332 Fixed Parsing error 2016-09-19 10:02:32 -07:00
Djamil Legato
744239ca76 Trying to move things around in Travis, to prevent PR from failing 2016-09-19 10:01:18 -07:00
Andy Miller
e942d1a1e6 Updated changelog to reflect new direct-install command 2016-09-19 06:41:53 -06:00
Andy Miller
a5430cda7b Fix for huge session timeouts #1050 2016-09-19 06:41:31 -06:00
Andy Miller
c57e43ea1d New bin/gpm direct-install command (#1038)
* initial push of DirectInstall command

* Refactored to support direct-install

* added info about dependencies, and continue question

* Cleanup per @w00fz comments

* put Grav destination check back.
2016-09-18 10:23:55 -06:00
Matias Griese
7710cba7ad Add batch() function to Page Collection class 2016-09-16 13:22:30 +03:00
Andy Miller
3047311652 Merge branch 'release/1.1.5' into develop 2016-09-09 16:29:16 -06:00
Andy Miller
3459fbc871 Merge branch 'release/1.1.5' 2016-09-09 16:29:15 -06:00
Andy Miller
370f683985 version update 2016-09-09 16:29:08 -06:00
Andy Miller
1baf19d486 Merge branch 'develop' of https://github.com/getgrav/grav into develop 2016-09-09 16:02:45 -06:00
Andy Miller
0e16a271b7 organization tweaks for system.yaml 2016-09-09 16:02:31 -06:00
Djamil Legato
4a756399f1 Updated composer 2016-09-09 11:57:06 -07:00
Djamil Legato
4401fbc6a6 Improved bin/plugin to list plugins with commands faster by limiting the depth of recursion 2016-09-09 11:56:02 -07:00
Flavio Copes
9a68f0784d Fix issue in calling page.summary when no content is present in a page
Related forum topic:
https://getgrav.org/forum#!/general:error-when-summary-is-emtpy
2016-09-09 14:50:12 +02:00
Matias Griese
8ca1d31b90 Twig variable base_url now supports multi-site by path feature 2016-09-09 13:24:49 +03:00
Matias Griese
cf3cd3d2d1 Follow symlinks in Folder::all() 2016-09-09 10:40:16 +03:00
Andy Miller
c16952a4c9 added changelog 2016-09-08 12:30:47 -06:00
Andy Miller
b6e785bd2a Refactored onPageNotFound event to fire after onPageInitialized 2016-09-08 12:30:34 -06:00
Andy Miller
da0dbeb6b3 update changelog 2016-09-08 09:11:00 -06:00
Andy Miller
76f5b99c52 Quietly skip missing streams on clear 2016-09-08 09:09:15 -06:00
Andy Miller
60986083dc Merge branch 'release/1.1.4' 2016-09-07 17:03:35 -06:00
Andy Miller
adec441065 Merge branch 'release/1.1.4' into develop 2016-09-07 17:03:35 -06:00
Andy Miller
fe8fb5fa42 version update 2016-09-07 17:03:19 -06:00
Andy Miller
d62de27f63 updated changelog 2016-09-07 16:41:19 -06:00
CSixtyFour
fd4c0d97a2 Update LanguageCodes.php (#1030)
added language orientation for a Persian and Urdu
2016-09-07 16:06:55 -06:00
Flavio Copes
2669e11c9d Move media blueprints out of core 2016-09-07 17:04:43 +02:00
Andy Miller
ee6b270776 Typo in GPM::install command 2016-09-06 18:16:28 -06:00
Andy Miller
9651ad7ef1 Updated version for testing RC plugins 2016-09-06 17:29:06 -06:00
Andy Miller
e19f2042bb Updated changelog 2016-09-06 15:42:02 -06:00
Andy Miller
a54f30b8ae Revamped the Html Truncator code to address issues with invalid HTML #1019 2016-09-06 15:38:36 -06:00
Andy Miller
10825d3f70 Updated to latest vendor libs 2016-09-03 11:12:40 -06:00
Andy Miller
fa35ba87e5 updated composer binary 2016-09-03 11:12:30 -06:00
Andy Miller
20c0b48070 -a 2016-09-03 09:34:18 -06:00
Andy Miller
09cae00038 Moved Image filter() call to end of save action #984 2016-09-02 15:38:00 -06:00
Djamil Legato
68557a8248 Merge branch 'develop' of https://github.com/getgrav/grav into develop 2016-09-02 14:18:38 -07:00
Djamil Legato
1021674f61 Remove no longer needed files validator 2016-09-02 14:18:35 -07:00
Andy Miller
3859d3149b updated vendor libs 2016-09-02 15:05:16 -06:00
Andy Miller
7eb76ee80c Fix for memcached connection #1020 2016-09-02 08:45:12 -06:00
Andy Miller
9b94ce6405 Allow environment to be passed to bin/gpm and bin/plugin commands 2016-09-01 15:44:43 -06:00
Andy Miller
9f2852a56e Merge branch 'develop' of https://github.com/getgrav/grav into develop 2016-09-01 12:54:47 -06:00
Andy Miller
69133c9118 181st day is in June! 2016-09-01 12:54:33 -06:00
Matias Griese
24ef246391 Add a way to set environment from CLI 2016-09-01 21:27:08 +03:00
Andy Miller
36882e09dd minor cleanup 2016-08-31 09:58:24 -06:00
Andy Miller
eb27e3e711 fix for bin/gpm index when filtered 2016-08-31 09:57:21 -06:00
Djamil Legato
dc882fffd8 Cleanup 2016-08-29 23:23:21 -07:00
Andy Miller
585a64f7ac Remove useless extra collection cmd check 2016-08-29 18:21:12 -06:00
Djamil Legato
1bfe99b9cd Allow Utils::setDotNotation to merge data, rather than just set. 2016-08-27 12:29:11 -07:00
Flavio Copes
b73f92c78c Changelog 2016-08-26 14:22:17 +02:00
Ole Vik
b787cdeda7 Use pages.markdown.extra in system.yaml (#1007)
Markdown Extra is incorrectly specified as `markdown_extra`, whereas it should be:

```
  markdown:
    extra: false
```
2016-08-26 14:21:16 +02:00
Andy Miller
773e6aef04 Updated to toolbox 2016-08-25 21:24:38 -06:00
Djamil Legato
ca5bfcaaed Fixed regression with Sessions and its path. Forcing $domain when creating a new session 2016-08-25 16:27:20 -07:00
Andy Miller
d29aa79996 cleanup 2016-08-25 15:49:40 -06:00
Andy Miller
b1e940c7d9 removed accidental xdebug_break() 2016-08-25 14:57:07 -06:00
Djamil Legato
272ddcd831 Force creation of user, cache and log if they don’t exist 2016-08-25 13:51:25 -07:00
Djamil Legato
175f3e3f0e Added override and force options for Streams setup 2016-08-25 13:47:14 -07:00
Djamil Legato
f29104ad5d Fixed tmp streams 2016-08-25 12:45:16 -07:00
Andy Miller
64ceef447c Fix for invalid HTML with StaticImageMedium #1001 2016-08-24 18:48:34 -06:00
Andy Miller
d44ee8814a Missing changelog entry added 2016-08-24 17:33:23 -06:00
Andy Miller
5c2be54ad6 composer update 2016-08-24 17:30:53 -06:00
Andy Miller
05b52469ea fixed a messed up changelog 2016-08-24 16:31:55 -06:00
Andy Miller
b58d107ba7 Added RTL in LanguageCodes plus ::getOrientation() and ::isRtl() methods 2016-08-24 16:22:19 -06:00
Andy Miller
3e29ae0923 More reliable Page::home() check 2016-08-24 15:49:46 -06:00
Flavio Copes
0496fc3790 Allow subfolder url rewrite (#896)
By setting `custom_base_url` in system.yaml, we can have Grav in a subfolder but run it in the domain root.

## Scenario 1

Grav is installed in `http://localhost:8080/grav-develop` but you want it to respond on `http://localhost:8080`

In system.yaml, set

```
custom_base_url: 'http://localhost:8080'
```

and set the session path to the new Grav site path, 

```
session:
  path: /
```

And in the domain root, set the redirect, e.g. with .htaccess:

```
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/grav-develop/
RewriteRule ^(.*)$ /grav-develop/$1
```

where `grav-develop` is the subfolder where Grav is.

## Scenario 2

Grav is installed in `http://localhost:8080/grav-develop` but you want it to respond on `http://localhost:8080/xxxxx`

In system.yaml, set

```
custom_base_url: 'http://localhost:8080/xxxxx'
```

and set the session path to the new Grav site path, 

```
session:
  path: /xxxxx
```

And in the new root folder, /xxxxx, set the redirect, e.g. with .htaccess:

```
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/grav-develop/
RewriteRule ^(.*)$ /grav-develop/$1
```

where `grav-develop` is the sister subfolder where Grav is.
2016-08-24 19:23:49 +02:00
Djamil Legato
b3ce52a6c8 Updated Selfupgrade and Install commands to use tmp:// for storing the downloaded packages 2016-08-23 15:03:12 -07:00
Djamil Legato
9540045a6f This wasn’t meant to be removed quite yet (ref: c24c1cd689) 2016-08-23 14:41:51 -07:00
Djamil Legato
c24c1cd689 Added new tmp folder at root
`tmp` is a new folder that serves as container for temporary files. It is very similar to `cache` but its content is meant to be more persistent than cache. Temporary files are meant to survive cache clearance, developers should be responsible of ensuring temporary files are properly removed when needed.

Accessible via stream `tmp://`.
Can be cleared with `bin/grav clear --tmp-only` as well as `--all`.
2016-08-23 14:38:46 -07:00
Andy Miller
7d7ef5ea74 Fix for filtering collections throwing null key error 2016-08-22 15:55:30 -06:00
Andy Miller
5ca2bf4ae8 removed safe email test.. It's random now, not much to test. 2016-08-22 13:50:18 -06:00
Andy Miller
a1039db7af Added more randomization to safe_email twig filter #998 2016-08-22 13:45:55 -06:00
Matias Griese
5adceea7e9 Add function to get direct access to blueprint schema 2016-08-22 19:53:32 +03:00
Andy Miller
7f83252e23 Fixed regex to not pick up multilang page files when no multilang enabled. 2016-08-22 10:16:35 -06:00
Andy Miller
de1d824439 Pre PHP 5.6 regression fix 2016-08-21 09:48:01 -06:00
Andy Miller
9e2cd09cd7 Changed page search to use SPL GlobIterator and more robust regex. Ever so slightly faster too! #995 2016-08-21 09:39:45 -06:00
Andy Miller
ae7c43bcfd Removed 307 as it breaks the admin and is not well supported by browsers - https://github.com/getgrav/grav-plugin-admin/issues/743 2016-08-19 11:28:05 -06:00
Djamil Legato
d660bae517 Fixed regression with selfupgrade command, preventing to get a status about the current version when symbolically linked.
Using proper Installer methods to check for symbolical link folder.
2016-08-18 16:00:15 -07:00
Andy Miller
c1ac1add27 changelog update 2016-08-18 16:47:39 -06:00
Andy Miller
35dbc444db Allow overwrite in GPM::selfupgrade with -o option 2016-08-18 16:40:33 -06:00
Andy Miller
0ec20681d2 Add overwrite option for update 2016-08-18 16:04:10 -06:00
Andy Miller
817fae5955 Removed redundant existing check 2016-08-18 16:03:50 -06:00
Andy Miller
426ec0cb67 added new GPM::getInstallable() method 2016-08-18 16:03:07 -06:00
Andy Miller
560c1c94b4 Fixed -y|--all-yes option for GPM::info 2016-08-18 15:15:40 -06:00
Andy Miller
126ca98252 tidying up 2016-08-18 14:02:36 -06:00
Andy Miller
398c56c20b Fixed -y|--all-yes option for GPM::uninstall 2016-08-18 14:00:02 -06:00
Andy Miller
cd816b6774 extra cleanup for GPM::Install 2016-08-18 13:53:31 -06:00
Andy Miller
613e985fdb Fixed -y|--all-yes option for GPM::install - #985 2016-08-18 13:37:43 -06:00
Andy Miller
84e64785bb Improved GPM::Uninstall command to take into account multiple similar dependencies and output format similar to GPM::Install 2016-08-18 11:59:38 -06:00
Andy Miller
ea9b4568bf Set default state for installation of dependencies to true 2016-08-18 11:58:42 -06:00
Andy Miller
76016cd3f8 remove default 'index' command for GPM to more easily see available commands 2016-08-17 19:16:34 -06:00
Andy Miller
af282312f1 Added support for @page.modular per #988 2016-08-17 18:54:20 -06:00
Andy Miller
97d8c63951 extra space 2016-08-17 18:03:40 -06:00
Andy Miller
97607ac033 Merge branch 'feature/climate_integration' into develop 2016-08-17 17:49:26 -06:00
Andy Miller
ea6dc3ef22 Added CLImate composer package for tables support in CLI 2016-08-17 17:49:06 -06:00
Andy Miller
6fb49a3a8a Improved UI for CLI GPM Index to use tables 2016-08-17 17:48:46 -06:00
Andy Miller
77a7e3da2e Added support for external_url: page header 2016-08-17 14:19:20 -06:00
Andy Miller
39745be4e8 Vendor updates for PHP 7.1 support 2016-08-16 18:31:41 -06:00
Andy Miller
f7c968128a updated changelog 2016-08-16 16:10:06 -06:00
Andy Miller
22387f42f3 Add flatten array utility function 2016-08-16 16:07:37 -06:00
Djamil Legato
3007d997bf Fixed missing GitHub Auth in Global Env for Travis 2016-08-14 13:18:22 -07:00
Andy Miller
7843b30796 Merge branch 'release/1.1.3' 2016-08-14 10:50:36 -06:00
Andy Miller
52ace4f5a7 Merge branch 'release/1.1.3' into develop 2016-08-14 10:50:36 -06:00
Andy Miller
7c42541a0b version update 2016-08-14 10:50:26 -06:00
Andy Miller
ae8ca63fa7 Fix for lightbox exception - #981 2016-08-14 10:42:34 -06:00
Andy Miller
1232ecacf7 Merge branch 'release/1.1.2' 2016-08-11 12:42:55 -06:00
Andy Miller
95f362c9ce Merge branch 'release/1.1.2' into develop 2016-08-11 12:42:55 -06:00
Andy Miller
060b55bc6e version update 2016-08-11 12:42:46 -06:00
Andy Miller
deda94a779 Fix for image not rendering 'id' attribute - #956 2016-08-11 09:56:27 -06:00
Matias Griese
f99f42a979 Debugger: Add support for dumping exceptions 2016-08-11 16:47:47 +03:00
Andy Miller
b3f35fb16e Add ability to look up page mime types from media.yaml - #966 2016-08-10 16:53:58 -06:00
Andy Miller
1c462e8784 quoted redirect rules 2016-08-09 13:33:51 -06:00
Andy Miller
07d95d189c Added logic to not add timestamp on remote references. 2016-08-01 16:20:02 -06:00
Andrew Murray
d8688975a2 Fixed typo (#964)
cheers
2016-07-31 00:06:50 -06:00
Flavio Copes
74f6890ce8 Prevent exception being thrown when calling the Collator constructor failed in a Windows environment with the Intl PHP Extension enabled [#961] 2016-07-27 17:23:01 +02:00
Andy Miller
ab17fb2fdd use Inflector class to hyphenize for consistency and safety 2016-07-22 09:46:52 -06:00
Flavio Copes
2bb6d1d4db Changelog 2016-07-22 17:22:17 +02:00
Flavio Copes
07beafc679 If no parent is set and I call a siblings collection, return a new collection
Fix for a plugins combination issue detailed in
https://github.com/getgrav/grav-plugin-sitemap/issues/22
2016-07-22 17:21:02 +02:00
Flavio Copes
a40c61a8fa Fix #952 hypenize the session name 2016-07-22 15:20:04 +02:00
Flavio Copes
e13ded1a5d Changelog 2016-07-20 17:45:30 +02:00
Flavio Copes
98534bc836 Force SSL on a page if system.force_ssl is enabled (#899)
* Force SSL on a page if system.force_ssl is enabled

* Blueprint for force_ssl option

* Add default false for force_ssl
2016-07-20 17:33:29 +02:00
Flavio Copes
9b673591db Changelog 2016-07-20 15:07:02 +02:00
Benny
496be79aa1 Improved authorize Twig extension (#948) 2016-07-20 15:00:15 +02:00
Djamil Legato
c0fcac3393 Fixed Folder::delete method to recursively remove files and folders and causing Upgrade to fail. 2016-07-19 11:16:13 -07:00
Flavio Copes
4d43812c77 Updated dependencies 2016-07-19 14:43:44 +02:00
Andy Miller
5a65269ef3 Merge branch 'release/1.1.1' into develop 2016-07-16 11:07:57 -06:00
110 changed files with 4858 additions and 1636 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Composer
.composer
/vendor
vendor/*
!*/vendor/*
# Sass
.sass-cache
@@ -25,6 +26,7 @@ user/plugins/*
user/themes/*
!user/themes/.*
user/localhost/config/security.yaml
user/config/security.yaml
# OS Generated
.DS_Store*

View File

@@ -13,6 +13,16 @@ RewriteEngine On
## End - RewriteBase
## Begin - X-Forwarded-Proto
# In some hosted or load balanced environments, SSL negotiation happens upstream.
# In order for Grav to recognize the connection as secure, you need to uncomment
# the following lines.
#
# RewriteCond %{HTTP:X-Forwarded-Proto} https
# RewriteRule .* - [E=HTTPS:on]
#
## End - X-Forwarded-Proto
## Begin - Exploits
# If you experience problems on your site block out the operations listed below
# This attempts to block the most common type of exploit `attempts` to Grav

View File

@@ -43,8 +43,6 @@ env:
- secure: "einUtSEkUWy2IrqLXyVjwUU+mwaaoiOXRRVdLBpA3Zye6bZx8cm5h/5AplkPWhM/NmCJoW/MwNZHHkFhlr3mDRov5iOxVmTTYfnXB+I5lxYTSgduOLLErS7mU8hfADpVDU8bHNU44fNGD3UEiG1PD4qQBX4DMlqIFmR20mjs81k="
# GH_API_USER [for curl]
- secure: "AQGcX1B2NrI8ajflY4AimZDNcK2kBA3F6mbtEFQ78NkDoWhMipsQHayWXiSTzRc0YJKvQl2Y16MTwQF4VHzjTAiiZFATgA8J88vQUjIPabi/kKjqSmcLFoaAOAxStQbW6e0z2GiQ6KBMcNF1y5iUuI63xVrBvtKrYX/w5y+ako8="
# Latest Release version
- TRAVIS_TAG=$(curl --fail -s https://api.github.com/repos/getgrav/grav/releases/latest | grep tag_name | head -n 1 | cut -d '"' -f 4)
before_install:
- export TZ=Pacific/Honolulu
@@ -56,6 +54,7 @@ before_install:
composer install --dev --prefer-dist;
fi
- if [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "5.6" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
export TRAVIS_TAG=$(curl --fail --user "${GH_API_USER}" -s https://api.github.com/repos/getgrav/grav/releases/latest | grep tag_name | head -n 1 | cut -d '"' -f 4);
go get github.com/aktau/github-release;
git clone --quiet --depth=50 --branch=master https://${BB_TOKEN}bitbucket.org/rockettheme/grav-devtools.git $RT_DEVTOOLS &>/dev/null;
if [ ! -z "$TRAVIS_TAG" ]; then
@@ -69,7 +68,7 @@ script:
- if [ $TRAVIS_BRANCH == 'develop' ] || [ $TRAVIS_PULL_REQUEST != 'false' ]; then
vendor/bin/codecept run;
fi
- echo $TRAVIS_TAG
- echo "Latest Release Tag - ${TRAVIS_TAG}"
- if [ ! -z "$TRAVIS_TAG" ] && [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "5.6" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
FILES="$RT_DEVTOOLS/grav-dist/*.zip";
for file in ${FILES[@]}; do

View File

@@ -1,3 +1,237 @@
# v1.1.14
## 01/18/2017
1. [](#bugfix)
* Fixed `page.collection()` returning array and not Collection object when header variable did not exist
* Revert `Content-Encoding: identity` fix, and let you set `cache: allow_webserver_gzip:` option to switch to `identity` [#548](https://github.com/getgrav/grav/issues/548)
# v1.1.13
## 01/17/2017
1. [](#new)
* Added new `never_cache_twig` page option in `system.yaml` and frontmatter. Allows dynamic Twig logic in regular and modular Twig templates [#1244](https://github.com/getgrav/grav/pull/1244)
1. [](#improved)
* Several improvements to aid theme development [#232](https://github.com/getgrav/grav/pull/1232)
* Added `hash` cache check option and made dropdown more descriptive [Admin #923](https://github.com/getgrav/grav-plugin-admin/issues/923)
1. [](#bugfix)
* Fixed cross volume file system operations [#635](https://github.com/getgrav/grav/issues/635)
* Fix issue with pages folders validation not accepting uppercase letters
* Fix renaming the folder name if the page, in the default language, had a custom slug set in its header
* Fixed issue with `Content-Encoding: none`. It should really be `Content-Encoding: identity` instead
* Fixed broken `hash` method on page modifications detection
* Fixed issue with multi-lang pages not caching independently without unique `.md` file [#1211](https://github.com/getgrav/grav/issues/1211)
* Fixed all `$_GET` parameters missing in Nginx (please update your nginx.conf) [#1245](https://github.com/getgrav/grav/issues/1245)
* Fixed issue in trying to process broken symlink [#1254](https://github.com/getgrav/grav/issues/1254)
# v1.1.12
## 12/26/2016
1. [](#bugfix)
* Fixed issue with JSON calls throwing errors due to debugger enabled [#1227](https://github.com/getgrav/grav/issues/1227)
# v1.1.11
## 12/22/2016
1. [](#improved)
* Fall back properly to HTML if template type not found
1. [](#bugfix)
* Fix issue with modular pages folders validation [#900](https://github.com/getgrav/grav-plugin-admin/issues/900)
# v1.1.10
## 12/21/2016
1. [](#improved)
* Improve detection of home path. Also allow `~/.grav` on Windows, drop `ConsoleTrait::isWindows()` method, used only for that [#1204](https://github.com/getgrav/grav/pull/1204)
* Reworked PHP CLI router [#1219](https://github.com/getgrav/grav/pull/1219)
* More robust theme/plugin logic in `bin/gpm direct-install`
1. [](#bugfix)
* Fixed case where extracting a package would cause an error during rename
* Fix issue with using `Yaml::parse` direcly on a filename, now deprecated
* Add pattern for frontend validation of folder slugs [#891](https://github.com/getgrav/grav-plugin-admin/issues/891)
* Fix issue with Inflector when translation is disabled [SimpleSearch #87](https://github.com/getgrav/grav-plugin-simplesearch/issues/87)
* Explicitly expose `array_unique` Twig filter [Admin #897](https://github.com/getgrav/grav-plugin-admin/issues/897)
# v1.1.9
## 12/13/2016
1. [](#new)
* RC released as stable
1. [](#improved)
* Better error handling in cache clear
* YAML syntax fixes for the future compatibility
* Added new parameter `remove` for `onBeforeCacheClear` event
* Add support for calling Media object as function to get medium by filename
1. [](#bugfix)
* Added checks before accessing admin reference during `Page::blueprints()` call. Allows to access `page.blueprints` from Twig in the frontend
# v1.1.9-rc.3
## 12/07/2016
1. [](#new)
* Add `ignore_empty` property to be used on array fields, if positive only save options with a value
* Use new `permissions` field in user account
* Add `range(int start, int end, int step)` twig function to generate an array of numbers between start and end, inclusive
* New retina Media image derivatives array support (`![](image.jpg?derivatives=[640,1024,1440])`) [#1147](https://github.com/getgrav/grav/pull/1147)
* Added stream support for images (`![Sepia Image](image://image.jpg?sepia)`)
* Added stream support for links (`[Download PDF](user://data/pdf/my.pdf)`)
* Added new `onBeforeCacheClear` event to add custom paths to cache clearing process
1. [](#improved)
* Added alias `selfupdate` to the `self-upgrade` `bin/gpm` CLI command
* Synced `webserver-configs/htaccess.txt` with `.htaccess`
* Use permissions field in group details.
* Updated vendor libraries
* Added a warning on GPM update to update Grav first if needed [#1194](https://github.com/getgrav/grav/pull/1194)
1. [](#bugfix)
* Fix page collections problem with `@page.modular` [#1178](https://github.com/getgrav/grav/pull/1178)
* Fix issue with using a multiple taxonomy filter of which one had no results, thanks to @hughbris [#1184](https://github.com/getgrav/grav/issues/1184)
* Fix saving permissions in group
* Fixed issue with redirect of a page getting moved to a different location
# v1.1.9-rc.2
## 11/26/2016
1. [](#new)
* Added two new sort order options for pages: `publish_date` and `unpublish_date` [#1173](https://github.com/getgrav/grav/pull/1173))
1. [](#improved)
* Multisite: Create image cache folder if it doesn't exist
* Add 2 new language values for French [#1174](https://github.com/getgrav/grav/issues/1174)
1. [](#bugfix)
* Fixed issue when we have a meta file without corresponding media [#1179](https://github.com/getgrav/grav/issues/1179)
* Update class namespace for Admin class [Admin #874](https://github.com/getgrav/grav-plugin-admin/issues/874)
# v1.1.9-rc.1
## 11/09/2016
1. [](#new)
* Added a `CompiledJsonFile` object to better handle Json files.
* Added Base32 encode/decode class
* Added a new `User::find()` method
1. [](#improved)
* Moved `messages` object into core Grav from login plugin
* Added `getTaxonomyItemKeys` to the Taxonomy object [#1124](https://github.com/getgrav/grav/issues/1124)
* Added a `redirect_me` Twig function [#1124](https://github.com/getgrav/grav/issues/1124)
* Added a Caddyfile for newer Caddy versions [#1115](https://github.com/getgrav/grav/issues/1115)
* Allow to override sorting flags for page header-based or default ordering. If the `intl` PHP extension is loaded, only these flags are available: https://secure.php.net/manual/en/collator.asort.php. Otherwise, you can use the PHP standard sorting flags (https://secure.php.net/manual/en/array.constants.php) [#1169](https://github.com/getgrav/grav/issues/1169)
1. [](#bugfix)
* Fixed an issue with site redirects/routes, not processing with extension (.html, .json, etc.)
* Don't truncate HTML if content length is less than summary size [#1125](https://github.com/getgrav/grav/issues/1125)
* Return max available number when calling random() on a collection passing an int > available items [#1135](https://github.com/getgrav/grav/issues/1135)
* Use correct ratio when applying image filters to image alternatives [#1147](https://github.com/getgrav/grav/issues/1147)
* Fixed URI path in multi-site when query parameters were used in front page
# v1.1.8
## 10/22/2016
1. [](#bugfix)
* Fixed warning with unset `ssl` option when using GPM [#1132](https://github.com/getgrav/grav/issues/1132)
# v1.1.7
## 10/22/2016
1. [](#improved)
* Improved the capabilities of Image derivatives [#1107](https://github.com/getgrav/grav/pull/1107)
1. [](#bugfix)
* Only pass verify_peer settings to cURL and fopen if the setting is disabled [#1120](https://github.com/getgrav/grav/issues/1120)
# v1.1.6
## 10/19/2016
1. [](#new)
* Added ability for Page to override the output format (`html`, `xml`, etc..) [#1067](https://github.com/getgrav/grav/issues/1067)
* Added `Utils::getExtensionByMime()` and cleaned up `Utils::getMimeByExtension` + tests
* Added a `cache.check.method: 'hash'` option in `system.yaml` that checks all files + dates inclusively
* Include jQuery 3.x in the Grav assets
* Added the option to automatically fix orientation on images based on their Exif data, by enabling `system.images.auto_fix_orientation`.
1. [](#improved)
* Add `batch()` function to Page Collection class
* Added new `cache.redis.socket` setting that allow to pass a UNIX socket as redis server
* It is now possible to opt-out of the SSL verification via the new `system.gpm.verify_peer` setting. This is sometimes necessary when receiving a "GPM Unable to Connect" error. More details in ([#1053](https://github.com/getgrav/grav/issues/1053))
* It is now possible to force the use of either `curl` or `fopen` as `Response` connection method, via the new `system.gpm.method` setting. By default this is set to 'auto' and gives priority to 'fopen' first, curl otherwise.
* InstallCommand can now handle Licenses
* Uses more helpful `1x`, `2x`, `3x`, etc names in the Retina derivatives cache files.
* Added new method `Plugins::isPluginActiveAdmin()` to check if plugin route is active in Admin plugin
* Added new `Cache::setEnabled` and `Cache::getEnabled` to enable outside control of cache
* Updated vendor libs including Twig `1.25.0`
* Avoid git ignoring any vendor folder in a Grav site subfolder (but still ignore the main `vendor/` folder)
* Added an option to get just a route back from `Uri::convertUrl()` function
* Added option to control split session [#1096](https://github.com/getgrav/grav/pull/1096)
* Added new `verbosity` levels to `system.error.display` to allow for system error messages [#1091](https://github.com/getgrav/grav/pull/1091)
* Improved the API for Grav plugins to access the Parsedown parser directly [#1062](https://github.com/getgrav/grav/pull/1062)
1. [](#bugfix)
* Fixed missing `progress` method in the DirectInstall Command
* `Response` class now handles better unsuccessful requests such as 404 and 401
* Fixed saving of `external` page types [Admin #789](https://github.com/getgrav/grav-plugin-admin/issues/789)
* Fixed issue deleting parent folder of folder with `param_sep` in the folder name [admin #796](https://github.com/getgrav/grav-plugin-admin/issues/796)
* Fixed an issue with streams in `bin/plugin`
* Fixed `jpeg` file format support in Media
# v1.1.5
## 09/09/2016
1. [](#new)
* Added new `bin/gpm direct-install` command to install local and remote zip archives
1. [](#improved)
* Refactored `onPageNotFound` event to fire after `onPageInitialized`
* Follow symlinks in `Folder::all()`
* Twig variable `base_url` now supports multi-site by path feature
* Improved `bin/plugin` to list plugins with commands faster by limiting the depth of recursion
1. [](#bugfix)
* Quietly skip missing streams in `Cache::clearCache()`
* Fix issue in calling page.summary when no content is present in a page
* Fix for HUGE session timeouts [#1050](https://github.com/getgrav/grav/issues/1050)
# v1.1.4
## 09/07/2016
1. [](#new)
* Added new `tmp` folder at root. Accessible via stream `tmp://`. Can be cleared with `bin/grav clear --tmp-only` as well as `--all`.
* Added support for RTL in `LanguageCodes` so you can determine if a language is RTL or not
* Ability to set `custom_base_url` in system configuration
* Added `override` and `force` options for Streams setup
1. [](#improved)
* Important vendor updates to provide PHP 7.1 beta support!
* Added a `Util::arrayFlatten()` static function
* Added support for 'external_url' page header to enable easier external URL based menu items
* Improved the UI for CLI GPM Index view to use a table
* Added `@page.modular` Collection type [#988](https://github.com/getgrav/grav/issues/988)
* Added support for `self@`, `page@`, `taxonomy@`, `root@` Collection syntax for cleaner YAML compatibility
* Improved GPM commands to allow for `-y` to automate **yes** responses and `-o` for **update** and **selfupgrade** to overwrite installations [#985](https://github.com/getgrav/grav/issues/985)
* Added randomization to `safe_email` Twig filter for greater security [#998](https://github.com/getgrav/grav/issues/998)
* Allow `Utils::setDotNotation` to merge data, rather than just set
* Moved default `Image::filter()` to the `save` action to ensure they are applied last [#984](https://github.com/getgrav/grav/issues/984)
* Improved the `Truncator` code to be more reliable [#1019](https://github.com/getgrav/grav/issues/1019)
* Moved media blueprints out of core (now in Admin plugin)
1. [](#bugfix)
* Removed 307 redirect code option as it is not well supported [#743](https://github.com/getgrav/grav-plugin-admin/issues/743)
* Fixed issue with folders with name `*.md` are not confused with pages [#995](https://github.com/getgrav/grav/issues/995)
* Fixed an issue when filtering collections causing null key
* Fix for invalid HTML when rendering GIF and Vector media [#1001](https://github.com/getgrav/grav/issues/1001)
* Use pages.markdown.extra in the user's system.yaml [#1007](https://github.com/getgrav/grav/issues/1007)
* Fix for `Memcached` connection [#1020](https://github.com/getgrav/grav/issues/1020)
# v1.1.3
## 08/14/2016
1. [](#bugfix)
* Fix for lightbox media function throwing error [#981](https://github.com/getgrav/grav/issues/981)
# v1.1.2
## 08/10/2016
1. [](#new)
* Allow forcing SSL by setting `system.force_ssl` (Force SSL in the Admin System Config) [#899](https://github.com/getgrav/grav/pull/899)
1. [](#improved)
* Improved `authorize` Twig extension to accept a nested array of authorizations [#948](https://github.com/getgrav/grav/issues/948)
* Don't add timestamps on remote assets as it can cause conflicts
* Grav now looks at types from `media.yaml` when retrieving page mime types [#966](https://github.com/getgrav/grav/issues/966)
* Added support for dumping exceptions in the Debugger
1. [](#bugfix)
* Fixed `Folder::delete` method to recursively remove files and folders and causing Upgrade to fail.
* Fix [#952](https://github.com/getgrav/grav/issues/952) hyphenize the session name.
* If no parent is set and siblings collection is called, return a new and empty collection [grav-plugin-sitemap/issues/22](https://github.com/getgrav/grav-plugin-sitemap/issues/22)
* Prevent exception being thrown when calling the Collator constructor failed in a Windows environment with the Intl PHP Extension enabled [#961](https://github.com/getgrav/grav/issues/961)
* Fix for markdown images not properly rendering `id` attribute [#956](https://github.com/getgrav/grav/issues/956)
# v1.1.1
## 07/16/2016
@@ -731,7 +965,7 @@
* Added new `onImageMediumSaved()` event (useful for post-image processing)
* Added `Vary: Accept-Encoding` option
2. [](#improved)
* Multilang-safe delimeter position
* Multilang-safe delimiter position
* Refactored Twig classes and added optional umask setting
* Removed `pageinit()` timing
* `Page->routable()` now takes `published()` state into account
@@ -817,7 +1051,7 @@
* Improved query string handling
* Optimization to image handling supporting URL encoded filenames
* Use global `composer` when available rather than Grv provided one
* Use `PHP_BINARY` contant rather than `php` executable
* Use `PHP_BINARY` constant rather than `php` executable
* Updated Doctrine Cache library
* Updated Symfony libraries
* Moved `convertUrl()` method to Uri object

Binary file not shown.

20
bin/gpm
View File

@@ -7,6 +7,7 @@ if (!file_exists(__DIR__ . '/../vendor')){
}
use Grav\Common\Composer;
use Grav\Common\Config\Setup;
if (!file_exists(__DIR__ . '/../vendor')){
// Before we can even start, we need to run composer first
@@ -37,9 +38,24 @@ 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();
$environment = $climate->arguments->get('environment');
// Set up environment based on params.
Setup::$environment = $environment;
$grav = Grav::instance(array('loader' => $autoload));
$grav['config']->init();
$grav['uri']->init();
$grav['config']->init();
$grav['streams'];
$app = new Application('Grav Package Manager', GRAV_VERSION);
@@ -51,7 +67,7 @@ $app->addCommands(array(
new \Grav\Console\Gpm\UninstallCommand(),
new \Grav\Console\Gpm\UpdateCommand(),
new \Grav\Console\Gpm\SelfupgradeCommand(),
new \Grav\Console\Gpm\DirectInstallCommand(),
));
$app->setDefaultCommand('index');
$app->run();

View File

@@ -21,6 +21,7 @@ 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\Config\Setup;
use Grav\Common\Filesystem\Folder;
$autoload = require_once(__DIR__ . '/../vendor/autoload.php');
@@ -37,12 +38,29 @@ if (!file_exists(ROOT_DIR . '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');
// Set up environment based on params.
Setup::$environment = $environment;
$grav = Grav::instance(array('loader' => $autoload));
$grav['uri']->init();
$grav['config']->init();
$grav['streams'];
$grav['plugins']->init();
$grav['themes']->init();
$app = new Application('Grav Plugins Commands', GRAV_VERSION);
$pattern = '([A-Z]\w+Command\.php)';
@@ -70,7 +88,7 @@ if (!$name) {
$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']);
$list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 2]);
if (count($list)) {
$available = [];
@@ -101,14 +119,15 @@ if ($plugin === null) {
$path = 'plugins://' . $name . '/cli';
try {
$commands = Folder::all($path, ['compare' => 'Filename', 'pattern' => '/' . $pattern . '$/usm']);
$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) {
require_once "plugins://{$name}/cli/{$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();

View File

@@ -19,15 +19,17 @@
"filp/whoops": "~2.0",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.0",
"gregwar/image": "~2.0",
"gregwar/image": "dev-master#72568cfbeb77515278f2ccb386fc344e874b7ae8",
"donatj/phpuseragentparser": "~0.3",
"pimple/pimple": "~3.0",
"rockettheme/toolbox": "dev-develop",
"rockettheme/toolbox": "~1.0",
"maximebf/debugbar": "~1.10",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-curl": "*",
"ext-zip": "*"
"ext-zip": "*",
"league/climate": "^3.2",
"antoligy/dom-string-iterators": "^1.0"
},
"require-dev": {
"codeception/codeception": "^2.1",

820
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,12 @@ if (!is_file($autoload)) {
die("Please run: <i>bin/grav install</i>");
}
if (PHP_SAPI == 'cli-server') {
if (!isset($_SERVER['PHP_CLI_ROUTER'])) {
die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER["SERVER_NAME"]}:{$_SERVER["SERVER_PORT"]} system/router.php</pre>");
}
}
use Grav\Common\Grav;
use RocketTheme\Toolbox\Event\Event;

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,13 @@ form:
placeholder: PLUGIN_ADMIN.SITE_TITLE_PLACEHOLDER
help: PLUGIN_ADMIN.SITE_TITLE_HELP
default_lang:
type: text
label: PLUGIN_ADMIN.SITE_DEFAULT_LANG
size: vsmall
placeholder: PLUGIN_ADMIN.SITE_DEFAULT_LANG_PLACEHOLDER
help: PLUGIN_ADMIN.SITE_DEFAULT_LANG_HELP
author.name:
type: text
size: large

View File

@@ -178,8 +178,9 @@ form:
help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP
options:
301: 301 - Permanent
302: 302 - Found
303: 303 - Other
307: 307 - Temporary
304: 304 - Not Modified
pages.redirect_trailing_slash:
type: toggle
@@ -243,6 +244,17 @@ form:
validate:
type: bool
pages.never_cache_twig:
type: toggle
label: PLUGIN_ADMIN.NEVER_CACHE_TWIG
help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
pages.frontmatter.process_twig:
type: toggle
label: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG
@@ -460,14 +472,15 @@ form:
cache.check.method:
type: select
size: small
size: medium
classes: fancy
label: PLUGIN_ADMIN.CACHE_CHECK_METHOD
help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP
options:
file: File
folder: Folder
none: None
file: Markdown + Yaml file timestamps
folder: Folder timestamps
hash: All files timestamps
none: No timestamp checking
cache.driver:
type: select
@@ -513,7 +526,14 @@ form:
label: PLUGIN_ADMIN.MEMCACHED_PORT
help: PLUGIN_ADMIN.MEMCACHED_PORT_HELP
placeholder: "11211"
cache.redis.socket:
type: text
size: medium
label: PLUGIN_ADMIN.REDIS_SOCKET
help: PLUGIN_ADMIN.REDIS_SOCKET_HELP
placeholder: "/var/run/redis/redis.sock"
cache.redis.server:
type: text
size: medium
@@ -555,6 +575,17 @@ form:
validate:
type: bool
cache.allow_webserver_gzip:
type: toggle
label: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP
help: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
twig:
type: section
title: PLUGIN_ADMIN.TWIG_TEMPLATING
@@ -756,15 +787,16 @@ form:
fields:
errors.display:
type: toggle
type: select
label: PLUGIN_ADMIN.DISPLAY_ERRORS
help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP
highlight: 0
size: medium
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
-1: PLUGIN_ADMIN.ERROR_SYSTEM
0: PLUGIN_ADMIN.ERROR_SIMPLE
1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE
errors.log:
type: toggle
@@ -843,7 +875,6 @@ form:
'0755': '0755'
'0775': '0775'
images.debug:
type: toggle
label: PLUGIN_ADMIN.IMAGES_DEBUG
@@ -855,6 +886,17 @@ form:
validate:
type: bool
images.auto_fix_orientation:
type: toggle
label: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION
help: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
media.upload_limit:
type: text
append: bytes
@@ -953,6 +995,24 @@ form:
validate:
type: bool
session.path:
type: text
size: small
label: PLUGIN_ADMIN.SESSION_PATH
help: PLUGIN_ADMIN.SESSION_PATH_HELP
session.split:
type: toggle
label: PLUGIN_ADMIN.SESSION_SPLIT
help: PLUGIN_ADMIN.SESSION_SPLIT_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool
advanced:
type: section
title: PLUGIN_ADMIN.ADVANCED
@@ -975,6 +1035,27 @@ form:
label: PLUGIN_ADMIN.PROXY_URL
help: PLUGIN_ADMIN.PROXY_URL_HELP
gpm.method:
type: toggle
label: PLUGIN_ADMIN.GPM_METHOD
highlight: auto
help: PLUGIN_ADMIN.GPM_METHOD_HELP
options:
auto: PLUGIN_ADMIN.AUTO
fopen: PLUGIN_ADMIN.FOPEN
curl: PLUGIN_ADMIN.CURL
gpm.verify_peer:
type: toggle
label: PLUGIN_ADMIN.GPM_VERIFY_PEER
highlight: 1
help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
reverse_proxy_setup:
type: toggle
label: PLUGIN_ADMIN.REVERSE_PROXY
@@ -1018,3 +1099,21 @@ form:
options:
':': ': (default)'
';': '; (for Apache running on Windows)'
force_ssl:
type: toggle
label: PLUGIN_ADMIN.FORCE_SSL
highlight: 0
help: PLUGIN_ADMIN.FORCE_SSL_HELP
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
custom_base_url:
type: text
size: medium
placeholder: "e.g. http://localhost:8080"
label: PLUGIN_ADMIN.CUSTOM_BASE_URL
help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP

View File

@@ -1,7 +0,0 @@
form:
validation: loose
fields:
alt_text:
type: string
label: Alt Text

View File

@@ -1,8 +0,0 @@
form:
validation: loose
fields:
route:
type: select
label: PLUGIN_ADMIN.PAGE
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parents'

View File

@@ -1,8 +0,0 @@
form:
validation: loose
fields:
new_file_name:
type: text
label: PLUGIN_ADMIN_PRO.NEW_FILE_NAME
validate:
required: true

View File

@@ -2,7 +2,7 @@ title: PLUGIN_ADMIN.DEFAULT
rules:
slug:
pattern: "[a-zа-я][a-zа-я0-9_\-]+"
pattern: '[a-zа-я][a-zа-я0-9_\-]+'
min: 2
max: 80
@@ -133,13 +133,14 @@ form:
label: PLUGIN_ADMIN.FOLDER_NAME
validate:
type: slug
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
route:
type: select
label: PLUGIN_ADMIN.PARENT
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
data-default@: '\Grav\Plugin\admin::rawRoute'
data-default@: '\Grav\Plugin\Admin\Admin::rawRoute'
options:
'/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT
@@ -224,6 +225,30 @@ form:
twig: Twig
use: keys
header.twig_first:
type: toggle
toggleable: true
label: PLUGIN_ADMIN.TWIG_FIRST
help: PLUGIN_ADMIN.TWIG_FIRST_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
header.never_cache_twig:
type: toggle
toggleable: true
label: PLUGIN_ADMIN.NEVER_CACHE_TWIG
help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
header.child_type:
type: select
toggleable: true

View File

@@ -0,0 +1,58 @@
title: PLUGIN_ADMIN:EXTERNAL
@extends:
type: default
context: blueprints://pages
rules:
slug:
pattern: '[a-zа-я][a-zа-я0-9_\-]+'
min: 2
max: 80
form:
validation: loose
fields:
tabs:
type: tabs
active: 1
fields:
content:
fields:
header.title:
type: text
autofocus: true
style: horizontal
label: PLUGIN_ADMIN.TITLE
content:
unset@: true
uploads:
unset@: true
header.external_url:
type: text
label: PLUGIN_ADMIN.EXTERNAL_URL
placeholder: https://getgrav.org
validate:
required: true
options:
fields:
publishing:
fields:
header.date:
unset@: true
header.metadata:
unset@: true
taxonomies:
unset@: true

View File

@@ -1,6 +1,6 @@
rules:
slug:
pattern: "[a-z][a-z0-9_\-]+"
pattern: '[a-z][a-z0-9_\-]+'
min: 2
max: 80
@@ -24,13 +24,14 @@ form:
validate:
type: slug
required: true
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
route:
type: select
label: PLUGIN_ADMIN.PAGE
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
data-default@: '\Grav\Plugin\admin::rawRoute'
data-default@: '\Grav\Plugin\Admin\Admin::rawRoute'
validate:
required: true

View File

@@ -1,6 +1,6 @@
rules:
slug:
pattern: "[a-z][a-z0-9_\-]+"
pattern: '[a-z][a-z0-9_\-]+'
min: 2
max: 80
@@ -73,13 +73,14 @@ form:
validate:
type: slug
required: true
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
route:
type: select
label: PLUGIN_ADMIN.PARENT
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
data-default@: '\Grav\Plugin\admin::rawRoute'
data-default@: '\Grav\Plugin\Admin\Admin::rawRoute'
options:
'': PLUGIN_ADMIN.DEFAULT_OPTION_SELECT
validate:

View File

@@ -1,6 +1,6 @@
rules:
slug:
pattern: "[a-z][a-z0-9_\-]+"
pattern: '[a-z][a-z0-9_\-]+'
min: 2
max: 80
@@ -12,6 +12,6 @@ form:
label: PLUGIN_ADMIN.PARENT
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
data-default@: '\Grav\Plugin\admin::rawRoute'
data-default@: '\Grav\Plugin\Admin\Admin::rawRoute'
options:
'/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT

View File

@@ -1,6 +1,6 @@
rules:
slug:
pattern: "[a-z][a-z0-9_\-]+"
pattern: '[a-z][a-z0-9_\-]+'
min: 2
max: 80
@@ -26,13 +26,14 @@ form:
validate:
type: slug
required: true
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
route:
type: select
label: PLUGIN_ADMIN.PARENT_PAGE
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
data-default@: '\Grav\Plugin\admin::getLastPageRoute'
data-default@: '\Grav\Plugin\Admin\Admin::getLastPageRoute'
options:
'/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT
validate:
@@ -44,7 +45,7 @@ form:
label: PLUGIN_ADMIN.PAGE_FILE
help: PLUGIN_ADMIN.PAGE_FILE_HELP
data-options@: '\Grav\Common\Page\Pages::types'
data-default@: '\Grav\Plugin\admin::getLastPageName'
data-default@: '\Grav\Plugin\Admin\Admin::getLastPageName'
validate:
required: true

View File

@@ -1,6 +1,6 @@
rules:
slug:
pattern: "[a-z][a-z0-9_\-]+"
pattern: '[a-z][a-z0-9_\-]+'
min: 2
max: 80
@@ -19,13 +19,14 @@ form:
validate:
type: slug
required: true
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
route:
type: select
label: PLUGIN_ADMIN.PARENT_PAGE
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
data-default@: '\Grav\Plugin\admin::getLastPageRoute'
data-default@: '\Grav\Plugin\Admin\Admin::getLastPageRoute'
options:
'/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT
validate:

View File

@@ -1,6 +1,6 @@
rules:
slug:
pattern: "[a-z][a-z0-9_\-]+"
pattern: '[a-z][a-z0-9_\-]+'
min: 2
max: 80
@@ -73,13 +73,14 @@ form:
validate:
type: slug
required: true
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
route:
type: select
label: PLUGIN_ADMIN.PARENT
classes: fancy
data-options@: '\Grav\Common\Page\Pages::parentsRawRoutes'
data-default@: '\Grav\Plugin\admin::rawRoute'
data-default@: '\Grav\Plugin\Admin\Admin::rawRoute'
options:
'/': PLUGIN_ADMIN.DEFAULT_OPTION_ROOT

View File

@@ -1,4 +1,4 @@
title: Site
title: Account
form:
validation: loose
@@ -56,7 +56,7 @@ form:
label: PLUGIN_ADMIN.LANGUAGE
size: medium
classes: fancy
data-options@: '\Grav\Plugin\admin::adminLanguages'
data-options@: '\Grav\Plugin\Admin\Admin::adminLanguages'
default: 'en'
help: PLUGIN_ADMIN.LANGUAGE_HELP
@@ -77,16 +77,9 @@ form:
validate:
type: commalist
access.admin:
type: array
label: PLUGIN_ADMIN.ADMIN_ACCESS
multiple: false
validate:
type: array
access.site:
type: array
label: PLUGIN_ADMIN.SITE_ACCESS
multiple: false
access:
type: permissions
label: PLUGIN_ADMIN.PERMISSIONS
ignore_empty: true
validate:
type: array

View File

@@ -29,16 +29,9 @@ form:
size: small
label: PLUGIN_ADMIN_PRO.ICON
access.admin:
type: array
label: PLUGIN_ADMIN.ADMIN_ACCESS
multiple: false
validate:
type: array
access.site:
type: array
label: PLUGIN_ADMIN.SITE_ACCESS
multiple: false
access:
type: permissions
label: PLUGIN_ADMIN.PERMISSIONS
ignore_empty: true
validate:
type: array

View File

@@ -114,6 +114,9 @@ types:
xlm:
type: file
mime: application/vnd.ms-excel
xlsm:
type: file
mime: application/vnd.ms-excel
xld:
type: file
mime: application/vnd.ms-excel

View File

@@ -1,4 +1,5 @@
title: Grav # Name of the site
default_lang: en # Default language for site (potentially used by theme)
author:
name: John Appleseed # Default author name
@@ -16,12 +17,12 @@ summary:
delimiter: === # The summary delimiter
redirects:
# /redirect-test: / # Redirect test goes to home page
# /old/(.*): /new/$1 # Would redirect /old/my-page to /new/my-page
# '/redirect-test': '/' # Redirect test goes to home page
# '/old/(.*)': '/new/$1' # Would redirect /old/my-page to /new/my-page
routes:
# /something/else: '/blog/sample-3' # Alias for /blog/sample-3
# /new/(.*): '/blog/$1' # Regex any /new/my-page URL to /blog/my-page Route
# '/something/else': '/blog/sample-3' # Alias for /blog/sample-3
# '/new/(.*)': '/blog/$1' # Regex any /new/my-page URL to /blog/my-page Route
blog:
route: '/blog' # Custom value added (accessible via system.blog.route)

View File

@@ -4,7 +4,8 @@ default_locale: # Default locale (defaults to system
param_sep: ':' # Parameter separator, use ';' for Apache on windows
wrapped_site: false # For themes/plugins to know if Grav is wrapped by another platform
reverse_proxy_setup: false # Running in a reverse proxy scenario with different webserver ports than proxy
force_ssl: false # If enabled, Grav forces to be accessed via HTTPS (NOTE: Not an ideal solution)
custom_base_url: '' # Set the base_url manually
languages:
supported: [] # List of languages supported. eg: [en, fr, de]
@@ -35,9 +36,10 @@ pages:
markdown: true # Process Markdown
twig: false # Process Twig
twig_first: false # Process Twig before markdown when processing both on a page
never_cache_twig: false # Only cache content, never cache twig processed in content (incompatible with `twig_first: true`)
events:
page: true # Enable page level events
twig: true # Enable twig level events
twig: true # Enable Twig level events
markdown:
extra: false # Enable support for Markdown Extra support (GFM by default)
auto_line_breaks: false # Enable automatic line breaks
@@ -61,19 +63,22 @@ pages:
url_taxonomy_filters: true # Enable auto-magic URL-based taxonomy filters for page collections
frontmatter:
process_twig: false # Should the frontmatter be processed to replace Twig variables?
ignore_fields: ['form'] # Fields that might contain Twig variables and should not be processed
ignore_fields: ['form','forms'] # Fields that might contain Twig variables and should not be processed
cache:
enabled: true # Set to true to enable caching
check:
method: file # Method to check for updates in pages: file|folder|none
method: file # Method to check for updates in pages: file|folder|hash|none
driver: auto # One of: auto|file|apc|xcache|memcache|wincache
prefix: 'g' # Cache prefix string (prevents cache conflicts)
lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite)
gzip: false # GZip compress the page output
allow_webserver_gzip: false # If true, `content-encoding: identity` but connection isn't closed before `onShutDown()` event
redis:
socket: false # Path to redis unix socket (e.g. /var/run/redis/redis.sock), false = use server and port to connect
twig:
cache: true # Set to true to enable twig caching
cache: true # Set to true to enable Twig caching
debug: true # Enable Twig debug
auto_reload: true # Refresh cache on changes
autoescape: false # Autoescape Twig vars
@@ -97,7 +102,7 @@ assets: # Configuration for Assets Manager (
jquery: system://assets/jquery/jquery-2.x.min.js
errors:
display: false # Display full backtrace-style error page
display: 0 # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error
log: true # Log errors to /logs folder
debugger:
@@ -110,6 +115,7 @@ images:
cache_all: false # Cache all image by default
cache_perms: '0755' # MUST BE IN QUOTES!! Default cache folder perms. Usually '0755' or '0775'
debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example
auto_fix_orientation: false # Automatically fix the image orientation based on the Exif data
media:
enable_media_timestamp: false # Enable media timetsamps
@@ -123,7 +129,11 @@ session:
name: grav-site # Name prefix of the session cookie. Use alphanumeric, dashes or underscores only. Do not use dots in the session name
secure: false # Set session secure. If true, indicates that communication for this cookie must be over an encrypted transmission. Enable this only on sites that run exclusively on HTTPS
httponly: true # Set session HTTP only. If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed.
split: true # Sessions should be independent between site and plugins (such as admin)
path:
gpm:
releases: stable # Set to either 'stable' or 'testing'
proxy_url: # Configure a manual proxy URL for GPM (eg 127.0.0.1:3128)
method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.

View File

@@ -8,7 +8,7 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.1.1');
define('GRAV_VERSION', '1.1.14');
define('GRAV_TESTING', false);
define('DS', '/');
define('GRAV_PHP_MIN', '5.5.9');

5
system/pages/notfound.md Normal file
View File

@@ -0,0 +1,5 @@
---
title: Not Found
routable: false
notfound: true
---

26
system/router.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
/**
* @package Grav.Core
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
if (PHP_SAPI !== 'cli-server') {
exit('This script cannot be run from browser. Run it from a CLI.');
}
$_SERVER['PHP_CLI_ROUTER'] = true;
if (is_file($_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $_SERVER['SCRIPT_NAME'])) {
return false;
}
$_SERVER = array_merge($_SERVER, $_ENV);
$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR . 'index.php';
$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR . 'index.php';
require 'index.php';
error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4);

View File

@@ -207,7 +207,7 @@ class Assets
$this->assets_url = $locator->findResource('asset://', false);
$this->config($asset_config);
$this->base_url = $base_url . '/';
$this->base_url = ($config->get('system.absolute_urls') ? '' : '/') . ltrim(ltrim($base_url, '/') . '/', '/');
// Register any preconfigured collections
foreach ($config->get('system.assets.collections', []) as $name => $collection) {
@@ -281,8 +281,9 @@ class Assets
return $this;
}
$modified = 0;
if (!$this->isRemoteLink($asset)) {
$modified = false;
$remote = $this->isRemoteLink($asset);
if (!$remote) {
$modified = $this->getLastModificationTime($asset);
$asset = $this->buildLocalLink($asset);
}
@@ -294,6 +295,7 @@ class Assets
$data = [
'asset' => $asset,
'remote' => $remote,
'priority' => intval($priority ?: 10),
'order' => count($this->css),
'pipeline' => (bool) $pipeline,
@@ -345,8 +347,9 @@ class Assets
return $this;
}
$modified = 0;
if (!$this->isRemoteLink($asset)) {
$modified = false;
$remote = $this->isRemoteLink($asset);
if (!$remote) {
$modified = $this->getLastModificationTime($asset);
$asset = $this->buildLocalLink($asset);
}
@@ -358,6 +361,7 @@ class Assets
$data = [
'asset' => $asset,
'remote' => $remote ,
'priority' => intval($priority ?: 10),
'order' => count($this->js),
'pipeline' => (bool) $pipeline,
@@ -558,7 +562,7 @@ class Assets
foreach ($this->css_no_pipeline as $file) {
if ($group && $file['group'] == $group) {
$media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
$output .= '<link href="' . $file['asset'] . $this->timestamp . '"' . $attributes . $media . ' />' . "\n";
$output .= '<link href="' . $file['asset'] . $this->getTimestamp($file) . '"' . $attributes . $media . ' />' . "\n";
}
}
if (!$this->css_pipeline_before_excludes && $pipeline_result) {
@@ -568,7 +572,7 @@ class Assets
foreach ($this->css as $file) {
if ($group && $file['group'] == $group) {
$media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
$output .= '<link href="' . $file['asset'] . $this->timestamp . '"' . $attributes . $media . ' />' . "\n";
$output .= '<link href="' . $file['asset'] . $this->getTimestamp($file) . '"' . $attributes . $media . ' />' . "\n";
}
}
}
@@ -636,7 +640,7 @@ class Assets
}
foreach ($this->js_no_pipeline as $file) {
if ($group && $file['group'] == $group) {
$output .= '<script src="' . $file['asset'] . $this->timestamp . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
$output .= '<script src="' . $file['asset'] . $this->getTimestamp($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
}
}
if (!$this->js_pipeline_before_excludes && $pipeline_result) {
@@ -645,7 +649,7 @@ class Assets
} else {
foreach ($this->js as $file) {
if ($group && $file['group'] == $group) {
$output .= '<script src="' . $file['asset'] . $this->timestamp . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
$output .= '<script src="' . $file['asset'] . $this->getTimestamp($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
}
}
}
@@ -693,14 +697,14 @@ class Assets
// If pipeline exist return it
if (file_exists($this->assets_dir . $file)) {
return $relative_path . $this->timestamp;
return $relative_path . $this->getTimestamp();
}
// Remove any non-pipeline files
foreach ($this->css as $id => $asset) {
if ($asset['group'] == $group) {
if (!$asset['pipeline'] ||
($this->isRemoteLink($asset['asset']) && $this->css_pipeline_include_externals === false)) {
($asset['remote'] && $this->css_pipeline_include_externals === false)) {
$this->css_no_pipeline[$id] = $asset;
} else {
$temp_css[$id] = $asset;
@@ -740,7 +744,7 @@ class Assets
if (strlen(trim($buffer)) > 0) {
file_put_contents($this->assets_dir . $file, $buffer);
return $relative_path . $this->timestamp;
return $relative_path . $this->getTimestamp();
} else {
return false;
}
@@ -775,14 +779,14 @@ class Assets
// If pipeline exist return it
if (file_exists($this->assets_dir . $file)) {
return $relative_path . $this->timestamp;
return $relative_path . $this->getTimestamp();
}
// Remove any non-pipeline files
foreach ($this->js as $id => $asset) {
if ($asset['group'] == $group) {
if (!$asset['pipeline'] ||
($this->isRemoteLink($asset['asset']) && $this->js_pipeline_include_externals === false)) {
($asset['remote'] && $this->js_pipeline_include_externals === false)) {
$this->js_no_pipeline[] = $asset;
} else {
$temp_js[$id] = $asset;
@@ -812,7 +816,7 @@ class Assets
if (strlen(trim($buffer)) > 0) {
file_put_contents($this->assets_dir . $file, $buffer);
return $relative_path . $this->timestamp;
return $relative_path . $this->getTimestamp();
} else {
return false;
}
@@ -1342,6 +1346,21 @@ class Assets
$this->timestamp = '?' . $value;
}
public function getTimestamp($asset = null)
{
if (is_array($asset)) {
if ($asset['remote'] === false) {
if (Utils::contains($asset['asset'], '?')) {
return str_replace('?', '&', $this->timestamp);
} else {
return $this->timestamp;
}
}
} elseif (empty($asset)) {
return $this->timestamp;
}
}
/**
* @return string
*/

View File

@@ -17,7 +17,8 @@ class ZipBackup
'backup',
'cache',
'images',
'logs'
'logs',
'tmp'
];
protected static $ignoreFolders = [

View File

@@ -11,7 +11,7 @@ namespace Grav\Common;
use \Doctrine\Common\Cache as DoctrineCache;
use Grav\Common\Config\Config;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use RocketTheme\Toolbox\Event\Event;
/**
* The GravCache object is used throughout Grav to store and retrieve cached data.
@@ -65,7 +65,8 @@ class Cache extends Getters
protected static $all_remove = [
'cache://',
'cache://images',
'asset://'
'asset://',
'tmp://'
];
protected static $assets_remove = [
@@ -80,6 +81,10 @@ class Cache extends Getters
'cache://'
];
protected static $tmp_remove = [
'tmp://'
];
/**
* Constructor
*
@@ -110,7 +115,9 @@ class Cache extends Getters
$prefix = $this->config->get('system.cache.prefix');
$this->enabled = (bool)$this->config->get('system.cache.enabled');
if (is_null($this->enabled)) {
$this->enabled = (bool)$this->config->get('system.cache.enabled');
}
// Cache key allows us to invalidate all cache on configuration changes.
$this->key = ($prefix ? $prefix : 'g') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION),
@@ -122,10 +129,36 @@ class Cache extends Getters
// Set the cache namespace to our unique key
$this->driver->setNamespace($this->key);
}
// Dump Cache state
$grav['debugger']->addMessage('Cache: [' . ($this->enabled ? 'true' : 'false') . '] Setting: [' . $this->driver_setting . '] Driver: [' . $this->driver_name . ']');
/**
* Public accessor to set the enabled state of the cache
*
* @param $enabled
*/
public function setEnabled($enabled)
{
$this->enabled = (bool) $enabled;
}
/**
* Returns the current enabled state
*
* @return bool
*/
public function getEnabled()
{
return $this->enabled;
}
/**
* Get cache state
*
* @return string
*/
public function getCacheStatus()
{
return 'Cache: [' . ($this->enabled ? 'true' : 'false') . '] Setting: [' . $this->driver_setting . '] Driver: [' . $this->driver_name . ']';
}
/**
@@ -183,7 +216,7 @@ class Cache extends Getters
case 'memcached':
$memcached = new \Memcached();
$memcached->connect($this->config->get('system.cache.memcached.server', 'localhost'),
$memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
$this->config->get('system.cache.memcached.port', 11211));
$driver = new DoctrineCache\MemcachedCache();
$driver->setMemcached($memcached);
@@ -191,8 +224,15 @@ class Cache extends Getters
case 'redis':
$redis = new \Redis();
$redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
$socket = $this->config->get('system.cache.redis.socket', false);
if ($socket) {
$redis->connect($socket);
} else {
$redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
$this->config->get('system.cache.redis.port', 6379));
}
$driver = new DoctrineCache\RedisCache();
$driver->setRedis($redis);
break;
@@ -309,39 +349,45 @@ class Cache extends Getters
case 'cache-only':
$remove_paths = self::$cache_remove;
break;
case 'tmp-only':
$remove_paths = self::$tmp_remove;
break;
default:
$remove_paths = self::$standard_remove;
}
// Clearing cache event to add paths to clear
Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
foreach ($remove_paths as $stream) {
// Convert stream to a real path
$path = $locator->findResource($stream, true, true);
// Make sure path exists before proceeding, otherwise we would wipe ROOT_DIR
if (!$path) {
throw new \RuntimeException("Stream '{$stream}' not found", 500);
}
try {
$path = $locator->findResource($stream, true, true);
$anything = false;
$files = glob($path . '/*');
$anything = false;
$files = glob($path . '/*');
if (is_array($files)) {
foreach ($files as $file) {
if (is_file($file)) {
if (@unlink($file)) {
$anything = true;
}
} elseif (is_dir($file)) {
if (Folder::delete($file)) {
$anything = true;
if (is_array($files)) {
foreach ($files as $file) {
if (is_file($file)) {
if (@unlink($file)) {
$anything = true;
}
} elseif (is_dir($file)) {
if (Folder::delete($file)) {
$anything = true;
}
}
}
}
}
if ($anything) {
$output[] = '<red>Cleared: </red>' . $path . '/*';
if ($anything) {
$output[] = '<red>Cleared: </red>' . $path . '/*';
}
} catch (\Exception $e) {
// stream not found or another error while deleting files.
$output[] = '<red>ERROR: </red>' . $e->getMessage();
}
}

View File

@@ -17,6 +17,8 @@ use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Setup extends Data
{
public static $environment;
protected $streams = [
'system' => [
'type' => 'ReadOnlyStream',
@@ -26,6 +28,7 @@ class Setup extends Data
],
'user' => [
'type' => 'ReadOnlyStream',
'force' => true,
'prefixes' => [
'' => ['user'],
]
@@ -78,6 +81,7 @@ class Setup extends Data
],
'cache' => [
'type' => 'Stream',
'force' => true,
'prefixes' => [
'' => ['cache'],
'images' => ['images']
@@ -85,16 +89,25 @@ class Setup extends Data
],
'log' => [
'type' => 'Stream',
'force' => true,
'prefixes' => [
'' => ['logs']
]
],
'backup' => [
'type' => 'Stream',
'force' => true,
'prefixes' => [
'' => ['backup']
]
],
'tmp' => [
'type' => 'Stream',
'force' => true,
'prefixes' => [
'' => ['tmp']
]
],
'image' => [
'type' => 'ReadOnlyStream',
'prefixes' => [
@@ -120,7 +133,7 @@ class Setup extends Data
*/
public function __construct($container)
{
$environment = $container['uri']->environment() ?: 'localhost';
$environment = isset(static::$environment) ? static::$environment : ($container['uri']->environment() ?: 'localhost');
// Pre-load setup.php which contains our initial configuration.
// Configuration may contain dynamic parts, which is why we need to always load it.
@@ -137,8 +150,8 @@ class Setup extends Data
parent::__construct($setup);
// Set up environment.
$this->def('environment', $environment);
$this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->environment}"]]);
$this->def('environment', $environment ?: 'cli');
$this->def('streams.schemes.environment.prefixes', ['' => ($environment ? ["user://{$this->environment}"] : [])]);
}
/**
@@ -194,9 +207,13 @@ class Setup extends Data
if (isset($config['paths'])) {
$locator->addPath($scheme, '', $config['paths']);
}
$override = isset($config['override']) ? $config['override'] : false;
$force = isset($config['force']) ? $config['force'] : false;
if (isset($config['prefixes'])) {
foreach ($config['prefixes'] as $prefix => $paths) {
$locator->addPath($scheme, $prefix, $paths);
$locator->addPath($scheme, $prefix, $paths, $override, $force);
}
}
}

View File

@@ -110,6 +110,18 @@ class Blueprint extends BlueprintForm
return $this->blueprintSchema->filter($data);
}
/**
* Return blueprint data schema.
*
* @return BlueprintSchema
*/
public function schema()
{
$this->initInternals();
return $this->blueprintSchema;
}
/**
* Initialize validator.
*/

View File

@@ -9,6 +9,7 @@
namespace Grav\Common\Data;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
@@ -37,12 +38,6 @@ class Validation
$field['type'] = 'text';
}
// Special case for files, value is never empty and errors with code 4 instead.
if (empty($validate['required']) && $field['type'] == 'file' && isset($value['error'])
&& ($value['error'] == UPLOAD_ERR_NO_FILE || in_array(UPLOAD_ERR_NO_FILE, $value['error']))) {
return $messages;
}
// Get language class.
$language = Grav::instance()['language'];
@@ -101,12 +96,6 @@ class Validation
$field['type'] = 'text';
}
// Special case for files, value is never empty and errors with code 4 instead.
if (empty($validate['required']) && $field['type'] == 'file' && isset($value['error'])
&& ($value['error'] == UPLOAD_ERR_NO_FILE || in_array(UPLOAD_ERR_NO_FILE, $value['error']))) {
return null;
}
// If this is a YAML field, simply parse it and return the value.
if (isset($field['yaml']) && $field['yaml'] === true) {
try {
@@ -581,6 +570,7 @@ class Validation
return null;
}
if ($options) {
$useKey = isset($field['use']) && $field['use'] == 'keys';
foreach ($values as $key => $value) {
@@ -592,9 +582,22 @@ class Validation
foreach ($values as $key => $value) {
if (is_array($value)) {
$value = implode(',', $value);
$values[$key] = array_map('trim', explode(',', $value));
} else {
$values[$key] = trim($value);
}
}
}
if (isset($field['ignore_empty']) && Utils::isPositive($field['ignore_empty'])) {
foreach ($values as $key => $value) {
foreach ($value as $inner_key => $inner_value) {
if ($inner_value == '') {
unset($value[$inner_key]);
}
}
$values[$key] = array_map('trim', explode(',', $value));
$values[$key] = $value;
}
}

View File

@@ -87,6 +87,14 @@ class Debugger
public function addAssets()
{
if ($this->enabled()) {
// Only add assets if Page is HTML
$page = $this->grav['page'];
if ($page->templateFormat() != 'html') {
$this->enabled = false;
return;
}
/** @var Assets $assets */
$assets = $this->grav['assets'];
@@ -217,4 +225,19 @@ class Debugger
return $this;
}
/**
* Dump exception into the Messages tab of the Debug Bar
*
* @param \Exception $e
* @return Debugger
*/
public function addException(\Exception $e)
{
if ($this->enabled()) {
$this->debugbar['exceptions']->addException($e);
}
return $this;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* @package Grav.Common.Errors
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Errors;
use Whoops\Handler\Handler;
class BareHandler extends Handler
{
/**
* @return int|null
*/
public function handle()
{
return Handler::QUIT;
}
}

View File

@@ -22,16 +22,30 @@ class Errors
// Setup Whoops-based error handler
$whoops = new \Whoops\Run;
$verbosity = 1;
if (isset($config['display'])) {
if ($config['display']) {
if (is_int($config['display'])) {
$verbosity = $config['display'];
} else {
$verbosity = $config['display'] ? 1 : 0;
}
}
switch ($verbosity) {
case 1:
$error_page = new Whoops\Handler\PrettyPageHandler;
$error_page->setPageTitle('Crikey! There was an error...');
$error_page->addResourcePath(GRAV_ROOT . '/system/assets');
$error_page->addCustomCss('whoops.css');
$whoops->pushHandler($error_page);
} else {
break;
case -1:
$whoops->pushHandler(new BareHandler);
break;
default:
$whoops->pushHandler(new SimplePageHandler);
}
break;
}
if (method_exists('Whoops\Util\Misc', 'isAjaxRequest')) { //Whoops 2.0

View File

@@ -23,58 +23,63 @@ trait CompiledFile
// Set some options
$this->settings(['native' => true, 'compat' => true]);
// If nothing has been loaded, attempt to get pre-compiled version of the file first.
if ($var === null && $this->raw === null && $this->content === null) {
$key = md5($this->filename);
$file = PhpFile::instance(CACHE_DIR . DS . "compiled/files/{$key}{$this->extension}.php");
try {
// If nothing has been loaded, attempt to get pre-compiled version of the file first.
if ($var === null && $this->raw === null && $this->content === null) {
$key = md5($this->filename);
$file = PhpFile::instance(CACHE_DIR . DS . "compiled/files/{$key}{$this->extension}.php");
$modified = $this->modified();
$modified = $this->modified();
if (!$modified) {
return $this->decode($this->raw());
}
$class = get_class($this);
$cache = $file->exists() ? $file->content() : null;
// Load real file if cache isn't up to date (or is invalid).
if (
!isset($cache['@class'])
|| $cache['@class'] != $class
|| $cache['modified'] != $modified
|| $cache['filename'] != $this->filename
) {
// Attempt to lock the file for writing.
try {
$file->lock(false);
} catch (\Exception $e) {
// Another process has locked the file; we will check this in a bit.
if (!$modified) {
return $this->decode($this->raw());
}
// Decode RAW file into compiled array.
$data = (array) $this->decode($this->raw());
$cache = [
'@class' => $class,
'filename' => $this->filename,
'modified' => $modified,
'data' => $data
];
$class = get_class($this);
// If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) {
$file->save($cache);
$file->unlock();
$cache = $file->exists() ? $file->content() : null;
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate')) {
opcache_invalidate($file->filename(), true);
// Load real file if cache isn't up to date (or is invalid).
if (
!isset($cache['@class'])
|| $cache['@class'] != $class
|| $cache['modified'] != $modified
|| $cache['filename'] != $this->filename
) {
// Attempt to lock the file for writing.
try {
$file->lock(false);
} catch (\Exception $e) {
// Another process has locked the file; we will check this in a bit.
}
// Decode RAW file into compiled array.
$data = (array)$this->decode($this->raw());
$cache = [
'@class' => $class,
'filename' => $this->filename,
'modified' => $modified,
'data' => $data
];
// If compiled file wasn't already locked by another process, save it.
if ($file->locked() !== false) {
$file->save($cache);
$file->unlock();
// Compile cached file into bytecode cache
if (function_exists('opcache_invalidate')) {
opcache_invalidate($file->filename(), true);
}
}
}
}
$file->free();
$file->free();
$this->content = $cache['data'];
$this->content = $cache['data'];
}
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Failed to read %s: %s', basename($this->filename), $e->getMessage()), 500, $e);
}
return parent::content($var);

View File

@@ -0,0 +1,28 @@
<?php
/**
* @package Grav.Common.File
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\File;
use RocketTheme\Toolbox\File\JsonFile;
class CompiledJsonFile extends JsonFile
{
use CompiledFile;
/**
* Decode RAW string into contents.
*
* @param string $var
* @param bool $assoc
* @return array mixed
*/
protected function decode($var, $assoc = true)
{
return (array) json_decode($var, $assoc);
}
}

View File

@@ -70,15 +70,47 @@ abstract class Folder
/** @var \RecursiveDirectoryIterator $file */
foreach ($iterator as $filepath => $file) {
$file_modified = $file->getMTime();
if ($file_modified > $last_modified) {
$last_modified = $file_modified;
try {
$file_modified = $file->getMTime();
if ($file_modified > $last_modified) {
$last_modified = $file_modified;
}
} catch (\Exception $e) {
Grav::instance()['log']->error('Could not process file: ' . $e->getMessage());
}
}
return $last_modified;
}
/**
* Recursively md5 hash all files in a path
*
* @param $path
* @return string
*/
public static function hashAllFiles($path)
{
$flags = \RecursiveDirectoryIterator::SKIP_DOTS;
$files = [];
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
$directory = new \RecursiveDirectoryIterator($path, $flags);
}
$iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $filepath => $file) {
$files[] = $file->getPath() . $file->getMTime();
}
return md5(serialize($files));
}
/**
* Get relative path between target and base path. If path isn't relative, return full path.
*
@@ -179,7 +211,8 @@ abstract class Folder
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
if ($recursive) {
$flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS + \FilesystemIterator::CURRENT_AS_SELF;
$flags = \RecursiveDirectoryIterator::SKIP_DOTS + \FilesystemIterator::UNIX_PATHS
+ \FilesystemIterator::CURRENT_AS_SELF + \FilesystemIterator::FOLLOW_SYMLINKS;
if ($locator->isStream($path)) {
$directory = $locator->getRecursiveIterator($path, $flags);
} else {
@@ -311,12 +344,10 @@ abstract class Folder
// Make sure that path to the target exists before moving.
self::create(dirname($target));
// Just rename the directory.
$success = @rename($source, $target);
if (!$success) {
$error = error_get_last();
throw new \RuntimeException($error['message']);
self::copy($source, $target);
self::delete($source);
}
// Make sure that the change will be detected when caching.
@@ -357,7 +388,6 @@ abstract class Folder
/**
* @param string $folder
* @throws \RuntimeException
* @internal
*/
public static function mkdir($folder)
{
@@ -367,7 +397,6 @@ abstract class Folder
/**
* @param string $folder
* @throws \RuntimeException
* @internal
*/
public static function create($folder)
{
@@ -402,10 +431,7 @@ abstract class Folder
// If the destination directory does not exist create it
if (!is_dir($dest)) {
if (!mkdir($dest)) {
// If the destination directory could not be created stop processing
return false;
}
Folder::mkdir($dest);
}
// Open the source directory to read in files
@@ -436,22 +462,11 @@ abstract class Folder
return @unlink($folder);
}
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
/** @var \DirectoryIterator $fileinfo */
foreach ($files as $fileinfo) {
if ($fileinfo->isDir()) {
if (false === @rmdir($fileinfo->getRealPath())) {
return false;
}
} else {
if (false === @unlink($fileinfo->getRealPath())) {
return false;
}
}
// Go through all items in filesystem and recursively remove everything.
$files = array_diff(scandir($folder), array('.', '..'));
foreach ($files as $file) {
$path = "{$folder}/{$file}";
(is_dir($path)) ? self::doDelete($path) : @unlink($path);
}
return $include_target ? @rmdir($folder) : true;

View File

@@ -61,14 +61,36 @@ class GPM extends Iterator
}
/**
* Returns the Locally installed packages
* @return Iterator The installed packages
* Return the locally installed packages
*
* @return Local\Packages
*/
public function getInstalled()
{
return $this->installed;
}
/**
* Returns the Locally installable packages
*
* @param array $list_type_installed
* @return Iterator The installed packages
*/
public function getInstallable($list_type_installed = ['plugins' => true, 'themes' => true])
{
$items = ['total' => 0];
foreach ($list_type_installed as $type => $type_installed) {
if ($type_installed === false) {
continue;
}
$methodInstallableType = 'getInstalled' . ucfirst($type);
$to_install = $this->$methodInstallableType();
$items[$type] = $to_install;
$items['total'] += count($to_install);
}
return $items;
}
/**
* Returns the amount of locally installed packages
* @return integer Amount of installed packages

View File

@@ -29,6 +29,8 @@ class Installer
const ZIP_OPEN_ERROR = 32;
/** @const Error while trying to extract the ZIP package */
const ZIP_EXTRACT_ERROR = 64;
/** @const Invalid source file */
const INVALID_SOURCE = 128;
/**
* Destination folder on which validation checks are applied
@@ -62,13 +64,13 @@ class Installer
/**
* Installs a given package to a given destination.
*
* @param string $package The local path to the ZIP package
* @param string $zip the local path to ZIP package
* @param string $destination The local path to the Grav Instance
* @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
*
* @return boolean True if everything went fine, False otherwise.
* @param array $options Options to use for installing. ie, ['install_path' => 'user/themes/antimatter']
* @param string $extracted The local path to the extacted ZIP package
* @return bool True if everything went fine, False otherwise.
*/
public static function install($package, $destination, $options = [])
public static function install($zip, $destination, $options = [], $extracted = null)
{
$destination = rtrim($destination, DS);
$options = array_merge(self::$options, $options);
@@ -86,34 +88,26 @@ class Installer
return false;
}
$zip = new \ZipArchive();
$archive = $zip->open($package);
$cache_dir = Grav::instance()['locator']->findResource('cache://', true);
$tmp = $cache_dir . DS . 'tmp/Grav-' . uniqid();
// Create a tmp location
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$tmp = $tmp_dir . '/Grav-' . uniqid();
if ($archive !== true) {
self::$error = self::ZIP_OPEN_ERROR;
if (!$extracted) {
$extracted = self::unZip($zip, $tmp);
if (!$extracted) {
Folder::delete($tmp);
return false;
}
}
if (!file_exists($extracted)) {
self::$error = self::INVALID_SOURCE;
return false;
}
Folder::mkdir($tmp);
$unzip = $zip->extractTo($tmp);
if (!$unzip) {
self::$error = self::ZIP_EXTRACT_ERROR;
$zip->close();
Folder::delete($tmp);
return false;
}
$package_folder_name = $zip->getNameIndex(0);
$installer_file_folder = $tmp . '/' . $package_folder_name;
$is_install = true;
$installer = self::loadInstaller($installer_file_folder, $is_install);
$installer = self::loadInstaller($extracted, $is_install);
if (isset($options['is_update']) && $options['is_update'] === true) {
$method = 'preUpdate';
@@ -135,16 +129,15 @@ class Installer
if (!$options['sophisticated']) {
if ($options['theme']) {
self::copyInstall($zip, $install_path, $tmp);
self::copyInstall($extracted, $install_path);
} else {
self::moveInstall($zip, $install_path, $tmp);
self::moveInstall($extracted, $install_path);
}
} else {
self::sophisticatedInstall($zip, $install_path, $tmp);
self::sophisticatedInstall($extracted, $install_path);
}
Folder::delete($tmp);
$zip->close();
if (isset($options['is_update']) && $options['is_update'] === true) {
$method = 'postUpdate';
@@ -163,6 +156,43 @@ class Installer
}
/**
* Unzip a file to somewhere
*
* @param $zip_file
* @param $destination
* @return bool|string
*/
public static function unZip($zip_file, $destination)
{
$zip = new \ZipArchive();
$archive = $zip->open($zip_file);
if ($archive === true) {
Folder::mkdir($destination);
$unzip = $zip->extractTo($destination);
if (!$unzip) {
self::$error = self::ZIP_EXTRACT_ERROR;
Folder::delete($destination);
$zip->close();
return false;
}
$package_folder_name = preg_replace('#\./$#', '', $zip->getNameIndex(0));
$zip->close();
$extracted_folder = $destination . '/' . $package_folder_name;
return $extracted_folder;
}
self::$error = self::ZIP_EXTRACT_ERROR;
return false;
}
/**
* Instantiates and returns the package installer class
*
@@ -211,80 +241,67 @@ class Installer
}
/**
* @param \ZipArchive $zip
* @param $source_path
* @param $install_path
* @param $tmp
*
* @return bool
*/
public static function moveInstall(\ZipArchive $zip, $install_path, $tmp)
public static function moveInstall($source_path, $install_path)
{
$container = $zip->getNameIndex(0);
if (file_exists($install_path)) {
Folder::delete($install_path);
}
Folder::move($tmp . DS . $container, $install_path);
Folder::move($source_path, $install_path);
return true;
}
/**
* @param \ZipArchive $zip
* @param $source_path
* @param $install_path
* @param $tmp
*
* @return bool
*/
public static function copyInstall(\ZipArchive $zip, $install_path, $tmp)
public static function copyInstall($source_path, $install_path)
{
$firstDir = $zip->getNameIndex(0);
if (empty($firstDir)) {
throw new \RuntimeException("Directory $firstDir is missing");
if (empty($source_path)) {
throw new \RuntimeException("Directory $source_path is missing");
} else {
$tmp = realpath($tmp . DS . $firstDir);
Folder::rcopy($tmp, $install_path);
Folder::rcopy($source_path, $install_path);
}
return true;
}
/**
* @param \ZipArchive $zip
* @param $source_path
* @param $install_path
* @param $tmp
*
* @return bool
*/
public static function sophisticatedInstall(\ZipArchive $zip, $install_path, $tmp)
public static function sophisticatedInstall($source_path, $install_path)
{
for ($i = 0, $l = $zip->numFiles; $i < $l; $i++) {
$filename = $zip->getNameIndex($i);
$fileinfo = pathinfo($filename);
$depth = count(explode(DS, rtrim($filename, '/')));
foreach (new \DirectoryIterator($source_path) as $file) {
if ($depth > 2) {
if ($file->isLink() || $file->isDot()) {
continue;
}
$path = $install_path . DS . $fileinfo['basename'];
$path = $install_path . DS . $file->getBasename();
if (is_link($path)) {
continue;
} else {
if (is_dir($path)) {
Folder::delete($path);
Folder::move($tmp . DS . $filename, $path);
if ($file->isDir()) {
Folder::delete($path);
Folder::move($file->getPathname(), $path);
if ($fileinfo['basename'] == 'bin') {
foreach (glob($path . DS . '*') as $file) {
@chmod($file, 0755);
}
if ($file->getBasename() == 'bin') {
foreach (glob($path . DS . '*') as $bin_file) {
@chmod($bin_file, 0755);
}
} else {
@unlink($path);
@copy($tmp . DS . $filename, $path);
}
} else {
@unlink($path);
@copy($file->getPathname(), $path);
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* @package Grav.Common.GPM
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\GPM;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
/**
* Class Licenses
*
* @package Grav\Common\GPM
*/
class Licenses
{
/**
* Regex to validate the format of a License
*
* @var string
*/
protected static $regex = '^(?:[A-F0-9]{8}-){3}(?:[A-F0-9]{8}){1}$';
protected static $file;
/**
* Returns the license for a Premium package
*
* @param $slug
* @param $license
*
* @return boolean
*/
public static function set($slug, $license)
{
$licenses = self::getLicenseFile();
$data = $licenses->content();
$slug = strtolower($slug);
if ($license && !self::validate($license)) {
return false;
}
if (!is_string($license)) {
if (isset($data['licenses'][$slug])) {
unset($data['licenses'][$slug]);
} else {
return false;
}
} else {
$data['licenses'][$slug] = $license;
}
$licenses->save($data);
$licenses->free();
return true;
}
/**
* Returns the license for a Premium package
*
* @param $slug
*
* @return string
*/
public static function get($slug = null)
{
$licenses = self::getLicenseFile();
$data = $licenses->content();
$licenses->free();
$slug = strtolower($slug);
if (!$slug) {
return isset($data['licenses']) ? $data['licenses'] : [];
}
if (!isset($data['licenses']) || !isset($data['licenses'][$slug])) {
return '';
}
return $data['licenses'][$slug];
}
/**
* Validates the License format
*
* @param $license
*
* @return bool
*/
public static function validate($license = null)
{
if (!is_string($license)) {
return false;
}
return preg_match('#' . self::$regex. '#', $license);
}
/**
* Get's the License File object
*
* @return \RocketTheme\Toolbox\File\FileInterface
*/
public static function getLicenseFile()
{
if (!isset(self::$file)) {
$path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml';;
if (!file_exists($path)) {
touch($path);
}
self::$file = CompiledYamlFile::instance($path);
}
return self::$file;
}
}

View File

@@ -39,8 +39,10 @@ class Response
CURLOPT_USERAGENT => 'Grav GPM',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_FAILONERROR => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HEADER => false,
//CURLOPT_SSL_VERIFYPEER => true, // this is set in the constructor since it's a setting
/**
* Example of callback parameters from within your own class
*/
@@ -48,11 +50,17 @@ class Response
//CURLOPT_PROGRESSFUNCTION => [$this, 'progress']
],
'fopen' => [
'method' => 'GET',
'user_agent' => 'Grav GPM',
'max_redirects' => 5,
'follow_location' => 1,
'timeout' => 15,
'method' => 'GET',
'user_agent' => 'Grav GPM',
'max_redirects' => 5,
'follow_location' => 1,
'timeout' => 15,
/* // this is set in the constructor since it's a setting
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
*/
/**
* Example of callback parameters from within your own class
*/
@@ -101,8 +109,61 @@ class Response
} catch (\Exception $e) {
}
$options = array_replace_recursive(self::$defaults, $options);
$method = 'get' . ucfirst(strtolower(self::$method));
$config = Grav::instance()['config'];
$overrides = [];
// SSL Verify Peer and Proxy Setting
$settings = [
'method' => $config->get('system.gpm.method', self::$method),
'verify_peer' => $config->get('system.gpm.verify_peer', true),
// `system.proxy_url` is for fallback
// introduced with 1.1.0-beta.1 probably safe to remove at some point
'proxy_url' => $config->get('system.gpm.proxy_url', $config->get('system.proxy_url', false)),
];
if (!$settings['verify_peer']) {
$overrides = array_replace_recursive([], $overrides, [
'curl' => [
CURLOPT_SSL_VERIFYPEER => $settings['verify_peer']
],
'fopen' => [
'ssl' => [
'verify_peer' => $settings['verify_peer'],
'verify_peer_name' => $settings['verify_peer'],
]
]
]);
}
// Proxy Setting
if ($settings['proxy_url']) {
$proxy = parse_url($settings['proxy_url']);
$fopen_proxy = ($proxy['scheme'] ?: 'http') . '://' . $proxy['host'] . (isset($proxy['port']) ? ':' . $proxy['port'] : '');
$overrides = array_replace_recursive([], $overrides, [
'curl' => [
CURLOPT_PROXY => $proxy['host'],
CURLOPT_PROXYTYPE => 'HTTP'
],
'fopen' => [
'proxy' => $fopen_proxy,
'request_fulluri' => true
]
]);
if (isset($proxy['port'])) {
$overrides['curl'][CURLOPT_PROXYPORT] = $proxy['port'];
}
if (isset($proxy['user']) && isset($proxy['pass'])) {
$fopen_auth = $auth = base64_encode($proxy['user'] . ':' . $proxy['pass']);
$overrides['curl'][CURLOPT_PROXYUSERPWD] = $proxy['user'] . ':' . $proxy['pass'];
$overrides['fopen']['header'] = "Proxy-Authorization: Basic $fopen_auth";
}
}
$options = array_replace_recursive(self::$defaults, $options, $overrides);
$method = 'get' . ucfirst(strtolower($settings['method']));
self::$callback = $callback;
return static::$method($uri, $options, $callback);
@@ -199,30 +260,39 @@ class Response
$options = $args[1];
$callback = $args[2];
// if proxy set add that
$config = Grav::instance()['config'];
$proxy_url = $config->get('system.gpm.proxy_url', $config->get('system.proxy_url'));
if ($proxy_url) {
$parsed_url = parse_url($proxy_url);
$options['fopen']['proxy'] = ($parsed_url['scheme'] ?: 'http') . '://' . $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
$options['fopen']['request_fulluri'] = true;
if (isset($parsed_url['user']) && isset($parsed_url['pass'])) {
$auth = base64_encode($parsed_url['user'] . ':' . $parsed_url['pass']);
$options['fopen']['header'] = "Proxy-Authorization: Basic $auth";
}
}
if ($callback) {
$options['fopen']['notification'] = ['self', 'progress'];
}
$stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
if (isset($options['fopen']['ssl'])) {
$ssl = $options['fopen']['ssl'];
unset($options['fopen']['ssl']);
$stream = stream_context_create([
'http' => $options['fopen'],
'ssl' => $ssl
], $options['fopen']);
} else {
$stream = stream_context_create(['http' => $options['fopen']], $options['fopen']);
}
$content = @file_get_contents($uri, false, $stream);
if ($content === false) {
throw new \RuntimeException("Error while trying to download '$uri'");
$code = null;
if (isset($http_response_header)) {
$code = explode(' ', $http_response_header[0])[1];
}
switch ($code) {
case '404':
throw new \RuntimeException("Page not found");
case '401':
throw new \RuntimeException("Invalid LICENSE");
default:
throw new \RuntimeException("Error while trying to download '$uri'\n");
}
}
return $content;
@@ -248,8 +318,17 @@ class Response
$errno = curl_errno($ch);
if ($errno) {
$error_message = curl_strerror($errno);
throw new \RuntimeException("cURL error ({$errno}):\n {$error_message}");
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error_message = curl_strerror($errno) . "\n" . curl_error($ch);
switch ($code) {
case '404':
throw new \RuntimeException("Page not found");
case '401':
throw new \RuntimeException("Invalid LICENSE");
default:
throw new \RuntimeException("Error while trying to download '$uri'\nMessage: $error_message");
}
}
curl_close($ch);
@@ -276,24 +355,6 @@ class Response
);
}
// if proxy set add that
$config = Grav::instance()['config'];
$proxy_url = $config->get('system.gpm.proxy_url', $config->get('system.proxy_url'));
if ($proxy_url) {
$parsed_url = parse_url($proxy_url);
$options['curl'][CURLOPT_PROXY] = $parsed_url['host'];
$options['curl'][CURLOPT_PROXYTYPE] = 'HTTP';
if (isset($parsed_url['port'])) {
$options['curl'][CURLOPT_PROXYPORT] = $parsed_url['port'];
}
if (isset($parsed_url['user']) && isset($parsed_url['pass'])) {
$options['curl'][CURLOPT_PROXYUSERPWD] = $parsed_url['user'] . ':' . $parsed_url['pass'];
}
}
// no open_basedir set, we can proceed normally
if (!ini_get('open_basedir')) {
curl_setopt_array($ch, $options['curl']);

View File

@@ -29,6 +29,7 @@ class Grav extends Container
'events' => 'RocketTheme\Toolbox\Event\EventDispatcher',
'cache' => 'Grav\Common\Cache',
'session' => 'Grav\Common\Session',
'Grav\Common\Service\MessagesServiceProvider',
'plugins' => 'Grav\Common\Plugins',
'themes' => 'Grav\Common\Themes',
'twig' => 'Grav\Common\Twig\Twig',
@@ -208,42 +209,17 @@ class Grav extends Container
}
}
/**
* Returns mime type for the file format.
*
* @param string $format
*
* @return string
*/
public function mime($format)
{
switch ($format) {
case 'json':
return 'application/json';
case 'html':
return 'text/html';
case 'atom':
return 'application/atom+xml';
case 'rss':
return 'application/rss+xml';
case 'xml':
return 'application/xml';
}
return 'text/html';
}
/**
* Set response header.
*/
public function header()
{
$extension = $this['uri']->extension();
/** @var Page $page */
$page = $this['page'];
header('Content-type: ' . $this->mime($extension));
$format = $page->templateFormat();
header('Content-type: ' . Utils::getMimeByExtension($format, 'text/html'));
// Calculate Expires Headers if set to > 0
$expires = $page->expires();
@@ -266,7 +242,7 @@ class Grav extends Container
}
// Set debugger data in headers
if (!($extension === null || $extension == 'html')) {
if (!($format === null || $format == 'html')) {
$this['debugger']->enabled(false);
}
@@ -330,7 +306,12 @@ class Grav extends Container
} else {
// Without gzip we have no other choice than to prevent server from compressing the output.
// This action turns off mod_deflate which would prevent us from closing the connection.
header('Content-Encoding: none');
if ($this['config']->get('system.cache.allow_webserver_gzip')) {
header('Content-Encoding: identity');
} else {
header('Content-Encoding: none');
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* @package Grav.Common.Helpers
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
class Base32 {
protected static $base32Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
protected static $base32Lookup = array(
0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
);
/**
* Encode in Base32
*
* @param $bytes
* @return string
*/
public static function encode( $bytes ) {
$i = 0; $index = 0; $digit = 0;
$base32 = "";
$bytes_len = strlen($bytes);
while( $i < $bytes_len ) {
$currByte = ord($bytes{$i});
/* Is the current digit going to span a byte boundary? */
if( $index > 3 ) {
if( ($i + 1) < $bytes_len ) {
$nextByte = ord($bytes{$i+1});
} else {
$nextByte = 0;
}
$digit = $currByte & (0xFF >> $index);
$index = ($index + 5) % 8;
$digit <<= $index;
$digit |= $nextByte >> (8 - $index);
$i++;
} else {
$digit = ($currByte >> (8 - ($index + 5))) & 0x1F;
$index = ($index + 5) % 8;
if( $index == 0 ) $i++;
}
$base32 .= self::$base32Chars{$digit};
}
return $base32;
}
/**
* Decode in Base32
*
* @param $base32
* @return string
*/
public static function decode( $base32 ) {
$bytes = array();
$base32_len = strlen($base32);
for( $i=$base32_len*5/8-1; $i>=0; --$i ) {
$bytes[] = 0;
}
for( $i = 0, $index = 0, $offset = 0; $i < $base32_len; $i++ ) {
$lookup = ord($base32{$i}) - ord('0');
/* Skip chars outside the lookup table */
if( $lookup < 0 || $lookup >= count(self::$base32Lookup) ) {
continue;
}
$digit = self::$base32Lookup[$lookup];
/* If this digit is not in the table, ignore it */
if( $digit == 0xFF ) continue;
if( $index <= 3 ) {
$index = ($index + 5) % 8;
if( $index == 0) {
$bytes[$offset] |= $digit;
$offset++;
if( $offset >= count($bytes) ) break;
} else {
$bytes[$offset] |= $digit << (8 - $index);
}
} else {
$index = ($index + 5) % 8;
$bytes[$offset] |= ($digit >> $index);
$offset++;
if ($offset >= count($bytes) ) break;
$bytes[$offset] |= $digit << (8 - $index);
}
}
$bites = "";
foreach( $bytes as $byte ) $bites .= chr($byte);
return $bites;
}
}

View File

@@ -0,0 +1,350 @@
<?php
/**
* @package Grav.Common.Helpers
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Helpers;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Page\Medium\Medium;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class Excerpts
{
/**
* Process Grav image media URL from HTML tag
*
* @param $html HTML tag e.g. `<img src="image.jpg" />`
* @param $page The current page object
* @return string Returns final HTML string
*/
public static function processImageHtml($html, $page)
{
$excerpt = static::getExcerptFromHtml($html, 'img');
$original_src = $excerpt['element']['attributes']['src'];
$excerpt['element']['attributes']['href'] = $original_src;
$excerpt = static::processLinkExcerpt($excerpt, $page, 'image');
$excerpt['element']['attributes']['src'] = $excerpt['element']['attributes']['href'];
unset ($excerpt['element']['attributes']['href']);
$excerpt = static::processImageExcerpt($excerpt, $page);
$excerpt['element']['attributes']['data-src'] = $original_src;
$html = static::getHtmlFromExcerpt($excerpt);
return $html;
}
/**
* Get an Excerpt array from a chunk of HTML
*
* @param $html Chunk of HTML
* @param $tag a tag, for example `img`
* @return array|null returns nested array excerpt
*/
public static function getExcerptFromHtml($html, $tag)
{
$doc = new \DOMDocument();
$doc->loadHtml($html);
$images = $doc->getElementsByTagName($tag);
$excerpt = null;
foreach ($images as $image) {
$attributes = [];
foreach ($image->attributes as $name => $value) {
$attributes[$name] = $value->value;
}
$excerpt = [
'element' => [
'name' => $image->tagName,
'attributes' => $attributes
]
];
}
return $excerpt;
}
/**
* Rebuild HTML tag from an excerpt array
*
* @param $excerpt
* @return string
*/
public static function getHtmlFromExcerpt($excerpt)
{
$element = $excerpt['element'];
$html = '<'.$element['name'];
if (isset($element['attributes'])) {
foreach ($element['attributes'] as $name => $value) {
if ($value === null) {
continue;
}
$html .= ' '.$name.'="'.$value.'"';
}
}
if (isset($element['text'])) {
$html .= '>';
$html .= $element['text'];
$html .= '</'.$element['name'].'>';
} else {
$html .= ' />';
}
return $html;
}
/**
* Process a Link excerpt
*
* @param $excerpt
* @param $page
* @param string $type
* @return mixed
*/
public static function processLinkExcerpt($excerpt, $page, $type = 'link')
{
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['href']));
$url_parts = static::parseUrl($url);
// If there is a query, then parse it and build action calls.
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
$carry[$parts[0]] = $value;
return $carry;
}, []);
// Valid attributes supported.
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
// Unless told to not process, go through actions.
if (array_key_exists('noprocess', $actions)) {
unset($actions['noprocess']);
} else {
// Loop through actions for the image and call them.
foreach ($actions as $attrib => $value) {
$key = $attrib;
if (in_array($attrib, $valid_attributes)) {
// support both class and classes.
if ($attrib == 'classes') {
$attrib = 'class';
}
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
unset($actions[$key]);
}
}
}
$url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
}
// If no query elements left, unset query.
if (empty($url_parts['query'])) {
unset ($url_parts['query']);
}
// Set path to / if not set.
if (empty($url_parts['path'])) {
$url_parts['path'] = '';
}
// If scheme isn't http(s)..
if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
// Handle custom streams.
if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
$url_parts['path'] = Grav::instance()['base_url_relative'] . '/' . static::resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
unset($url_parts['stream'], $url_parts['scheme']);
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
}
return $excerpt;
}
// Handle paths and such.
$url_parts = Uri::convertUrl($page, $url_parts, $type);
// Build the URL from the component parts and set it on the element.
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
/**
* Process an image excerpt
*
* @param $excerpt
* @param $page
* @return mixed
*/
public static function processImageExcerpt($excerpt, $page)
{
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
$url_parts = static::parseUrl($url);
$media = null;
$filename = null;
if (!empty($url_parts['stream'])) {
$filename = $url_parts['scheme'] . '://' . (isset($url_parts['path']) ? $url_parts['path'] : '');
$media = $page->media();
} else {
// File is also local if scheme is http(s) and host matches.
$local_file = isset($url_parts['path'])
&& (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https']))
&& (empty($url_parts['host']) || $url_parts['host'] == Grav::instance()['uri']->host());
if ($local_file) {
$filename = basename($url_parts['path']);
$folder = dirname($url_parts['path']);
// Get the local path to page media if possible.
if ($folder === $page->url(false, false, false)) {
// Get the media objects for this page.
$media = $page->media();
} else {
// see if this is an external page to this one
$base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
$ext_page = Grav::instance()['pages']->dispatch($page_route, true);
if ($ext_page) {
$media = $ext_page->media();
} else {
Grav::instance()->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
}
}
}
}
// If there is a media file that matches the path referenced..
if ($media && $filename && isset($media[$filename])) {
// Get the medium object.
/** @var Medium $medium */
$medium = $media[$filename];
// Process operations
$medium = static::processMediaActions($medium, $url_parts);
$alt = isset($excerpt['element']['attributes']['alt']) ? $excerpt['element']['attributes']['alt'] : '';
$title = isset($excerpt['element']['attributes']['title']) ? $excerpt['element']['attributes']['title'] : '';
$class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
$id = isset($excerpt['element']['attributes']['id']) ? $excerpt['element']['attributes']['id'] : '';
$excerpt['element'] = $medium->parseDownElement($title, $alt, $class, $id, true);
} else {
// Not a current page media file, see if it needs converting to relative.
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
}
return $excerpt;
}
/**
* Process media actions
*
* @param $medium
* @param $url
* @return mixed
*/
public static function processMediaActions($medium, $url)
{
if (!is_array($url)) {
$url_parts = parse_url($url);
} else {
$url_parts = $url;
}
$actions = [];
// if there is a query, then parse it and build action calls
if (isset($url_parts['query'])) {
$actions = array_reduce(explode('&', $url_parts['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? $parts[1] : null;
$carry[] = ['method' => $parts[0], 'params' => $value];
return $carry;
}, []);
}
if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) {
$actions[] = ['method' => 'fixOrientation', 'params' => ''];
}
// loop through actions for the image and call them
foreach ($actions as $action) {
$matches = [];
if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
$args = [explode(',', $matches[1])];
} else {
$args = explode(',', $action['params']);
}
$medium = call_user_func_array([$medium, $action['method']], $args);
}
if (isset($url_parts['fragment'])) {
$medium->urlHash($url_parts['fragment']);
}
return $medium;
}
/**
* Variation of parse_url() which works also with local streams.
*
* @param string $url
* @return array|bool
*/
protected static function parseUrl($url)
{
$url_parts = parse_url($url);
if (isset($url_parts['scheme'])) {
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Special handling for the streams.
if ($locator->schemeExists($url_parts['scheme'])) {
if (isset($url_parts['host'])) {
// Merge host and path into a path.
$url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
unset($url_parts['host']);
}
$url_parts['stream'] = true;
}
}
return $url_parts;
}
protected static function resolveStream($url)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
return $locator->isStream($url) ? ($locator->findResource($url, false) ?: $locator->findResource($url, false, true)) : $url;
}
}

View File

@@ -8,12 +8,16 @@
namespace Grav\Common\Helpers;
use DOMText;
use DOMDocument;
use DOMWordsIterator;
use DOMLettersIterator;
/**
* This file is part of urodoz/truncateHTML.
* This file is part of https://github.com/Bluetel-Solutions/twig-truncate-extension
*
* (c) Albert Lacarta <urodoz@gmail.com>
* Copyright (c) 2015 Bluetel Solutions developers@bluetel.co.uk
* Copyright (c) 2015 Alex Wilson ajw@bluetel.co.uk
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
@@ -21,181 +25,192 @@ use DOMDocument;
class Truncator {
public static $default_options = array(
'ellipsis' => '…',
'break' => ' ',
'length_in_chars' => false,
'word_safe' => false,
);
/**
* Safely truncates HTML by a given number of words.
* @param string $html Input HTML.
* @param integer $limit Limit to how many words we preserve.
* @param string $ellipsis String to use as ellipsis (if any).
* @return string Safe truncated HTML.
*/
public static function truncateWords($html, $limit = 0, $ellipsis = "")
{
if ($limit <= 0) {
return $html;
}
// These tags are allowed to have an ellipsis inside
public static $ellipsable_tags = array(
'p', 'ol', 'ul', 'li',
'div', 'header', 'article', 'nav',
'section', 'footer', 'aside',
'dd', 'dt', 'dl',
);
$dom = self::htmlToDomDocument($html);
public static $self_closing_tags = array(
'br', 'hr', 'img',
);
// Grab the body of our DOM.
$body = $dom->getElementsByTagName("body")->item(0);
// Iterate over words.
$words = new DOMWordsIterator($body);
foreach ($words as $word) {
// If we have exceeded the limit, we delete the remainder of the content.
if ($words->key() >= $limit) {
// Grab current position.
$currentWordPosition = $words->currentWordPosition();
$curNode = $currentWordPosition[0];
$offset = $currentWordPosition[1];
$words = $currentWordPosition[2];
$curNode->nodeValue = substr(
$curNode->nodeValue,
0,
$words[$offset][1] + strlen($words[$offset][0])
);
self::removeProceedingNodes($curNode, $body);
if (!empty($ellipsis)) {
self::insertEllipsis($curNode, $ellipsis);
}
break;
}
}
return self::innerHTML($body);
}
/**
* Truncate given HTML string to specified length.
* If length_in_chars is false it's trimmed by number
* of words, otherwise by number of characters.
*
* @param string $html
* @param integer $length
* @param string|array $opts
* @return string
* Safely truncates HTML by a given number of letters.
* @param string $html Input HTML.
* @param integer $limit Limit to how many letters we preserve.
* @param string $ellipsis String to use as ellipsis (if any).
* @return string Safe truncated HTML.
*/
public static function truncate($html, $length, $opts=array())
public static function truncateLetters($html, $limit = 0, $ellipsis = "")
{
if (is_string($opts)) $opts = array('ellipsis' => $opts);
$opts = array_merge(static::$default_options, $opts);
// wrap the html in case it consists of adjacent nodes like <p>foo</p><p>bar</p>
$html = mb_convert_encoding("<div>".$html."</div>", 'HTML-ENTITIES', 'UTF-8');
$root_node = null;
// Parse using HTML5Lib if it's available.
if (class_exists('HTML5Lib\\Parser')) {
try {
$doc = \HTML5Lib\Parser::parse($html);
$root_node = $doc->documentElement->lastChild->lastChild;
}
catch (\Exception $e) {
;
}
if ($limit <= 0) {
return $html;
}
if ($root_node === null) {
// HTML5Lib not available so we'll have to use DOMDocument
// We'll only be able to parse HTML5 if it's valid XML
$doc = new DOMDocument('4.01', 'utf-8');
$doc->formatOutput = false;
$doc->preserveWhiteSpace = true;
// loadHTML will fail with HTML5 tags (article, nav, etc)
// so we need to suppress errors and if it fails to parse we
// retry with the XML parser instead
$prev_use_errors = libxml_use_internal_errors(true);
if ($doc->loadHTML($html)) {
$root_node = $doc->documentElement->lastChild->lastChild;
}
else if ($doc->loadXML($html)) {
$root_node = $doc->documentElement;
}
else {
libxml_use_internal_errors($prev_use_errors);
throw new \RuntimeException;
}
libxml_use_internal_errors($prev_use_errors);
}
list($text, $_, $opts) = static::truncateNode($doc, $root_node, $length, $opts);
$text = mb_substr(mb_substr($text, 0, -6), 5);
$dom = self::htmlToDomDocument($html);
return $text;
}
// Grab the body of our DOM.
$body = $dom->getElementsByTagName("body")->item(0);
protected static function truncateNode($doc, $node, $length, $opts)
{
if ($length === 0 && !static::ellipsable($node)) {
return array('', 1, $opts);
}
list($inner, $remaining, $opts) = static::innerTruncate($doc, $node, $length, $opts);
if (0 === mb_strlen($inner)) {
return array(in_array(mb_strtolower($node->nodeName), static::$self_closing_tags) ? $doc->saveXML($node) : "", $length - $remaining, $opts);
}
while($node->firstChild) {
$node->removeChild($node->firstChild);
}
$newNode = $doc->createDocumentFragment();
// handle the ampersand
$newNode->appendXml(static::xmlEscape($inner));
$node->appendChild($newNode);
return array($doc->saveXML($node), $length - $remaining, $opts);
}
// Iterate over letters.
$letters = new DOMLettersIterator($body);
foreach ($letters as $letter) {
protected static function innerTruncate($doc, $node, $length, $opts)
{
$inner = '';
$remaining = $length;
foreach($node->childNodes as $childNode) {
if ($childNode->nodeType === XML_ELEMENT_NODE) {
list($txt, $nb, $opts) = static::truncateNode($doc, $childNode, $remaining, $opts);
}
else if ($childNode->nodeType === XML_TEXT_NODE) {
list($txt, $nb, $opts) = static::truncateText($childNode, $remaining, $opts);
} else {
$txt = '';
$nb = 0;
}
// If we have exceeded the limit, we want to delete the remainder of this document.
if ($letters->key() >= $limit) {
// unhandle the ampersand
$txt = static::xmlUnescape($txt);
$currentText = $letters->currentTextPosition();
$currentText[0]->nodeValue = substr($currentText[0]->nodeValue, 0, $currentText[1] + 1);
self::removeProceedingNodes($currentText[0], $body);
$remaining -= $nb;
$inner .= $txt;
if ($remaining < 0) {
if (static::ellipsable($node)) {
$inner = preg_replace('/(?:[\s\pP]+|(?:&(?:[a-z]+|#[0-9]+);?))*$/u', '', $inner).$opts['ellipsis'];
$opts['ellipsis'] = '';
$opts['was_truncated'] = true;
if (!empty($ellipsis)) {
self::insertEllipsis($currentText[0], $ellipsis);
}
break;
}
}
return array($inner, $remaining, $opts);
return self::innerHTML($body);
}
protected static function truncateText($node, $length, $opts)
/**
* Builds a DOMDocument object from a string containing HTML.
* @param string HTML to load
* @returns DOMDocument Returns a DOMDocument object.
*/
public static function htmlToDomDocument($html)
{
$string = $node->textContent;
if (!$html) {
$html = '<p></p>';
}
if ($opts['length_in_chars']) {
$count = mb_strlen($string);
if ($count <= $length && $length > 0) {
return array($string, $count, $opts);
}
if ($opts['word_safe']) {
if (false !== ($breakpoint = mb_strpos($string, $opts['break'], $length))) {
if ($breakpoint < mb_strlen($string) - 1) {
$string = mb_substr($string, 0, $breakpoint) . $opts['break'];
}
// Transform multibyte entities which otherwise display incorrectly.
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
// Internal errors enabled as HTML5 not fully supported.
libxml_use_internal_errors(true);
// Instantiate new DOMDocument object, and then load in UTF-8 HTML.
$dom = new DOMDocument();
$dom->encoding = 'UTF-8';
$dom->loadHTML($html);
return $dom;
}
/**
* Removes all nodes after the current node.
* @param DOMNode|DOMElement $domNode
* @param DOMNode|DOMElement $topNode
* @return void
*/
private static function removeProceedingNodes($domNode, $topNode)
{
$nextNode = $domNode->nextSibling;
if ($nextNode !== null) {
self::removeProceedingNodes($nextNode, $topNode);
$domNode->parentNode->removeChild($nextNode);
} else {
//scan upwards till we find a sibling
$curNode = $domNode->parentNode;
while ($curNode !== $topNode) {
if ($curNode->nextSibling !== null) {
$curNode = $curNode->nextSibling;
self::removeProceedingNodes($curNode, $topNode);
$curNode->parentNode->removeChild($curNode);
break;
}
return array($string, $count, $opts);
$curNode = $curNode->parentNode;
}
return array(mb_substr($node->textContent, 0, $length), $count, $opts);
}
else {
preg_match_all('/\s*\S+/', $string, $words);
$words = $words[0];
$count = count($words);
if ($count <= $length && $length > 0) {
return array($string, $count, $opts);
}
return array(implode('', array_slice($words, 0, $length)), $count, $opts);
}
}
protected static function ellipsable($node)
/**
* Inserts an ellipsis
* @param DOMNode|DOMElement $domNode Element to insert after.
* @param string $ellipsis Text used to suffix our document.
* @return void
*/
private static function insertEllipsis($domNode, $ellipsis)
{
return ($node instanceof DOMDocument)
|| in_array(mb_strtolower($node->nodeName), static::$ellipsable_tags)
;
$avoid = array('a', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5'); //html tags to avoid appending the ellipsis to
if (in_array($domNode->parentNode->nodeName, $avoid) && $domNode->parentNode->parentNode !== null) {
// Append as text node to parent instead
$textNode = new DOMText($ellipsis);
if ($domNode->parentNode->parentNode->nextSibling) {
$domNode->parentNode->parentNode->insertBefore($textNode, $domNode->parentNode->parentNode->nextSibling);
} else {
$domNode->parentNode->parentNode->appendChild($textNode);
}
} else {
// Append to current node
$domNode->nodeValue = rtrim($domNode->nodeValue) . $ellipsis;
}
}
protected static function xmlEscape($string)
{
$string = str_replace('&', '&amp;', $string);
$string = str_replace('<?', '&lt;?', $string);
return $string;
/**
* Returns the innerHTML of a particular DOMElement
*
* @param $element
* @return string
*/
private static function innerHTML($element) {
$innerHTML = "";
$children = $element->childNodes;
foreach ($children as $child)
{
$tmp_dom = new DOMDocument();
$tmp_dom->appendChild($tmp_dom->importNode($child, true));
$innerHTML.=trim($tmp_dom->saveHTML());
}
return $innerHTML;
}
protected static function xmlUnescape($string)
{
$string = str_replace('&amp;', '&', $string);
$string = str_replace('&lt;?', '<?', $string);
return $string;
}
}

View File

@@ -26,11 +26,11 @@ class Inflector
{
if (empty($this->plural)) {
$language = Grav::instance()['language'];
$this->plural = $language->translate('INFLECTOR_PLURALS', null, true);
$this->singular = $language->translate('INFLECTOR_SINGULAR', null, true);
$this->uncountable = $language->translate('INFLECTOR_UNCOUNTABLE', null, true);
$this->irregular = $language->translate('INFLECTOR_IRREGULAR', null, true);
$this->ordinals = $language->translate('INFLECTOR_ORDINALS', null, true);
$this->plural = $language->translate('INFLECTOR_PLURALS', null, true) ?: [];
$this->singular = $language->translate('INFLECTOR_SINGULAR', null, true) ?: [];
$this->uncountable = $language->translate('INFLECTOR_UNCOUNTABLE', null, true) ?: [];
$this->irregular = $language->translate('INFLECTOR_IRREGULAR', null, true) ?: [];
$this->ordinals = $language->translate('INFLECTOR_ORDINALS', null, true) ?: [];
}
}

View File

@@ -189,6 +189,10 @@ class Iterator implements \ArrayAccess, \Iterator, \Countable, \Serializable
*/
public function random($num = 1)
{
if ($num > count($this->items)) {
$num = count($this->items);
}
$this->items = array_intersect_key($this->items, array_flip((array)array_rand($this->items, $num)));
return $this;

View File

@@ -14,7 +14,7 @@ class LanguageCodes
'af' => [ 'name' => 'Afrikaans', 'nativeName' => 'Afrikaans' ],
'ak' => [ 'name' => 'Akan', 'nativeName' => 'Akan' ], // unverified native name
'ast' => [ 'name' => 'Asturian', 'nativeName' => 'Asturianu' ],
'ar' => [ 'name' => 'Arabic', 'nativeName' => 'عربي' ],
'ar' => [ 'name' => 'Arabic', 'nativeName' => 'عربي', 'orientation' => 'rtl'],
'as' => [ 'name' => 'Assamese', 'nativeName' => 'অসমীয়া' ],
'be' => [ 'name' => 'Belarusian', 'nativeName' => 'Беларуская' ],
'bg' => [ 'name' => 'Bulgarian', 'nativeName' => 'Български' ],
@@ -48,10 +48,12 @@ class LanguageCodes
'es-MX' => [ 'name' => 'Spanish (Mexico)', 'nativeName' => 'Español (de México)' ],
'et' => [ 'name' => 'Estonian', 'nativeName' => 'Eesti keel' ],
'eu' => [ 'name' => 'Basque', 'nativeName' => 'Euskara' ],
'fa' => [ 'name' => 'Persian', 'nativeName' => 'فارسی' ],
'fa' => [ 'name' => 'Persian', 'nativeName' => 'فارسی' , 'orientation' => 'rtl' ],
'fi' => [ 'name' => 'Finnish', 'nativeName' => 'Suomi' ],
'fj-FJ' => [ 'name' => 'Fijian', 'nativeName' => 'Vosa vaka-Viti' ],
'fr' => [ 'name' => 'French', 'nativeName' => 'Français' ],
'fr-CA' => [ 'name' => 'French (Canada)', 'nativeName' => 'Français (Canada)' ],
'fr-FR' => [ 'name' => 'French (France)', 'nativeName' => 'Français (France)' ],
'fur' => [ 'name' => 'Friulian', 'nativeName' => 'Furlan' ],
'fur-IT' => [ 'name' => 'Friulian', 'nativeName' => 'Furlan' ],
'fy' => [ 'name' => 'Frisian', 'nativeName' => 'Frysk' ],
@@ -62,7 +64,7 @@ class LanguageCodes
'gl' => [ 'name' => 'Galician', 'nativeName' => 'Galego' ],
'gu' => [ 'name' => 'Gujarati', 'nativeName' => 'ગુજરાતી' ],
'gu-IN' => [ 'name' => 'Gujarati', 'nativeName' => 'ગુજરાતી' ],
'he' => [ 'name' => 'Hebrew', 'nativeName' => 'עברית' ],
'he' => [ 'name' => 'Hebrew', 'nativeName' => 'עברית', 'orientation' => 'rtl' ],
'hi' => [ 'name' => 'Hindi', 'nativeName' => 'हिन्दी' ],
'hi-IN' => [ 'name' => 'Hindi (India)', 'nativeName' => 'हिन्दी (भारत)' ],
'hr' => [ 'name' => 'Croatian', 'nativeName' => 'Hrvatski' ],
@@ -135,7 +137,7 @@ class LanguageCodes
'tt' => [ 'name' => 'Tatar', 'nativeName' => 'Tatarça' ],
'tt-RU' => [ 'name' => 'Tatar', 'nativeName' => 'Tatarça' ],
'uk' => [ 'name' => 'Ukrainian', 'nativeName' => 'Українська' ],
'ur' => [ 'name' => 'Urdu', 'nativeName' => 'اُردو' ],
'ur' => [ 'name' => 'Urdu', 'nativeName' => 'اُردو', 'orientation' => 'rtl' ],
've' => [ 'name' => 'Venda', 'nativeName' => 'Tshivenḓa' ],
'vi' => [ 'name' => 'Vietnamese', 'nativeName' => 'Tiếng Việt' ],
'wo' => [ 'name' => 'Wolof', 'nativeName' => 'Wolof' ],
@@ -165,6 +167,24 @@ class LanguageCodes
}
}
public static function getOrientation($code)
{
if (isset(static::$codes[$code])) {
if (isset(static::$codes[$code]['orientation'])) {
return static::get($code, 'orientation');
}
}
return 'ltr';
}
public static function isRtl($code)
{
if (static::getOrientation($code) == 'rtl') {
return true;
}
return false;
}
public static function getNames(array $keys)
{
$results = [];

View File

@@ -9,8 +9,7 @@
namespace Grav\Common\Markdown;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Helpers\Excerpts;
use RocketTheme\Toolbox\Event\Event;
trait ParsedownGravTrait
@@ -18,13 +17,6 @@ trait ParsedownGravTrait
/** @var Page $page */
protected $page;
/** @var Pages $pages */
protected $pages;
/** @var Uri $uri */
protected $uri;
protected $pages_dir;
protected $special_chars;
protected $twig_link_regex = '/\!*\[(?:.*)\]\((\{([\{%#])\s*(.*?)\s*(?:\2|\})\})\)/';
@@ -42,10 +34,7 @@ trait ParsedownGravTrait
$grav = Grav::instance();
$this->page = $page;
$this->pages = $grav['pages'];
$this->uri = $grav['uri'];
$this->BlockTypes['{'] [] = "TwigTag";
$this->pages_dir = Grav::instance()['locator']->findResource('page://');
$this->special_chars = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
if ($defaults === null) {
@@ -69,16 +58,23 @@ trait ParsedownGravTrait
*/
public function addBlockType($type, $tag, $continuable = false, $completable = false, $index = null)
{
$block = &$this->unmarkedBlockTypes;
if ($type) {
if (!isset($this->BlockTypes[$type])) {
$this->BlockTypes[$type] = [];
}
$block = &$this->BlockTypes[$type];
}
if (!isset($index)) {
$this->BlockTypes[$type] [] = $tag;
$block[] = $tag;
} else {
array_splice($this->BlockTypes[$type], $index, 0, $tag);
array_splice($block, $index, 0, [$tag]);
}
if ($continuable) {
$this->continuable_blocks[] = $tag;
}
if ($completable) {
$this->completable_blocks[] = $tag;
}
@@ -92,10 +88,10 @@ trait ParsedownGravTrait
*/
public function addInlineType($type, $tag, $index = null)
{
if (!isset($index)) {
if (!isset($index) || !isset($this->InlineTypes[$type])) {
$this->InlineTypes[$type] [] = $tag;
} else {
array_splice($this->InlineTypes[$type], $index, 0, $tag);
array_splice($this->InlineTypes[$type], $index, 0, [$tag]);
}
if (strpos($this->inlineMarkerList, $type) === false) {
@@ -151,7 +147,7 @@ trait ParsedownGravTrait
*
* @return $this
*/
function setSpecialChars($special_chars)
public function setSpecialChars($special_chars)
{
$this->special_chars = $special_chars;
@@ -203,74 +199,9 @@ trait ParsedownGravTrait
$excerpt = parent::inlineImage($excerpt);
}
// Some stuff we will need
$actions = [];
$media = null;
// if this is an image
// if this is an image process it
if (isset($excerpt['element']['attributes']['src'])) {
$alt = $excerpt['element']['attributes']['alt'] ?: '';
$title = $excerpt['element']['attributes']['title'] ?: '';
$class = isset($excerpt['element']['attributes']['class']) ? $excerpt['element']['attributes']['class'] : '';
//get the url and parse it
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['src']));
$this_host = isset($url['host']) && $url['host'] == $this->uri->host();
// if there is no host set but there is a path, the file is local
if ((!isset($url['host']) || $this_host) && isset($url['path'])) {
$path_parts = pathinfo($url['path']);
// get the local path to page media if possible
if ($path_parts['dirname'] == $this->page->url(false, false, false)) {
// get the media objects for this page
$media = $this->page->media();
} else {
// see if this is an external page to this one
$base_url = rtrim(Grav::instance()['base_url_relative'] . Grav::instance()['pages']->base(), '/');
$page_route = '/' . ltrim(str_replace($base_url, '', $path_parts['dirname']), '/');
$ext_page = $this->pages->dispatch($page_route, true);
if ($ext_page) {
$media = $ext_page->media();
}
}
// if there is a media file that matches the path referenced..
if ($media && isset($media->all()[$path_parts['basename']])) {
// get the medium object
$medium = $media->all()[$path_parts['basename']];
// if there is a query, then parse it and build action calls
if (isset($url['query'])) {
$url['query'] = htmlspecialchars_decode(urldecode($url['query']));
$actions = array_reduce(explode('&', $url['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? $parts[1] : null;
$carry[] = ['method' => $parts[0], 'params' => $value];
return $carry;
}, []);
}
// loop through actions for the image and call them
foreach ($actions as $action) {
$medium = call_user_func_array([$medium, $action['method']],
explode(',', urldecode($action['params'])));
}
if (isset($url['fragment'])) {
$medium->urlHash($url['fragment']);
}
$excerpt['element'] = $medium->parseDownElement($title, $alt, $class, true);
} else {
// not a current page media file, see if it needs converting to relative
$excerpt['element']['attributes']['src'] = Uri::buildUrl($url);
}
}
$excerpt = Excerpts::processImageExcerpt($excerpt, $this->page);
}
return $excerpt;
@@ -298,63 +229,7 @@ trait ParsedownGravTrait
// if this is a link
if (isset($excerpt['element']['attributes']['href'])) {
$url = parse_url(htmlspecialchars_decode($excerpt['element']['attributes']['href']));
// if there is a query, then parse it and build action calls
if (isset($url['query'])) {
$actions = array_reduce(explode('&', $url['query']), function ($carry, $item) {
$parts = explode('=', $item, 2);
$value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
$carry[$parts[0]] = $value;
return $carry;
}, []);
// valid attributes supported
$valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
// Unless told to not process, go through actions
if (array_key_exists('noprocess', $actions)) {
unset($actions['noprocess']);
} else {
// loop through actions for the image and call them
foreach ($actions as $attrib => $value) {
$key = $attrib;
if (in_array($attrib, $valid_attributes)) {
// support both class and classes
if ($attrib == 'classes') {
$attrib = 'class';
}
$excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
unset($actions[$key]);
}
}
}
$url['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
}
// if no query elements left, unset query
if (empty($url['query'])) {
unset ($url['query']);
}
// set path to / if not set
if (empty($url['path'])) {
$url['path'] = '';
}
// if special scheme, just return
if(isset($url['scheme']) && !Utils::startsWith($url['scheme'], 'http')) {
return $excerpt;
}
// handle paths and such
$url = Uri::convertUrl($this->page, $url, $type);
// build the URL from the component parts and set it on the element
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url);
$excerpt = Excerpts::processLinkExcerpt($excerpt, $this->page, $type);
}
return $excerpt;

View File

@@ -84,7 +84,6 @@ class Collection extends Iterator
public function setParams(array $params)
{
$this->params = array_merge($this->params, $params);
return $this;
}
@@ -124,6 +123,24 @@ class Collection extends Iterator
return !empty($this->items[$offset]) ? $this->pages->get($offset) : null;
}
/**
* Split collection into array of smaller collections.
*
* @param $size
* @return array|Collection[]
*/
public function batch($size)
{
$chunks = array_chunk($this->items, $size, true);
$list = [];
foreach ($chunks as $chunk) {
$list[] = new static($chunk, $this->params, $this->pages);
}
return $list;
}
/**
* Remove item from the list.
*
@@ -154,12 +171,13 @@ class Collection extends Iterator
* @param string $by
* @param string $dir
* @param array $manual
* @param string $sort_flags
*
* @return $this
*/
public function order($by, $dir = 'asc', $manual = null)
public function order($by, $dir = 'asc', $manual = null, $sort_flags = null)
{
$this->items = $this->pages->sortCollection($this, $by, $dir, $manual);
$this->items = $this->pages->sortCollection($this, $by, $dir, $manual, $sort_flags);
return $this;
}

View File

@@ -8,51 +8,77 @@
namespace Grav\Common\Page;
use Grav\Common\Getters;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Medium\AbstractMedia;
use Grav\Common\Page\Medium\GlobalMedia;
use Grav\Common\Page\Medium\MediumFactory;
class Media extends Getters
class Media extends AbstractMedia
{
protected $gettersVariable = 'instances';
protected $path;
protected static $global;
protected $instances = [];
protected $images = [];
protected $videos = [];
protected $audios = [];
protected $files = [];
protected $path;
/**
* @param $path
*/
public function __construct($path)
{
$this->path = $path;
if (!isset(static::$global)) {
// Add fallback to global media.
static::$global = new GlobalMedia($path);
}
$this->init();
}
/**
* @param mixed $offset
*
* @return bool
*/
public function offsetExists($offset)
{
return parent::offsetExists($offset) ?: isset(static::$global[$offset]);
}
/**
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
return parent::offsetGet($offset) ?: static::$global[$offset];
}
/**
* Initialize class.
*/
protected function init()
{
// Handle special cases where page doesn't exist in filesystem.
if (!is_dir($path)) {
if (!is_dir($this->path)) {
return;
}
$this->path = $path;
$iterator = new \FilesystemIterator($path, \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
$iterator = new \FilesystemIterator($this->path, \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS);
$media = [];
/** @var \DirectoryIterator $info */
foreach ($iterator as $path => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() == 'md' || $info->getBasename() === '.DS_Store') {
if (!$info->isFile() || $info->getExtension() == 'md' || $info->getBasename()[0] === '.') {
continue;
}
// Find out what type we're dealing with
list($basename, $ext, $type, $extra) = $this->getFileParts($info->getFilename());
$media["{$basename}.{$ext}"] = isset($media["{$basename}.{$ext}"]) ? $media["{$basename}.{$ext}"] : [];
if ($type === 'alternative') {
$media["{$basename}.{$ext}"][$type] = isset($media["{$basename}.{$ext}"][$type]) ? $media["{$basename}.{$ext}"][$type] : [];
$media["{$basename}.{$ext}"][$type][$extra] = [ 'file' => $path, 'size' => $info->getSize() ];
} else {
$media["{$basename}.{$ext}"][$type] = [ 'file' => $path, 'size' => $info->getSize() ];
@@ -74,16 +100,16 @@ class Media extends Getters
}
// Create the base medium
if (!empty($types['base'])) {
if (empty($types['base'])) {
if (!isset($types['alternative'])) {
continue;
}
$max = max(array_keys($types['alternative']));
$medium = $types['alternative'][$max]['file'];
$medium = MediumFactory::scaledFromMedium($medium, $max, 1)['file'];
} else {
$medium = MediumFactory::fromFile($types['base']['file']);
$medium && $medium->set('size', $types['base']['size']);
} else if (!empty($types['alternative'])) {
$altMedium = reset($types['alternative']);
$ratio = key($types['alternative']);
$altMedium = $altMedium['file'];
$medium = MediumFactory::scaledFromMedium($altMedium, $ratio, 1)['file'];
}
if (empty($medium)) {
@@ -103,10 +129,9 @@ class Media extends Getters
// Build missing alternatives
if (!empty($types['alternative'])) {
$alternatives = $types['alternative'];
$max = max(array_keys($alternatives));
for ($i=2; $i < $max; $i++) {
for ($i=$max; $i > 1; $i--) {
if (isset($alternatives[$i])) {
continue;
}
@@ -114,142 +139,15 @@ class Media extends Getters
$types['alternative'][$i] = MediumFactory::scaledFromMedium($alternatives[$max]['file'], $max, $i);
}
foreach ($types['alternative'] as $ratio => $altMedium) {
$medium->addAlternative($ratio, $altMedium['file']);
foreach ($types['alternative'] as $altMedium) {
if ($altMedium['file'] != $medium) {
$ratio = $altMedium['file']->get('width') / $medium->get('width');
$medium->addAlternative($ratio, $altMedium['file']);
}
}
}
$this->add($name, $medium);
}
}
/**
* Get medium by filename.
*
* @param string $filename
* @return Medium|null
*/
public function get($filename)
{
return isset($this->instances[$filename]) ? $this->instances[$filename] : null;
}
/**
* Get a list of all media.
*
* @return array|Medium[]
*/
public function all()
{
ksort($this->instances, SORT_NATURAL | SORT_FLAG_CASE);
return $this->instances;
}
/**
* Get a list of all image media.
*
* @return array|Medium[]
*/
public function images()
{
ksort($this->images, SORT_NATURAL | SORT_FLAG_CASE);
return $this->images;
}
/**
* Get a list of all video media.
*
* @return array|Medium[]
*/
public function videos()
{
ksort($this->videos, SORT_NATURAL | SORT_FLAG_CASE);
return $this->videos;
}
/**
* Get a list of all audio media.
*
* @return array|Medium[]
*/
public function audios()
{
ksort($this->audios, SORT_NATURAL | SORT_FLAG_CASE);
return $this->audios;
}
/**
* Get a list of all file media.
*
* @return array|Medium[]
*/
public function files()
{
ksort($this->files, SORT_NATURAL | SORT_FLAG_CASE);
return $this->files;
}
/**
* @internal
*/
protected function add($name, $file)
{
$this->instances[$name] = $file;
switch ($file->type) {
case 'image':
$this->images[$name] = $file;
break;
case 'video':
$this->videos[$name] = $file;
break;
case 'audio':
$this->audios[$name] = $file;
break;
default:
$this->files[$name] = $file;
}
}
/**
* Get filename, extension and meta part.
*
* @param string $filename
* @return array
*/
protected function getFileParts($filename)
{
$fileParts = explode('.', $filename);
$name = array_shift($fileParts);
$type = 'base';
$extra = null;
if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) {
$name = $matches[1];
$extension = $matches[3];
$extra = (int) $matches[2];
$type = 'alternative';
if ($extra === 1) {
$type = 'base';
$extra = null;
}
} else {
$extension = null;
while (($part = array_shift($fileParts)) !== null) {
if ($part != 'meta' && $part != 'thumb') {
if (isset($extension)) {
$name .= '.' . $extension;
}
$extension = $part;
} else {
$type = $part;
$extra = '.' . $part . '.' . implode('.', $fileParts);
break;
}
}
}
return array($name, $extension, $type, $extra);
}
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Getters;
abstract class AbstractMedia extends Getters
{
protected $gettersVariable = 'instances';
protected $instances = [];
protected $images = [];
protected $videos = [];
protected $audios = [];
protected $files = [];
/**
* Get medium by filename.
*
* @param string $filename
* @return Medium|null
*/
public function get($filename)
{
return $this->offsetGet($filename);
}
/**
* Call object as function to get medium by filename.
*
* @param string $filename
* @return mixed
*/
public function __invoke($filename)
{
return $this->offsetGet($filename);
}
/**
* Get a list of all media.
*
* @return array|Medium[]
*/
public function all()
{
ksort($this->instances, SORT_NATURAL | SORT_FLAG_CASE);
return $this->instances;
}
/**
* Get a list of all image media.
*
* @return array|Medium[]
*/
public function images()
{
ksort($this->images, SORT_NATURAL | SORT_FLAG_CASE);
return $this->images;
}
/**
* Get a list of all video media.
*
* @return array|Medium[]
*/
public function videos()
{
ksort($this->videos, SORT_NATURAL | SORT_FLAG_CASE);
return $this->videos;
}
/**
* Get a list of all audio media.
*
* @return array|Medium[]
*/
public function audios()
{
ksort($this->audios, SORT_NATURAL | SORT_FLAG_CASE);
return $this->audios;
}
/**
* Get a list of all file media.
*
* @return array|Medium[]
*/
public function files()
{
ksort($this->files, SORT_NATURAL | SORT_FLAG_CASE);
return $this->files;
}
/**
* @param string $name
* @param Medium $file
*/
protected function add($name, $file)
{
$this->instances[$name] = $file;
switch ($file->type) {
case 'image':
$this->images[$name] = $file;
break;
case 'video':
$this->videos[$name] = $file;
break;
case 'audio':
$this->audios[$name] = $file;
break;
default:
$this->files[$name] = $file;
}
}
/**
* Get filename, extension and meta part.
*
* @param string $filename
* @return array
*/
protected function getFileParts($filename)
{
if (preg_match('/(.*)@(\d+)x\.(.*)$/', $filename, $matches)) {
$name = $matches[1];
$extension = $matches[3];
$extra = (int) $matches[2];
$type = 'alternative';
if ($extra === 1) {
$type = 'base';
$extra = null;
}
} else {
$fileParts = explode('.', $filename);
$name = array_shift($fileParts);
$extension = null;
$extra = null;
$type = 'base';
while (($part = array_shift($fileParts)) !== null) {
if ($part != 'meta' && $part != 'thumb') {
if (isset($extension)) {
$name .= '.' . $extension;
}
$extension = $part;
} else {
$type = $part;
$extra = '.' . $part . '.' . implode('.', $fileParts);
break;
}
}
}
return array($name, $extension, $type, $extra);
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* @package Grav.Common.Page
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Page\Medium;
use Grav\Common\Grav;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class GlobalMedia extends AbstractMedia
{
/**
* @param mixed $offset
*
* @return bool
*/
public function offsetExists($offset)
{
return parent::offsetExists($offset) ?: !empty($this->resolveStream($offset));
}
/**
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
return parent::offsetGet($offset) ?: $this->addMedium($offset);
}
/**
* @param string $filename
* @return string|null
*/
protected function resolveStream($filename)
{
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
return $locator->isStream($filename) ? ($locator->findResource($filename) ?: null) : null;
}
/**
* @param string $stream
* @return Medium|null
*/
protected function addMedium($stream)
{
$filename = $this->resolveStream($stream);
if (!$filename) {
return null;
}
$path = dirname($filename);
list($basename, $ext,, $extra) = $this->getFileParts(basename($filename));
$medium = MediumFactory::fromFile($filename);
if (empty($medium)) {
return null;
}
$medium->set('size', filesize($filename));
$scale = (int) ($extra ?: 1);
if ($scale !== 1) {
$altMedium = $medium;
// Create scaled down regular sized image.
$medium = MediumFactory::scaledFromMedium($altMedium, $scale, 1)['file'];
if (empty($medium)) {
return null;
}
// Add original sized image as alternative.
$medium->addAlternative($scale, $altMedium['file']);
// Locate or generate smaller retina images.
for ($i = $scale-1; $i > 1; $i--) {
$altFilename = "{$path}/{$basename}@{$i}x.{$ext}";
if (file_exists($altFilename)) {
$scaled = MediumFactory::fromFile($altFilename);
} else {
$scaled = MediumFactory::scaledFromMedium($altMedium, $scale, $i)['file'];
}
if ($scaled) {
$medium->addAlternative($i, $scaled);
}
}
}
$meta = "{$path}/{$basename}.{$ext}.yaml";
if (file_exists($meta)) {
$medium->addMetaFile($meta);
}
$meta = "{$path}/{$basename}.{$ext}.meta.yaml";
if (file_exists($meta)) {
$medium->addMetaFile($meta);
}
$thumb = "{$path}/{$basename}.thumb.{$ext}";
if (file_exists($thumb)) {
$medium->set('thumbnails.page', $thumb);
}
$this->add($stream, $medium);
return $medium;
}
}

View File

@@ -17,7 +17,7 @@ class ImageMedium extends Medium
/**
* @var array
*/
protected $thumbnailTypes = [ 'page', 'media', 'default' ];
protected $thumbnailTypes = ['page', 'media', 'default'];
/**
* @var ImageFile
@@ -58,18 +58,13 @@ class ImageMedium extends Medium
* @var array
*/
public static $magic_resize_actions = [
'resize' => [ 0, 1 ],
'forceResize' => [ 0, 1 ],
'cropResize' => [ 0, 1 ],
'crop' => [ 0, 1, 2, 3 ],
'zoomCrop' => [ 0, 1 ]
'resize' => [0, 1],
'forceResize' => [0, 1],
'cropResize' => [0, 1],
'crop' => [0, 1, 2, 3],
'zoomCrop' => [0, 1]
];
/**
* @var array
*/
protected $derivatives = [];
/**
* @var string
*/
@@ -197,66 +192,120 @@ class ImageMedium extends Medium
*/
public function srcset($reset = true)
{
if (empty($this->alternatives) && empty($this->derivatives)) {
if (empty($this->alternatives)) {
if ($reset) {
$this->reset();
}
return '';
}
if (!empty($this->derivatives)) {
asort($this->derivatives);
foreach ($this->derivatives as $url => $width) {
$srcset[] = $url . ' ' . $width . 'w';
}
$srcset[] = $this->url($reset) . ' ' . $this->get('width') . 'w';
}
else {
$srcset = [ $this->url($reset) . ' ' . $this->get('width') . 'w' ];
foreach ($this->alternatives as $ratio => $medium) {
$srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
}
$srcset = [];
foreach ($this->alternatives as $ratio => $medium) {
$srcset[] = $medium->url($reset) . ' ' . $medium->get('width') . 'w';
}
$srcset[] = $this->url($reset) . ' ' . $this->get('width') . 'w';
return implode(', ', $srcset);
}
/**
* Generate derivatives
* Allows the ability to override the Inmage's Pretty name stored in cache
*
* @param int $min_width
* @param int $max_width
* @param int $step
* @return $this
* @param $name
*/
public function derivatives($min_width, $max_width, $step = 200) {
$width = $min_width;
// Do not upscale images.
if ($max_width > $this->get('width')) {
$max_width = $this->get('width');
}
while ($width <= $max_width) {
$ratio = $width / $this->get('width');
$derivative = MediumFactory::scaledFromMedium($this, 1, $ratio);
if (is_array($derivative)) {
$this->addDerivative($derivative['file']);
public function setImagePrettyName($name)
{
$this->set('prettyname', $name);
if ($this->image) {
$this->image->setPrettyName($name);
}
}
public function getImagePrettyName()
{
if ($this->get('prettyname')) {
return $this->get('prettyname');
} else {
$basename = $this->get('basename');
if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
$basename = $matches[1];
}
return $basename;
}
$width += $step;
}
return $this;
}
/**
* Add a derivative
* Generate alternative image widths, using either an array of integers, or
* a min width, a max width, and a step parameter to fill out the necessary
* widths. Existing image alternatives won't be overwritten.
*
* @param ImageMedium $image
* @param int|int[] $min_width
* @param int [$max_width=2500]
* @param int [$step=200]
* @return $this
*/
public function addDerivative(ImageMedium $image) {
$this->derivatives[$image->url()] = $image->get('width');
public function derivatives($min_width, $max_width = 2500, $step = 200) {
if (!empty($this->alternatives)) {
$max = max(array_keys($this->alternatives));
$base = $this->alternatives[$max];
} else {
$base = $this;
}
$widths = [];
if (func_num_args() === 1) {
foreach ((array) func_get_arg(0) as $width) {
if ($width < $base->get('width')) {
$widths[] = $width;
}
}
} else {
$max_width = min($max_width, $base->get('width'));
for ($width = $min_width; $width < $max_width; $width = $width + $step) {
$widths[] = $width;
}
}
foreach ($widths as $width) {
// Only generate image alternatives that don't already exist
if (array_key_exists((int) $width, $this->alternatives)) {
continue;
}
$derivative = MediumFactory::fromFile($base->get('filepath'));
// It's possible that MediumFactory::fromFile returns null if the
// original image file no longer exists and this class instance was
// retrieved from the page cache
if (isset($derivative)) {
$index = 2;
$alt_widths = array_keys($this->alternatives);
sort($alt_widths);
foreach ($alt_widths as $i => $key) {
if ($width > $key) {
$index += max($i, 1);
}
}
$basename = preg_replace('/(@\d+x){0,1}$/', "@{$width}w", $base->get('basename'), 1);
$derivative->setImagePrettyName($basename);
$ratio = $base->get('width') / $width;
$height = $derivative->get('height') / $ratio;
$derivative->resize($width, $height);
$derivative->set('width', $width);
$derivative->set('height', $height);
$this->addAlternative($ratio, $derivative);
}
}
return $this;
}
/**
@@ -463,7 +512,11 @@ class ImageMedium extends Medium
try {
call_user_func_array([$this->image, $method], $args);
foreach ($this->alternatives as $ratio => $medium) {
foreach ($this->alternatives as $medium) {
if (!$medium->image) {
$medium->image();
}
$args_copy = $args;
// regular image: resize 400x400 -> 200x200
@@ -471,7 +524,7 @@ class ImageMedium extends Medium
if (isset(self::$magic_resize_actions[$method])) {
foreach (self::$magic_resize_actions[$method] as $param) {
if (isset($args_copy[$param])) {
$args_copy[$param] = (int) $args_copy[$param] * $ratio;
$args_copy[$param] *= $medium->get('ratio');
}
}
}
@@ -494,14 +547,14 @@ class ImageMedium extends Medium
$locator = Grav::instance()['locator'];
$file = $this->get('filepath');
$cacheDir = $locator->findResource('cache://images', true);
// Use existing cache folder or if it doesn't exist, create it.
$cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
$this->image = ImageFile::open($file)
->setCacheDir($cacheDir)
->setActualCacheDir($cacheDir)
->setPrettyName(basename($this->get('basename')));
$this->filter();
->setPrettyName($this->getImagePrettyName());
return $this;
}
@@ -517,6 +570,8 @@ class ImageMedium extends Medium
return parent::path(false);
}
$this->filter();
if (isset($this->result)) {
return $this->result;
}

View File

@@ -36,12 +36,13 @@ class Link implements RenderableInterface
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param boolean $reset
* @return array
*/
public function parsedownElement($title = null, $alt = null, $class = null, $reset = true)
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
$innerElement = $this->source->parsedownElement($title, $alt, $class, $reset);
$innerElement = $this->source->parsedownElement($title, $alt, $class, $id, $reset);
return [
'name' => 'a',

View File

@@ -100,7 +100,9 @@ class Medium extends Data implements RenderableInterface
}
$alternative->set('ratio', $ratio);
$this->alternatives[(float) $ratio] = $alternative;
$width = $alternative->get('width');
$this->alternatives[$width] = $alternative;
}
/**
@@ -148,8 +150,8 @@ class Medium extends Data implements RenderableInterface
/**
* Get/set querystring for the file's url
*
* @param string $hash
* @param boolean $withHash
* @param string $querystring
* @param boolean $withQuestionmark
* @return string
*/
public function querystring($querystring = null, $withQuestionmark = true)
@@ -199,10 +201,11 @@ class Medium extends Data implements RenderableInterface
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param boolean $reset
* @return array
*/
public function parsedownElement($title = null, $alt = null, $class = null, $reset = true)
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
$attributes = $this->attributes;
@@ -241,6 +244,14 @@ class Medium extends Data implements RenderableInterface
}
}
if (empty($attributes['id'])) {
if (!empty($id)) {
$attributes['id'] = $id;
} elseif (!empty($this->items['id'])) {
$attributes['id'] = $this->items['id'];
}
}
switch ($this->mode) {
case 'text':
$element = $this->textParsedownElement($attributes, false);

View File

@@ -119,23 +119,27 @@ class MediumFactory
}
$ratio = $to / $from;
$width = (int) ($medium->get('width') * $ratio);
$height = (int) ($medium->get('height') * $ratio);
$width = $medium->get('width') * $ratio;
$height = $medium->get('height') * $ratio;
$basename = $medium->get('basename');
$basename = str_replace('@'.$from.'x', '@'.$to.'x', $basename);
$prev_basename = $medium->get('basename');
$basename = str_replace('@'.$from.'x', '@'.$to.'x', $prev_basename);
$debug = $medium->get('debug');
$medium->set('debug', false);
$medium->setImagePrettyName($basename);
$file = $medium->resize($width, $height)->path();
$medium->set('debug', $debug);
$medium->setImagePrettyName($prev_basename);
$size = filesize($file);
$medium = self::fromFile($file);
$medium->set('size', $size);
if ($medium) {
$medium->set('size', $size);
}
return ['file' => $medium, 'size' => $size];
}

View File

@@ -21,13 +21,15 @@ trait ParsedownHtmlTrait
* Return HTML markup from the medium.
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param bool $reset
* @return string
*/
public function html($title = null, $alt = null, $class = null, $reset = true)
public function html($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
$element = $this->parsedownElement($title, $alt, $class, $reset);
$element = $this->parsedownElement($title, $alt, $class, $id, $reset);
if (!$this->parsedown) {
$this->parsedown = new Parsedown(null, null);

View File

@@ -27,8 +27,9 @@ interface RenderableInterface
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param bool $reset
* @return string
*/
public function parsedownElement($title = null, $alt = null, $class = null, $reset = true);
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true);
}

View File

@@ -23,6 +23,6 @@ class StaticImageMedium extends Medium
{
empty($attributes['src']) && $attributes['src'] = $this->url($reset);
return [ 'name' => 'image', 'attributes' => $attributes ];
return [ 'name' => 'img', 'attributes' => $attributes ];
}
}

View File

@@ -34,15 +34,16 @@ class ThumbnailImageMedium extends ImageMedium
/**
* Get an element (is array) that can be rendered by the Parsedown engine
*
* @param string $title
* @param string $alt
* @param string $class
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param boolean $reset
* @return array
*/
public function parsedownElement($title = null, $alt = null, $class = null, $reset = true)
public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
return $this->bubble('parsedownElement', [$title, $alt, $class, $reset]);
return $this->bubble('parsedownElement', [$title, $alt, $class, $id, $reset]);
}
/**
@@ -51,12 +52,13 @@ class ThumbnailImageMedium extends ImageMedium
* @param string $title
* @param string $alt
* @param string $class
* @param string $id
* @param bool $reset
* @return string
*/
public function html($title = null, $alt = null, $class = null, $reset = true)
public function html($title = null, $alt = null, $class = null, $id = null, $reset = true)
{
return $this->bubble('html', [$title, $alt, $class, $reset]);
return $this->bubble('html', [$title, $alt, $class, $id, $reset]);
}
/**

View File

@@ -54,6 +54,7 @@ class Page
protected $routable;
protected $modified;
protected $redirect;
protected $external_url;
protected $items;
protected $header;
protected $frontmatter;
@@ -84,6 +85,7 @@ class Page
protected $home_route;
protected $hide_home_route;
protected $ssl;
protected $template_format;
/**
* @var Page Unmodified (original) version of the page. Used for copying and moving the page.
@@ -162,8 +164,8 @@ class Page
unset($process_fields[$field]);
}
}
$text_header = Grav::instance()['twig']->processString(json_encode($process_fields), ['page'=>$this]);
$this->header((object) (json_decode($text_header, true) + $ignored_fields));
$text_header = Grav::instance()['twig']->processString(json_encode($process_fields), ['page' => $this]);
$this->header((object)(json_decode($text_header, true) + $ignored_fields));
}
}
@@ -364,6 +366,9 @@ class Page
if (isset($this->header->redirect)) {
$this->redirect = trim($this->header->redirect);
}
if (isset($this->header->external_url)) {
$this->external_url = trim($this->header->external_url);
}
if (isset($this->header->order_dir)) {
$this->order_dir = trim($this->header->order_dir);
}
@@ -414,7 +419,10 @@ class Page
$this->last_modified = (bool)$this->header->last_modified;
}
if (isset($this->header->ssl)) {
$this->ssl = (bool) $this->header->ssl;
$this->ssl = (bool)$this->header->ssl;
}
if (isset($this->header->template_format)) {
$this->template_format = $this->header->template_format;
}
}
@@ -498,7 +506,9 @@ class Page
$size = 300;
}
return html_entity_decode(Utils::truncateHTML($content, $size));
$summary = Utils::truncateHTML($content, $size);
return html_entity_decode($summary);
}
/**
@@ -562,16 +572,16 @@ class Page
$twig_first = isset($this->header->twig_first) ? $this->header->twig_first : $config->get('system.pages.twig_first',
true);
// never cache twig means it's always run after content
$never_cache_twig = isset($this->header->never_cache_twig) ? $this->header->never_cache_twig : $config->get('system.pages.never_cache_twig',
false);
// if no cached-content run everything
if ($this->content === false || $cache_enable === false) {
$this->content = $this->raw_content;
Grav::instance()->fireEvent('onPageContentRaw', new Event(['page' => $this]));
if ($never_cache_twig) {
if ($this->content === false || $cache_enable === false) {
$this->content = $this->raw_content;
Grav::instance()->fireEvent('onPageContentRaw', new Event(['page' => $this]));
if ($twig_first) {
if ($process_twig) {
$this->processTwig();
}
if ($process_markdown) {
$this->processMarkdown();
}
@@ -579,21 +589,47 @@ class Page
// Content Processed but not cached yet
Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
} else {
if ($process_markdown) {
$this->processMarkdown();
}
// Content Processed but not cached yet
Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
if ($process_twig) {
$this->processTwig();
if ($cache_enable) {
$this->cachePageContent();
}
}
if ($cache_enable) {
$this->cachePageContent();
if ($process_twig) {
$this->processTwig();
}
} else {
if ($this->content === false || $cache_enable === false) {
$this->content = $this->raw_content;
Grav::instance()->fireEvent('onPageContentRaw', new Event(['page' => $this]));
if ($twig_first) {
if ($process_twig) {
$this->processTwig();
}
if ($process_markdown) {
$this->processMarkdown();
}
// Content Processed but not cached yet
Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
} else {
if ($process_markdown) {
$this->processMarkdown();
}
// Content Processed but not cached yet
Grav::instance()->fireEvent('onPageContentProcessed', new Event(['page' => $this]));
if ($process_twig) {
$this->processTwig();
}
}
if ($cache_enable) {
$this->cachePageContent();
}
}
}
@@ -620,6 +656,7 @@ class Page
if ($this->content === null) {
$this->content();
}
return $this->getContentMeta();
}
@@ -638,18 +675,20 @@ class Page
* Return the whole contentMeta array as it currently stands
*
* @param null $name
*
* @return mixed
*/
public function getContentMeta($name = null)
{
if ($name) {
if(isset($this->content_meta[$name])) {
if (isset($this->content_meta[$name])) {
return $this->content_meta[$name];
} else {
return null;
}
}
return $this->content_meta;
}
@@ -657,6 +696,7 @@ class Page
* Sets the whole content meta array in one shot
*
* @param $content_meta
*
* @return mixed
*/
public function setContentMeta($content_meta)
@@ -757,9 +797,7 @@ class Page
return (bool)$this->order();
}
if ($name == 'folder') {
$regex = '/^[0-9]+\./u';
return preg_replace($regex, '', $this->folder);
return preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder);
}
if ($name == 'name') {
$language = $this->language() ? '.' . $this->language() : '';
@@ -890,6 +928,8 @@ class Page
$this->route(Grav::instance()['pages']->root()->route() . '/' . $this->slug());
}
$this->raw_route = null;
return $this;
}
@@ -1094,6 +1134,27 @@ class Page
return $this->template;
}
/**
* Allows a page to override the output render format, usually the extension provided
* in the URL. (e.g. `html`, `json`, `xml`, etc).
*
* @param null $var
*
* @return null
*/
public function templateFormat($var = null)
{
if ($var !== null) {
$this->template_format = $var;
}
if (empty($this->template_format)) {
$this->template_format = Grav::instance()['uri']->extension('html');
}
return $this->template_format;
}
/**
* Gets and sets the extension field.
*
@@ -1127,7 +1188,8 @@ class Page
// if not set in the page get the value from system config
if (empty($this->url_extension)) {
$this->url_extension = trim(isset($this->header->append_url_extension) ? $this->header->append_url_extension : Grav::instance()['config']->get('system.pages.append_url_extension', false));
$this->url_extension = trim(isset($this->header->append_url_extension) ? $this->header->append_url_extension : Grav::instance()['config']->get('system.pages.append_url_extension',
false));
}
return $this->url_extension;
@@ -1204,8 +1266,7 @@ class Page
if ($this->visible === null) {
// Set item visibility in menu if folder is different from slug
// eg folder = 01.Home and slug = Home
$regex = '/^[0-9]+\./u';
if (preg_match($regex, $this->folder)) {
if (preg_match(PAGE_ORDER_PREFIX_REGEX, $this->folder)) {
$this->visible = true;
} else {
$this->visible = false;
@@ -1289,7 +1350,7 @@ class Page
public function ssl($var = null)
{
if ($var !== null) {
$this->ssl = (bool) $var;
$this->ssl = (bool)$var;
}
return $this->ssl;
@@ -1350,21 +1411,28 @@ class Page
// Backward compatibility for nested arrays in metas
if (is_array($value)) {
foreach ($value as $property => $prop_value) {
$prop_key = $key . ":" . $property;
$this->metadata[$prop_key] = ['name' => $prop_key, 'property' => $prop_key, 'content' => htmlspecialchars($prop_value, ENT_QUOTES, 'UTF-8')];
$prop_key = $key . ":" . $property;
$this->metadata[$prop_key] = [
'name' => $prop_key,
'property' => $prop_key,
'content' => htmlspecialchars($prop_value, ENT_QUOTES, 'UTF-8')
];
}
} else {
// If it this is a standard meta data type
if ($value) {
if (in_array($key, $header_tag_http_equivs)) {
$this->metadata[$key] = ['http_equiv' => $key, 'content' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')];
$this->metadata[$key] = [
'http_equiv' => $key,
'content' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')
];
} elseif ($key == 'charset') {
$this->metadata[$key] = ['charset' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')];
} else {
// if it's a social metadata with separator, render as property
$separator = strpos($key, ':');
$separator = strpos($key, ':');
$hasSeparator = $separator && $separator < strlen($key) - 1;
$entry = ['name' => $key, 'content' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')];
$entry = ['name' => $key, 'content' => htmlspecialchars($value, ENT_QUOTES, 'UTF-8')];
if ($hasSeparator) {
$entry['property'] = $key;
@@ -1392,8 +1460,8 @@ class Page
{
if ($var !== null && $var !== "") {
$this->slug = $var;
if(!preg_match('/^[a-z0-9][-a-z0-9]*$/', $this->slug)){
Grav::instance()['log']->notice("Invalid slug set in YAML frontmatter: " . $this->rawRoute() . " => ". $this->slug);
if (!preg_match('/^[a-z0-9][-a-z0-9]*$/', $this->slug)) {
Grav::instance()['log']->notice("Invalid slug set in YAML frontmatter: " . $this->rawRoute() . " => " . $this->slug);
}
}
@@ -1402,7 +1470,6 @@ class Page
}
return $this->slug;
}
@@ -1417,22 +1484,16 @@ class Page
{
if ($var !== null) {
$order = !empty($var) ? sprintf('%02d.', (int)$var) : '';
$this->folder($order . $this->slug());
$this->folder($order . preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder));
return $order;
}
preg_match(PAGE_ORDER_PREFIX_REGEX, $this->folder, $order);
return isset($order[0]) ? $order[0] : false;
}
/**
* Gets the URL with host information, aka Permalink.
* @return string The permalink.
*/
public function permalink()
{
return $this->url(true);
}
/**
* Gets the URL for a page - alias of url().
*
@@ -1445,16 +1506,36 @@ class Page
return $this->url($include_host);
}
/**
* Gets the URL with host information, aka Permalink.
* @return string The permalink.
*/
public function permalink()
{
return $this->url(true, false, true, true);
}
/**
* Returns the canonical URL for a page
*
* @param bool $include_lang
* @return string
*/
public function canonical($include_lang = true)
{
return $this->url(true, true, $include_lang);
}
/**
* Gets the url for the Page.
*
* @param bool $include_host Defaults false, but true would include http://yourhost.com
* @param bool $canonical true to return the canonical URL
* @param bool $canonical true to return the canonical URL
* @param bool $include_lang
*
* @param bool $raw_route
* @return string The url.
*/
public function url($include_host = false, $canonical = false, $include_lang = true)
public function url($include_host = false, $canonical = false, $include_lang = true, $raw_route = false)
{
$grav = Grav::instance();
@@ -1470,6 +1551,11 @@ class Page
/** @var Uri $uri */
$uri = $grav['uri'];
// Override any URL when external_url is set
if (isset($this->external_url)) {
return $this->external_url;
}
// get pre-route
if ($include_lang && $language->enabled()) {
$pre_route = $language->getLanguageURLPrefix();
@@ -1485,6 +1571,8 @@ class Page
// get canonical route if requested
if ($canonical) {
$route = $pre_route . $this->routeCanonical();
} elseif ($raw_route) {
$route = $pre_route . $this->rawRoute();
} else {
$route = $pre_route . $this->route();
}
@@ -1566,8 +1654,7 @@ class Page
if (empty($this->raw_route)) {
$baseRoute = $this->parent ? (string)$this->parent()->rawRoute() : null;
$regex = '/^[0-9]+\./u';
$slug = preg_replace($regex, '', $this->folder);
$slug = preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder);
$this->raw_route = isset($baseRoute) ? $baseRoute . '/' . $slug : null;
}
@@ -1626,7 +1713,10 @@ class Page
public function id($var = null)
{
if ($var !== null) {
$this->id = $var;
// store unique per language
$active_lang = Grav::instance()['language']->getLanguage() ?: '';
$id = $active_lang . $var;
$this->id = $id;
}
return $this->id;
@@ -2144,7 +2234,10 @@ class Page
*/
public function home()
{
return $this->find('/') == $this;
$home = Grav::instance()['config']->get('system.home.alias');
$is_home = ($this->route() == $home || $this->rawRoute() == $home);
return $is_home;
}
/**
@@ -2195,7 +2288,7 @@ class Page
}
if (!isset($params['items'])) {
return [];
return new Collection();
}
$collection = $this->evaluate($params['items']);
@@ -2223,10 +2316,11 @@ class Page
continue;
}
foreach ($items as $item) {
$item = rawurldecode($item);
if (empty($page->taxonomy[$taxonomy]) || !in_array(htmlspecialchars_decode($item,
ENT_QUOTES), $page->taxonomy[$taxonomy])
) {
$collection->remove();
$collection->remove($page->path());
}
}
}
@@ -2245,7 +2339,16 @@ class Page
$by = isset($params['order']['by']) ? $params['order']['by'] : 'default';
$dir = isset($params['order']['dir']) ? $params['order']['dir'] : 'asc';
$custom = isset($params['order']['custom']) ? $params['order']['custom'] : null;
$collection->order($by, $dir, $custom);
$sort_flags = isset($params['order']['sort_flags']) ? $params['order']['sort_flags'] : null;
if (is_array($sort_flags)) {
$sort_flags = array_map('constant', $sort_flags); //transform strings to constant value
$sort_flags = array_reduce($sort_flags, function ($a, $b) {
return $a | $b;
}, 0); //merge constant values using bit or
}
$collection->order($by, $dir, $custom, $sort_flags);
}
/** @var Grav $grav */
@@ -2300,11 +2403,6 @@ class Page
return new Collection($result);
}
// We only evaluate commands which start with @
if (empty($cmd) || $cmd[0] != '@') {
return $value;
}
/** @var Pages $pages */
$pages = Grav::instance()['pages'];
@@ -2315,6 +2413,7 @@ class Page
$results = new Collection();
switch ($current) {
case 'self@':
case '@self':
if (!empty($parts)) {
switch ($parts[0]) {
@@ -2337,6 +2436,9 @@ class Page
$results = $collection->addPage($this->parent());
break;
case 'siblings':
if (!$this->parent()) {
return new Collection();
}
$results = $this->parent()->children()->remove($this->path());
break;
case 'descendants':
@@ -2348,6 +2450,7 @@ class Page
$results = $results->published();
break;
case 'page@':
case '@page':
$page = null;
@@ -2363,27 +2466,36 @@ class Page
// Handle a @page.descendants
if (!empty($parts)) {
switch ($parts[0]) {
case 'modular':
$results = new Collection();
foreach ($page->children() as $child) {
$results = $results->addPage($child);
}
$results->modular();
break;
case 'page':
case 'self':
$results = new Collection();
$results = $results->addPage($page);
$results = $results->addPage($page)->nonModular();
break;
case 'descendants':
$results = $pages->all($page)->remove($page->path());
$results = $pages->all($page)->remove($page->path())->nonModular();
break;
case 'children':
$results = $page->children();
$results = $page->children()->nonModular();
break;
}
} else {
$results = $page->children();
$results = $page->children()->nonModular();
}
$results = $results->nonModular()->published();
$results = $results->published();
break;
case 'root@':
case '@root':
if (!empty($parts) && $parts[0] == 'descendants') {
$results = $pages->all($pages->root())->nonModular()->published();
@@ -2392,7 +2504,7 @@ class Page
}
break;
case 'taxonomy@':
case '@taxonomy':
// Gets a collection of pages by using one of the following formats:
// @taxonomy.category: blog
@@ -2545,7 +2657,7 @@ class Page
protected function setPublishState()
{
// Handle publishing dates if no explict published option set
// Handle publishing dates if no explicit published option set
if (Grav::instance()['config']->get('system.pages.publish_dates') && !isset($this->header->published)) {
// unpublish if required, if not clear cache right before page should be unpublished
if ($this->unpublishDate()) {

View File

@@ -21,6 +21,7 @@ use Grav\Plugin\Admin;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Whoops\Exception\ErrorException;
use Collator as Collator;
class Pages
{
@@ -197,7 +198,7 @@ class Pages
*
* @return array
*/
public function sort(Page $page, $order_by = null, $order_dir = null)
public function sort(Page $page, $order_by = null, $order_dir = null, $sort_flags = null)
{
if ($order_by === null) {
$order_by = $page->orderBy();
@@ -214,7 +215,7 @@ class Pages
}
if (!isset($this->sort[$path][$order_by])) {
$this->buildSort($path, $children, $order_by, $page->orderManual());
$this->buildSort($path, $children, $order_by, $page->orderManual(), $sort_flags);
}
$sort = $this->sort[$path][$order_by];
@@ -235,7 +236,7 @@ class Pages
* @return array
* @internal
*/
public function sortCollection(Collection $collection, $orderBy, $orderDir = 'asc', $orderManual = null)
public function sortCollection(Collection $collection, $orderBy, $orderDir = 'asc', $orderManual = null, $sort_flags = null)
{
$items = $collection->toArray();
if (!$items) {
@@ -244,7 +245,7 @@ class Pages
$lookup = md5(json_encode($items) . json_encode($orderManual) . $orderBy . $orderDir);
if (!isset($this->sort[$lookup][$orderBy])) {
$this->buildSort($lookup, $items, $orderBy, $orderManual);
$this->buildSort($lookup, $items, $orderBy, $orderManual, $sort_flags);
}
$sort = $this->sort[$lookup][$orderBy];
@@ -342,13 +343,19 @@ class Pages
$page = $this->dispatch($route, $all);
} else {
// Try Regex style redirects
$source_url = $url;
$extension = $this->grav['uri']->extension();
if (isset($extension)) {
$source_url.= '.' . $extension;
}
$site_redirects = $config->get("site.redirects");
if (is_array($site_redirects)) {
foreach ((array)$site_redirects as $pattern => $replace) {
$pattern = '#' . $pattern . '#';
try {
$found = preg_replace($pattern, $replace, $url);
if ($found != $url) {
$found = preg_replace($pattern, $replace, $source_url);
if ($found != $source_url) {
$this->grav->redirectLangSafe($found);
}
} catch (ErrorException $e) {
@@ -363,8 +370,8 @@ class Pages
foreach ((array)$site_routes as $pattern => $replace) {
$pattern = '#' . $pattern . '#';
try {
$found = preg_replace($pattern, $replace, $url);
if ($found != $url) {
$found = preg_replace($pattern, $replace, $source_url);
if ($found != $source_url) {
$page = $this->dispatch($found, $all);
}
} catch (ErrorException $e) {
@@ -564,17 +571,21 @@ class Pages
*/
public static function pageTypes()
{
/** @var Admin $admin */
$admin = Grav::instance()['admin'];
if (isset(Grav::instance()['admin'])) {
/** @var Admin $admin */
$admin = Grav::instance()['admin'];
/** @var Page $page */
$page = $admin->getPage($admin->route);
/** @var Page $page */
$page = $admin->getPage($admin->route);
if ($page && $page->modular()) {
return static::modularTypes();
if ($page && $page->modular()) {
return static::modularTypes();
}
return static::types();
}
return static::types();
return [];
}
/**
@@ -647,11 +658,12 @@ class Pages
$parents = $pages->getList(null, 0, $rawRoutes);
/** @var Admin $admin */
$admin = $grav['admin'];
if (isset($grav['admin'])) {
// Remove current route from parents
/** @var Admin $admin */
$admin = $grav['admin'];
// Remove current route from parents
if (isset($admin)) {
$page = $admin->getPage($admin->route);
$page_route = $page->route();
if (isset($parents[$page_route])) {
@@ -745,16 +757,19 @@ class Pages
switch (strtolower($config->get('system.cache.check.method', 'file'))) {
case 'none':
case 'off':
$last_modified = 0;
$hash = 0;
break;
case 'folder':
$last_modified = Folder::lastModifiedFolder($pages_dir);
$hash = Folder::lastModifiedFolder($pages_dir);
break;
case 'hash':
$hash = Folder::hashAllFiles($pages_dir);
break;
default:
$last_modified = Folder::lastModifiedFile($pages_dir);
$hash = Folder::lastModifiedFile($pages_dir);
}
$page_cache_id = md5($pages_dir . $last_modified . $language->getActive() . $config->checksum());
$page_cache_id = md5($pages_dir . $hash . $language->getActive() . $config->checksum());
list($this->instances, $this->routes, $this->children, $taxonomy_map, $this->sort) = $cache->fetch($page_cache_id);
if (!$this->instances) {
@@ -846,14 +861,22 @@ class Pages
}
$content_exists = false;
$pages_found = glob($directory . '/*' . CONTENT_EXT);
$pages_found = new \GlobIterator($directory . '/*' . CONTENT_EXT);
$page_found = null;
$page_extension = '';
if ($pages_found) {
if ($pages_found && count($pages_found) > 0) {
$page_extensions = $language->getFallbackPageExtensions();
foreach ($page_extensions as $extension) {
foreach ($pages_found as $found) {
if (preg_match('/^.*\/[0-9A-Za-z\-\_]+(' . $extension . ')$/', $found)) {
if ($found->isDir()) {
continue;
}
$regex = '/^[^\.]*' . preg_quote($extension) . '$/';
if (preg_match($regex, $found->getFilename())) {
$page_found = $found;
$page_extension = $extension;
break 2;
@@ -863,8 +886,7 @@ class Pages
}
if ($parent && !empty($page_found)) {
$file = new \SplFileInfo($page_found);
$page->init($file, $page_extension);
$page->init($page_found, $page_extension);
$content_exists = true;
@@ -895,6 +917,12 @@ class Pages
$last_modified = $modified;
}
} elseif ($file->isDir() && !in_array($file->getFilename(), $this->ignore_folders)) {
// if folder contains separator, continue
if (Utils::contains($file->getFilename(), $config->get('system.param_sep', ':'))) {
continue;
}
if (!$page->path()) {
$page->path($file->getPath());
}
@@ -1002,12 +1030,11 @@ class Pages
* @throws \RuntimeException
* @internal
*/
protected function buildSort($path, array $pages, $order_by = 'default', $manual = null)
protected function buildSort($path, array $pages, $order_by = 'default', $manual = null, $sort_flags = null)
{
$list = [];
$header_default = null;
$header_query = null;
$sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
// do this header query work only once
if (strpos($order_by, 'header.') === 0) {
@@ -1035,6 +1062,14 @@ class Pages
$list[$key] = $child->modified();
$sort_flags = SORT_REGULAR;
break;
case 'publish_date':
$list[$key] = $child->publishDate();
$sort_flags = SORT_REGULAR;
break;
case 'unpublish_date':
$list[$key] = $child->unpublishDate();
$sort_flags = SORT_REGULAR;
break;
case 'slug':
$list[$key] = $child->slug();
break;
@@ -1049,24 +1084,33 @@ class Pages
} else {
$list[$key] = $header_default ?: $key;
}
$sort_flags = SORT_REGULAR;
$sort_flags = $sort_flags ?: SORT_REGULAR;
break;
case 'manual':
case 'default':
default:
$list[$key] = $key;
$sort_flags = SORT_REGULAR;
$sort_flags = $sort_flags ?: SORT_REGULAR;
}
}
if (!$sort_flags) {
$sort_flags = SORT_NATURAL | SORT_FLAG_CASE;
}
// handle special case when order_by is random
if ($order_by == 'random') {
$list = $this->arrayShuffle($list);
} else {
// else just sort the list according to specified key
if (extension_loaded('intl')) {
$col = new \Collator(setlocale(LC_COLLATE, 0)); //`setlocale` with a 0 param returns the current locale set
$col->asort($list, $sort_flags);
$locale = setlocale(LC_COLLATE, 0); //`setlocale` with a 0 param returns the current locale set
$col = Collator::create($locale);
if ($col) {
$col->asort($list, $sort_flags);
} else {
asort($list, $sort_flags);
}
} else {
asort($list, $sort_flags);
}

View File

@@ -52,6 +52,8 @@ class Types implements \ArrayAccess, \Iterator, \Countable
// Register default by default.
$this->register('default');
$this->register('external');
}
foreach ($this->findBlueprints($uri) as $type => $blueprint) {
@@ -80,8 +82,10 @@ class Types implements \ArrayAccess, \Iterator, \Countable
}
$modular_uri = rtrim($uri, '/') . '/modular';
foreach (Folder::all($modular_uri, $options) as $type) {
$this->register('modular/' . $type);
if (is_dir($modular_uri)) {
foreach (Folder::all($modular_uri, $options) as $type) {
$this->register('modular/' . $type);
}
}
}

View File

@@ -98,6 +98,8 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
}
/**
* Determine if this is running under the admin
*
* @return bool
*/
public function isAdmin()
@@ -105,6 +107,27 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
return Utils::isAdminPlugin();
}
/**
* Determine if this route is in Admin and active for the plugin
*
* @param $plugin_route
* @return bool
*/
protected function isPluginActiveAdmin($plugin_route)
{
$should_run = false;
$uri = $this->grav['uri'];
if (strpos($uri->path(), $this->config->get('plugins.admin.route') . '/' . $plugin_route) === false) {
$should_run = false;
} elseif (isset($uri->paths()[1]) && $uri->paths()[1] == $plugin_route) {
$should_run = true;
}
return $should_run;
}
/**
* @param array $events
*/
@@ -223,21 +246,23 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
/**
* Merge global and page configurations.
*
* @param Page $page The page to merge the configurations with the
* @param Page $page The page to merge the configurations with the
* plugin settings.
* @param bool $deep Should you use deep or shallow merging
* @param array $params Array of additional configuration options to
* @param mixed $deep false = shallow|true = recursive|merge = recursive+unique
* @param array $params Array of additional configuration options to
* merge with the plugin settings.
* @param string $type Is this 'plugins' or 'themes'
*
* @return \Grav\Common\Data\Data
* @return Data
*/
protected function mergeConfig(Page $page, $deep = false, $params = [])
protected function mergeConfig(Page $page, $deep = false, $params = [], $type = 'plugins')
{
$class_name = $this->name;
$class_name_merged = $class_name . '.merged';
$defaults = $this->config->get('plugins.' . $class_name, []);
$defaults = $this->config->get($type . '.' . $class_name, []);
$page_header = $page->header();
$header = [];
if (!isset($page_header->$class_name_merged) && isset($page_header->$class_name)) {
// Get default plugin configurations and retrieve page header configuration
$config = $page_header->$class_name;
@@ -246,11 +271,8 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
$config = ['enabled' => $config];
}
// Merge page header settings using deep or shallow merging technique
if ($deep) {
$header = array_replace_recursive($defaults, $config);
} else {
$header = array_merge($defaults, $config);
}
$header = $this->mergeArrays($deep, $defaults, $config);
// Create new config object and set it on the page object so it's cached for next time
$page->modifyHeader($class_name_merged, new Data($header));
} else if (isset($page_header->$class_name_merged)) {
@@ -261,16 +283,31 @@ class Plugin implements EventSubscriberInterface, \ArrayAccess
$header = $defaults;
}
// Merge additional parameter with configuration options
if ($deep) {
$header = array_replace_recursive($header, $params);
} else {
$header = array_merge($header, $params);
}
$header = $this->mergeArrays($deep, $header, $params);
// Return configurations as a new data config class
return new Data($header);
}
/**
* Merge arrays based on deepness
*
* @param bool $deep
* @param $array1
* @param $array2
* @return array|mixed
*/
private function mergeArrays($deep = false, $array1, $array2)
{
if ($deep == 'merge') {
return Utils::arrayMergeRecursiveUnique($array1, $array2);
} elseif ($deep == true) {
return array_replace_recursive($array1, $array2);
} else {
return array_merge($array1, $array2);
}
}
/**
* Persists to disk the plugin parameters currently stored in the Grav Config object
*

View File

@@ -8,15 +8,38 @@
namespace Grav\Common\Processors;
class PagesProcessor extends ProcessorBase implements ProcessorInterface {
use Grav\Common\Page\Page;
class PagesProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = 'pages';
public $title = 'Pages';
public function process() {
$this->container['pages']->init();
$this->container->fireEvent('onPagesInitialized');
$this->container->fireEvent('onPageInitialized');
public function process()
{
// Dump Cache state
$this->container['debugger']->addMessage($this->container['cache']->getCacheStatus());
$this->container['pages']->init();
$this->container->fireEvent('onPagesInitialized');
$this->container->fireEvent('onPageInitialized');
/** @var Page $page */
$page = $this->container['page'];
if (!$page->routable()) {
// If no page found, fire event
$event = $this->container->fireEvent('onPageNotFound');
if (isset($event->page)) {
unset ($this->container['page']);
$this->container['page'] = $event->page;
} else {
throw new \RuntimeException('Page Not Found', 404);
}
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* @package Grav.Common.Service
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Service;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\Session\Message;
class MessagesServiceProvider implements ServiceProviderInterface
{
public function register(Container $container)
{
// Define session message service.
$container['messages'] = function ($c) {
$session = $c['session'];
if (!isset($session->messages)) {
$session->messages = new Message;
}
return $session->messages;
};
}
}

View File

@@ -15,8 +15,7 @@ class OutputServiceProvider implements ServiceProviderInterface
{
public function register(Container $container) {
$container['output'] = function ($c) {
/** @var Grav $c */
return $c['twig']->processSite($c['uri']->extension());
return $c['twig']->processSite($c['page']->templateFormat());
};
}
}

View File

@@ -8,6 +8,7 @@
namespace Grav\Common\Service;
use Grav\Common\Page\Page;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
@@ -33,6 +34,12 @@ class PageServiceProvider implements ServiceProviderInterface
// Redirection tests
if ($page) {
if ($c['config']->get('system.force_ssl')) {
if (!isset($_SERVER['HTTPS']) || $_SERVER["HTTPS"] != "on") {
$url = "https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
$c->redirect($url);
}
}
$url = $page->route();
@@ -71,14 +78,13 @@ class PageServiceProvider implements ServiceProviderInterface
// Try fallback URL stuff...
$c->fallbackUrl($path);
// If no page found, fire event
$event = $c->fireEvent('onPageNotFound');
if (isset($event->page)) {
$page = $event->page;
} else {
throw new \RuntimeException('Page Not Found', 404);
if (!$page) {
$path = $c['locator']->findResource('system://pages/notfound.md');
$page = new Page();
$page->init(new \SplFileInfo($path));
$page->routable(false);
}
}
return $page;

View File

@@ -38,7 +38,10 @@ class Session extends BaseSession
$base_url = $uri->rootUrl(false);
$session_timeout = $config->get('system.session.timeout', 1800);
$session_path = $config->get('system.session.path', '/' . ltrim($base_url, '/'));
$session_path = $config->get('system.session.path');
if (!$session_path) {
$session_path = '/' . ltrim($base_url, '/');
}
// Activate admin if we're inside the admin path.
if ($config->get('plugins.admin.enabled')) {
@@ -56,18 +59,30 @@ class Session extends BaseSession
}
if ($config->get('system.session.enabled') || $is_admin) {
// Define session service.
parent::__construct($session_timeout, $session_path);
$domain = $uri->host();
if ($domain === 'localhost') {
$domain = '';
}
// Fix for HUGE session timeouts
if ($session_timeout > 99999999999) {
$session_timeout = 9999999999;
}
// Define session service.
parent::__construct($session_timeout, $session_path, $domain);
$secure = $config->get('system.session.secure', false);
$httponly = $config->get('system.session.httponly', true);
$unique_identifier = GRAV_ROOT;
$this->setName($config->get('system.session.name', 'grav_site') . '-' . substr(md5($unique_identifier), 0, 7) . ($is_admin ? '-admin' : ''));
$inflector = new Inflector();
$session_name = $inflector->hyphenize($config->get('system.session.name', 'grav_site')) . '-' . substr(md5($unique_identifier), 0, 7);
$split_session = $config->get('system.session.split', true);
if ($is_admin && $split_session) {
$session_name .= '-admin';
}
$this->setName($session_name);
$this->start();
setcookie(session_name(), session_id(), time() + $session_timeout, $session_path, $domain, $secure, $httponly);
}

View File

@@ -93,6 +93,8 @@ class Taxonomy
foreach ((array)$items as $item) {
if (isset($this->taxonomy_map[$taxonomy][$item])) {
$matches[] = $this->taxonomy_map[$taxonomy][$item];
} else {
$matches[] = [];
}
}
}
@@ -126,4 +128,20 @@ class Taxonomy
return $this->taxonomy_map;
}
/**
* Gets item keys per taxonomy
*
* @param string $taxonomy taxonomy name
*
* @return array keys of this taxonomy
*/
public function getTaxonomyItemKeys($taxonomy) {
if (isset($this->taxonomy_map[$taxonomy])) {
$results = array_keys($this->taxonomy_map[$taxonomy]);
return $results;
}
}
}

View File

@@ -8,6 +8,7 @@
namespace Grav\Common;
use Grav\Common\Page\Page;
use Grav\Common\Config\Config;
use RocketTheme\Toolbox\File\YamlFile;
@@ -59,6 +60,13 @@ class Theme extends Plugin
return true;
}
/**
* Override the mergeConfig method to work for themes
*/
protected function mergeConfig(Page $page, $deep = 'merge', $params = [], $type = 'themes') {
return parent::mergeConfig($page, $deep, $params, $type);
}
/**
* Simpler getter for the theme blueprint
*

View File

@@ -83,9 +83,9 @@ class Twig
$active_language = $language->getActive();
$language_append = '';
$path_append = rtrim($this->grav['pages']->base(), '/');
if ($language->getDefault() != $active_language || $config->get('system.languages.include_default_lang') === true) {
$language_append = $active_language ? '/' . $active_language : '';
$path_append .= $active_language ? '/' . $active_language : '';
}
// handle language templates if available
@@ -152,21 +152,28 @@ class Twig
$this->grav->fireEvent('onTwigExtensions');
$base_url = $this->grav['base_url'] . $path_append;
// Set some standard variables for twig
$this->twig_vars = $this->twig_vars + [
'config' => $config,
'uri' => $this->grav['uri'],
'base_dir' => rtrim(ROOT_DIR, '/'),
'base_url' => $this->grav['base_url'] . $language_append,
'base_url_simple' => $this->grav['base_url'],
'base_url_absolute' => $this->grav['base_url_absolute'] . $language_append,
'base_url_relative' => $this->grav['base_url_relative'] . $language_append,
'theme_dir' => $locator->findResource('theme://'),
'theme_url' => $this->grav['base_url'] . '/' . $locator->findResource('theme://', false),
'system' => $config->get('system'),
'theme' => $config->get('theme'),
'site' => $config->get('site'),
'uri' => $this->grav['uri'],
'assets' => $this->grav['assets'],
'taxonomy' => $this->grav['taxonomy'],
'browser' => $this->grav['browser'],
'base_dir' => rtrim(ROOT_DIR, '/'),
'base_url' => $base_url,
'base_url_simple' => $this->grav['base_url'],
'base_url_absolute' => $this->grav['base_url_absolute'] . $path_append,
'base_url_relative' => $this->grav['base_url_relative'] . $path_append,
'home_url' => $base_url == '' ? '/' : $base_url,
'theme_dir' => $locator->findResource('theme://'),
'theme_url' => $this->grav['base_url'] . '/' . $locator->findResource('theme://', false),
'html_lang' => $this->grav['language']->getActive() ?: $config->get('site.default_lang', 'en'),
];
}
}
@@ -320,6 +327,7 @@ class Twig
$twig_vars = $this->twig_vars;
$twig_vars['theme'] = $this->grav['config']->get('theme');
$twig_vars['pages'] = $pages->root();
$twig_vars['page'] = $page;
$twig_vars['header'] = $page->header();
@@ -343,6 +351,7 @@ class Twig
// Try html version of this template if initial template was NOT html
if ($ext != '.html' . TWIG_EXT) {
try {
$page->templateFormat('html');
$output = $this->twig->render($page->template() . '.html' . TWIG_EXT, $twig_vars);
} catch (\Twig_Error_Loader $e) {
throw new \RuntimeException($error_msg, 400, $e);

View File

@@ -87,6 +87,7 @@ class TwigExtension extends \Twig_Extension
new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils', 'truncate']),
new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']),
new \Twig_SimpleFilter('json_decode', [$this, 'jsonDecodeFilter']),
new \Twig_SimpleFilter('array_unique', 'array_unique'),
];
}
@@ -115,6 +116,8 @@ class TwigExtension extends \Twig_Extension
new \Twig_SimpleFunction('url', [$this, 'urlFunc']),
new \Twig_SimpleFunction('json_decode', [$this, 'jsonDecodeFilter']),
new \Twig_SimpleFunction('get_cookie', [$this, 'getCookie']),
new \Twig_SimpleFunction('redirect_me', [$this, 'redirectFunc']),
new \Twig_SimpleFunction('range', [$this, 'rangeFunc']),
];
}
@@ -142,12 +145,16 @@ class TwigExtension extends \Twig_Extension
public function safeEmailFilter($str)
{
$email = '';
$str_len = strlen($str);
for ($i = 0; $i < $str_len; $i++) {
$email .= "&#" . ord($str[$i]) . ";";
for ( $i = 0, $len = strlen( $str ); $i < $len; $i++ ) {
$j = rand( 0, 1);
if ( $j == 0 ) {
$email .= '&#' . ord( $str[$i] ) . ';';
} elseif ( $j == 1 ) {
$email .= $str[$i];
}
}
return $email;
return str_replace( '@', '&#64;', $email );
}
/**
@@ -729,11 +736,15 @@ class TwigExtension extends \Twig_Extension
}
/**
* Authorize an action. Returns true if the user is logged in and has the right to execute $action.
* Authorize an action. Returns true if the user is logged in and
* has the right to execute $action.
*
* @param string $action
*
* @return bool
* @param string|array $action An action or a list of actions. Each
* entry can be a string like 'group.action'
* or without dot notation an associative
* array.
* @return bool Returns TRUE if the user is authorized to
* perform the action, FALSE otherwise.
*/
public function authorize($action)
{
@@ -741,11 +752,14 @@ class TwigExtension extends \Twig_Extension
return false;
}
$action = (array)$action;
foreach ($action as $a) {
if ($this->grav['user']->authorize($a)) {
return true;
$action = (array) $action;
foreach ($action as $key => $perms) {
$prefix = is_int($key) ? '' : $key . '.';
$perms = $prefix ? (array) $perms : [$perms => true];
foreach ($perms as $action => $authenticated) {
if ($this->grav['user']->authorize($prefix . $action)) {
return $authenticated;
}
}
}
@@ -809,4 +823,30 @@ class TwigExtension extends \Twig_Extension
{
return preg_replace($pattern, $replace, $subject, $limit);
}
/**
* redirect browser from twig
*
* @param string $url the url to redirect to
* @param int $statusCode statusCode, default 303
*/
public function redirectFunc($url, $statusCode = 303)
{
header('Location: ' . $url, true, $statusCode);
die();
}
/**
* Generates an array containing a range of elements, optionally stepped
*
* @param int $start Minimum number, default 0
* @param int $end Maximum number, default `getrandmax()`
* @param int $step Increment between elements in the sequence, default 1
*
* @return array
*/
public function rangeFunc($start = 0, $end = 100, $step = 1)
{
return range($start, $end, $step);
}
}

View File

@@ -8,6 +8,7 @@
namespace Grav\Common;
use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
class Uri
@@ -207,6 +208,7 @@ class Uri
$grav = Grav::instance();
/** @var Language $language */
$language = $grav['language'];
$uri_bits = Uri::parseUrl($url);
@@ -275,15 +277,12 @@ class Uri
}
// Set some defaults
$this->root = $this->base . $this->root_path;
$this->root = $grav['config']->get('system.custom_base_url') ?: $this->base . $this->root_path;
$this->url = $this->base . $this->uri;
// get any params and remove them
$uri = str_replace($this->root, '', $this->url);
// remove double slashes
$uri = preg_replace('#/{2,}#', '/', $uri);
// remove the setup.php based base if set:
$setup_base = $grav['pages']->base();
if ($setup_base) {
@@ -292,7 +291,7 @@ class Uri
// If configured to, redirect trailing slash URI's with a 301 redirect
if ($config->get('system.pages.redirect_trailing_slash', false) && $uri != '/' && Utils::endsWith($uri, '/')) {
$grav->redirect(rtrim($uri, '/'), 301);
$grav->redirect(str_replace($this->root, '', rtrim($uri, '/')), 301);
}
// process params
@@ -305,11 +304,10 @@ class Uri
$bits = parse_url($uri);
// process query string
if (isset($bits['query']) && isset($bits['path'])) {
if (isset($bits['query'])) {
if (!$this->query) {
$this->query = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
}
$uri = $bits['path'];
}
//process fragment
@@ -317,8 +315,11 @@ class Uri
$this->fragment = $bits['fragment'];
}
// Get the path. If there's no path, make sure pathinfo() still returns dirname variable
$path = isset($bits['path']) ? $bits['path'] : '/';
// remove the extension if there is one set
$parts = pathinfo($uri);
$parts = pathinfo($path);
// set the original basename
$this->basename = $parts['basename'];
@@ -332,19 +333,19 @@ class Uri
// Strip the file extension for valid page types
if (preg_match('/\.(' . $valid_page_types . ')$/', $parts['basename'])) {
$uri = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, $parts['dirname']), DS) . '/' . $parts['filename'];
$path = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, $parts['dirname']), DS) . '/' . $parts['filename'];
}
// set the new url
$this->url = $this->root . $uri;
$this->path = $uri;
$this->url = $this->root . $path;
$this->path = $path;
$this->content_path = trim(str_replace($this->base, '', $this->path), '/');
if ($this->content_path != '') {
$this->paths = explode('/', $this->content_path);
}
// Set some Grav stuff
$grav['base_url_absolute'] = $this->rootUrl(true);
$grav['base_url_absolute'] = $grav['config']->get('system.custom_base_url') ?: $this->rootUrl(true);
$grav['base_url_relative'] = $this->rootUrl(false);
$grav['base_url'] = $grav['config']->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative'];
}
@@ -767,14 +768,14 @@ class Uri
/**
* Converts links from absolute '/' or relative (../..) to a Grav friendly format
*
* @param Page $page the current page to use as reference
* @param Page $page the current page to use as reference
* @param string $url the URL as it was written in the markdown
* @param string $type the type of URL, image | link
* @param bool $absolute if null, will use system default, if true will use absolute links internally
*
* @param string $type the type of URL, image | link
* @param bool $absolute if null, will use system default, if true will use absolute links internally
* @param bool $route_only only return the route, not full URL path
* @return string the more friendly formatted url
*/
public static function convertUrl(Page $page, $url, $type = 'link', $absolute = false)
public static function convertUrl(Page $page, $url, $type = 'link', $absolute = false, $route_only = false)
{
$grav = Grav::instance();
@@ -923,6 +924,10 @@ class Uri
$url = $url_path;
}
if ($route_only) {
$url = str_replace($base_url, '', $url);
}
return $url;
}

View File

@@ -83,13 +83,13 @@ class Group extends Data
foreach ($fields as $field) {
if ($field['type'] == 'text') {
$value = $field['name'];
if (isset($this->items[$value])) {
$config->set("groups.$this->groupname.$value", $this->items[$value]);
if (isset($this->items['data'][$value])) {
$config->set("groups.$this->groupname.$value", $this->items['data'][$value]);
}
}
if ($field['type'] == 'array') {
if ($field['type'] == 'array' || $field['type'] == 'permissions') {
$value = $field['name'];
$arrayValues = Utils::getDotNotation($this->items, $field['name']);
$arrayValues = Utils::getDotNotation($this->items['data'], $field['name']);
if ($arrayValues) {
foreach ($arrayValues as $arrayIndex => $arrayValue) {

View File

@@ -54,6 +54,42 @@ class User extends Data
return $user;
}
/**
* Find a user by username, email, etc
*
* @param $query the query to search for
* @param array $fields the fields to search
* @return User
*/
public static function find($query, $fields = ['username', 'email'])
{
$account_dir = Grav::instance()['locator']->findResource('account://');
$files = array_diff(scandir($account_dir), ['.', '..']);
// Try with username first, you never know!
if (in_array('username', $fields)) {
$user = User::load($query);
unset($fields[array_search('username', $fields)]);
} else {
$user = User::load('');
}
// If not found, try the fields
if (!$user->exists()) {
foreach ($files as $file) {
if (Utils::endsWith($file, YAML_EXT)) {
$find_user = User::load(trim(pathinfo($file, PATHINFO_FILENAME)));
foreach ($fields as $field) {
if ($find_user[$field] == $query) {
return $find_user;
}
}
}
}
}
return $user;
}
/**
* Remove user account.
*

View File

@@ -9,8 +9,6 @@
namespace Grav\Common;
use DateTime;
use DateTimeZone;
use Grav\Common\Grav;
use Grav\Common\Helpers\Truncator;
use RocketTheme\Toolbox\Event\Event;
@@ -111,6 +109,26 @@ abstract class Utils
return (object)array_merge((array)$obj1, (array)$obj2);
}
/**
* Recursive Merge with uniqueness
*
* @param $array1
* @param $array2
* @return mixed
*/
public static function arrayMergeRecursiveUnique($array1, $array2)
{
if (empty($array1)) return $array2; //optimize the base case
foreach ($array2 as $key => $value) {
if (is_array($value) && is_array(@$array1[$key])) {
$value = static::arrayMergeRecursiveUnique($array1[$key], $value);
}
$array1[$key] = $value;
}
return $array1;
}
/**
* Return the Grav date formats allowed
*
@@ -183,26 +201,32 @@ abstract class Utils
* Truncate HTML by number of characters. not "word-safe"!
*
* @param string $text
* @param int $length
* @param int $length in characters
* @param string $ellipsis
*
* @return string
*/
public static function truncateHtml($text, $length = 100)
public static function truncateHtml($text, $length = 100, $ellipsis = '...')
{
return Truncator::truncate($text, $length, ['length_in_chars' => true]);
if (mb_strlen($text) <= $length) {
return $text;
} else {
return Truncator::truncateLetters($text, $length, $ellipsis);
}
}
/**
* Truncate HTML by number of characters in a "word-safe" manor.
*
* @param string $text
* @param int $length
* @param int $length in words
* @param string $ellipsis
*
* @return string
*/
public static function safeTruncateHtml($text, $length = 100)
public static function safeTruncateHtml($text, $length = 25, $ellipsis = '...')
{
return Truncator::truncate($text, $length, ['length_in_chars' => true, 'word_safe' => true]);
return Truncator::truncateWords($text, $length, $ellipsis);
}
/**
@@ -233,7 +257,7 @@ abstract class Utils
Grav::instance()->fireEvent('onBeforeDownload', new Event(['file' => $file]));
$file_parts = pathinfo($file);
$mimetype = Utils::getMimeType($file_parts['extension']);
$mimetype = Utils::getMimeByExtension($file_parts['extension']);
$size = filesize($file); // File size
// clean all buffers
@@ -319,22 +343,84 @@ abstract class Utils
}
/**
* Return the mimetype based on filename
* Return the mimetype based on filename extension
*
* @param string $extension Extension of file (eg "txt")
* @param string $default
*
* @return string
*/
public static function getMimeType($extension)
public static function getMimeByExtension($extension, $default = 'application/octet-stream')
{
$extension = strtolower($extension);
$config = Grav::instance()['config']->get('media.types');
if (isset($config[$extension])) {
return $config[$extension]['mime'];
// look for some standard types
switch ($extension) {
case null:
return $default;
case 'json':
return 'application/json';
case 'html':
return 'text/html';
case 'atom':
return 'application/atom+xml';
case 'rss':
return 'application/rss+xml';
case 'xml':
return 'application/xml';
}
return 'application/octet-stream';
$media_types = Grav::instance()['config']->get('media.types');
if (isset($media_types[$extension])) {
if (isset($media_types[$extension]['mime'])) {
return $media_types[$extension]['mime'];
}
}
return $default;
}
/**
* Return the mimetype based on filename extension
*
* @param string $mime mime type (eg "text/html")
* @param string $default default value
*
* @return string
*/
public static function getExtensionByMime($mime, $default = 'html')
{
$mime = strtolower($mime);
// look for some standard mime types
switch ($mime) {
case '*/*':
case 'text/*':
case 'text/html':
return 'html';
case 'application/json':
return 'json';
case 'application/atom+xml':
return 'atom';
case 'application/rss+xml':
return 'rss';
case 'application/xml':
return 'xml';
}
$media_types = Grav::instance()['config']->get('media.types');
foreach ($media_types as $extension => $type) {
if ($extension == 'defaults') {
continue;
}
if (isset($type['mime']) && $type['mime'] == $mime) {
return $extension;
}
}
return $default;
}
/**
@@ -432,6 +518,27 @@ abstract class Utils
return $result;
}
/**
* Flatten an array
*
* @param $array
* @return array
*/
public static function arrayFlatten($array)
{
$flatten = array();
foreach ($array as $key => $inner){
if (is_array($inner)) {
foreach ($inner as $inner_key => $value) {
$flatten[$inner_key] = $value;
}
} else {
$flatten[$key] = $inner;
}
}
return $flatten;
}
/**
* Checks if the passed path contains the language code prefix
*
@@ -690,12 +797,14 @@ abstract class Utils
* Set portion of array (passed by reference) for a dot-notation key
* and set the value
*
* @param $array
* @param $key
* @param $value
* @param $array
* @param $key
* @param $value
* @param bool $merge
*
* @return mixed
*/
public static function setDotNotation(&$array, $key, $value)
public static function setDotNotation(&$array, $key, $value, $merge = false)
{
if (is_null($key)) return $array = $value;
@@ -713,8 +822,24 @@ abstract class Utils
$array =& $array[$key];
}
$array[array_shift($keys)] = $value;
$key = array_shift($keys);
if (!$merge || !isset($array[$key])) {
$array[$key] = $value;
} else {
$array[$key] = array_merge($array[$key], $value);
}
return $array;
}
/**
* Utility method to determine if the current OS is Windows
*
* @return bool
*/
public static function isWindows() {
return strncasecmp(PHP_OS, 'WIN', 3) == 0;
}
}

View File

@@ -94,6 +94,7 @@ class CleanCommand extends Command
'vendor/ircmaxell/password-compat/version-test.php',
'vendor/ircmaxell/password-compat/.travis.yml',
'vendor/ircmaxell/password-compat/test',
'vendor/league/climate/composer.json',
'vendor/matthiasmullie/minify/bin',
'vendor/matthiasmullie/minify/composer.json',
'vendor/matthiasmullie/minify/CONTRIBUTING.md',
@@ -124,6 +125,7 @@ class CleanCommand extends Command
'vendor/rockettheme/toolbox/.travis.yml',
'vendor/rockettheme/toolbox/composer.json',
'vendor/rockettheme/toolbox/phpunit.xml',
'vendor/seld/cli-prompt/composer.json',
'vendor/symfony/console/composer.json',
'vendor/symfony/console/phpunit.xml.dist',
'vendor/symfony/console/.gitignore',

View File

@@ -27,6 +27,7 @@ class ClearCacheCommand extends ConsoleCommand
->addOption('assets-only', null, InputOption::VALUE_NONE, 'If set will remove only assets/*')
->addOption('images-only', null, InputOption::VALUE_NONE, 'If set will remove only images/*')
->addOption('cache-only', null, InputOption::VALUE_NONE, 'If set will remove only cache/*')
->addOption('tmp-only', null, InputOption::VALUE_NONE, 'If set will remove only tmp/*')
->setHelp('The <info>clear-cache</info> deletes all cache files');
}
@@ -55,6 +56,8 @@ class ClearCacheCommand extends ConsoleCommand
$remove = 'images-only';
} elseif ($this->input->getOption('cache-only')) {
$remove = 'cache-only';
} elseif ($this->input->getOption('tmp-only')) {
$remove = 'tmp-only';
} else {
$remove = 'standard';
}

View File

@@ -65,20 +65,15 @@ class InstallCommand extends ConsoleCommand
// fix trailing slash
$this->destination = rtrim($this->destination, DS) . DS;
$this->user_path = $this->destination . USER_PATH;
if (false === $this->isWindows()) {
$local_config_file = exec('eval echo ~/.grav/config');
if (file_exists($local_config_file)) {
$this->local_config = Yaml::parse($local_config_file);
$this->output->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
}
if ($local_config_file = $this->loadLocalConfig()) {
$this->output->writeln('Read local config from <cyan>' . $local_config_file . '</cyan>');
}
// Look for dependencies file in ROOT and USER dir
if (file_exists($this->user_path . $dependencies_file)) {
$this->config = Yaml::parse($this->user_path . $dependencies_file);
$this->config = Yaml::parse(file_get_contents($this->user_path . $dependencies_file));
} elseif (file_exists($this->destination . $dependencies_file)) {
$this->config = Yaml::parse($this->destination . $dependencies_file);
$this->config = Yaml::parse(file_get_contents($this->destination . $dependencies_file));
} else {
$this->output->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
}

View File

@@ -19,15 +19,16 @@ class SandboxCommand extends ConsoleCommand
* @var array
*/
protected $directories = [
'/assets',
'/backup',
'/cache',
'/logs',
'/images',
'/assets',
'/logs',
'/tmp',
'/user/accounts',
'/user/config',
'/user/pages',
'/user/data',
'/user/pages',
'/user/plugins',
'/user/themes',
];
@@ -46,7 +47,6 @@ class SandboxCommand extends ConsoleCommand
* @var array
*/
protected $mappings = [
'/.editorconfig' => '/.editorconfig',
'/.gitignore' => '/.gitignore',
'/CHANGELOG.md' => '/CHANGELOG.md',
'/LICENSE.txt' => '/LICENSE.txt',
@@ -58,7 +58,6 @@ class SandboxCommand extends ConsoleCommand
'/system' => '/system',
'/vendor' => '/vendor',
'/webserver-configs' => '/webserver-configs',
'/codeception.yml' => '/codeception.yml',
];
/**

View File

@@ -16,6 +16,7 @@ use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Yaml;
trait ConsoleTrait
{
@@ -113,19 +114,20 @@ trait ConsoleTrait
}
/**
* Validate if the system is based on windows or not.
* Load the local config file
*
* @return bool
* @return mixed string the local config file name. false if local config does not exist
*/
public function isWindows()
public function loadLocalConfig()
{
$keys = [
'CYGWIN_NT-5.1',
'WIN32',
'WINNT',
'Windows'
];
$home_folder = getenv('HOME') ?: getenv('HOMEDRIVE') . getenv('HOMEPATH');
$local_config_file = $home_folder . '/.grav/config';
return array_key_exists(PHP_OS, $keys);
if (file_exists($local_config_file)) {
$this->local_config = Yaml::parse(file_get_contents($local_config_file));
return $local_config_file;
}
return false;
}
}

View File

@@ -0,0 +1,378 @@
<?php
/**
* @package Grav.Console
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Gpm;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Common\Filesystem\Folder;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Response;
use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Yaml\Yaml;
class DirectInstallCommand extends ConsoleCommand
{
/**
*
*/
protected function configure()
{
$this
->setName("direct-install")
->setAliases(['directinstall'])
->addArgument(
'package-file',
InputArgument::REQUIRED,
'The local location or remote URL to an installable package file'
)
->setDescription("Installs Grav, plugin, or theme directly from a file or a URL")
->setHelp('The <info>direct-install</info> command installs Grav, plugin, or theme directly from a file or a URL');
}
/**
* @return int|null|void
*/
protected function serve()
{
$package_file = $this->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 = $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln("exiting...");
$this->output->writeln('');
exit;
}
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$tmp_zip = $tmp_dir . '/Grav-' . uniqid();
$this->output->writeln("");
$this->output->writeln("Preparing to install <cyan>" . $package_file . "</cyan>");
if ($this->isRemote($package_file)) {
$zip = $this->downloadPackage($package_file, $tmp_zip);
} else {
$zip = $this->copyPackage($package_file, $tmp_zip);
}
if (file_exists($zip)) {
$tmp_source = $tmp_dir . '/Grav-' . uniqid();
$this->output->write(" |- Extracting package... ");
$extracted = Installer::unZip($zip, $tmp_source);
if (!$extracted) {
$this->output->write("\x0D");
$this->output->writeln(" |- Extracting package... <red>failed</red>");
exit;
}
$this->output->write("\x0D");
$this->output->writeln(" |- Extracting package... <green>ok</green>");
$type = $this->getPackageType($extracted);
if (!$type) {
$this->output->writeln(" '- <red>ERROR: Not a valid Grav package</red>");
$this->output->writeln('');
exit;
}
$blueprint = $this->getBlueprints($extracted);
if ($blueprint) {
if (isset($blueprint['dependencies'])) {
$depencencies = [];
foreach ($blueprint['dependencies'] as $dependency) {
if (is_array($dependency) && isset($dependency['name'])) {
$depencencies[] = $dependency['name'];
} else {
$depencencies[] = $dependency;
}
}
$this->output->writeln(" |- Dependencies found... <cyan>[" . implode(',', $depencencies) . "]</cyan>");
$question = new ConfirmationQuestion(" | '- Dependencies will not be satisfied. Continue ? [y|N] ", false);
$answer = $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln("exiting...");
$this->output->writeln('');
exit;
}
}
}
if ($type == 'grav') {
$this->output->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('');
exit;
}
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <green>ok</green>");
$this->output->write(" |- Installing package... ");
Installer::install($zip, GRAV_ROOT, ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true], $extracted);
} else {
$name = $this->getPackageName($extracted);
if (!$name) {
$this->output->writeln("<red>ERROR: Name could not be determined.</red> Please specify with --name|-n");
$this->output->writeln('');
exit;
}
$install_path = $this->getInstallPath($type, $name);
$is_update = file_exists($install_path);
$this->output->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('');
exit;
} else {
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <green>ok</green>");
}
$this->output->write(" |- Installing package... ");
Installer::install($zip, GRAV_ROOT, ['install_path' => $install_path, 'theme' => (($type == 'theme')), 'is_update' => $is_update], $extracted);
}
Folder::delete($tmp_source);
$this->output->write("\x0D");
if(Installer::lastErrorCode()) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
} else {
$this->output->writeln(" |- Installing package... <green>ok</green>");
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
}
} else {
$this->output->writeln(" '- <red>ERROR: ZIP package could not be found</red>");
}
Folder::delete($tmp_zip);
// clear cache after successful upgrade
$this->clearCache();
return true;
}
/**
* Get the install path for a name and a particular type of package
*
* @param $type
* @param $name
* @return string
*/
protected function getInstallPath($type, $name)
{
$locator = Grav::instance()['locator'];
if ($type == 'theme') {
$install_path = $locator->findResource('themes://', false) . DS . $name;
} else {
$install_path = $locator->findResource('plugins://', false) . DS . $name;
}
return $install_path;
}
/**
* Try to guess the package name from the source files
*
* @param $source
* @return bool|string
*/
protected function getPackageName($source)
{
foreach (glob($source . "*.yaml") as $filename) {
$name = strtolower(basename($filename, '.yaml'));
if ($name == 'blueprints') {
continue;
}
return $name;
}
return false;
}
/**
* Try to guess the package type from the source files
*
* @param $source
* @return bool|string
*/
protected function getPackageType($source)
{
$plugin_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Plugin/m';
$theme_regex = '/^class\\s{1,}[a-zA-Z0-9]{1,}\\s{1,}extends.+Theme/m';
if (
file_exists($source . 'system/defines.php') &&
file_exists($source . 'system/config/system.yaml')
) {
return 'grav';
} else {
// must have a blueprint
if (!file_exists($source . 'blueprints.yaml')) {
return false;
}
// either theme or plugin
$name = basename($source);
if (Utils::contains($name, 'theme')) {
return 'theme';
} elseif (Utils::contains($name, 'plugin')) {
return 'plugin';
}
foreach (glob($source . "*.php") as $filename) {
$contents = file_get_contents($filename);
if (preg_match($theme_regex, $contents)) {
return 'theme';
} elseif (preg_match($plugin_regex, $contents)) {
return 'plugin';
}
}
// Assume it's a theme
return 'theme';
}
}
/**
* Determine if this is a local or a remote file
*
* @param $file
* @return bool
*/
protected function isRemote($file)
{
return (bool) filter_var($file, FILTER_VALIDATE_URL);
}
/**
* Find/Parse the blueprint file
*
* @param $source
* @return array|bool
*/
protected function getBlueprints($source)
{
$blueprint_file = $source . 'blueprints.yaml';
if (!file_exists($blueprint_file)) {
return false;
}
$blueprint = (array)Yaml::parse(file_get_contents($blueprint_file));
return $blueprint;
}
/**
* Download the zip package via the URL
*
* @param $package_file
* @param $tmp
* @return null|string
*/
private function downloadPackage($package_file, $tmp)
{
$this->output->write(" |- Downloading package... 0%");
$package = parse_url($package_file);
$filename = basename($package['path']);
$output = Response::get($package_file, [], [$this, 'progress']);
if ($output) {
Folder::mkdir($tmp);
$this->output->write("\x0D");
$this->output->write(" |- Downloading package... 100%");
$this->output->writeln('');
file_put_contents($tmp . DS . $filename, $output);
return $tmp . DS . $filename;
}
return null;
}
/**
* Copy the local zip package to tmp
*
* @param $package_file
* @param $tmp
* @return null|string
*/
private function copyPackage($package_file, $tmp)
{
$this->output->write(" |- Copying package... 0%");
$package_file = realpath($package_file);
if (file_exists($package_file)) {
$filename = basename($package_file);
Folder::mkdir($tmp);
$this->output->write("\x0D");
$this->output->write(" |- Copying package... 100%");
$this->output->writeln('');
copy(realpath($package_file), $tmp . DS . $filename);
return $tmp . DS . $filename;
}
return null;
}
/**
* @param $progress
*/
public function progress($progress)
{
$this->output->write("\x0D");
$this->output->write(" |- Downloading package... " . str_pad($progress['percent'], 5, " ",
STR_PAD_LEFT) . '%');
}
}

View File

@@ -9,7 +9,9 @@
namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Common\Utils;
use Grav\Console\ConsoleCommand;
use League\CLImate\CLImate;
use Symfony\Component\Console\Input\InputOption;
class IndexCommand extends ConsoleCommand
@@ -100,31 +102,47 @@ class IndexCommand extends ConsoleCommand
protected function serve()
{
$this->options = $this->input->getOptions();
$this->gpm = new GPM($this->options['force']);
$this->displayGPMRelease();
$this->data = $this->gpm->getRepository();
$data = $this->filter($this->data);
foreach ($data as $type => $packages) {
$this->output->writeln("<green>" . ucfirst($type) . "</green> [ " . count($packages) . " ]");
$climate = new CLImate;
$climate->extend('Grav\Console\TerminalObjects\Table');
$index = 0;
if (!$data) {
$this->output->writeln('No data was found in the GPM repository stored locally.');
$this->output->writeln('Please try clearing cache and running the <green>bin/gpm index -f</green> command again');
$this->output->writeln('If this doesn\'t work try tweaking your GPM system settings.');
$this->output->writeln('');
$this->output->writeln('For more help go to:');
$this->output->writeln(' -> <yellow>https://learn.getgrav.org/troubleshooting/common-problems#cannot-connect-to-the-gpm</yellow>');
die;
}
foreach ($data as $type => $packages) {
$this->output->writeln("<green>" . strtoupper($type) . "</green> [ " . count($packages) . " ]");
$packages = $this->sort($packages);
foreach ($packages as $slug => $package) {
$this->output->writeln(
// index
str_pad($index++ + 1, 2, '0', STR_PAD_LEFT) . ". " .
// package name
"<cyan>" . str_pad($package->name, 20) . "</cyan> " .
// slug
"[" . str_pad($slug, 20, ' ', STR_PAD_BOTH) . "] " .
// version details
$this->versionDetails($package)
);
if (!empty($packages)) {
$table = [];
$index = 0;
foreach ($packages as $slug => $package) {
$row = [
'Count' => $index++ + 1,
'Name' => "<cyan>" . Utils::truncate($package->name, 20, false, ' ', '...') . "</cyan> ",
'Slug' => $slug,
'Version'=> $this->version($package),
'Installed' => $this->installed($package)
];
$table[] = $row;
}
$climate->table($table);
}
$this->output->writeln('');
@@ -143,7 +161,7 @@ class IndexCommand extends ConsoleCommand
*
* @return string
*/
private function versionDetails($package)
private function version($package)
{
$list = $this->gpm->{'getUpdatable' . ucfirst($package->package_type)}();
$package = isset($list[$package->slug]) ? $list[$package->slug] : $package;
@@ -154,21 +172,30 @@ class IndexCommand extends ConsoleCommand
if (!$installed || !$updatable) {
$version = $installed ? $local->version : $package->version;
$installed = !$installed ? ' (<magenta>not installed</magenta>)' : ' (<cyan>installed</cyan>)';
return str_pad(" [v<green>" . $version . "</green>]", 35) . $installed;
return "v<green>" . $version . "</green>";
}
if ($updatable) {
$installed = !$installed ? ' (<magenta>not installed</magenta>)' : ' (<cyan>installed</cyan>)';
return str_pad(" [v<red>" . $package->version . "</red> <cyan>➜</cyan> v<green>" . $package->available . "</green>]",
61) . $installed;
return "v<red>" . $package->version . "</red> <cyan>-></cyan> v<green>" . $package->available . "</green>";
}
return '';
}
/**
* @param $package
*
* @return string
*/
private function installed($package)
{
$package = isset($list[$package->slug]) ? $list[$package->slug] : $package;
$type = ucfirst(preg_replace("/s$/", '', $package->package_type));
$installed = $this->gpm->{'is' . $type . 'Installed'}($package->slug);
return !$installed ? '<magenta>not installed</magenta>' : '<cyan>installed</cyan>';
}
/**
* @param $data
*

View File

@@ -25,6 +25,8 @@ class InfoCommand extends ConsoleCommand
*/
protected $gpm;
protected $all_yes;
/**
*
*/
@@ -60,6 +62,8 @@ class InfoCommand extends ConsoleCommand
{
$this->gpm = new GPM($this->input->getOption('force'));
$this->all_yes = $this->input->getOption('all-yes');
$this->displayGPMRelease();
$foundPackage = $this->gpm->findPackage($this->input->getArgument('package'));
@@ -133,38 +137,34 @@ class InfoCommand extends ConsoleCommand
// display changelog information
$questionHelper = $this->getHelper('question');
$skipPrompt = $this->input->getOption('all-yes');
$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);
if (!$skipPrompt) {
$question = new ConfirmationQuestion("Would you like to read the changelog? [y|N] ",
false);
$answer = $questionHelper->ask($this->input, $this->output, $question);
if ($answer) {
$changelog = $foundPackage->changelog;
if ($answer) {
$changelog = $foundPackage->changelog;
$this->output->writeln("");
foreach ($changelog as $version => $log) {
$title = $version . ' [' . $log['date'] . ']';
$content = preg_replace_callback("/\d\.\s\[\]\(#(.*)\)/", 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("");
foreach ($changelog as $version => $log) {
$title = $version . ' [' . $log['date'] . ']';
$content = preg_replace_callback("/\d\.\s\[\]\(#(.*)\)/", 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("");
$question = new ConfirmationQuestion("Press [ENTER] to continue or [q] to quit ", true);
if (!$questionHelper->ask($this->input, $this->output, $question)) {
break;
}
$this->output->writeln("");
$question = new ConfirmationQuestion("Press [ENTER] to continue or [q] to quit ", true);
$answer = $this->all_yes ? false : $questionHelper->ask($this->input, $this->output, $question);
if (!$answer) {
break;
}
$this->output->writeln("");
}
}
$this->output->writeln('');
if ($installed && $updatable) {

View File

@@ -11,6 +11,7 @@ namespace Grav\Console\Gpm;
use Grav\Common\Filesystem\Folder;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Common\GPM\Licenses;
use Grav\Common\GPM\Response;
use Grav\Common\GPM\Remote\Package as Package;
use Grav\Common\Grav;
@@ -19,7 +20,6 @@ use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Yaml\Yaml;
define('GIT_REGEX', '/http[s]?:\/\/(?:.*@)?(github|bitbucket)(?:.org|.com)\/.*\/(.*)/');
@@ -49,6 +49,8 @@ class InstallCommand extends ConsoleCommand
/** @var array */
protected $demo_processing = [];
protected $all_yes;
/**
*
*/
@@ -101,19 +103,15 @@ class InstallCommand extends ConsoleCommand
{
$this->gpm = new GPM($this->input->getOption('force'));
$this->all_yes = $this->input->getOption('all-yes');
$this->displayGPMRelease();
$this->destination = realpath($this->input->getOption('destination'));
$packages = array_map('strtolower', $this->input->getArgument('package'));
$this->data = $this->gpm->findPackages($packages);
if (false === $this->isWindows() && @is_file(getenv("HOME") . '/.grav/config')) {
$local_config_file = exec('eval echo ~/.grav/config');
if (file_exists($local_config_file)) {
$this->local_config = Yaml::parse($local_config_file);
}
}
$this->loadLocalConfig();
if (
!Installer::isGravInstance($this->destination) ||
@@ -139,16 +137,20 @@ class InstallCommand extends ConsoleCommand
unset($this->data['not_found']);
unset($this->data['total']);
if (isset($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);
if ($helper->ask($this->input, $this->output, $question)) {
$answer = $this->all_yes ? false : $helper->ask($this->input, $this->output, $question);
if ($answer) {
$this->use_symlinks = true;
}
}
$this->output->writeln('');
@@ -199,8 +201,9 @@ class InstallCommand extends ConsoleCommand
$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);
if ($helper->ask($this->input, $this->output, $question)) {
if ($answer) {
$is_update = true;
$this->processPackage($package, true, $is_update);
} else {
@@ -208,8 +211,8 @@ class InstallCommand extends ConsoleCommand
}
} else {
if (Installer::lastErrorCode() == Installer::IS_LINK) {
$this->output->writeln("<red>Cannot overwrite existing symlink</red>");
return false;
$this->output->writeln("<red>Cannot overwrite existing symlink for </red><cyan>$package_name</cyan>");
$this->output->writeln("");
}
}
}
@@ -244,6 +247,11 @@ class InstallCommand extends ConsoleCommand
$major_version_changed = explode('.', $new_version)[0] !== explode('.', $old_version)[0];
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>");
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)) {
@@ -297,9 +305,10 @@ class InstallCommand extends ConsoleCommand
$questionNoun = 'packages';
}
$question = new ConfirmationQuestion("$questionAction $questionArticle $questionNoun? [y|N] ", false);
$question = new ConfirmationQuestion("$questionAction $questionArticle $questionNoun? [Y|n] ", true);
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
if ($helper->ask($this->input, $this->output, $question)) {
if ($answer) {
foreach ($packages as $dependencyName => $dependencyVersion) {
$package = $this->gpm->findPackage($dependencyName);
$this->processPackage($package, true, ($type == 'update') ? true : false);
@@ -315,10 +324,9 @@ class InstallCommand extends ConsoleCommand
/**
* @param $package
* @param bool $skip_prompt
* @param bool $update True if the package is an update
* @param bool $is_update True if the package is an update
*/
private function processPackage($package, $skip_prompt = false, $is_update = false)
private function processPackage($package, $is_update = false)
{
if (!$package) {
$this->output->writeln("<red>Package not found on the GPM!</red> ");
@@ -333,7 +341,7 @@ class InstallCommand extends ConsoleCommand
}
}
$symlink ? $this->processSymlink($package, $skip_prompt) : $this->processGpm($package, $skip_prompt, $is_update);
$symlink ? $this->processSymlink($package) : $this->processGpm($package, $is_update);
$this->processDemo($package);
}
@@ -369,7 +377,9 @@ class InstallCommand extends ConsoleCommand
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Do you wish to install this demo content? [y|N] ', false);
if (!$helper->ask($this->input, $this->output, $question)) {
$answer = $this->all_yes ? true : $helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" '- <red>Skipped!</red> ");
$this->output->writeln('');
@@ -380,8 +390,9 @@ 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);
if (!$helper->ask($this->input, $this->output, $question)) {
if (!$answer) {
$this->output->writeln(" '- <red>Skipped!</red> ");
$this->output->writeln('');
@@ -452,9 +463,8 @@ class InstallCommand extends ConsoleCommand
/**
* @param $package
* @param bool $skip_prompt
*/
private function processSymlink($package, $skip_prompt = false)
private function processSymlink($package)
{
exec('cd ' . $this->destination);
@@ -469,7 +479,7 @@ class InstallCommand extends ConsoleCommand
$this->output->writeln("<green>ok</green>");
$this->output->write(" |- Checking destination... ");
$checks = $this->checkDestination($package, $skip_prompt);
$checks = $this->checkDestination($package);
if (!$checks) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
@@ -497,19 +507,29 @@ class InstallCommand extends ConsoleCommand
/**
* @param $package
* @param bool $skip_prompt
* @param bool $is_update
*
* @return bool
*/
private function processGpm($package, $skip_prompt = false, $is_update = false)
private function processGpm($package, $is_update = false)
{
$version = isset($package->available) ? $package->available : $package->version;
$license = Licenses::get($package->slug);
$this->output->writeln("Preparing to install <cyan>" . $package->name . "</cyan> [v" . $version . "]");
$this->output->write(" |- Downloading package... 0%");
$this->file = $this->downloadPackage($package);
$this->file = $this->downloadPackage($package, $license);
if (!$this->file) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
return false;
}
$this->output->write(" |- Checking destination... ");
$checks = $this->checkDestination($package, $skip_prompt);
$checks = $this->checkDestination($package);
if (!$checks) {
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
@@ -530,14 +550,41 @@ class InstallCommand extends ConsoleCommand
/**
* @param Package $package
*
* @param string $license
*
* @return string
*/
private function downloadPackage($package)
private function downloadPackage($package, $license = null)
{
$cache_dir = Grav::instance()['locator']->findResource('cache://', true);
$this->tmp = $cache_dir . DS . 'tmp/Grav-' . uniqid();
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/Grav-' . uniqid();
$filename = $package->slug . basename($package->zipball_url);
$output = Response::get($package->zipball_url, [], [$this, 'progress']);
$query = '';
if ($package->premium) {
$query = \json_encode(array_merge(
$package->premium,
[
'slug' => $package->slug,
'filename' => $package->premium['filename'],
'license_key' => $license
]
));
$query = '?d=' . base64_encode($query);
}
try {
$output = Response::get($package->zipball_url . $query, [], [$this, 'progress']);
} catch (\Exception $e) {
$error = str_replace("\n", "\n | '- ", $e->getMessage());
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(" |- Downloading package... <red>error</red> ");
$this->output->writeln(" | '- " . $error);
return false;
}
Folder::mkdir($this->tmp);
@@ -553,42 +600,19 @@ class InstallCommand extends ConsoleCommand
/**
* @param $package
*
* @param bool $skip_prompt
*
* @return bool
*/
private function checkDestination($package, $skip_prompt = false)
private function checkDestination($package)
{
$question_helper = $this->getHelper('question');
if (!$skip_prompt) {
$skip_prompt = $this->input->getOption('all-yes');
}
Installer::isValidDestination($this->destination . DS . $package->install_path);
if (Installer::lastErrorCode() == Installer::EXISTS) {
if (!$skip_prompt) {
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <yellow>exists</yellow>");
$question = new ConfirmationQuestion(" | '- The package is already installed, do you want to overwrite it? [y|N] ",
false);
$answer = $question_helper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" | '- <red>You decided to not overwrite the already installed package.</red>");
return false;
}
}
}
if (Installer::lastErrorCode() == Installer::IS_LINK) {
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <yellow>symbolic link</yellow>");
if ($skip_prompt) {
if ($this->all_yes) {
$this->output->writeln(" | '- <yellow>Skipped automatically.</yellow>");
return false;

View File

@@ -49,6 +49,9 @@ class SelfupgradeCommand extends ConsoleCommand
*/
private $upgrader;
protected $all_yes;
protected $overwrite;
/**
*
*/
@@ -56,7 +59,7 @@ class SelfupgradeCommand extends ConsoleCommand
{
$this
->setName("self-upgrade")
->setAliases(['selfupgrade'])
->setAliases(['selfupgrade', 'selfupdate'])
->addOption(
'force',
'f',
@@ -69,6 +72,12 @@ class SelfupgradeCommand extends ConsoleCommand
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addOption(
'overwrite',
'o',
InputOption::VALUE_NONE,
'Option to overwrite packages if they already exist'
)
->setDescription("Detects and performs an update of Grav itself when available")
->setHelp('The <info>update</info> command updates Grav itself when a new version is available');
}
@@ -79,6 +88,8 @@ class SelfupgradeCommand extends ConsoleCommand
protected function serve()
{
$this->upgrader = new Upgrader($this->input->getOption('force'));
$this->all_yes = $this->input->getOption('all-yes');
$this->overwrite = $this->input->getOption('overwrite');
$this->displayGPMRelease();
@@ -89,7 +100,6 @@ class SelfupgradeCommand extends ConsoleCommand
$release = strftime('%c', strtotime($this->upgrader->getReleaseDate()));
if (!$this->upgrader->meetsRequirements()) {
$this->output->writeln("");
$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>" . PHP_VERSION . "</red>, but PHP <green>" . GRAV_PHP_MIN . "</green> is required.");
@@ -100,21 +110,29 @@ class SelfupgradeCommand extends ConsoleCommand
exit;
}
if (!$this->upgrader->isUpgradable()) {
if (!$this->overwrite && !$this->upgrader->isUpgradable()) {
$this->output->writeln("You are already running the latest version of Grav (v" . $local . ") released on " . $release);
exit;
}
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 . ".");
exit;
}
// not used but preloaded just in case!
new ArrayInput([]);
$questionHelper = $this->getHelper('question');
$skipPrompt = $this->input->getOption('all-yes');
$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>.");
if (!$skipPrompt) {
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);
@@ -177,8 +195,8 @@ class SelfupgradeCommand extends ConsoleCommand
*/
private function download($package)
{
$cache_dir = Grav::instance()['locator']->findResource('cache://', true);
$this->tmp = $cache_dir . DS . 'tmp/Grav-' . uniqid();
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
$this->tmp = $tmp_dir . '/Grav-' . uniqid();
$output = Response::get($package['download'], [], [$this, 'progress']);
Folder::mkdir($this->tmp);

View File

@@ -39,6 +39,10 @@ class UninstallCommand extends ConsoleCommand
*/
protected $tmp;
protected $dependencies= [];
protected $all_yes;
/**
*
*/
@@ -68,6 +72,8 @@ class UninstallCommand extends ConsoleCommand
{
$this->gpm = new GPM();
$this->all_yes = $this->input->getOption('all-yes');
$packages = array_map('strtolower', $this->input->getArgument('package'));
$this->data = ['total' => 0, 'not_found' => []];
@@ -108,15 +114,12 @@ class UninstallCommand extends ConsoleCommand
$this->output->writeln(" '- <red>Installation failed or aborted.</red>");
$this->output->writeln('');
} else {
$this->output->write(" |- Uninstalling package... ");
$uninstall = $this->uninstallPackage($slug, $package);
if (!$uninstall) {
$this->output->writeln(" '- <red>Uninstallation failed or aborted.</red>");
$this->output->writeln('');
} else {
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln('');
}
}
@@ -156,36 +159,29 @@ class UninstallCommand extends ConsoleCommand
return false;
}
$locator = Grav::instance()['locator'];
$path = $locator->findResource($package->package_type . '://' . $slug);
Installer::uninstall($path);
$errorCode = Installer::lastErrorCode();
if ($errorCode && $errorCode !== Installer::IS_LINK && $errorCode !== Installer::EXISTS) {
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(" |- Uninstalling package... <red>error</red> ");
$this->output->writeln(" | '- " . Installer::lastErrorMsg());
return false;
}
$message = Installer::getMessage();
if ($message) {
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(" |- " . $message);
}
$this->output->write("\x0D");
// extra white spaces to clear out the buffer properly
$this->output->writeln(" |- Uninstalling package... <green>ok</green> ");
if (isset($package->dependencies)) {
$dependencies = $package->dependencies;
if ($is_dependency) {
foreach ($dependencies as $key => $dependency) {
if (in_array($dependency['name'], $this->dependencies)) {
unset($dependencies[$key]);
}
}
} else {
if (count($dependencies) > 0) {
$this->output->writeln(' `- Dependencies found...');
$this->output->writeln('');
}
}
$questionHelper = $this->getHelper('question');
foreach($package->dependencies as $dependency) {
foreach ($dependencies as $dependency) {
$this->dependencies[] = $dependency['name'];
if (is_array($dependency)) {
$dependency = $dependency['name'];
}
@@ -194,25 +190,59 @@ class UninstallCommand extends ConsoleCommand
}
$dependencyPackage = $this->gpm->findPackage($dependency);
$question = new ConfirmationQuestion(" | '- Delete dependency <cyan>" . $dependency . "</cyan> too? [y|N] ", false);
$answer = $questionHelper->ask($this->input, $this->output, $question);
if ($answer) {
$this->output->writeln(" | '- <red>You decided to delete " . $dependency . ".</red>");
$dependency_exists = $this->packageExists($dependency, $dependencyPackage);
$uninstall = $this->uninstallPackage($dependency, $dependencyPackage, true);
if ($dependency_exists == Installer::EXISTS) {
$this->output->writeln("A dependency on <cyan>" . $dependencyPackage->name . "</cyan> [v" . $dependencyPackage->version . "] was found");
if (!$uninstall) {
$this->output->writeln(" '- <red>Uninstallation failed or aborted.</red>");
$question = new ConfirmationQuestion(" |- Uninstall <cyan>" . $dependencyPackage->name . "</cyan>? [y|N] ", false);
$answer = $this->all_yes ? true : $questionHelper->ask($this->input, $this->output, $question);
if ($answer) {
$uninstall = $this->uninstallPackage($dependency, $dependencyPackage, true);
if (!$uninstall) {
$this->output->writeln(" '- <red>Uninstallation failed or aborted.</red>");
} else {
$this->output->writeln(" '- <green>Success!</green> ");
}
$this->output->writeln('');
} else {
$this->output->writeln(" '- <green>Success!</green> ");
$this->output->writeln(" '- <yellow>You decided not to uninstall " . $dependencyPackage->name . ".</yellow>");
$this->output->writeln('');
}
}
}
}
$locator = Grav::instance()['locator'];
$path = $locator->findResource($package->package_type . '://' . $slug);
Installer::uninstall($path);
$errorCode = Installer::lastErrorCode();
if ($errorCode && $errorCode !== Installer::IS_LINK && $errorCode !== Installer::EXISTS) {
$this->output->writeln(" |- Uninstalling " . $package->name . " package... <red>error</red> ");
$this->output->writeln(" | '- <yellow>" . Installer::lastErrorMsg()."</yellow>");
return false;
}
$message = Installer::getMessage();
if ($message) {
$this->output->writeln(" |- " . $message);
}
if (!$is_dependency && $this->dependencies) {
$this->output->writeln("Finishing up uninstalling <cyan>" . $package->name . "</cyan>");
}
$this->output->writeln(" |- Uninstalling " . $package->name . " package... <green>ok</green> ");
return true;
}
@@ -225,17 +255,15 @@ class UninstallCommand extends ConsoleCommand
private function checkDestination($slug, $package)
{
$path = Grav::instance()['locator']->findResource($package->package_type . '://' . $slug);
$questionHelper = $this->getHelper('question');
$skipPrompt = $this->input->getOption('all-yes');
Installer::isValidDestination($path);
$exists = $this->packageExists($slug, $package);
if (Installer::lastErrorCode() == Installer::IS_LINK) {
if ($exists == Installer::IS_LINK) {
$this->output->write("\x0D");
$this->output->writeln(" |- Checking destination... <yellow>symbolic link</yellow>");
if ($skipPrompt) {
if ($this->all_yes) {
$this->output->writeln(" | '- <yellow>Skipped automatically.</yellow>");
return false;
@@ -243,10 +271,10 @@ class UninstallCommand extends ConsoleCommand
$question = new ConfirmationQuestion(" | '- Destination has been detected as symlink, delete symbolic link first? [y|N] ",
false);
$answer = $questionHelper->ask($this->input, $this->output, $question);
$answer = $this->all_yes ? true : $questionHelper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln(" | '- <red>You decided to not delete the symlink automatically.</red>");
$this->output->writeln(" | '- <red>You decided not to delete the symlink automatically.</red>");
return false;
}
@@ -257,4 +285,18 @@ class UninstallCommand extends ConsoleCommand
return true;
}
/**
* Check if package exists
*
* @param $slug
* @param $package
* @return int
*/
private function packageExists($slug, $package)
{
$path = Grav::instance()['locator']->findResource($package->package_type . '://' . $slug);
Installer::isValidDestination($path);
return Installer::lastErrorCode();
}
}

View File

@@ -11,6 +11,7 @@ namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Installer;
use Grav\Console\ConsoleCommand;
use Grav\Common\GPM\Upgrader;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@@ -47,6 +48,10 @@ class UpdateCommand extends ConsoleCommand
*/
protected $gpm;
protected $all_yes;
protected $overwrite;
/**
*
*/
@@ -73,6 +78,12 @@ class UpdateCommand extends ConsoleCommand
InputOption::VALUE_NONE,
'Assumes yes (or best approach) instead of prompting'
)
->addOption(
'overwrite',
'o',
InputOption::VALUE_NONE,
'Option to overwrite packages if they already exist'
)
->addOption(
'plugins',
'p',
@@ -99,33 +110,58 @@ class UpdateCommand extends ConsoleCommand
*/
protected function serve()
{
$this->upgrader = new Upgrader($this->input->getOption('force'));
$local = $this->upgrader->getLocalVersion();
$remote = $this->upgrader->getRemoteVersion();
if ($local !== $remote) {
$this->output->writeln("<yellow>WARNING</yellow>: A new version of Grav is available. You should update Grav before updating plugins and themes. If you continue without updating Grav, some plugins or themes may stop working.");
$this->output->writeln("");
$questionHelper = $this->getHelper('question');
$question = new ConfirmationQuestion("Continue with the update process? [Y|n] ", true);
$answer = $questionHelper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln("<red>Update aborted. Exiting...</red>");
exit;
}
}
$this->gpm = new GPM($this->input->getOption('force'));
$this->all_yes = $this->input->getOption('all-yes');
$this->overwrite = $this->input->getOption('overwrite');
$this->displayGPMRelease();
$this->destination = realpath($this->input->getOption('destination'));
$skip_prompt = $this->input->getOption('all-yes');
if (!Installer::isGravInstance($this->destination)) {
$this->output->writeln("<red>ERROR</red>: " . Installer::lastErrorMsg());
exit;
}
if ($this->input->getOption('plugins') === false and $this->input->getOption('themes') === false) {
$list_type_update = ['plugins' => true, 'themes' => true];
if ($this->input->getOption('plugins') === false && $this->input->getOption('themes') === false) {
$list_type = ['plugins' => true, 'themes' => true];
} else {
$list_type_update['plugins'] = $this->input->getOption('plugins');
$list_type_update['themes'] = $this->input->getOption('themes');
$list_type['plugins'] = $this->input->getOption('plugins');
$list_type['themes'] = $this->input->getOption('themes');
}
if ($this->overwrite) {
$this->data = $this->gpm->getInstallable($list_type);
$description = " can be overwritten";
} else {
$this->data = $this->gpm->getUpdatable($list_type);
$description = " need updating";
}
$this->data = $this->gpm->getUpdatable($list_type_update);
$only_packages = array_map('strtolower', $this->input->getArgument('package'));
if (!$this->data['total']) {
if (!$this->overwrite && !$this->data['total']) {
$this->output->writeln("Nothing to update.");
exit;
}
$this->output->write("Found <green>" . $this->gpm->countInstalled() . "</green> extensions installed of which <magenta>" . $this->data['total'] . "</magenta> need updating");
$this->output->write("Found <green>" . $this->gpm->countInstalled() . "</green> packages installed of which <magenta>" . $this->data['total'] . "</magenta>" . $description);
$limit_to = $this->userInputPackages($only_packages);
@@ -145,19 +181,23 @@ class UpdateCommand extends ConsoleCommand
continue;
}
if (!$package->available) {
$package->available = $package->version;
}
$this->output->writeln(
// index
str_pad($index++ + 1, 2, '0', STR_PAD_LEFT) . ". " .
// name
"<cyan>" . str_pad($package->name, 15) . "</cyan> " .
// version
"[v<magenta>" . $package->version . "</magenta> v<green>" . $package->available . "</green>]"
"[v<magenta>" . $package->version . "</magenta> -> v<green>" . $package->available . "</green>]"
);
$slugs[] = $slug;
}
}
if (!$skip_prompt) {
if (!$this->all_yes) {
// prompt to continue
$this->output->writeln("");
$questionHelper = $this->getHelper('question');
@@ -165,7 +205,7 @@ class UpdateCommand extends ConsoleCommand
$answer = $questionHelper->ask($this->input, $this->output, $question);
if (!$answer) {
$this->output->writeln("Update aborted. Exiting...");
$this->output->writeln("<red>Update aborted. Exiting...</red>");
exit;
}
}
@@ -183,7 +223,7 @@ class UpdateCommand extends ConsoleCommand
$command_exec = $install_command->run($args, $this->output);
if ($command_exec != 0) {
$this->output->writeln("<red>Error:</red> An error occurred while trying to install the extensions");
$this->output->writeln("<red>Error:</red> An error occurred while trying to install the packages");
exit;
}
}
@@ -204,7 +244,7 @@ class UpdateCommand extends ConsoleCommand
foreach ($only_packages as $only_package) {
$find = $this->gpm->findPackage($only_package);
if (!$find || !$this->gpm->isUpdatable($find->slug)) {
if (!$find || (!$this->overwrite && !$this->gpm->isUpdatable($find->slug))) {
$name = isset($find->slug) ? $find->slug : $only_package;
$ignore[$name] = $name;
} else {

View File

@@ -0,0 +1,29 @@
<?php
/**
* @package Grav.Console
*
* @copyright Copyright (C) 2014 - 2016 RocketTheme, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\TerminalObjects;
class Table extends \League\CLImate\TerminalObject\Basic\Table
{
public function result()
{
$this->column_widths = $this->getColumnWidths();
$this->table_width = $this->getWidth();
$this->border = $this->getBorder();
$this->buildHeaderRow();
foreach ($this->data as $key => $columns) {
$this->rows[] = $this->buildRow($columns);
}
$this->rows[] = $this->border;
return $this->rows;
}
}

View File

@@ -37,6 +37,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getCss();
$this->assertSame([
'asset' => '/test.css',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -51,6 +52,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getCss();
$this->assertSame([
'asset' => '/test.css',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -67,6 +69,24 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getCss();
$this->assertSame([
'asset' => '/test.css',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
'group' => 'head',
'modified' => false
], reset($array));
//test addCss(). Testing with remote URL
$this->assets->reset();
$this->assets->addCSS('http://www.somesite.com/test.css');
$css = $this->assets->css();
$this->assertSame('<link href="http://www.somesite.com/test.css" type="text/css" rel="stylesheet" />' . PHP_EOL, $css);
$array = $this->assets->getCss();
$this->assertSame([
'asset' => 'http://www.somesite.com/test.css',
'remote' => true,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -89,6 +109,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getJs();
$this->assertSame([
'asset' => '/test.js',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -108,6 +129,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getCss();
$this->assertSame([
'asset' => '/test.css',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -126,6 +148,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getJs();
$this->assertSame([
'asset' => '/test.js',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -143,6 +166,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getJs();
$this->assertSame([
'asset' => '/test.js',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -159,6 +183,7 @@ class AssetsTest extends \Codeception\TestCase\Test
$array = $this->assets->getJs();
$this->assertSame([
'asset' => '/test.js',
'remote' => false,
'priority' => 10,
'order' => 0,
'pipeline' => true,
@@ -298,6 +323,19 @@ class AssetsTest extends \Codeception\TestCase\Test
$this->assertContains('type="text/css" rel="stylesheet" />', $css);
}
public function testPipelineWithTimestamp()
{
$this->assets->reset();
$this->assets->setTimestamp('foo');
//Add a core Grav CSS file, which is found. Pipeline will now return a file
$this->assets->add('/system/assets/debugger.css', null, true);
$css = $this->assets->css();
$this->assertContains('<link href=', $css);
$this->assertContains('type="text/css" rel="stylesheet" />', $css);
$this->assertContains($this->assets->getTimestamp(), $css);
}
public function testAddAsyncJs()
{
$this->assets->reset();
@@ -314,6 +352,65 @@ class AssetsTest extends \Codeception\TestCase\Test
$this->assertSame('<script src="/system/assets/jquery/jquery-2.x.min.js" type="text/javascript" defer></script>' . PHP_EOL, $js);
}
public function testTimestamps()
{
// local CSS nothing extra
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addCSS('test.css');
$css = $this->assets->css();
$this->assertSame('<link href="/test.css?foo" type="text/css" rel="stylesheet" />' . PHP_EOL, $css);
// local CSS already with param
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addCSS('test.css?bar');
$css = $this->assets->css();
$this->assertSame('<link href="/test.css?bar&foo" type="text/css" rel="stylesheet" />' . PHP_EOL, $css);
// external CSS already
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addCSS('http://somesite.com/test.css');
$css = $this->assets->css();
$this->assertSame('<link href="http://somesite.com/test.css" type="text/css" rel="stylesheet" />' . PHP_EOL, $css);
// external CSS already with param
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addCSS('http://somesite.com/test.css?bar');
$css = $this->assets->css();
$this->assertSame('<link href="http://somesite.com/test.css?bar" type="text/css" rel="stylesheet" />' . PHP_EOL, $css);
// local JS nothing extra
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addJs('test.js');
$css = $this->assets->js();
$this->assertSame('<script src="/test.js?foo" type="text/javascript" ></script>' . PHP_EOL, $css);
// local JS already with param
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addJs('test.js?bar');
$css = $this->assets->js();
$this->assertSame('<script src="/test.js?bar&foo" type="text/javascript" ></script>' . PHP_EOL, $css);
// external JS already
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addJs('http://somesite.com/test.js');
$css = $this->assets->js();
$this->assertSame('<script src="http://somesite.com/test.js" type="text/javascript" ></script>' . PHP_EOL, $css);
// external JS already with param
$this->assets->reset();
$this->assets->setTimestamp('foo');
$this->assets->addJs('http://somesite.com/test.js?bar');
$css = $this->assets->js();
$this->assertSame('<script src="http://somesite.com/test.js?bar" type="text/javascript" ></script>' . PHP_EOL, $css);
}
public function testAddInlineCss()
{
$this->assets->reset();

View File

@@ -0,0 +1,85 @@
<?php
use Codeception\Util\Fixtures;
use Grav\Common\Helpers\Excerpts;
use Grav\Common\Grav;
use Grav\Common\Uri;
use Grav\Common\Config\Config;
use Grav\Common\Page\Pages;
use Grav\Common\Language\Language;
/**
* Class ExcerptsTest
*/
class ExcerptsTest extends \Codeception\TestCase\Test
{
/** @var Parsedown $parsedown */
protected $parsedown;
/** @var Grav $grav */
protected $grav;
/** @var Page $page */
protected $page;
/** @var Pages $pages */
protected $pages;
/** @var Config $config */
protected $config;
/** @var Uri $uri */
protected $uri;
/** @var Language $language */
protected $language;
protected $old_home;
protected function _before()
{
$grav = Fixtures::get('grav');
$this->grav = $grav();
$this->pages = $this->grav['pages'];
$this->config = $this->grav['config'];
$this->uri = $this->grav['uri'];
$this->language = $this->grav['language'];
$this->old_home = $this->config->get('system.home.alias');
$this->config->set('system.home.alias', '/item1');
$this->config->set('system.absolute_urls', false);
$this->config->set('system.languages.supported', []);
unset($this->grav['language']);
$this->grav['language'] = new Language($this->grav);
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
$locator->addPath('page', '', 'tests/fake/nested-site/user/pages', false);
$this->pages->init();
$defaults = [
'extra' => false,
'auto_line_breaks' => false,
'auto_url_links' => false,
'escape_markup' => false,
'special_chars' => ['>' => 'gt', '<' => 'lt'],
];
$this->page = $this->pages->dispatch('/item2/item2-2');
$this->uri->initializeWithURL('http://testing.dev/item2/item2-2')->init();
}
protected function _after()
{
$this->config->set('system.home.alias', $this->old_home);
}
public function testProcessImageHtml()
{
$this->assertRegexp('|<img alt="Sample Image" src="\/images\/.*-sample-image.jpe?g\" data-src="sample-image\.jpg\?cropZoom=300,300" \/>|',
Excerpts::processImageHtml('<img src="sample-image.jpg?cropZoom=300,300" alt="Sample Image" />', $this->page));
$this->assertRegexp('|<img alt="Sample Image" class="foo" src="\/images\/.*-sample-image.jpe?g\" data-src="sample-image\.jpg\?classes=foo" \/>|',
Excerpts::processImageHtml('<img src="sample-image.jpg?classes=foo" alt="Sample Image" />', $this->page));
}
}

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