Compare commits

...

306 Commits
1.4.0 ... 1.5.5

Author SHA1 Message Date
Andy Miller
9405418572 Merge branch 'release/1.5.5' 2018-11-12 15:56:13 -07:00
Andy Miller
377751416b Prepare for release 2018-11-12 15:56:04 -07:00
Andy Miller
116c279f01 update changelog 2018-11-12 15:55:29 -07:00
Aaron Dalton
bf86b5a924 Propagate error code if between 400 and 600 for production sites (errors:display = false or -1) (#2181) 2018-11-11 21:22:47 -07:00
Emil Hesslow
d0b34d114d Register theme prefixes as namespaces in twig (#2210) 2018-11-11 21:18:57 -07:00
Makara Sok
b9dc2baef1 Remove hardcoded 302 when redirecting trailing slash (#2155)
When `system.pages.redirect_trailing_slash` is enabled, it's always a 302 even though `redirect_default_code` is set to something else. 

By removing it, it works as intended.
2018-11-09 22:30:35 -07:00
Andy Miller
72cc5b9d07 Merge tag '1.5.4' into develop
Release v1.5.4
2018-11-05 15:42:04 -07:00
Andy Miller
b9c28c5a7c Merge branch 'release/1.5.4' 2018-11-05 15:42:03 -07:00
Andy Miller
c56d7ac793 prepare for release 2018-11-05 15:41:54 -07:00
Andy Miller
000bac8cfc Updated changelog 2018-11-05 15:41:19 -07:00
Scott Hamper
e7d660149e Fixed markdown parsing for telephone links (#2235)
Telephone links use the `+` character to specify a country code, but Grav was replacing the `+` with a space character.
2018-11-03 14:57:02 -06:00
John Hamrick
829638c143 Update default.md (#2245)
Making documentation consistent between sources:  Issue Typo! #650

In (grav-learn/pages/01.basics/04.basic-tutorial/docs.md) the text under the heading Adding a New Page is 02.mypage 
In (grav/user/pages/01.home/default.md) the text under the heading Adding a New Page is 03.mypage
2018-11-03 14:56:21 -06:00
Matias Griese
d8a627898e Fixed fatal error if calling $session->invalidate() when there's no active session 2018-10-25 16:29:53 +03:00
MattAppleton
a3caa13c23 fix .webm typo (#2220)
Media type should be 'video' not file!
2018-10-16 20:18:32 -06:00
Andy Miller
9944486c17 Merge branch 'release/1.5.3' 2018-10-08 17:41:28 -06:00
Andy Miller
da5c9e415f Merge tag '1.5.3' into develop
Release v1.5.3
2018-10-08 17:41:28 -06:00
Andy Miller
7b5a1b2c14 Prepare for release 2018-10-08 17:41:18 -06:00
Andy Miller
235a5cc765 vendor updates 2018-10-08 17:33:49 -06:00
Andy Miller
073d601b67 Updated changelog 2018-10-06 16:37:11 -06:00
Andy Miller
ad1bbba0b3 Added configurable dangerous upload extensions 2018-10-06 16:35:54 -06:00
Matias Griese
b6b5e329aa Added Utils::getMimeByFilename(), Utils::getMimeByLocalFile() and Utils::checkFilename() methods 2018-10-04 14:44:04 +03:00
Andy Miller
0e973dab07 Merge branch 'release/1.5.2' 2018-10-01 15:33:13 -06:00
Andy Miller
15e371564a Merge tag '1.5.2' into develop
Release v1.5.2
2018-10-01 15:33:13 -06:00
Andy Miller
f0e33dc242 prepare for release 2018-10-01 15:32:29 -06:00
Andy Miller
e67c3c1091 updated changelog 2018-10-01 15:31:39 -06:00
Andy Miller
d5ce0bd93c updated vendor libs 2018-10-01 15:26:44 -06:00
Andy Miller
44dbcdf2b1 Added new XSS Twig function 2018-10-01 14:07:14 -06:00
Andy Miller
3216442946 Merge branch 'develop' of github.com:getgrav/grav into develop 2018-10-01 12:34:14 -06:00
Andy Miller
9d4471b196 Security refactor 2018-10-01 12:34:09 -06:00
Matias Griese
c48107acd9 Merge remote-tracking branch 'origin/develop' into develop 2018-10-01 21:02:11 +03:00
Matias Griese
4671518409 Fixed missing slug in Page::init() 2018-10-01 21:02:04 +03:00
Andy Miller
41bf943f49 get raw content for all pages 2018-09-30 21:11:46 -06:00
Andy Miller
f40c6a8617 Changelog updated 2018-09-30 18:37:42 -06:00
Andy Miller
fb98ca7b19 Added a new Security CLI command 2018-09-30 18:34:53 -06:00
Andy Miller
451ec49d9c refactor 2018-09-30 17:45:45 -06:00
Andy Miller
1709eb038c Fix for array method 2018-09-30 15:24:01 -06:00
Andy Miller
e69d6cefee ordering 2018-09-30 00:10:44 -06:00
Andy Miller
7abe01ed8c vertical style 2018-09-30 00:10:04 -06:00
Andy Miller
17a371d86a lang stuff 2018-09-29 21:37:01 -06:00
Andy Miller
5b787d56e6 Add default XSS security config 2018-09-29 21:24:58 -06:00
Andy Miller
33d98114ba XSS enhancements 2018-09-29 21:24:21 -06:00
Andy Miller
51f29e112a updated composer.json 2018-09-19 13:56:09 -06:00
Matias Griese
ca8805683d Added onHttpPostFilter event to allow plugins to globally clean up XSS in the forms and tasks 2018-09-19 12:09:32 +03:00
Matias Griese
8295bd8243 Added Utils::detectXssFromArray() and Utils::detectXss() methods 2018-09-19 12:06:49 +03:00
Matias Griese
da95d1bb1e Session expires in 30 mins independent from config settings (https://github.com/getgrav/grav-plugin-login/issues/178) 2018-09-13 17:31:11 +03:00
Matias Griese
bbc4fd6c79 Allow twig tags {% script %}, {% style %} and {% switch %} to be placed outside of blocks 2018-09-07 13:13:33 +03:00
Matias Griese
732ff8ecab Fixed nicetime() twig function 2018-09-07 10:36:56 +03:00
Matias Griese
41b7aadbda Fixed duplicate language strings (Yaml 4.1) 2018-09-06 12:34:50 +03:00
Matias Griese
834d6938db Fixed is_safe twig filter option 2018-09-06 10:28:40 +03:00
Andy Miller
dfabceb3d2 Fix for Page::translatedLanguages() #2163 2018-09-05 19:08:42 -06:00
Matias Griese
1808fd3d6e Allow $page->slug() to be called before $page->init() without breaking the page 2018-08-29 15:46:46 +03:00
Matias Griese
0b5c1dcfa7 Deprecation handling fixes 2018-08-26 11:01:17 +03:00
Matias Griese
1369f941f2 Commented out deprecation error on twig for now 2018-08-25 22:08:59 +03:00
Matias Griese
2101c6d0dc Further improve deprecated notices handling 2018-08-24 19:12:59 +03:00
Matias Griese
1993fc6a2c Better detect deprecation notices 2018-08-24 18:26:50 +03:00
Matias Griese
b9b43d1f05 Fixed notice on new deprecation logic 2018-08-24 18:22:07 +03:00
Matias Griese
8d53cf3c77 Add backtraces to the deprecation messages 2018-08-24 14:51:05 +03:00
Matias Griese
756ddaa97d Added Deprecated tab to DebugBar to catch future incompatibilities with later Grav versions 2018-08-24 11:31:51 +03:00
Djamil Legato
89f64e423d Fixed error message 2018-08-23 15:28:16 -07:00
Djamil Legato
ec5596b1a3 Fixed check for install command with symlinks, erroring out when no symlink available 2018-08-23 15:25:57 -07:00
Andy Miller
2de89e31c0 Merge branch 'release/1.5.1' 2018-08-23 13:02:49 -06:00
Andy Miller
9ca5598b6f Merge tag '1.5.1' into develop
Release v1.5.1
2018-08-23 13:02:49 -06:00
Andy Miller
05863276ef prepare for release 2018-08-23 13:02:37 -06:00
Andy Miller
5ac518f311 cast inline/indent to int 2018-08-22 12:54:45 -06:00
Andy Miller
41f488f8da Switch to Grav YAML wrapper that supports native and fallback YAML libs 2018-08-22 12:42:45 -06:00
Matias Griese
6cc6e51878 Added static Grav\Common\Yaml class which should be used instead of Symfony\Component\Yaml\Yaml 2018-08-22 20:59:00 +03:00
Andy Miller
78bcf84127 Merge branch 'develop' of github.com:getgrav/grav into develop
# Conflicts:
#	CHANGELOG.md
2018-08-21 14:10:02 -06:00
Andy Miller
6b224823f1 typo 2018-08-21 14:09:33 -06:00
Andy Miller
2734b2f605 Broken handling of user folder in Grav URI object #2151 2018-08-21 14:09:25 -06:00
Matias Griese
1ee88d5836 Updated deprecated Twig code so it works in both in Twig 1.34+ and Twig 2.4+ 2018-08-20 10:51:58 +03:00
Andy Miller
33fffa6a50 Merge tag '1.5.0' into develop
Release v1.5.0
2018-08-17 11:24:56 -06:00
Andy Miller
dbd825f0b6 Merge branch 'release/1.5.0' 2018-08-17 11:24:55 -06:00
Andy Miller
8ab0078d5a Prepare for release 2018-08-17 11:24:43 -06:00
Andy Miller
c381bc8304 PHP 7.2 by default now 2018-08-16 14:58:19 -06:00
Andy Miller
fb20b58369 changelog update 2018-08-15 17:12:20 -06:00
Andy Miller
906017e0c1 Added system blueprint for strict_mode settings 2018-08-15 17:12:10 -06:00
Andy Miller
266369ee04 unified 1.5.0 changelog entry for clarity 2018-08-15 16:15:37 -06:00
Andy Miller
308ac14dbe Updated changelog 2018-08-15 16:11:47 -06:00
Andy Miller
2a9da76512 Merge branch 'develop' into 1.5 2018-08-15 16:08:42 -06:00
Andy Miller
8e43550841 Updated changelog 2018-08-15 15:52:31 -06:00
Djamil Legato
75ac0201d8 Added support for multiple repos lookup (as array) in .grav/config
This will allow to keep clones of repositories on different folders and still be able to symlink them.

Example of ~/.grav/config:

```
github_repos:
    - /Users/my_user/Projects/grav/
    - /Users/my_user/Projects/personal/
    - /Users/my_user/Projects/work/
```
2018-08-15 13:38:18 -07:00
Andy Miller
8d9efe4ff7 Extra semicolon 2018-08-14 19:47:16 -06:00
Andy Miller
593400743a Fix for plugin order 2018-08-14 19:46:52 -06:00
Matias Griese
42ff8eaeb0 Make ObjectTrait::serialize() overrides easier 2018-08-13 22:28:12 +03:00
Matias Griese
5c2f9946f8 Merge branch 'develop' of https://github.com/getgrav/grav into 1.5 2018-08-13 09:36:07 +03:00
Jascha Geerds
63161e62a2 Fix broken nounce handling (#2121)
* Remove deprecated "getNonceOldStyle" function

This commit removes the following functions:

- getNonceOldStyle
- generateNonceStringOldStyle

The functions have been replaced in newer versions of
grav. It seems to me that they only existed in order to make a
upgrade to a newer version of grav painless (i.e. accept both types of
nonce tokens). Nowadays, existing old style nonces are expired long
time ago so it should be save to delete the deprecated funtions.

* Fix caching of nonces in static class variable

Currently, the behavior of `getNonce` is broken because it saves the
generated nonce in an array and only use the $action as the
key. However, the generated nonce does not only depend on the $action,
but also on $plusOneTick.

* Fix broken "plusOneTick" for nonces

It looks to me that there is a bug in the current implemention of
verifyNonce. Here is an example:

- 2018-08-01 10:00: We respond to a request and generate a nonce. The
  current tick is at 35489

- 2018-08-01 10:05: We use the previously generated nonce to make
  another request. We compare the given nounce with a new generated
  one (based on the same tick). The result is exactly the same and the
  request succeeds.

- 2018-08-01 14:00: We're now one tick ahead. Remember: A day (24
  hours) is separated into two ticks (each 12 hours). A request comes
  in, we compare the given nounce with a newly generated one based on
  the current tick (now at 35490). They don't match (which is totally
  okay).

  If the comparison fails, we then compare the given nounce with a
  another, newly generated one. This time, we pass "plusOneTick", to
  the function, which increases the current tick by one. Our tick is
  now at 35491. We generate a nonce based on that tick and of course,
  it still does not match the given nonce.

  Instead of increasing the tick, we should rather decreasing it by
  one (i.e. use the previous tick). If the first comparison fails, we
  use the current tick (35490), decrease it by one (35489) and then
  compare it again. 35489 is the same tick as in the very first
  request.

This bug leads to a maximum life time of 12 hours for a nonce and in
worst case only a few seconds (!)

I would like to prove the bug with an unit test but I'm too unexperienced
in PHP. Furthermore it seems that we need some kind of library which
is able to mock builtin functions (like "time"). Maybe
<https://github.com/Codeception/AspectMock> would be a good canditate?
2018-08-09 16:05:24 -06:00
lucaswillering
c84983ad5b Add muted and playsinline attributes (#2124)
Fixes: #2099

To be able to add videos to sites that behave as GIFs, two attributes are needed for the videos to be properly handled on iOS and in Chrome: muted and playsinline.

Muted
Chrome only allows videos to autoplay when the contain the muted attribute. Non-muted videos will not autoplay unless the user has interacted with the site. More details here:  https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#new-behaviors

Playsinline
The playsinline attribute allows developers to specify videos on iPhone should play inline and not automatically enter fullscreen mode when playback begins. More details here: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#new-behaviors
2018-08-09 15:32:53 -06:00
atyner
3cee53508e Typo 'Subscxripted' on default Typography page (#2136) 2018-08-09 15:32:34 -06:00
Matias Griese
fde75e1ed5 Composer update (toolbox) 2018-08-08 21:09:25 +03:00
Matias Griese
16d2f607c8 Fixed Uri::parseUrl($url) with no path (part 2) 2018-08-07 23:12:38 +03:00
Matias Griese
816a3ebd93 Fixed Uri::parseUrl($url) with no path 2018-08-07 22:46:23 +03:00
Matias Griese
d59fe2fa3c Display better exception message if Grav fails to initialize 2018-08-07 22:32:29 +03:00
Matias Griese
ef55e7d219 Added option to disable SimpleCache key validation 2018-08-07 22:30:45 +03:00
Andy Miller
424da520cf Fix #2134 - inheritance of theme classes that include digits in camelcase 2018-08-06 15:37:59 -06:00
Andy Miller
08cb311e5e Fix truncator tests 2018-08-06 15:36:38 -06:00
Andy Miller
e1b5875c5b Update vendor libs 2018-08-06 15:22:55 -06:00
Andy Miller
7d27206fec Fixed #2133 - uppercase fallback media urls 2018-08-06 14:56:00 -06:00
Andy Miller
18d405d798 Improved Utils::url() to support query strings 2018-08-06 13:09:15 -06:00
Matias Griese
34fa50fcf0 Added FormatterInterface::getDefaultFileExtension() 2018-08-03 13:19:26 +03:00
Matias Griese
ca3cf2ea3c Added FormatterInterface::getSupportedFileExtensions() method, deprecated getFileExtension() 2018-08-03 13:03:51 +03:00
Matias Griese
76fb11366b Added Uri::method() to get current HTTP method (GET/POST etc) 2018-08-02 22:41:54 +03:00
Andy Miller
e4f2808870 prepare for release 2018-07-31 11:27:49 -06:00
Andy Miller
f7496b5341 Merge branch 'develop' into 1.5
# Conflicts:
#	CHANGELOG.md
#	composer.lock
#	system/defines.php
2018-07-31 11:27:08 -06:00
Andy Miller
2f0d600e86 Merge tag '1.4.8' into develop
Release v1.4.8
2018-07-31 11:23:36 -06:00
Andy Miller
fa7e6be95a Merge branch 'release/1.4.8' 2018-07-31 11:23:35 -06:00
Andy Miller
cea43a2d21 Prepare for release 2018-07-31 11:23:24 -06:00
Andy Miller
b7387c8741 vendor updates 2018-07-31 11:23:15 -06:00
Andy Miller
c83852f4e1 update changelog 2018-07-31 11:20:49 -06:00
Andy Miller
ce271cf389 Merge branch 'develop' of github.com:getgrav/grav into develop 2018-07-31 11:17:09 -06:00
Matias Griese
ead125d599 Merge branch 'develop' of https://github.com/getgrav/grav into 1.5 2018-07-30 12:10:24 +03:00
Andy Miller
8ee367e52e Don't allow null to be set as Page content 2018-07-27 15:05:45 -06:00
Jeremy Gonyea
db03091cff Added nginx config for hosting in a ddev project (#2117) 2018-07-27 14:23:35 -06:00
Matias Griese
6b5849b207 Added MediaTrait::getMediaCache() to allow custom caching 2018-07-20 22:54:22 +03:00
Matias Griese
ba0a8c4092 Added MediaTrait::clearMediaCache() to allow cache to be cleared 2018-07-20 22:25:22 +03:00
Matias Griese
c8ab5d34f7 Merge branch 'develop' into 1.5
# Conflicts:
#	CHANGELOG.md
#	system/defines.php
2018-07-19 10:05:26 +03:00
Hugh Barnes
c9367ba4f3 Add Grav version to debug bar Messages tab (#2106) 2018-07-18 19:03:25 -06:00
Matias Griese
a754f697d7 Added twig filters for casting values: |string, |int, |bool, |float, |array
Made `|markdown` filter HTML safe
2018-07-14 13:22:35 +03:00
Matias Griese
dd75ce515f Code style fix on Twig tags 2018-07-14 12:12:53 +03:00
Andy Miller
ea83b46bfb Prepare for release 2018-07-13 16:54:32 -06:00
Andy Miller
e7f628233d Update changelog 2018-07-13 16:47:49 -06:00
Andy Miller
24edf15e16 Merge branch 'release/1.4.7' 2018-07-13 16:38:56 -06:00
Andy Miller
70e65129d7 Merge tag '1.4.7' into develop
Release v1.4.7
2018-07-13 16:38:56 -06:00
Andy Miller
a5e97ef846 Prepare for release 2018-07-13 16:38:46 -06:00
Andy Miller
8a1f0d4932 update changelog 2018-07-13 16:38:04 -06:00
Andy Miller
f29997a5cf Minor vendor updates 2018-07-13 16:35:51 -06:00
Matias Griese
4daec6908c Merge branch 'develop' of https://github.com/getgrav/grav into 1.5 2018-07-06 09:17:41 +03:00
Timothy Cyrus
79bff58021 Change getBasename to getFilename where possible (#2087)
* Update Pages.php

* Update Themes.php

* Update Installer.php

* Update Plugins.php

* Update ConfigFileFinder.php
2018-07-05 14:14:57 -06:00
Matias Griese
05028d0d9b Changelog update 2018-07-05 13:14:17 +03:00
Matias Griese
b4148804e1 Criteria: Added support for LENGTH(), LOWER(), UPPER(), LTRIM(), RTRIM() and TRIM() 2018-07-05 13:12:59 +03:00
Matias Griese
07f8dfb1c5 Made ObjectCollection::matching() criteria expressions to behave more like in Twig 2018-07-05 12:27:28 +03:00
Matias Griese
f3c559f1c7 Composer update (Fixes blueprint issues) 2018-07-05 10:13:36 +03:00
Matias Griese
48a3228efd Merge remote-tracking branch 'origin/1.5' into 1.5
# Conflicts:
#	CHANGELOG.md
2018-07-05 10:07:14 +03:00
Matias Griese
be661e8685 Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	CHANGELOG.md
2018-07-05 10:06:27 +03:00
Timothy Cyrus
a0918dfc4f Update Media.php (#2083)
Fixes getgrav/grav-plugin-admin#1330
2018-07-02 23:46:44 +01:00
Andy Miller
d214080974 Merge branch 'develop' into 1.5
# Conflicts:
#	CHANGELOG.md
2018-07-02 16:05:56 -06:00
Andy Miller
a09c6b1088 Fix for Page::routeCanonical accpeting string #2069 2018-06-22 11:59:22 +01:00
Andy Miller
dfed333e1b Set html in admin not in here… 2018-06-20 23:38:41 +01:00
Andy Miller
578e12940b Fix for modular page preview #2066 2018-06-20 22:56:27 +01:00
Andy Miller
7d215f95cf Merge tag '1.4.6' into develop
Release v1.4.6
2018-06-20 19:38:40 +01:00
Andy Miller
5435ee60d8 Merge branch 'release/1.4.6' 2018-06-20 19:38:39 +01:00
Andy Miller
761d79272c Prepare for release 2018-06-20 19:37:15 +01:00
Matias Griese
68a9552877 Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	composer.json
#	composer.lock
2018-06-20 11:56:55 +03:00
Matias Griese
d72eca7fb5 Force Rockettheme/Toolbox to version 1.3.* in Grav 1.4 series 2018-06-20 11:11:46 +03:00
Matias Griese
ca9dba1372 Move wrongly placed items in changelog to the latest release 2018-06-20 10:57:57 +03:00
Matias Griese
7aa688ecbb Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	CHANGELOG.md
#	system/defines.php
2018-06-20 10:51:52 +03:00
Andy Miller
794db2e3e5 set version as beta.1 2018-06-19 19:15:22 +01:00
Andy Miller
ba457f7bf3 composer update 2018-06-19 19:11:22 +01:00
Andy Miller
64715573a1 Update changelog 2018-06-19 18:46:42 +01:00
Andy Miller
8288551531 Merge tag '1.4.6' into develop
Release v1.4.6

# Conflicts:
#	system/defines.php
2018-06-19 18:45:32 +01:00
Andy Miller
34cc3781d6 Merge branch 'release/1.4.6' 2018-06-19 18:42:55 +01:00
Andy Miller
4eb986643c Merge tag '1.4.6' into develop
Release v1.4.6
2018-06-19 18:42:55 +01:00
Andy Miller
290e5be534 Prepare for release 2018-06-19 18:42:40 +01:00
Andy Miller
aea26f4db9 Updated changelog 2018-06-19 18:38:48 +01:00
Matias Griese
bf5e742a7f Revert Medium object cloning for now (has unexpected side effects) [#2045] 2018-06-15 09:51:42 +03:00
Matias Griese
9816b538f9 Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	composer.lock
2018-06-15 09:22:46 +03:00
Matias Griese
bd21b7f966 Fixed image resource memory deallocation (part 2) [#2045] 2018-06-15 09:17:50 +03:00
Matias Griese
021fbb8ecd Merge remote-tracking branch 'origin/develop' into develop 2018-06-15 09:03:57 +03:00
Matias Griese
6d7e9ba107 Fixed image resource memory deallocation [#2045] 2018-06-15 09:03:51 +03:00
Matias Griese
bbfbdec483 Added setting to disable sessions from the site [#2013] 2018-06-14 19:23:35 +03:00
Andy Miller
62a32ab5c5 vendor updates 2018-06-14 16:00:31 +01:00
Matias Griese
2e3a64fcef Fixed incorrect routing with setup.php based base [#1892] 2018-06-14 14:55:08 +03:00
Matias Griese
ecdbff68d8 Fixed crash on IIS (Windows) with open_basedir in effect [#2053] 2018-06-14 14:31:29 +03:00
Matias Griese
9ca427e369 Composer update (rockettheme/toolbox => 1.4) 2018-06-14 12:46:27 +03:00
Matias Griese
228757a5ba Composer update (rockettheme/toolbox => dev-develop) 2018-06-12 13:06:58 +03:00
Matias Griese
027a760ce2 Added function Session::clear() 2018-06-12 12:06:14 +03:00
Matias Griese
0a3cadc6b2 Composer update 2018-06-12 10:55:38 +03:00
Matias Griese
04ea069280 Fixed routing issues with multisite and multilanguage (Grav 1.5 edition) (#2046)
* Fixed routing issues with multisite and multilanguage (#1501)
2018-06-12 10:48:13 +03:00
Matias Griese
280d54057c Merge branch 'develop' of https://github.com/getgrav/grav into 1.5 2018-06-12 10:40:42 +03:00
Jean-Baptiste Alleaume
036fc2d2af Modular template extension follows the master page extension (#2044) 2018-06-08 22:00:07 +00:00
Hydraner
ab58cca3f7 Add getter methods for original and action to the Page object. (#2005) 2018-06-08 21:37:44 +00:00
Jeremy Bouquain
f883820c6a Handle multibyte strings in truncateLetters (#2007) 2018-06-08 21:37:06 +00:00
Lauri Nurmi
9053f9ab44 Use @example.com in default email addresses. (#2020)
It could be a bad idea to use default email addresses that could really
exist. Instead, use example.com, which RFC2606 defines as one of the
domains reserved for examples.
2018-06-08 21:36:38 +00:00
Felipe Fonseca
bd7706a38e pointing fastcgi_pass path to match php7.0-fpm (on debian stretch) (#2021) 2018-06-08 21:36:11 +00:00
Tuukka Norri
da7a93527d Fix image memory use (#2045)
* Add a method for freeing the image file, free each derivative after it has been created

* Save the scaled derivative on disk before freeing it, store its path
2018-06-08 21:33:40 +00:00
Matias Griese
9f7534153e Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	CHANGELOG.md
2018-06-01 12:37:13 +03:00
Andy Miller
3b4296c7a4 fix for URI’s with no authority // eg mailto:blah@blah.com 2018-05-31 17:48:48 -06:00
Andy Miller
8e065e1109 Fix classes on non-http based protocol links #2034 2018-05-31 14:28:48 -06:00
Andy Miller
93f3fa9685 Updated changelog 2018-05-31 14:08:01 -06:00
Andy Miller
27a9390ec7 Re-added SSL off-loading that was lost with Grav v1.4.0 merge #1888 2018-05-31 14:05:33 -06:00
Matias Griese
62a8d8b203 Add method MediaInterface::getMediaOrder() and implement it 2018-05-31 20:04:16 +03:00
Matias Griese
d7bd0bf1df Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	CHANGELOG.md
2018-05-31 12:28:19 +03:00
Ante Drnasin
9eded2ef39 Update robots.txt (#2043)
When using "Fetch and Render as Google" in Google Search console it will report "partial" rendering due to the blocked images in /user/images directory which is blocked because of the Disallow /user/ rule. Proposing this small change as it improves google rendering of the page.
2018-05-30 11:47:21 -06:00
Andy Miller
636bc97d29 handle errors.display properly admin#1452 2018-05-29 09:26:52 -06:00
Matias Griese
3ccadded97 Fixed blueprint field validation: Allow numeric inputs in text fields 2018-05-22 10:42:30 +03:00
Andy Miller
b82c17ea56 Fixed typo in trucate #1943 2018-05-20 12:01:38 -06:00
Matias Griese
a0946c67b9 Implement SessionInterface 2018-05-17 10:46:13 +03:00
Matias Griese
00376d3118 Session code cleanup, add changelog entries 2018-05-17 10:22:14 +03:00
Matias Griese
e8fd5405a7 Merge branch 'feature/session' of https://github.com/getgrav/grav into 1.5 2018-05-17 10:14:10 +03:00
Matias Griese
eae017a30a Merge branch 'develop' of https://github.com/getgrav/grav into feature/session 2018-05-17 10:10:16 +03:00
Andy Miller
718dfa9b5d Merge branch 'develop' into 1.5
# Conflicts:
#	CHANGELOG.md
#	system/defines.php
#	system/src/Grav/Common/User/User.php
2018-05-15 17:55:50 -06:00
Andy Miller
03a0c42795 Merge branch 'release/1.4.5' 2018-05-15 12:55:39 -06:00
Andy Miller
1976471982 Merge tag '1.4.5' into develop
Release v1.4.5
2018-05-15 12:55:39 -06:00
Andy Miller
1426a7ec95 prepare for release 2018-05-15 12:55:27 -06:00
Andy Miller
2a759eed74 updated changelog 2018-05-15 11:46:54 -06:00
Andy Miller
8980b78220 Revert "Add MIME type to video <source> tag (#1992)"
This reverts commit e60fd82400.
2018-05-15 11:45:36 -06:00
Andy Miller
12b0a839e7 Merge branch 'develop' of github.com:getgrav/grav into develop 2018-05-15 11:43:42 -06:00
Andy Miller
b4d570fd21 updated changelog 2018-05-15 11:43:37 -06:00
Christian Weiske
e60fd82400 Add MIME type to video <source> tag (#1992) 2018-05-15 11:39:50 -06:00
Davide Liessi
a1abcfd067 Move fix for #1114 to Truncator::truncateLetters (#2004)
* Move fix for #1114 to Truncator::truncateLetters

The original fix provided by #1125 in Utils::truncateHtml compared the
summary size with the full HTML string length.
Thus, the string was still being truncated (and the HTML rewritten) even
when only the HTML string length, and not the text length, exceeded the
summary size.

* Add fix for #1114 also to Truncator::truncateWords
2018-05-15 11:38:48 -06:00
Christian Weiske
7f90ad8474 Do not crash when generating URLs with arrays as parameters (#2018)
When using a default image derivatives (#1979) setting like

  images:
    defaults:
      derivatives: [300,600]

then Grav crashes when a video gets embedded.
The following error occurs:

  rawurlencode() expects parameter 1 to be string, array given
  system/src/Grav/Common/Page/Medium/Medium.php:521

This patch encodes array URL parameters correctly.
2018-05-15 11:37:18 -06:00
Andy Miller
e1d52181a3 updated changelog 2018-05-15 11:36:20 -06:00
Matias Griese
d4494cb502 Fixed users getting logged out after Grav upgrade 2018-05-15 16:26:11 +03:00
Andy Miller
11266ce8f8 Revert "Add special handling for User authenticated and authorized properties"
This reverts commit 8e0e3e8718.
2018-05-14 18:25:36 -06:00
Andy Miller
2f17b3fa7d Revert "Add special handling for User authenticated and authorized properties"
This reverts commit 8e0e3e8718.
2018-05-14 18:25:19 -06:00
Matias Griese
dab595f571 Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	system/defines.php
2018-05-14 21:25:35 +03:00
Matias Griese
afe72d0783 Merge remote-tracking branch 'origin/develop' into develop 2018-05-14 21:24:03 +03:00
Matias Griese
8e0e3e8718 Add special handling for User authenticated and authorized properties 2018-05-14 21:23:56 +03:00
Matias Griese
5ab956a8ec Fixed Route::withQueryParam() to accept array values 2018-05-14 12:30:30 +03:00
Andy Miller
a17554c951 Merge branch 'release/1.4.4' 2018-05-11 16:07:38 -06:00
Andy Miller
30ff986603 Merge tag '1.4.4' into develop
Releaase v1.4.4
2018-05-11 16:07:38 -06:00
Andy Miller
18f46d902d Prepare for release 2018-05-11 16:07:27 -06:00
Andy Miller
3948ed5618 updated changelog 2018-05-11 16:06:54 -06:00
Matias Griese
e6dd91f698 Added authorized support (2FA)
(cherry picked from commit 2c82e15)
2018-05-10 19:58:39 +03:00
Matias Griese
2c82e15fa1 Added authorized support (2FA) 2018-05-10 19:56:56 +03:00
Andy Miller
2c7d866724 alignment of parens 2018-05-09 22:43:26 -06:00
Andy Miller
a977023e45 minor vendor updates 2018-05-09 14:43:28 -06:00
Djamil Legato
9c7008e225 Enable markdown support for 2FA account field description 2018-05-09 10:41:23 -07:00
Matias Griese
342aa0ff04 Added MediaTrait::getMediaUri() 2018-05-09 14:01:20 +03:00
Matias Griese
d434d51d42 Merge remote-tracking branch 'origin/1.5' into 1.5 2018-05-08 21:00:24 +03:00
Matias Griese
f03eb693e6 Rename object key back to _key 2018-05-08 21:00:16 +03:00
Andy Miller
18928d6962 Revert "fire onPluginsInitialized event"
This reverts commit f7832e78dc.
2018-05-08 09:48:56 -06:00
Andy Miller
f7832e78dc fire onPluginsInitialized event 2018-05-07 21:32:00 -06:00
Matias Griese
68428a714a Change exception type in ContentBlock::fromArray() 2018-05-07 15:24:43 +03:00
Matias Griese
58db31a7d8 Added support for ContentBlock checksums 2018-05-07 14:58:50 +03:00
Matias Griese
2917345b56 Improve ObjectTrait 2018-05-04 21:43:12 +03:00
Matias Griese
1cfd3482bb Added Grav\Common\Media interfaces and trait; use those in Page and Media classes
Added `Grav\Common\Page` interface to allow custom page types in the future
2018-05-04 19:06:37 +03:00
Matias Griese
4d690938a8 Merge branch 'develop' of https://github.com/getgrav/grav into 1.5 2018-04-30 12:37:01 +03:00
Dmitry Yakovlev
40b475ecb9 Fix hardcoded string (#1993)
Fix hard-coded string "Modular Setup" to improve internationalization. Needs new string PLUGIN_ADMIN.MODULAR_SETUP in Admin plugin language file.
2018-04-28 11:29:28 -06:00
Gabriel Caruso
4ab0a601ad Use array_key_exists instead of in_array + array_keys (#1991) 2018-04-28 11:25:41 -06:00
Gabriel Caruso
40ba5e9106 Use dedicated PHPUnit assertions (#1990)
Signed-off-by: Gabriel Caruso <carusogabriel34@gmail.com>
2018-04-28 11:24:43 -06:00
Christian Weiske
d4ec2a25d4 Add default configuration for images (#1979)
This patch allows configuring image processing instructions
for all images.
A use case is generating a fixed set of sizes (derivatives) for
all images, without having to specify them in the content markdown.

Example configuration in user/config/system.yaml:

images:
  defaults:
    derivatives: "[300,600,1200]"
2018-04-28 11:24:14 -06:00
Andy Miller
0b1c18d63e Added Uri::post() and Uri::getConentType() 2018-04-27 16:44:38 -06:00
Andy Miller
f681f1c60b resolve streams earlier 2018-04-27 16:18:16 -06:00
Andy Miller
27df27d1df resolve streams earlier 2018-04-27 16:17:52 -06:00
Andy Miller
027dbcf6fa Added Uri::post() and Uri::getConentType() 2018-04-27 15:40:03 -06:00
Matias Griese
91e98cd32e Added RouteFactory::createFromString() rename MarkdownFormatter configuration option 2018-04-27 20:41:05 +03:00
Matias Griese
1cef2a182a Added Grav\Common\Media interfaces and trait; use those in Page and Media classes
Added `Grav\Common\Page` interface to allow custom page types in the future
2018-04-27 20:38:57 +03:00
Matias Griese
ad8764897a New classes have wrong namespace 2018-04-24 15:47:31 +03:00
Matias Griese
d2e700eea2 Formatters: Better error handling, allow custom file extension 2018-04-24 12:22:06 +03:00
Matias Griese
895e145d82 Added new Grav\Framework\File\Formatter classes for encoding/decoding YAML, MarkDown, JSON, INI and PHP serialized formats 2018-04-24 11:41:55 +03:00
Matias Griese
78ab2aa476 Minor improvement on session check (using PHP 5.4+ way) 2018-04-23 09:40:31 +03:00
Matias Griese
b9a7341e5e Composer update to fix YAML issues 2018-04-22 19:09:05 +03:00
Matias Griese
261ea62472 Added compatibility mode to fall back to Symfony YAML 2.8 when needed 2018-04-20 14:03:49 +03:00
Matias Griese
fae2aa4582 Added compatibility mode to fall back to Symfony YAML 2.8 when needed 2018-04-20 11:18:01 +03:00
Matias Griese
fb7230ec9a Merge branch 'develop' of https://github.com/getgrav/grav into 1.5
# Conflicts:
#	CHANGELOG.md
2018-04-20 10:24:06 +03:00
Andy Miller
692aff3c89 SORT_REGULAR for dates #1910 2018-04-18 18:37:34 -06:00
Andy Miller
3091a14223 Improve support for regex redirects with query and params #1983 2018-04-18 15:02:53 -06:00
Andy Miller
a49f3d460e allow type to be passed 2018-04-18 10:43:27 -06:00
Andy Miller
b6e0f8b75a Added a Medium::thumbnailExists() method 2018-04-17 23:29:23 -06:00
Andy Miller
ac8a1191f8 Fix for custom_base_url not routing properly 2018-04-17 19:04:16 -06:00
Matias Griese
830c9524eb Merge branch 'develop' of https://github.com/getgrav/grav into feature/session 2018-04-16 09:33:07 +03:00
Matias Griese
ce1e635451 Minimum PHP requirement was 5.6.4, not 5.6.0 2018-04-14 17:42:13 +03:00
Matias Griese
8b0c1b7937 Updated Symfony Components to 3.4, causing some compatibility issues with YAML files 2018-04-14 13:23:54 +03:00
Matias Griese
33cfa17899 Fix couple of deprecated extend@s 2018-04-14 13:00:40 +03:00
Matias Griese
87b0d80de0 PHP 5.6.0 minimum, updated to Doctrine Collections 1.4 2018-04-14 12:28:00 +03:00
Matias Griese
b8c61e34c9 Improve session error if it fails to start 2018-04-14 11:50:22 +03:00
Matias Griese
02555ba3f5 Merge branch 'develop' of https://github.com/getgrav/grav into feature/session 2018-04-13 21:04:40 +03:00
Andy Miller
4db2b75699 Merge tag '1.4.3' into develop
Release v1.4.3
2018-04-12 15:54:26 -06:00
Andy Miller
c64590af16 Merge branch 'release/1.4.3' 2018-04-12 15:54:25 -06:00
Andy Miller
3607124e55 Prepare for release 2018-04-12 15:54:16 -06:00
Andy Miller
e7405a13fc Revert "Test fixes for session/parsedown changes, jquery update, quark default theme"
This reverts commit cad10b6095.

# Conflicts:
#	CHANGELOG.md
2018-04-12 15:28:53 -06:00
Andy Miller
46f16ce4db Note about updated vendor libs 2018-04-12 15:26:26 -06:00
Andy Miller
d3a4466d9b Updated changelog 2018-04-12 15:25:29 -06:00
Andy Miller
46d682f889 Revert "Fix for audio/video parsedown #1924"
This reverts commit 33cfa5e104.

# Conflicts:
#	CHANGELOG.md
2018-04-12 15:25:19 -06:00
Matias Griese
2b17bf70de Merge branch 'develop' of https://github.com/getgrav/grav into feature/session 2018-04-10 09:47:59 +03:00
Andy Miller
8e019b7958 Updated changelog 2018-04-09 14:38:46 -06:00
Andy Miller
47037e3f5e moved sortArrayByKey logic into Utils:: 2018-04-09 14:35:44 -06:00
Andy Miller
1ec653268d Parsedown 1.8 breaks things, need to fix when final is released 2018-04-09 12:00:07 -06:00
Andy Miller
3ee140e77f Revert "Temporarily using parsedown dev-master to address HTML encoding issues #1921"
This reverts commit ff2df04a58.
2018-04-09 11:58:52 -06:00
Rex Kelly
cb490a1762 Add overriding of setup.php config file, via constants (#1945)
I'm working on a custom install of Grav, where being able to dynamically swap setup files was required. I've modified the $file assignment to permit overriding it's value via a defined constant, "GRAV_SETUP_PATH".

And I thought that might be useful to others, so I'm submitting a pull request.
2018-04-09 11:35:49 -06:00
Raul E Watson
6d8ba5ed4d Update CHANGELOG.md (#1958)
Fixed small typos in changelog
2018-04-09 11:34:56 -06:00
michielStaes
7020130511 Readme.md was missing info regarding the 'composer test-windows' command (#1970) 2018-04-09 11:34:36 -06:00
Andy Miller
ff2df04a58 Temporarily using parsedown dev-master to address HTML encoding issues #1921 2018-04-02 18:04:40 -06:00
Djamil Legato
3248b97997 Reverted zip changes while adding extra details about unzip failure 2018-03-29 13:07:11 -07:00
Djamil Legato
3c26d831fd Refactored Installer (zip) change to maintain consistency in the errorCode 2018-03-29 12:15:34 -07:00
Andy Miller
f4e584cda1 Fix for bad ZipArchive references 2018-03-24 13:49:56 -06:00
Andy Miller
df7e9b9c1f Merge tag '1.4.2' into develop
Release v1.4.2
2018-03-21 11:49:38 -06:00
Andy Miller
1108d063ef Merge branch 'release/1.4.2' 2018-03-21 11:49:37 -06:00
Andy Miller
a6306aae05 Prepare for release 2018-03-21 11:49:28 -06:00
Andy Miller
fdf79caf71 updated changelog 2018-03-21 11:19:47 -06:00
Matias Griese
f31f7f0962 Added Grav\Framework\Session class to replace RocketTheme\Toolbox\Session\Session
Improved session handling, allow all session configuration options in `system.session.options`
2018-03-21 12:45:57 +02:00
Matias Griese
df185621ad Composer update (Add missing PSR-7 requirements) 2018-03-21 11:29:26 +02:00
Andy Miller
301429d992 Merge branch 'develop' of github.com:getgrav/grav into develop 2018-03-20 10:33:32 -06:00
Andy Miller
07db8a2f9d Updated composer binary 2018-03-20 10:33:26 -06:00
Matias Griese
3a207843c7 Merge branch 'develop' of https://github.com/getgrav/grav into feature/1.4 2018-03-20 09:22:19 +02:00
Andy Miller
8f1639c10b case for case sensitive metadata grav-admin-plugin#1370 2018-03-18 21:37:17 -06:00
Andy Miller
b515a5add0 updated changelog 2018-03-18 12:57:40 -06:00
Andy Miller
33cfa5e104 Fix for audio/video parsedown #1924 2018-03-18 12:55:20 -06:00
Mujib Azizi
83b85e2cac Re-add .gitkeep to keep the backup folder. (#1913) 2018-03-18 11:59:19 -06:00
Mathieu Santostefano
861eb43efa Remove duplicate entry (#1916) 2018-03-18 11:58:40 -06:00
Al-Demon
a20d6d7230 Update LanguageCodes.php (#1917)
Add Bosnian LanguageCode
2018-03-18 11:57:16 -06:00
Andy Miller
40e7ee79b4 Added more detailed error handling (#1922) 2018-03-18 11:56:59 -06:00
Paul Massendari
dce97221c5 Small typo in default page (#1923) 2018-03-16 12:37:08 -07:00
Matias Griese
fea02736c5 Changelog: fixed typo 2018-03-15 10:45:37 +02:00
Andy Miller
d58ad3749d Fixed a test 2018-03-13 16:17:46 -06:00
Andy Miller
727f759b41 Added some twig stuff 2018-03-13 16:10:12 -06:00
Andy Miller
f42d59409b updaetd clean command with some minor changes 2018-03-11 16:28:50 -06:00
Andy Miller
4adaead4ec Merge branch 'release/1.4.1' 2018-03-11 16:13:03 -06:00
Andy Miller
8dbe248df1 Merge tag '1.4.1' into develop
Release v1.4.1
2018-03-11 16:13:03 -06:00
Andy Miller
0e26422613 Prepare for release 2018-03-11 16:12:55 -06:00
Matias Griese
234555b208 Fixed session timing out because of session cookie was not being sent 2018-03-11 21:52:49 +02:00
Andy Miller
3cfc3f1cbe Merge tag '1.4.0' into develop
Release v1.4.0
2018-03-09 13:03:06 -07:00
mahagr
d5060a2012 Framework\Route::getRoute() return relative path only if offset isn't 0 2018-02-25 17:38:55 -06:00
mahagr
bf16e2e854 Framework\Route::getRoute() with parameters should return relative path 2018-02-25 17:37:23 -06:00
mahagr
ec78319993 Add Framework\Route::getRoute() parameters 2018-02-25 17:35:01 -06:00
mahagr
67b5649ee4 Add Framework\Route::getRouteParts() function 2018-02-25 17:29:29 -06:00
115 changed files with 4307 additions and 1199 deletions

2
.gitignore vendored
View File

@@ -37,9 +37,9 @@ Thumbs.db
# phpstorm
.idea/*
user/config/security.yaml
tests/_output/*
tests/_support/_generated/*
tests/cache/*
tests/error.log
/system/templates/testing

View File

@@ -1,6 +1,5 @@
language: php
php:
- '5.5'
- '5.6'
- '7.0.21'
- '7.1'

View File

@@ -1,3 +1,195 @@
# v1.5.5
## 11/12/2018
1. [](#new)
* Register theme prefixes as namespaces in Twig [#2210](https://github.com/getgrav/grav/pull/2210)
1. [](#improved)
* Propogate error code between 400 and 600 for production sites [#2181](https://github.com/getgrav/grav/pull/2181)
1. [](#bugfix)
* Remove hardcoded `302` when redirecting trailing slash [#2155](https://github.com/getgrav/grav/pull/2155)
# v1.5.4
## 11/05/2018
1. [](#improved)
* Updated default page `index.md` with some consistency fixes [#2245](https://github.com/getgrav/grav/pull/2245)
1. [](#bugfix)
* Fixed fatal error if calling `$session->invalidate()` when there's no active session
* Fixed typo in media.yaml for `webm` extension [#2220](https://github.com/getgrav/grav/pull/2220)
* Fixed markdown processing for telephone links [#2235](https://github.com/getgrav/grav/pull/2235)
# v1.5.3
## 10/08/2018
1. [](#new)
* Added `Utils::getMimeByFilename()`, `Utils::getMimeByLocalFile()` and `Utils::checkFilename()` methods
* Added configurable dangerous upload extensions in `security.yaml`
1. [](#improved)
* Updated vendor libraries to latest
# v1.5.2
## 10/01/2018
1. [](#new)
* Added new `Security` class for Grav security functionality including XSS checks
* Added new `bin/grav security` command to scan for security issues
* Added new `xss()` Twig function to allow for XSS checks on strings and arrays
* Added `onHttpPostFilter` event to allow plugins to globally clean up XSS in the forms and tasks
* Added `Deprecated` tab to DebugBar to catch future incompatibilities with later Grav versions
* Added deprecation notices for features which will be removed in Grav 2.0
1. [](#improved)
* Updated vendor libraries to latest
1. [](#bugfix)
* Allow `$page->slug()` to be called before `$page->init()` without breaking the page
* Fix for `Page::translatedLanguages()` to use routes always [#2163](https://github.com/getgrav/grav/issues/2163)
* Fixed `nicetime()` twig function
* Allow twig tags `{% script %}`, `{% style %}` and `{% switch %}` to be placed outside of blocks
* Session expires in 30 mins independent from config settings [login#178](https://github.com/getgrav/grav-plugin-login/issues/178)
# v1.5.1
## 08/23/2018
1. [](#new)
* Added static `Grav\Common\Yaml` class which should be used instead of `Symfony\Component\Yaml\Yaml`
1. [](#improved)
* Updated deprecated Twig code so it works in both in Twig 1.34+ and Twig 2.4+
* Switched to new Grav Yaml class to support Native + Fallback YAML libraries
1. [](#bugfix)
* Broken handling of user folder in Grav URI object [#2151](https://github.com/getgrav/grav/issues/2151)
# v1.5.0
## 08/17/2018
1. [](#new)
* Set minimum requirements to [PHP 5.6.4](https://getgrav.org/blog/raising-php-requirements-2018)
* Updated Doctrine Collections to 1.4
* Updated Symfony Components to 3.4 (with compatibility mode to fall back to Symfony YAML 2.8)
* Added `Uri::method()` to get current HTTP method (GET/POST etc)
* `FormatterInterface`: Added `getSupportedFileExtensions()` and `getDefaultFileExtension()` methods
* Added option to disable `SimpleCache` key validation
* Added support for multiple repo locations for `bin/grav install` command
* Added twig filters for casting values: `|string`, `|int`, `|bool`, `|float`, `|array`
* Made `ObjectCollection::matching()` criteria expressions to behave more like in Twig
* Criteria: Added support for `LENGTH()`, `LOWER()`, `UPPER()`, `LTRIM()`, `RTRIM()` and `TRIM()`
* Added `Grav\Framework\File\Formatter` classes for encoding/decoding YAML, Markdown, JSON, INI and PHP serialized strings
* Added `Grav\Framework\Session` class to replace `RocketTheme\Toolbox\Session\Session`
* Added `Grav\Common\Media` interfaces and trait; use those in `Page` and `Media` classes
* Added `Grav\Common\Page` interface to allow custom page types in the future
* Added setting to disable sessions from the site [#2013](https://github.com/getgrav/grav/issues/2013)
* Added new `strict_mode` settings in `system.yaml` for compatibility
1. [](#improved)
* Improved `Utils::url()` to support query strings
* Display better exception message if Grav fails to initialize
* Added `muted` and `playsinline` support to videos [#2124](https://github.com/getgrav/grav/pull/2124)
* Added `MediaTrait::clearMediaCache()` to allow cache to be cleared
* Added `MediaTrait::getMediaCache()` to allow custom caching
* Improved session handling, allow all session configuration options in `system.session.options`
1. [](#bugfix)
* Fix broken form nonce logic [#2121](https://github.com/getgrav/grav/pull/2121)
* Fixed issue with uppercase extensions and fallback media URLs [#2133](https://github.com/getgrav/grav/issues/2133)
* Fixed theme inheritance issue with `camel-case` that includes numbers [#2134](https://github.com/getgrav/grav/issues/2134)
* Typo in demo typography page [#2136](https://github.com/getgrav/grav/pull/2136)
* Fix for incorrect plugin order in debugger panel
* Made `|markdown` filter HTML safe
* Fixed bug in `ContentBlock` serialization
* Fixed `Route::withQueryParam()` to accept array values
* Fixed typo in truncate function [#1943](https://github.com/getgrav/grav/issues/1943)
* Fixed blueprint field validation: Allow numeric inputs in text fields
# v1.4.8
## 07/31/2018
1. [](#improved)
* Add Grav version to debug bar messages tab [#2106](https://github.com/getgrav/grav/pull/2106)
* Add Nginx config for ddev project to `webserver-configs` [#2117](https://github.com/getgrav/grav/pull/2117)
* Vendor library updates
1. [](#bugfix)
* Don't allow `null` to be set as Page content
# v1.4.7
## 07/13/2018
1. [](#improved)
* Use `getFilename` instead of `getBasename` [#2087](https://github.com/getgrav/grav/issues/2087)
1. [](#bugfix)
* Fix for modular page preview [#2066](https://github.com/getgrav/grav/issues/2066)
* `Page::routeCanonical()` should be string not array [#2069](https://github.com/getgrav/grav/issues/2069)
# v1.4.6
## 06/20/2018
1. [](#improved)
* Manually re-added the improved SSL off-loading that was lost with Grav v1.4.0 merge [#1888](https://github.com/getgrav/grav/pull/1888)
* Handle multibyte strings in `truncateLetters()` [#2007](https://github.com/getgrav/grav/pull/2007)
* Updated robots.txt to include `/user/images/` folder [#2043](https://github.com/getgrav/grav/pull/2043)
* Add getter methods for original and action to the Page object [#2005](https://github.com/getgrav/grav/pull/2005)
* Modular template extension follows the master page extension [#2044](https://github.com/getgrav/grav/pull/2044)
* Vendor library updates
1. [](#bugfix)
* Handle `errors.display` system property better in admin plugin [admin#1452](https://github.com/getgrav/grav-plugin-admin/issues/1452)
* Fix classes on non-http based protocol links [#2034](https://github.com/getgrav/grav/issues/2034)
* Fixed crash on IIS (Windows) with open_basedir in effect [#2053](https://github.com/getgrav/grav/issues/2053)
* Fixed incorrect routing with setup.php based base [#1892](https://github.com/getgrav/grav/issues/1892)
* Fixed image resource memory deallocation [#2045](https://github.com/getgrav/grav/pull/2045)
* Fixed issue with Errors `display:` option not handling integers properly [admin#1452](https://github.com/getgrav/grav-plugin-admin/issues/1452)
# v1.4.5
## 05/15/2018
1. [](#bugfix)
* Fixed an issue with some users getting **2FA** prompt after upgrade [admin#1442](https://github.com/getgrav/grav-plugin-admin/issues/1442)
* Do not crash when generating URLs with arrays as parameters [#2018](https://github.com/getgrav/grav/pull/2018)
* Utils::truncateHTML removes whitespace when generating summaries [#2004](https://github.com/getgrav/grav/pull/2004)
# v1.4.4
## 05/11/2018
1. [](#new)
* Added support for `Uri::post()` and `Uri::getConentType()`
* Added a new `Medium:thumbnailExists()` function [#1966](https://github.com/getgrav/grav/issues/1966)
* Added `authorized` support for 2FA
1. [](#improved)
* Added default configuration for images [#1979](https://github.com/getgrav/grav/pull/1979)
* Added dedicated PHPUnit assertions [#1990](https://github.com/getgrav/grav/pull/1990)
1. [](#bugfix)
* Use `array_key_exists` instead of `in_array + array_keys` [#1991](https://github.com/getgrav/grav/pull/1991)
* Fixed an issue with `custom_base_url` always causing 404 errors
* Improve support for regex redirects with query and params [#1983](https://github.com/getgrav/grav/issues/1983)
* Changed collection-based date sorting to `SORT_REGULAR` for better server compatibility [#1910](https://github.com/getgrav/grav/issues/1910)
* Fix hardcoded string in modular blueprint [#1933](https://github.com/getgrav/grav/pull/1993)
# v1.4.3
## 04/12/2018
1. [](#new)
* moved Twig `sortArrayByKey` logic into `Utils::` class
1. [](#improved)
* Rolled back Parsedown library to stable `1.6.4` until a better solution for `1.8.0` compatibility can fe found
* Updated vendor libraries to latest versions
1. [](#bugfix)
* Fix for bad reference to `ZipArchive` in `GPM::Installer`
# v1.4.2
## 03/21/2018
1. [](#new)
* Added new `|nicefilesize` Twig filter for pretty file (auto converts to bytes, kB, MB, GB, etc)
* Added new `regex_filter()` Twig function to values in arrays
1. [](#improved)
* Added bosnian to lang codes [#1917](https://github.com/getgrav/grav/issues/1917)
* Improved Zip extraction error codes [#1922](https://github.com/getgrav/grav/issues/1922)
1. [](#bugfix)
* Fixed an issue with Markdown Video and Audio that broke after Parsedown 1.7.0 Security updates [#1924](https://github.com/getgrav/grav/issues/1924)
* Fix for case-sensitive page metadata [admin#1370](https://github.com/getgrav/grav-plugin-admin/issues/1370)
* Fixed missing composer requirements for the new `Grav\Framework\Uri` classes
* Added missing PSR-7 vendor library required for URI additions in Grav 1.4.0
# v1.4.1
## 03/11/2018
1. [](#bugfix)
* Fixed session timing out because of session cookie was not being sent
# v1.4.0
## 03/09/2018
@@ -34,21 +226,20 @@
* Made `modular` blueprint more flexible
* Code optimizations to `Utils` class [#1830](https://github.com/getgrav/grav/pull/1830)
* Objects: Add protected function `getElement()` to get serialized value for a single property
* `ObjectPropertyTrait`: Added protected functions `isPropertyLoaded()`, `offsetLoad()`, `offsetPrepare()` and `offsetSerialize()`
* `ObjectPropertyTrait`: Added protected functions `isPropertyLoaded()`, `offsetLoad()`, `offsetPrepare()` and `offsetSerialize()`
* `Grav\Framework\Cache`: Allow unlimited TTL
* Optimizations & refactoring to the test suite [#1779](https://github.com/getgrav/grav/pull/1779)
* Slight modification of Whoops error colors
* Added new configuration option `system.session.initialize` to delay session initialization if needed by a plugin
* Vendor library updated to latest
* Updated vendor libraries to latest versions
* Removed constructor from `ObjectInterface`
* Make it possible to include debug bar also into non-HTML responses
* Updated built-in JQuery to latest 3.3.1
1. [](#bugfix)
1. [](#bugfix)
* Fixed issue with image alt tag always getting empted out unless set in markdown
* Fixed issue with remote PHP version determination for Grav updates [#1883](https://github.com/getgrav/grav/issues/1883)
* Fixed issue with _illegal scheme offset_ in `Uri::convertUrl()` [page-inject#8](https://github.com/getgrav/grav-plugin-page-inject/issues/8)
* Properly validate YAML blueprint fields so admin can save as proper YAML now [addresses many issues]
* Properly validate YAML blueprint fields so admin can save as proper YAML now [addresses many issues]
* Fixed OpenGraph metatags so only Twitter uses `name=`, and all others use `property=` [#1849](https://github.com/getgrav/grav/issues/1849)
* Fixed an issue with `evaluate()` and `evaluate_twig()` Twig functions that throws invalid template error
* Fixed issue with `|sort_by_key` twig filter if the input was null or not an array

View File

@@ -18,7 +18,7 @@ The underlying architecture of Grav is designed to use well-established and _bes
# Requirements
- PHP 5.5.9 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
- PHP 5.6.4 or higher. Check the [required modules list](https://learn.getgrav.org/basics/requirements#php-requirements)
- Check the [Apache](https://learn.getgrav.org/basics/requirements#apache-requirements) or [IIS](https://learn.getgrav.org/basics/requirements#iis-requirements) requirements
# QuickStart
@@ -183,7 +183,7 @@ See [LICENSE](LICENSE.txt)
# Running Tests
First install the dev dependencies by running `composer update` from the Grav root.
Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.
First install the dev dependencies by running `composer update` from the Grav root.
Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.
Windows users should use the `composer test-windows` command.
You can also run a single unit test file, e.g. `composer test tests/unit/Grav/Common/AssetsTest.php`

0
backup/.gitkeep Normal file
View File

Binary file not shown.

View File

@@ -41,5 +41,6 @@ $app->addCommands(array(
new \Grav\Console\Cli\ClearCacheCommand(),
new \Grav\Console\Cli\BackupCommand(),
new \Grav\Console\Cli\NewProjectCommand(),
new \Grav\Console\Cli\SecurityCommand(),
));
$app->run();

View File

@@ -6,31 +6,33 @@
"homepage": "http://getgrav.org",
"license": "MIT",
"require": {
"roave/security-advisories": "dev-master",
"php": ">=5.5.9",
"php": ">=5.6.4",
"twig/twig": "~1.24",
"erusev/parsedown": "~1.6",
"erusev/parsedown": "1.6.4",
"erusev/parsedown-extra": "~0.7",
"symfony/yaml": "~2.8",
"symfony/console": "~2.8",
"symfony/event-dispatcher": "~2.8",
"symfony/var-dumper": "~2.8",
"symfony/yaml": "~3.4",
"symfony/console": "~3.4",
"symfony/event-dispatcher": "~3.4",
"symfony/var-dumper": "~3.4",
"symfony/polyfill-iconv": "~1.0",
"doctrine/cache": "^1.6",
"doctrine/collections": "1.3",
"doctrine/collections": "^1.4",
"psr/simple-cache": "^1.0",
"psr/http-message": "^1.0",
"guzzlehttp/psr7": "^1.4",
"filp/whoops": "~2.0",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "~1.0",
"gregwar/image": "2.*",
"donatj/phpuseragentparser": "~0.3",
"pimple/pimple": "~3.0",
"rockettheme/toolbox": "~1.3",
"pimple/pimple": "~3.2",
"rockettheme/toolbox": "~1.4",
"maximebf/debugbar": "~1.10",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-curl": "*",
"ext-zip": "*",
"ext-json": "*",
"league/climate": "^3.2",
"antoligy/dom-string-iterators": "^1.0",
"miljar/php-exif": "^0.6.3",
@@ -44,7 +46,7 @@
},
"config": {
"platform": {
"php": "5.5.9"
"php": "5.6.4"
}
},
"repositories": [

941
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<?php
/**
* @package Grav.Core
*
@@ -7,7 +8,8 @@
*/
namespace Grav;
define('GRAV_PHP_MIN', '5.5.9');
define('GRAV_PHP_MIN', '5.6.4');
// Ensure vendor libraries exist
$autoload = __DIR__ . '/vendor/autoload.php';
@@ -15,7 +17,7 @@ if (!is_file($autoload)) {
die("Please run: <i>bin/grav install</i>");
}
if (PHP_SAPI == 'cli-server') {
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>");
}
@@ -29,7 +31,7 @@ if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
}
// Register the auto-loader.
$loader = require_once $autoload;
$loader = require $autoload;
// Set timezone to default, falls back to system if php.ini not set
date_default_timezone_set(@date_default_timezone_get());

View File

@@ -9,3 +9,4 @@ Disallow: /vendor/
Disallow: /user/
Allow: /user/pages/
Allow: /user/themes/
Allow: /user/images/

View File

@@ -0,0 +1,99 @@
title: PLUGIN_ADMIN.SECURITY
form:
validation: loose
fields:
xss_section:
type: section
title: PLUGIN_ADMIN.XSS_SECURITY
underline: true
xss_whitelist:
type: selectize
size: large
label: PLUGIN_ADMIN.XSS_WHITELIST_PERMISSIONS
help: PLUGIN_ADMIN.XSS_WHITELIST_PERMISSIONS_HELP
placeholder: 'admin.super'
classes: fancy
validate:
type: commalist
xss_enabled.on_events:
type: toggle
label: PLUGIN_ADMIN.XSS_ON_EVENTS
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool
xss_enabled.invalid_protocols:
type: toggle
label: PLUGIN_ADMIN.XSS_INVALID_PROTOCOLS
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool
xss_enabled.moz_binding:
type: toggle
label: PLUGIN_ADMIN.XSS_MOZ_BINDINGS
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool
xss_enabled.html_inline_styles:
type: toggle
label: PLUGIN_ADMIN.XSS_HTML_INLINE_STYLES
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool
xss_enabled.dangerous_tags:
type: toggle
label: PLUGIN_ADMIN.XSS_DANGEROUS_TAGS
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool
xss_dangerous_tags:
type: selectize
size: large
label: PLUGIN_ADMIN.XSS_DANGEROUS_TAGS_LIST
classes: fancy
validate:
type: commalist
uploads_section:
type: section
title: PLUGIN_ADMIN.UPLOADS_SECURITY
underline: true
uploads_dangerous_extensions:
type: selectize
size: large
label: PLUGIN_ADMIN.UPLOADS_DANGEROUS_EXTENSIONS
help: PLUGIN_ADMIN.UPLOADS_DANGEROUS_EXTENSIONS_HELP
classes: fancy
validate:
type: commalist

View File

@@ -835,6 +835,8 @@ form:
-1: PLUGIN_ADMIN.ERROR_SYSTEM
0: PLUGIN_ADMIN.ERROR_SIMPLE
1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE
validate:
type: int
errors.log:
@@ -994,6 +996,18 @@ form:
validate:
type: bool
session.initialize:
type: toggle
label: PLUGIN_ADMIN.SESSION_INITIALIZE
help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
default: true
validate:
type: bool
session.timeout:
type: text
size: small
@@ -1204,3 +1218,27 @@ form:
placeholder: "e.g. http://yoursite.com/yourpath"
label: PLUGIN_ADMIN.CUSTOM_BASE_URL
help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
strict_mode.yaml_compat:
type: toggle
label: PLUGIN_ADMIN.STRICT_YAML_COMPAT
highlight: 1
default: 1
help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
strict_mode.twig_compat:
type: toggle
label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
highlight: 1
default: 1
help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool

View File

@@ -21,6 +21,9 @@ form:
title: PLUGIN_ADMIN.CONTENT
fields:
xss_check:
type: xss
header.title:
type: text
autofocus: true

View File

@@ -1,5 +1,5 @@
title: PLUGIN_ADMIN:EXTERNAL
@extends:
extends@:
type: default
context: blueprints://pages

View File

@@ -1,5 +1,5 @@
title: PLUGIN_ADMIN.MODULAR
'@extends': default
extends@: default
form:
fields:
@@ -13,7 +13,7 @@ form:
modular_title:
type: spacer
title: Modular Setup
title: PLUGIN_ADMIN.MODULAR_SETUP
header.content.items:
type: text

View File

@@ -94,6 +94,7 @@ form:
twofa_secret:
type: 2fa_secret
outerclasses: 'twofa-secret'
markdown: true
label: PLUGIN_ADMIN.2FA_SECRET
sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP

View File

@@ -53,7 +53,7 @@ types:
thumb: media/thumb-flv.png
mime: video/x-flv
webm:
type: file
type: video
thumb: media/thumb-webm.png
mime: video/webm
ogv:

View File

@@ -0,0 +1,31 @@
xss_whitelist: [admin.super] # Whitelist of user access that should 'skip' XSS checking
xss_enabled:
on_events: true
invalid_protocols: true
moz_binding: true
html_inline_styles: true
dangerous_tags: true
xss_dangerous_tags:
- applet
- meta
- xml
- blink
- link
- style
- script
- embed
- object
- iframe
- frame
- frameset
- ilayer
- layer
- bgsound
- title
- base
uploads_dangerous_extensions:
- php
- html
- htm
- js
- exe

View File

@@ -3,7 +3,7 @@ default_lang: en # Default language for site (potenti
author:
name: John Appleseed # Default author name
email: 'john@email.com' # Default author email
email: 'john@example.com' # Default author email
taxonomies: [category,tag] # Arbitrary list of taxonomy types

View File

@@ -88,7 +88,7 @@ twig:
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
autoescape: false # Autoescape Twig vars (DEPRECATED, always enabled in strict mode)
undefined_functions: true # Allow undefined functions
undefined_filters: true # Allow undefined filters
umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775
@@ -146,3 +146,7 @@ gpm:
method: 'auto' # Either 'curl', 'fopen' or 'auto'. 'auto' will try fopen first and if not available cURL
verify_peer: true # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
official_gpm_only: true # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
strict_mode:
yaml_compat: true # Grav 1.5+: Enables YAML backwards compatibility
twig_compat: true # Grav 1.5+: Enables deprecated Twig autoescape setting (autoescape: false)

View File

@@ -8,12 +8,12 @@
// Some standard defines
define('GRAV', true);
define('GRAV_VERSION', '1.4.0');
define('GRAV_VERSION', '1.5.5');
define('GRAV_TESTING', false);
define('DS', '/');
if (!defined('GRAV_PHP_MIN')) {
define('GRAV_PHP_MIN', '5.5.9');
define('GRAV_PHP_MIN', '5.6.4');
}
// Directories and Paths

View File

@@ -72,7 +72,6 @@ NICETIME:
SEC: sec
MIN: min
HR: hr
DAY: day
WK: wk
MO: mo
YR: yr
@@ -88,7 +87,6 @@ NICETIME:
SEC_PLURAL: secs
MIN_PLURAL: mins
HR_PLURAL: hrs
DAY_PLURAL: days
WK_PLURAL: wks
MO_PLURAL: mos
YR_PLURAL: yrs

View File

@@ -30,7 +30,6 @@ NICETIME:
SEC:
MIN:
HR:
DAY:
WK:
MO:
YR:
@@ -46,7 +45,6 @@ NICETIME:
SEC_PLURAL:
MIN_PLURAL:
HR_PLURAL:
DAY_PLURAL:
WK_PLURAL:
MO_PLURAL:
YR_PLURAL:

View File

@@ -109,6 +109,8 @@ class Config extends Data
*/
public function getLanguages()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use Grav::instance()[\'languages\'] instead', E_USER_DEPRECATED);
return Grav::instance()['languages'];
}
}

View File

@@ -207,7 +207,7 @@ class ConfigFileFinder
continue;
}
$name = $directory->getBasename();
$name = $directory->getFilename();
$find = ($lookup ?: $name) . '.yaml';
$filename = "{$path}/{$name}/{$find}";

View File

@@ -137,7 +137,8 @@ class Setup extends Data
// Pre-load setup.php which contains our initial configuration.
// Configuration may contain dynamic parts, which is why we need to always load it.
$file = GRAV_ROOT . '/setup.php';
// If "GRAVE_SETUP_PATH" has been defined, use it, otherwise use defaults.
$file = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH : GRAV_ROOT . '/setup.php';
$setup = is_file($file) ? (array) include $file : [];
// Add default streams defined in beginning of the class.
@@ -261,18 +262,22 @@ class Setup extends Data
);
}
if (!$locator->findResource('environment://config', true)) {
// If environment does not have its own directory, remove it from the lookup.
$this->set('streams.schemes.environment.prefixes', ['config' => []]);
$this->initializeLocator($locator);
}
try {
if (!$locator->findResource('environment://config', true)) {
// If environment does not have its own directory, remove it from the lookup.
$this->set('streams.schemes.environment.prefixes', ['config' => []]);
$this->initializeLocator($locator);
}
// Create security.yaml if it doesn't exist.
$filename = $locator->findResource('config://security.yaml', true, true);
$file = YamlFile::instance($filename);
if (!$file->exists()) {
$file->save(['salt' => Utils::generateRandomString(14)]);
$file->free();
// Create security.yaml if it doesn't exist.
$filename = $locator->findResource('config://security.yaml', true, true);
$file = YamlFile::instance($filename);
if (!$file->exists()) {
$file->save(['salt' => Utils::generateRandomString(14)]);
$file->free();
}
} catch (\RuntimeException $e) {
throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
}
}
}

View File

@@ -10,9 +10,8 @@ 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;
use Grav\Common\Yaml;
use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYaml;
class Validation
{
@@ -107,7 +106,7 @@ class Validation
$method = 'filter' . ucfirst(strtr($type, '-', '_'));
// If this is a YAML field validate/filter as such
if ($type != 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
if ($type !== 'yaml' && isset($field['yaml']) && $field['yaml'] === true) {
$method = 'filterYaml';
}
@@ -128,10 +127,12 @@ class Validation
*/
public static function typeText($value, array $params, array $field)
{
if (!is_string($value)) {
if (!is_string($value) && !is_numeric($value)) {
return false;
}
$value = (string)$value;
if (isset($params['min']) && strlen($value) < $params['min']) {
return false;
}
@@ -643,15 +644,12 @@ class Validation
public static function filterYaml($value, $params)
{
try {
if (is_string($value)) {
return (array) Yaml::parse($value);
} else {
return $value;
}
} catch (ParseException $e) {
if (!is_string($value)) {
return $value;
}
return (array) Yaml::parse($value);
}
/**

View File

@@ -9,6 +9,7 @@
namespace Grav\Common;
use DebugBar\DataCollector\ConfigCollector;
use DebugBar\DataCollector\MessagesCollector;
use DebugBar\JavascriptRenderer;
use DebugBar\StandardDebugBar;
use Grav\Common\Config\Config;
@@ -31,6 +32,11 @@ class Debugger
protected $timers = [];
/** @var string[] $deprecations */
protected $deprecations = [];
protected $errorHandler;
/**
* Debugger constructor.
*/
@@ -41,6 +47,9 @@ class Debugger
$this->debugbar = new StandardDebugBar();
$this->debugbar['time']->addMeasure('Loading', $this->debugbar['time']->getRequestStartTime(), microtime(true));
// Set deprecation collector.
$this->setErrorHandler();
}
/**
@@ -58,8 +67,15 @@ class Debugger
$this->enabled = $this->config->get('system.debugger.enabled');
if ($this->enabled()) {
$plugins_config = (array)$this->config->get('plugins');
ksort($plugins_config);
$this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('system'), 'Config'));
$this->debugbar->addCollector(new ConfigCollector((array)$this->config->get('plugins'), 'Plugins'));
$this->debugbar->addCollector(new ConfigCollector($plugins_config, 'Plugins'));
$this->addMessage('Grav v' . GRAV_VERSION);
}
return $this;
@@ -121,9 +137,9 @@ class Debugger
return $this;
}
public function getCaller($ignore = 2)
public function getCaller($limit = 2)
{
$trace = debug_backtrace(false, $ignore);
$trace = debug_backtrace(false, $limit);
return array_pop($trace);
}
@@ -170,6 +186,8 @@ class Debugger
return $this;
}
$this->addDeprecations();
echo $this->renderer->render();
}
@@ -184,6 +202,7 @@ class Debugger
public function sendDataInHeaders()
{
if ($this->enabled()) {
$this->addDeprecations();
$this->debugbar->sendDataInHeaders();
}
@@ -201,6 +220,7 @@ class Debugger
return null;
}
$this->addDeprecations();
$this->timers = [];
return $this->debugbar->getData();
@@ -272,4 +292,152 @@ class Debugger
return $this;
}
public function setErrorHandler()
{
$this->errorHandler = set_error_handler(
[$this, 'deprecatedErrorHandler']
);
}
/**
* @param int $errno
* @param string $errstr
* @param string $errfile
* @param int $errline
* @return bool
*/
public function deprecatedErrorHandler($errno, $errstr, $errfile, $errline)
{
if ($errno !== E_USER_DEPRECATED) {
if ($this->errorHandler) {
return \call_user_func($this->errorHandler, $errno, $errstr, $errfile, $errline);
}
return true;
}
if (!$this->enabled()) {
return true;
}
$backtrace = debug_backtrace(false);
// Skip current call.
array_shift($backtrace);
// Skip vendor libraries and the method where error was triggered.
while ($current = array_shift($backtrace)) {
if (isset($current['file']) && strpos($current['file'], 'vendor') !== false) {
continue;
}
if (isset($current['function']) && ($current['function'] === 'user_error' || $current['function'] === 'trigger_error')) {
$current = array_shift($backtrace);
}
break;
}
// Add back last call.
array_unshift($backtrace, $current);
// Filter arguments.
foreach ($backtrace as &$current) {
if (isset($current['args'])) {
$args = [];
foreach ($current['args'] as $arg) {
if (\is_string($arg)) {
$args[] = "'" . $arg . "'";
} elseif (\is_bool($arg)) {
$args[] = $arg ? 'true' : 'false';
} elseif (\is_scalar($arg)) {
$args[] = $arg;
} elseif (\is_object($arg)) {
$args[] = get_class($arg) . ' $object';
} elseif (\is_array($arg)) {
$args[] = '$array';
} else {
$args[] = '$object';
}
}
$current['args'] = $args;
}
}
unset($current);
$this->deprecations[] = [
'message' => $errstr,
'file' => $errfile,
'line' => $errline,
'trace' => $backtrace,
];
// Do not pass forward.
return true;
}
protected function addDeprecations()
{
if (!$this->deprecations) {
return;
}
$collector = new MessagesCollector('deprecated');
$this->addCollector($collector);
$collector->addMessage('Your site is using following deprecated features:');
/** @var array $deprecated */
foreach ($this->deprecations as $deprecated) {
list($message, $scope) = $this->getDepracatedMessage($deprecated);
$collector->addMessage($message, $scope);
}
}
protected function getDepracatedMessage($deprecated)
{
$scope = 'unknown';
if (stripos($deprecated['message'], 'grav') !== false) {
$scope = 'grav';
} elseif (!isset($deprecated['file'])) {
$scope = 'unknown';
} elseif (stripos($deprecated['file'], 'twig') !== false) {
$scope = 'twig';
} elseif (stripos($deprecated['file'], 'yaml') !== false) {
$scope = 'yaml';
} elseif (stripos($deprecated['file'], 'vendor') !== false) {
$scope = 'vendor';
}
$trace = [];
foreach ($deprecated['trace'] as $current) {
$class = isset($current['class']) ? $current['class'] : '';
$type = isset($current['type']) ? $current['type'] : '';
$function = $this->getFunction($current);
if (isset($current['file'])) {
$current['file'] = str_replace(GRAV_ROOT . '/', '', $current['file']);
}
unset($current['class'], $current['type'], $current['function'], $current['args']);
$trace[] = ['call' => $class . $type . $function] + $current;
}
return [
[
'message' => $deprecated['message'],
'trace' => $trace
],
$scope
];
}
protected function getFunction($trace)
{
if (!isset($trace['function'])) {
return '';
}
return $trace['function'] . '(' . implode(', ', $trace['args']) . ')';
}
}

View File

@@ -18,6 +18,13 @@ class BareHandler extends Handler
*/
public function handle()
{
$inspector = $this->getInspector();
$code = $inspector->getException()->getCode();
if ( ($code >= 400) && ($code < 600) )
{
$this->getRun()->sendHttpCode($code);
}
return Handler::QUIT;
}

View File

@@ -74,5 +74,8 @@ class Errors
}
$whoops->register();
// Re-register deprecation handler.
$grav['debugger']->setErrorHandler();
}
}

View File

@@ -35,6 +35,10 @@ class SimplePageHandler extends Handler
$cssFile = $this->getResource("error.css");
$code = $inspector->getException()->getCode();
if ( ($code >= 400) && ($code < 600) )
{
$this->getRun()->sendHttpCode($code);
}
$message = $inspector->getException()->getMessage();
if ($inspector->getException() instanceof \ErrorException) {

View File

@@ -20,9 +20,6 @@ trait CompiledFile
*/
public function content($var = null)
{
// Set some options
$this->settings(['native' => true, 'compat' => true]);
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) {

View File

@@ -13,7 +13,7 @@ use Grav\Common\Filesystem\Folder;
use Grav\Common\Inflector;
use Grav\Common\Iterator;
use Grav\Common\Utils;
use Symfony\Component\Yaml\Yaml;
use RocketTheme\Toolbox\File\YamlFile;
class GPM extends Iterator
{
@@ -624,7 +624,10 @@ class GPM extends Iterator
return false;
}
$blueprint = (array)Yaml::parse(file_get_contents($blueprint_file));
$file = YamlFile::instance($blueprint_file);
$blueprint = (array)$file->content();
$file->free();
return $blueprint;
}
@@ -873,7 +876,9 @@ class GPM extends Iterator
// get currently installed version
$locator = Grav::instance()['locator'];
$blueprints_path = $locator->findResource('plugins://' . $dependency_slug . DS . 'blueprints.yaml');
$package_yaml = Yaml::parse(file_get_contents($blueprints_path));
$file = YamlFile::instance($blueprints_path);
$package_yaml = $file->content();
$file->free();
$currentlyInstalledVersion = $package_yaml['version'];
// if requirement is next significant release, check is compatible with currently installed version, might not be

View File

@@ -43,6 +43,11 @@ class Installer
*/
protected static $error = 0;
/**
* @var integer Zip Error Code
*/
protected static $error_zip = 0;
/**
* @var string Post install message
*/
@@ -190,10 +195,10 @@ class Installer
}
self::$error = self::ZIP_EXTRACT_ERROR;
self::$error_zip = $archive;
return false;
}
/**
* Instantiates and returns the package installer class
*
@@ -291,17 +296,17 @@ class Installer
{
foreach (new \DirectoryIterator($source_path) as $file) {
if ($file->isLink() || $file->isDot() || in_array($file->getBasename(),$ignores)) {
if ($file->isLink() || $file->isDot() || in_array($file->getFilename(), $ignores)) {
continue;
}
$path = $install_path . DS . $file->getBasename();
$path = $install_path . DS . $file->getFilename();
if ($file->isDir()) {
Folder::delete($path);
Folder::move($file->getPathname(), $path);
if ($file->getBasename() == 'bin') {
if ($file->getFilename() === 'bin') {
foreach (glob($path . DS . '*') as $bin_file) {
@chmod($bin_file, 0755);
}
@@ -460,7 +465,42 @@ class Installer
break;
case self::ZIP_EXTRACT_ERROR:
$msg = 'An error occurred while extracting the package';
$msg = 'Unable to extract the package. ';
if (self::$error_zip) {
switch(self::$error_zip) {
case \ZipArchive::ER_EXISTS:
$msg .= "File already exists.";
break;
case \ZipArchive::ER_INCONS:
$msg .= "Zip archive inconsistent.";
break;
case \ZipArchive::ER_MEMORY:
$msg .= "Malloc failure.";
break;
case \ZipArchive::ER_NOENT:
$msg .= "No such file.";
break;
case \ZipArchive::ER_NOZIP:
$msg .= "Not a zip archive.";
break;
case \ZipArchive::ER_OPEN:
$msg .= "Can't open file.";
break;
case \ZipArchive::ER_READ:
$msg .= "Read error.";
break;
case \ZipArchive::ER_SEEK:
$msg .= "Seek error.";
break;
}
}
break;
default:

View File

@@ -114,7 +114,7 @@ class Licenses
{
if (!isset(self::$file)) {
$path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml';;
$path = Grav::instance()['locator']->findResource('user://data') . '/licenses.yaml';
if (!file_exists($path)) {
touch($path);
}

View File

@@ -9,7 +9,6 @@
namespace Grav\Common;
use Grav\Common\Config\Config;
use Grav\Common\Language\Language;
use Grav\Common\Page\Medium\ImageMedium;
use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Page;
@@ -205,11 +204,8 @@ class Grav extends Container
*/
public function redirectLangSafe($route, $code = null)
{
/** @var Language $language */
$language = $this['language'];
if (!$this['uri']->isExternal($route) && $language->enabled() && $language->isIncludeDefaultLanguage()) {
$this->redirect($language->getLanguage() . $route, $code);
if (!$this['uri']->isExternal($route)) {
$this->redirect($this['pages']->route($route), $code);
} else {
$this->redirect($route, $code);
}
@@ -443,7 +439,7 @@ class Grav extends Container
/** @var Config $config */
$config = $this['config'];
$uri_extension = $uri->extension();
$uri_extension = strtolower($uri->extension());
$fallback_types = $config->get('system.media.allowed_fallback_types', null);
$supported_types = $config->get('media.types');

View File

@@ -9,7 +9,7 @@
namespace Grav\Common;
/**
* @deprecated 2.0
* @deprecated 1.4 Use Grav::instance() instead
*/
trait GravTrait
{
@@ -24,8 +24,7 @@ trait GravTrait
self::$grav = Grav::instance();
}
$caller = self::$grav['debugger']->getCaller();
self::$grav['debugger']->addMessage("Deprecated GravTrait used in {$caller['file']}", 'deprecated');
user_error(__TRAIT__ . ' is deprecated since Grav 1.4, use Grav::instance() instead', E_USER_DEPRECATED);
return self::$grav;
}

View File

@@ -117,7 +117,7 @@ class Excerpts
*/
public static function processLinkExcerpt($excerpt, Page $page, $type = 'link')
{
$url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['href']));
$url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
$url_parts = static::parseUrl($url);
@@ -172,10 +172,9 @@ class Excerpts
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);
}
$excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
return $excerpt;
}
@@ -294,6 +293,15 @@ class Excerpts
if (Grav::instance()['config']->get('system.images.auto_fix_orientation')) {
$actions[] = ['method' => 'fixOrientation', 'params' => ''];
}
$defaults = Grav::instance()['config']->get('system.images.defaults');
if (is_array($defaults) && count($defaults)) {
foreach ($defaults as $method => $params) {
$actions[] = [
'method' => $method,
'params' => $params,
];
}
}
// loop through actions for the image and call them
foreach ($actions as $action) {

View File

@@ -47,6 +47,7 @@ class Truncator {
// Iterate over words.
$words = new DOMWordsIterator($body);
$truncated = false;
foreach ($words as $word) {
// If we have exceeded the limit, we delete the remainder of the content.
@@ -70,12 +71,19 @@ class Truncator {
self::insertEllipsis($curNode, $ellipsis);
}
$truncated = true;
break;
}
}
return self::innerHTML($body);
// Return original HTML if not truncated.
if ($truncated) {
return self::innerHTML($body);
} else {
return $html;
}
}
/**
@@ -98,24 +106,32 @@ class Truncator {
// Iterate over letters.
$letters = new DOMLettersIterator($body);
$truncated = false;
foreach ($letters as $letter) {
// If we have exceeded the limit, we want to delete the remainder of this document.
if ($letters->key() >= $limit) {
$currentText = $letters->currentTextPosition();
$currentText[0]->nodeValue = substr($currentText[0]->nodeValue, 0, $currentText[1] + 1);
$currentText[0]->nodeValue = mb_substr($currentText[0]->nodeValue, 0, $currentText[1] + 1);
self::removeProceedingNodes($currentText[0], $body);
if (!empty($ellipsis)) {
self::insertEllipsis($currentText[0], $ellipsis);
}
$truncated = true;
break;
}
}
return self::innerHTML($body);
// Return original HTML if not truncated.
if ($truncated) {
return self::innerHTML($body);
} else {
return $html;
}
}
/**

View File

@@ -190,10 +190,11 @@ class Inflector
public function hyphenize($word)
{
$regex1 = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1-\2', $word);
$regex2 = preg_replace('/([a-zd])([A-Z])/', '\1-\2', $regex1);
$regex3 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex2);
$regex2 = preg_replace('/([a-z])([A-Z])/', '\1-\2', $regex1);
$regex3 = preg_replace('/([0-9])([A-Z])/', '\1-\2', $regex2);
$regex4 = preg_replace('/[^A-Z^a-z^0-9]+/', '-', $regex3);
return strtolower($regex3);
return strtolower($regex4);
}
/**

View File

@@ -181,7 +181,7 @@ class Language
$uri = preg_replace("/\\" . $matches[1] . '/', '', $uri, 1);
// Store in session if language is different.
if (isset($this->grav['session']) && $this->grav['session']->started()
if (isset($this->grav['session']) && $this->grav['session']->isStarted()
&& $this->config->get('system.languages.session_store_active', true)
&& $this->grav['session']->active_language != $this->active
) {
@@ -189,7 +189,7 @@ class Language
}
} else {
// Try getting language from the session, else no active.
if (isset($this->grav['session']) && $this->grav['session']->started()
if (isset($this->grav['session']) && $this->grav['session']->isStarted()
&& $this->config->get('system.languages.session_store_active', true)) {
$this->active = $this->grav['session']->active_language ?: null;
}

View File

@@ -22,6 +22,7 @@ class LanguageCodes
'bn-BD' => [ 'name' => 'Bengali (Bangladesh)', 'nativeName' => 'বাংলা (বাংলাদেশ)' ],
'bn-IN' => [ 'name' => 'Bengali (India)', 'nativeName' => 'বাংলা (ভারত)' ],
'br' => [ 'name' => 'Breton', 'nativeName' => 'Brezhoneg' ],
'bs' => [ 'name' => 'Bosnian', 'nativeName' => 'Bosanski' ],
'ca' => [ 'name' => 'Catalan', 'nativeName' => 'Català' ],
'ca-valencia'=> [ 'name' => 'Catalan (Valencian)', 'nativeName' => 'Català (valencià)' ], // not iso-639-1. a=l10n-drivers
'cs' => [ 'name' => 'Czech', 'nativeName' => 'Čeština' ],

View File

@@ -0,0 +1,9 @@
<?php
namespace Grav\Common\Media\Interfaces;
/**
* Class implements media collection interface.
*/
interface MediaCollectionInterface
{
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Grav\Common\Media\Interfaces;
/**
* Class implements media interface.
*/
interface MediaInterface
{
/**
* Gets the associated media collection.
*
* @return MediaCollectionInterface Collection of associated media.
*/
public function getMedia();
/**
* Get filesystem path to the associated media.
*
* @return string|null Media path or null if the object doesn't have media folder.
*/
public function getMediaFolder();
/**
* Get display order for the associated media.
*
* @return array Empty array means default ordering.
*/
public function getMediaOrder();
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Grav\Common\Media\Interfaces;
/**
* Class implements media object interface.
*/
interface MediaObjectInterface
{
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Grav\Common\Media\Traits;
use Grav\Common\Cache;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Page\Media;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
trait MediaTrait
{
protected $media;
/**
* Get filesystem path to the associated media.
*
* @return string|null
*/
abstract public function getMediaFolder();
/**
* Get display order for the associated media.
*
* @return array Empty array means default ordering.
*/
abstract public function getMediaOrder();
/**
* Get URI ot the associated media. Method will return null if path isn't URI.
*
* @return null|string
*/
public function getMediaUri()
{
$folder = $this->getMediaFolder();
if (strpos($folder, '://')) {
return $folder;
}
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$user = $locator->findResource('user://');
if (strpos($folder, $user) === 0) {
return 'user://' . substr($folder, strlen($user)+1);
}
return null;
}
/**
* Gets the associated media collection.
*
* @return MediaCollectionInterface Representation of associated media.
*/
public function getMedia()
{
$cache = $this->getMediaCache();
if ($this->media === null) {
// Use cached media if possible.
$cacheKey = md5('media' . $this->getCacheKey());
if (!$media = $cache->fetch($cacheKey)) {
$media = new Media($this->getMediaFolder(), $this->getMediaOrder());
$cache->save($cacheKey, $media);
}
$this->media = $media;
}
return $this->media;
}
/**
* Sets the associated media collection.
*
* @param MediaCollectionInterface $media Representation of associated media.
* @return $this
*/
protected function setMedia(MediaCollectionInterface $media)
{
$cache = $this->getMediaCache();
$cacheKey = md5('media' . $this->getCacheKey());
$cache->save($cacheKey, $media);
$this->media = $media;
return $this;
}
/**
* Clear media cache.
*/
protected function clearMediaCache()
{
$cache = $this->getMediaCache();
$cacheKey = md5('media' . $this->getCacheKey());
$cache->delete($cacheKey);
}
/**
* @return Cache
*/
protected function getMediaCache()
{
return Grav::instance()['cache'];
}
/**
* @return string
*/
abstract protected function getCacheKey();
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Grav\Common\Page\Interfaces;
/**
* Class implements page interface.
*/
interface PageInterface
{
}

View File

@@ -9,11 +9,11 @@
namespace Grav\Common\Page;
use Grav\Common\Grav;
use Grav\Common\Yaml;
use Grav\Common\Page\Medium\AbstractMedia;
use Grav\Common\Page\Medium\GlobalMedia;
use Grav\Common\Page\Medium\MediumFactory;
use RocketTheme\Toolbox\File\File;
use Symfony\Component\Yaml\Yaml;
class Media extends AbstractMedia
{
@@ -24,11 +24,13 @@ class Media extends AbstractMedia
protected $standard_exif = ['FileSize', 'MimeType', 'height', 'width'];
/**
* @param $path
* @param string $path
* @param array $media_order
*/
public function __construct($path)
public function __construct($path, array $media_order = null)
{
$this->path = $path;
$this->media_order = $media_order;
$this->__wakeup();
$this->init();
@@ -86,7 +88,7 @@ class Media extends AbstractMedia
/** @var \DirectoryIterator $info */
foreach ($iterator as $path => $info) {
// Ignore folders and Markdown files.
if (!$info->isFile() || $info->getExtension() === 'md' || $info->getBasename()[0] === '.') {
if (!$info->isFile() || $info->getExtension() === 'md' || $info->getFilename()[0] === '.') {
continue;
}

View File

@@ -10,9 +10,11 @@ namespace Grav\Common\Page\Medium;
use Grav\Common\Getters;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaCollectionInterface;
use Grav\Common\Media\Interfaces\MediaObjectInterface;
use Grav\Common\Utils;
abstract class AbstractMedia extends Getters
abstract class AbstractMedia extends Getters implements MediaCollectionInterface
{
protected $gettersVariable = 'instances';
@@ -21,6 +23,7 @@ abstract class AbstractMedia extends Getters
protected $videos = [];
protected $audios = [];
protected $files = [];
protected $media_order;
/**
* Get medium by filename.
@@ -44,10 +47,25 @@ abstract class AbstractMedia extends Getters
return $this->offsetGet($filename);
}
/**
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet($offset)
{
$object = parent::offsetGet($offset);
// It would be nice if previous image modification would not affect the later ones.
//$object = $object ? clone($object) : null;
return $object;
}
/**
* Get a list of all media.
*
* @return array|Medium[]
* @return array|MediaObjectInterface[]
*/
public function all()
{
@@ -59,7 +77,7 @@ abstract class AbstractMedia extends Getters
/**
* Get a list of all image media.
*
* @return array|Medium[]
* @return array|MediaObjectInterface[]
*/
public function images()
{
@@ -70,7 +88,7 @@ abstract class AbstractMedia extends Getters
/**
* Get a list of all video media.
*
* @return array|Medium[]
* @return array|MediaObjectInterface[]
*/
public function videos()
{
@@ -81,7 +99,7 @@ abstract class AbstractMedia extends Getters
/**
* Get a list of all audio media.
*
* @return array|Medium[]
* @return array|MediaObjectInterface[]
*/
public function audios()
{
@@ -92,7 +110,7 @@ abstract class AbstractMedia extends Getters
/**
* Get a list of all file media.
*
* @return array|Medium[]
* @return array|MediaObjectInterface[]
*/
public function files()
{
@@ -102,7 +120,7 @@ abstract class AbstractMedia extends Getters
/**
* @param string $name
* @param Medium $file
* @param MediaObjectInterface $file
*/
protected function add($name, $file)
{
@@ -130,14 +148,20 @@ abstract class AbstractMedia extends Getters
*/
protected function orderMedia($media)
{
$page = Grav::instance()['pages']->get($this->path);
if (null === $this->media_order) {
$page = Grav::instance()['pages']->get($this->path);
if ($page && isset($page->header()->media_order)) {
$media_order = array_map('trim', explode(',', $page->header()->media_order));
$media = Utils::sortArrayByArray($media, $media_order);
if ($page && isset($page->header()->media_order)) {
$this->media_order = array_map('trim', explode(',', $page->header()->media_order));
}
}
if (!empty($this->media_order) && is_array($this->media_order)) {
$media = Utils::sortArrayByArray($media, $this->media_order);
} else {
ksort($media, SORT_NATURAL | SORT_FLAG_CASE);
}
return $media;
}
@@ -168,8 +192,8 @@ abstract class AbstractMedia extends Getters
$type = 'base';
while (($part = array_shift($fileParts)) !== null) {
if ($part != 'meta' && $part != 'thumb') {
if (isset($extension)) {
if ($part !== 'meta' && $part !== 'thumb') {
if (null !== $extension) {
$name .= '.' . $extension;
}
$extension = $part;

View File

@@ -11,10 +11,16 @@ namespace Grav\Common\Page\Medium;
use Grav\Common\Grav;
use Gregwar\Image\Exceptions\GenerationError;
use Gregwar\Image\Image;
use Gregwar\Image\Source;
use RocketTheme\Toolbox\Event\Event;
class ImageFile extends Image
{
public function __destruct()
{
$this->getAdapter()->deinit();
}
/**
* Clear previously applied operations
*/
@@ -30,15 +36,15 @@ class ImageFile extends Image
* @param int $quality the quality (for JPEG)
* @param bool $actual
*
* @return mixed|string
* @return string
*/
public function cacheFile($type = 'jpg', $quality = 80, $actual = false)
{
if ($type == 'guess') {
if ($type === 'guess') {
$type = $this->guessType();
}
if (!count($this->operations) && $type == $this->guessType() && !$this->forceCache) {
if (!$this->forceCache && !count($this->operations) && $type === $this->guessType()) {
return $this->getFilename($this->getFilePath());
}
@@ -60,8 +66,7 @@ class ImageFile extends Image
$cacheFile .= $this->prettyName;
}
$cacheFile .= '.'.$type;
$cacheFile .= '.' . $type;
// If the files does not exists, save it
$image = $this;
@@ -76,7 +81,7 @@ class ImageFile extends Image
$generate = function ($target) use ($image, $type, $quality) {
$result = $image->save($target, $type, $quality);
if ($result != $target) {
if ($result !== $target) {
throw new GenerationError($result);
}
@@ -87,15 +92,19 @@ class ImageFile extends Image
try {
$perms = Grav::instance()['config']->get('system.images.cache_perms', '0755');
$perms = octdec($perms);
$file = $this->cache->setDirectoryMode($perms)->getOrCreateFile($cacheFile, $conditions, $generate, $actual);
$file = $this->getCacheSystem()->setDirectoryMode($perms)->getOrCreateFile($cacheFile, $conditions, $generate, $actual);
} catch (GenerationError $e) {
$file = $e->getNewFile();
}
// Nulling the resource
$this->getAdapter()->setSource(new Source\File($file));
$this->getAdapter()->deinit();
if ($actual) {
return $file;
} else {
return $this->getFilename($file);
}
return $this->getFilename($file);
}
}

View File

@@ -11,6 +11,7 @@ namespace Grav\Common\Page\Medium;
use Grav\Common\Data\Blueprint;
use Grav\Common\Grav;
use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class ImageMedium extends Medium
{
@@ -103,6 +104,18 @@ class ImageMedium extends Medium
}
}
public function __destruct()
{
unset($this->image);
}
public function __clone()
{
$this->image = $this->image ? clone $this->image : null;
parent::__clone();
}
/**
* Add meta file for the medium.
*
@@ -152,14 +165,20 @@ class ImageMedium extends Medium
*/
public function url($reset = true)
{
$image_path = Grav::instance()['locator']->findResource('cache://images', true);
$image_dir = Grav::instance()['locator']->findResource('cache://images', false);
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$image_path = $locator->findResource('cache://images', true);
$image_dir = $locator->findResource('cache://images', false);
$saved_image_path = $this->saveImage();
$output = preg_replace('|^' . preg_quote(GRAV_ROOT) . '|', '', $saved_image_path);
$output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $saved_image_path);
if ($locator->isStream($output)) {
$output = $locator->findResource($output, false);
}
if (Utils::startsWith($output, $image_path)) {
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path) . '|', '', $output);
$output = '/' . $image_dir . preg_replace('|^' . preg_quote($image_path, '|') . '|', '', $output);
}
if ($reset) {
@@ -226,13 +245,13 @@ class ImageMedium extends Medium
{
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;
}
$basename = $this->get('basename');
if (preg_match('/[a-z0-9]{40}-(.*)/', $basename, $matches)) {
$basename = $matches[1];
}
return $basename;
}
/**
@@ -280,7 +299,7 @@ class ImageMedium extends Medium
// 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)) {
if (null !== $derivative) {
$index = 2;
$alt_widths = array_keys($this->alternatives);
sort($alt_widths);
@@ -339,7 +358,6 @@ class ImageMedium extends Medium
if ($this->image) {
$this->image();
$this->image->clearOperations(); // Clear previously applied operations
$this->querystring('');
$this->filter();
$this->clearAlternatives();
@@ -432,7 +450,7 @@ class ImageMedium extends Medium
* Set or get sizes parameter for srcset media action
*
* @param string $sizes
* @return $this
* @return string
*/
public function sizes($sizes = null)
{
@@ -459,7 +477,7 @@ class ImageMedium extends Medium
*/
public function width($value = 'auto')
{
if (!$value || $value == 'auto')
if (!$value || $value === 'auto')
$this->attributes['width'] = $this->get('width');
else
$this->attributes['width'] = $value;
@@ -480,7 +498,7 @@ class ImageMedium extends Medium
*/
public function height($value = 'auto')
{
if (!$value || $value == 'auto')
if (!$value || $value === 'auto')
$this->attributes['height'] = $this->get('height');
else
$this->attributes['height'] = $value;
@@ -496,11 +514,11 @@ class ImageMedium extends Medium
*/
public function __call($method, $args)
{
if ($method == 'cropZoom') {
if ($method === 'cropZoom') {
$method = 'zoomCrop';
}
if (!in_array($method, self::$magic_actions)) {
if (!\in_array($method, self::$magic_actions, true)) {
return parent::__call($method, $args);
}
@@ -551,6 +569,9 @@ class ImageMedium extends Medium
// Use existing cache folder or if it doesn't exist, create it.
$cacheDir = $locator->findResource('cache://images', true) ?: $locator->findResource('cache://images', true, true);
// Make sure we free previous image.
unset($this->image);
$this->image = ImageFile::open($file)
->setCacheDir($cacheDir)
->setActualCacheDir($cacheDir)
@@ -562,7 +583,7 @@ class ImageMedium extends Medium
/**
* Save the image with cache.
*
* @return mixed|string
* @return string
*/
protected function saveImage()
{
@@ -576,7 +597,7 @@ class ImageMedium extends Medium
return $this->result;
}
if ($this->get('debug') && !$this->debug_watermarked) {
if (!$this->debug_watermarked && $this->get('debug')) {
$ratio = $this->get('ratio');
if (!$ratio) {
$ratio = 1;
@@ -623,9 +644,9 @@ class ImageMedium extends Medium
}
return $max;
} else {
return $this;
}
return $this;
}
}

View File

@@ -12,8 +12,9 @@ use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
use Grav\Common\Data\Data;
use Grav\Common\Data\Blueprint;
use Grav\Common\Media\Interfaces\MediaObjectInterface;
class Medium extends Data implements RenderableInterface
class Medium extends Data implements RenderableInterface, MediaObjectInterface
{
use ParsedownHtmlTrait;
@@ -72,6 +73,11 @@ class Medium extends Data implements RenderableInterface
$this->reset();
}
public function __clone()
{
// Allows future compatibility as parent::__clone() works.
}
/**
* Create a copy of this media object
*
@@ -79,7 +85,7 @@ class Medium extends Data implements RenderableInterface
*/
public function copy()
{
return clone($this);
return clone $this;
}
/**
@@ -193,7 +199,12 @@ class Medium extends Data implements RenderableInterface
*/
public function url($reset = true)
{
$output = preg_replace('|^' . preg_quote(GRAV_ROOT) . '|', '', $this->get('filepath'));
$output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath'));
$locator = Grav::instance()['locator'];
if ($locator->isStream($output)) {
$output = $locator->findResource($output, false);
}
if ($reset) {
$this->reset();
@@ -398,6 +409,22 @@ class Medium extends Data implements RenderableInterface
return $mode === 'thumbnail' ? ($this->getThumbnail() ? $this->getThumbnail()->reset() : null) : $this->reset();
}
/**
* Helper method to determine if this media item has a thumbnail or not
*
* @param string $type;
*
* @return bool
*/
public function thumbnailExists($type = 'page')
{
$thumbs = $this->get('thumbnails');
if (isset($thumbs[$type])) {
return true;
}
return false;
}
/**
* Switch thumbnail.
*
@@ -420,6 +447,7 @@ class Medium extends Data implements RenderableInterface
return $this;
}
/**
* Turn the current Medium into a Link
*
@@ -518,7 +546,12 @@ class Medium extends Data implements RenderableInterface
{
$qs = $method;
if (count($args) > 1 || (count($args) == 1 && !empty($args[0]))) {
$qs .= '=' . implode(',', array_map(function ($a) { return rawurlencode($a); }, $args));
$qs .= '=' . implode(',', array_map(function ($a) {
if (is_array($a)) {
$a = '[' . implode(',', $a) . ']';
}
return rawurlencode($a);
}, $args));
}
if (!empty($qs)) {

View File

@@ -94,6 +94,40 @@ class VideoMedium extends Medium
return $this;
}
/**
* Allows to set the playsinline attribute
*
* @param bool $status
* @return $this
*/
public function playsinline($status = false)
{
if($status) {
$this->attributes['playsinline'] = true;
} else {
unset($this->attributes['playsinline']);
}
return $this;
}
/**
* Allows to set the muted attribute
*
* @param bool $status
* @return $this
*/
public function muted($status = false)
{
if($status) {
$this->attributes['muted'] = true;
} else {
unset($this->attributes['muted']);
}
return $this;
}
/**
* Reset medium.
*

View File

@@ -12,23 +12,26 @@ use Exception;
use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Data\Blueprint;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Media\Traits\MediaTrait;
use Grav\Common\Taxonomy;
use Grav\Common\Uri;
use Grav\Common\Utils;
use Grav\Common\Yaml;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\MarkdownFile;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
define('PAGE_ORDER_PREFIX_REGEX', '/^[0-9]+\./u');
class Page
class Page implements PageInterface
{
use MediaTrait;
/**
* @var string Filename. Leave as null if page is folder.
*/
@@ -65,7 +68,6 @@ class Page
protected $summary;
protected $raw_content;
protected $pagination;
protected $media;
protected $metadata;
protected $title;
protected $max_count;
@@ -135,7 +137,7 @@ class Page
$this->metadata();
$this->url();
$this->visible();
$this->modularTwig($this->slug[0] === '_');
$this->modularTwig(strpos($this->slug(), '_') === 0);
$this->setPublishState();
$this->published();
$this->urlExtension();
@@ -193,7 +195,7 @@ class Page
$route = isset($aPage->header()->routes['default']) ? $aPage->header()->routes['default'] : $aPage->rawRoute();
if (!$route) {
$route = $aPage->slug();
$route = $aPage->route();
}
if ($onlyPublished && !$aPage->published()) {
@@ -318,8 +320,6 @@ class Page
if (!$this->header) {
$file = $this->file();
if ($file) {
// Set some options
$file->settings(['native' => true, 'compat' => true]);
try {
$this->raw_content = $file->markdown();
$this->frontmatter = $file->frontmatter();
@@ -328,11 +328,12 @@ class Page
if (!Utils::isAdminPlugin()) {
// If there's a `frontmatter.yaml` file merge that in with the page header
// note page's own frontmatter has precedence and will overwrite any defaults
$frontmatter_file = $this->path . '/' . $this->folder . '/frontmatter.yaml';
if (file_exists($frontmatter_file)) {
$frontmatter_data = (array)Yaml::parse(file_get_contents($frontmatter_file));
$frontmatterFile = CompiledYamlFile::instance($this->path . '/' . $this->folder . '/frontmatter.yaml');
if ($frontmatterFile->exists()) {
$frontmatter_data = (array)$frontmatterFile->content();
$this->header = (object)array_replace_recursive($frontmatter_data,
(array)$this->header);
$frontmatterFile->free();
}
// Process frontmatter with Twig if enabled
if (Grav::instance()['config']->get('system.pages.frontmatter.process_twig') === true) {
@@ -763,6 +764,8 @@ class Page
// pages.markdown_extra is deprecated, but still check it...
if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) {
user_error('Configuration option \'system.pages.markdown_extra\' is deprecated since Grav 1.5, use \'system.pages.markdown.extra\' instead', E_USER_DEPRECATED);
$defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra');
}
@@ -813,6 +816,8 @@ class Page
*/
public function setRawContent($content)
{
$content = $content === null ? '': $content;
$this->content = $content;
}
@@ -1122,6 +1127,14 @@ class Page
return json_encode($this->toArray());
}
/**
* @return string
*/
protected function getCacheKey()
{
return $this->id();
}
/**
* Gets and sets the associated media as found in the page folder.
*
@@ -1131,23 +1144,33 @@ class Page
*/
public function media($var = null)
{
/** @var Cache $cache */
$cache = Grav::instance()['cache'];
if ($var) {
$this->media = $var;
}
if ($this->media === null) {
// Use cached media if possible.
$media_cache_id = md5('media' . $this->id());
if (!$media = $cache->fetch($media_cache_id)) {
$media = new Media($this->path());
$cache->save($media_cache_id, $media);
}
$this->media = $media;
$this->setMedia($var);
}
return $this->media;
return $this->getMedia();
}
/**
* Get filesystem path to the associated media.
*
* @return string|null
*/
public function getMediaFolder()
{
return $this->path();
}
/**
* Get display order for the associated media.
*
* @return array Empty array means default ordering.
*/
public function getMediaOrder()
{
$header = $this->header();
return isset($header->media_order) ? array_map('trim', explode(',', (string)$header->media_order)) : [];
}
/**
@@ -1499,6 +1522,8 @@ class Page
// Build an array of meta objects..
foreach ((array)$metadata as $key => $value) {
// Lowercase the key
$key = strtolower($key);
// If this is a property type metadata: "og", "twitter", "facebook" etc
// Backward compatibility for nested arrays in metas
if (is_array($value)) {
@@ -1559,7 +1584,7 @@ class Page
}
if (empty($this->slug)) {
$this->slug = $this->adjustRouteCase(preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder));
$this->slug = $this->adjustRouteCase(preg_replace(PAGE_ORDER_PREFIX_REGEX, '', $this->folder)) ?: null;
}
@@ -1624,14 +1649,19 @@ class Page
* 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 $include_lang
* @param bool $canonical True to return the canonical URL
* @param bool $include_base Include base url on multisite as well as language code
* @param bool $raw_route
*
* @return string The url.
*/
public function url($include_host = false, $canonical = false, $include_lang = true, $raw_route = false)
public function url($include_host = false, $canonical = false, $include_base = true, $raw_route = false)
{
// Override any URL when external_url is set
if (isset($this->external_url)) {
return $this->external_url;
}
$grav = Grav::instance();
/** @var Pages $pages */
@@ -1640,41 +1670,25 @@ class Page
/** @var Config $config */
$config = $grav['config'];
/** @var Language $language */
$language = $grav['language'];
/** @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();
} else {
$pre_route = '';
}
// get base route (multisite base and language)
$route = $include_base ? $pages->baseRoute() : '';
// add full route if configured to do so
if ($config->get('system.absolute_urls', false)) {
if (!$include_host && $config->get('system.absolute_urls', false)) {
$include_host = true;
}
// get canonical route if requested
if ($canonical) {
$route = $pre_route . $this->routeCanonical();
$route .= $this->routeCanonical();
} elseif ($raw_route) {
$route = $pre_route . $this->rawRoute();
$route .= $this->rawRoute();
} else {
$route = $pre_route . $this->route();
$route .= $this->route();
}
$rootUrl = $uri->rootUrl($include_host) . $pages->base();
$url = $rootUrl . '/' . trim($route, '/') . $this->urlExtension();
/** @var Uri $uri */
$uri = $grav['uri'];
$url = $uri->rootUrl($include_host) . '/' . trim($route, '/') . $this->urlExtension();
// trim trailing / if not root
if ($url !== '/') {
@@ -1788,7 +1802,7 @@ class Page
public function routeCanonical($var = null)
{
if ($var !== null) {
$this->routes['canonical'] = (array)$var;
$this->routes['canonical'] = $var;
}
if (!empty($this->routes) && isset($this->routes['canonical'])) {
@@ -2953,4 +2967,26 @@ class Page
return $route;
}
}
/**
* Gets the Page Unmodified (original) version of the page.
*
* @return Page
* The original version of the page.
*/
public function getOriginal()
{
return $this->_original;
}
/**
* Gets the action.
*
* @return string
* The Action string.
*/
public function getAction()
{
return $this->_action;
}
}

View File

@@ -49,7 +49,7 @@ class Pages
/**
* @var array|string[]
*/
protected $baseUrl = [];
protected $baseRoute = [];
/**
* @var array|string[]
@@ -120,47 +120,69 @@ class Pages
if ($path !== null) {
$path = trim($path, '/');
$this->base = $path ? '/' . $path : null;
$this->baseUrl = [];
$this->baseRoute = [];
}
return $this->base;
}
/**
*
* Get base route for Grav pages.
*
* @param string $lang Optional language code for multilingual routes.
*
* @return string
*/
public function baseRoute($lang = null)
{
$key = $lang ?: 'default';
if (!isset($this->baseRoute[$key])) {
/** @var Language $language */
$language = $this->grav['language'];
$path_base = rtrim($this->base(), '/');
$path_lang = $language->enabled() ? $language->getLanguageURLPrefix($lang) : '';
$this->baseRoute[$key] = $path_base . $path_lang;
}
return $this->baseRoute[$key];
}
/**
*
* Get route for Grav site.
*
* @param string $route Optional route to the page.
* @param string $lang Optional language code for multilingual links.
*
* @return string
*/
public function route($route = '/', $lang = null)
{
if (!$route || $route === '/') {
return $this->baseRoute($lang) ?: '/';
}
return $this->baseRoute($lang) . $route;
}
/**
*
* Get base URL for Grav pages.
*
* @param string $lang Optional language code for multilingual links.
* @param bool $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
* @param string $lang Optional language code for multilingual links.
* @param bool|null $absolute If true, return absolute url, if false, return relative url. Otherwise return default.
*
* @return string
*/
public function baseUrl($lang = null, $absolute = null)
{
$lang = (string) $lang;
$type = $absolute === null ? 'base_url' : ($absolute ? 'base_url_absolute' : 'base_url_relative');
$key = "{$lang} {$type}";
if (!isset($this->baseUrl[$key])) {
/** @var Config $config */
$config = $this->grav['config'];
/** @var Language $language */
$language = $this->grav['language'];
if (!$lang) {
$lang = $language->getActive();
}
$path_append = rtrim($this->grav['pages']->base(), '/');
if ($language->getDefault() !== $lang || $config->get('system.languages.include_default_lang') === true) {
$path_append .= $lang ? '/' . $lang : '';
}
$this->baseUrl[$key] = $this->grav[$type] . $path_append;
}
return $this->baseUrl[$key];
return $this->grav[$type] . $this->baseRoute($lang);
}
/**
@@ -179,7 +201,7 @@ class Pages
/**
*
* Get home URL for Grav site.
* Get URL for Grav site.
*
* @param string $route Optional route to the page.
* @param string $lang Optional language code for multilingual links.
@@ -189,7 +211,7 @@ class Pages
*/
public function url($route = '/', $lang = null, $absolute = null)
{
if ($route === '/') {
if (!$route || $route === '/') {
return $this->homeUrl($lang, $absolute);
}
@@ -469,14 +491,13 @@ class Pages
if ($site_route) {
$page = $this->dispatch($site_route, $all);
} else {
// Try Regex style redirects
$uri = $this->grav['uri'];
$source_url = $route;
$extension = $uri->extension();
if (isset($extension) && !Utils::endsWith($uri->url(), $extension)) {
$source_url.= '.' . $extension;
}
/** @var Uri $uri */
$uri = $this->grav['uri'];
/** @var \Grav\Framework\Uri\Uri $source_url */
$source_url = $uri->uri(false);
// Try Regex style redirects
$site_redirects = $config->get("site.redirects");
if (is_array($site_redirects)) {
foreach ((array)$site_redirects as $pattern => $replace) {
@@ -1008,32 +1029,63 @@ class Pages
throw new \RuntimeException('Fatal error when creating page instances.');
}
$content_exists = false;
$pages_found = new \GlobIterator($directory . '/*' . CONTENT_EXT);
// Build regular expression for all the allowed page extensions.
$page_extensions = $language->getFallbackPageExtensions();
$regex = '/^[^\.]*(' . implode('|', array_map(
function ($str) {
return preg_quote($str, '/');
},
$page_extensions
)) . ')$/';
$folders = [];
$page_found = null;
$page_extension = '';
$last_modified = 0;
if ($pages_found && count($pages_found) > 0) {
$iterator = new \FilesystemIterator($directory);
/** @var \FilesystemIterator $file */
foreach ($iterator as $file) {
$filename = $file->getFilename();
$page_extensions = $language->getFallbackPageExtensions();
// Ignore all hidden files if set.
if ($this->ignore_hidden && $filename && $filename[0] === '.') {
continue;
}
foreach ($page_extensions as $extension) {
foreach ($pages_found as $found) {
if ($found->isDir()) {
continue;
}
$regex = '/^[^\.]*' . preg_quote($extension, '/') . '$/';
if (preg_match($regex, $found->getFilename())) {
$page_found = $found;
$page_extension = $extension;
break 2;
}
// Handle folders later.
if ($file->isDir()) {
// But ignore all folders in ignore list.
if (!\in_array($filename, $this->ignore_folders, true)) {
$folders[] = $file;
}
continue;
}
// Ignore all files in ignore list.
if (\in_array($filename, $this->ignore_files, true)) {
continue;
}
// Update last modified date to match the last updated file in the folder.
$modified = $file->getMTime();
if ($modified > $last_modified) {
$last_modified = $modified;
}
// Page is the one that matches to $page_extensions list with the lowest index number.
if (preg_match($regex, $filename, $matches, PREG_OFFSET_CAPTURE)) {
$ext = $matches[1][0];
if ($page_found === null || array_search($ext, $page_extensions, true) < array_search($page_extension, $page_extensions, true)) {
$page_found = $file;
$page_extension = $ext;
}
}
}
if ($parent && !empty($page_found)) {
$content_exists = false;
if ($parent && $page_found) {
$page->init($page_found, $page_extension);
$content_exists = true;
@@ -1043,48 +1095,31 @@ class Pages
}
}
// set current modified of page
$last_modified = $page->modified();
// Now handle all the folders under the page.
/** @var \FilesystemIterator $file */
foreach ($folders as $file) {
$filename = $file->getFilename();
$iterator = new \FilesystemIterator($directory);
/** @var \DirectoryIterator $file */
foreach ($iterator as $file) {
$name = $file->getFilename();
// Ignore all hidden files if set.
if ($this->ignore_hidden && $name && $name[0] === '.') {
// if folder contains separator, continue
if (Utils::contains($file->getFilename(), $config->get('system.param_sep', ':'))) {
continue;
}
if ($file->isFile()) {
// Update the last modified if it's newer than already found
if (!in_array($file->getBasename(), $this->ignore_files) && ($modified = $file->getMTime()) > $last_modified) {
$last_modified = $modified;
}
} elseif ($file->isDir() && !in_array($file->getFilename(), $this->ignore_folders)) {
if (!$page->path()) {
$page->path($file->getPath());
}
// if folder contains separator, continue
if (Utils::contains($file->getFilename(), $config->get('system.param_sep', ':'))) {
continue;
}
$path = $directory . DS . $filename;
$child = $this->recurse($path, $page);
if (!$page->path()) {
$page->path($file->getPath());
}
if (Utils::startsWith($filename, '_')) {
$child->routable(false);
}
$path = $directory . DS . $name;
$child = $this->recurse($path, $page);
$this->children[$page->path()][$child->path()] = ['slug' => $child->slug()];
if (Utils::startsWith($name, '_')) {
$child->routable(false);
}
$this->children[$page->path()][$child->path()] = ['slug' => $child->slug()];
if ($config->get('system.pages.events.page')) {
$this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
}
if ($config->get('system.pages.events.page')) {
$this->grav->fireEvent('onFolderProcessed', new Event(['page' => $page]));
}
}
@@ -1203,19 +1238,19 @@ class Pages
break;
case 'date':
$list[$key] = $child->date();
$sort_flags = SORT_NUMERIC;
$sort_flags = SORT_REGULAR;
break;
case 'modified':
$list[$key] = $child->modified();
$sort_flags = SORT_NUMERIC;
$sort_flags = SORT_REGULAR;
break;
case 'publish_date':
$list[$key] = $child->publishDate();
$sort_flags = SORT_NUMERIC;
$sort_flags = SORT_REGULAR;
break;
case 'unpublish_date':
$list[$key] = $child->unpublishDate();
$sort_flags = SORT_NUMERIC;
$sort_flags = SORT_REGULAR;
break;
case 'slug':
$list[$key] = $child->slug();

View File

@@ -33,7 +33,7 @@ class Plugins extends Iterator
if (!$directory->isDir()) {
continue;
}
$plugins[] = $directory->getBasename();
$plugins[] = $directory->getFilename();
}
natsort($plugins);

View File

@@ -8,6 +8,10 @@
namespace Grav\Common\Processors;
use Grav\Common\Config\Config;
use Grav\Common\Uri;
use Grav\Common\Utils;
class InitializeProcessor extends ProcessorBase implements ProcessorInterface
{
public $id = 'init';
@@ -15,29 +19,36 @@ class InitializeProcessor extends ProcessorBase implements ProcessorInterface
public function process()
{
$this->container['config']->debug();
/** @var Config $config */
$config = $this->container['config'];
$config->debug();
// Use output buffering to prevent headers from being sent too early.
ob_start();
if ($this->container['config']->get('system.cache.gzip')) {
if ($config->get('system.cache.gzip') && !@ob_start('ob_gzhandler')) {
// Enable zip/deflate with a fallback in case of if browser does not support compressing.
if (!@ob_start("ob_gzhandler")) {
ob_start();
}
ob_start();
}
// Initialize the timezone.
if ($this->container['config']->get('system.timezone')) {
if ($config->get('system.timezone')) {
date_default_timezone_set($this->container['config']->get('system.timezone'));
}
// FIXME: Initialize session should happen later after plugins have been loaded. This is a workaround to fix session issues in AWS.
if ($this->container['config']->get('system.session.initialize', 1) && isset($this->container['session'])) {
if (isset($this->container['session']) && $config->get('system.session.initialize', true)) {
$this->container['session']->init();
}
// Initialize uri.
$this->container['uri']->init();
/** @var Uri $uri */
$uri = $this->container['uri'];
$uri->init();
// Redirect pages with trailing slash if configured to do so.
$path = $uri->path() ?: '/';
if ($path !== '/' && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($path, '/')) {
$this->container->redirect(rtrim($path, '/'));
}
$this->container->setLocale();
}

View File

@@ -0,0 +1,158 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
class Security
{
public static function detectXssFromPages($pages, callable $status = null)
{
$routes = $pages->routes();
// Remove duplicate for homepage
unset($routes['/']);
$list = [];
// // This needs Symfony 4.1 to work
// $status && $status([
// 'type' => 'count',
// 'steps' => count($routes),
// ]);
foreach ($routes as $path) {
$status && $status([
'type' => 'progress',
]);
try {
$page = $pages->get($path);
// call the content to load/cache it
$header = (array) $page->header();
$content = $page->value('content');
$data = ['header' => $header, 'content' => $content];
$results = Security::detectXssFromArray($data);
if (!empty($results)) {
$list[$page->filePathClean()] = $results;
}
} catch (\Exception $e) {
continue;
}
}
return $list;
}
/**
* @param array $array Array such as $_POST or $_GET
* @param string $prefix Prefix for returned values.
* @return array Returns flatten list of potentially dangerous input values, such as 'data.content'.
*/
public static function detectXssFromArray(array $array, $prefix = '')
{
$list = [];
foreach ($array as $key => $value) {
if (\is_array($value)) {
$list[] = static::detectXssFromArray($value, $prefix . $key . '.');
}
if ($result = static::detectXss($value)) {
$list[] = [$prefix . $key => $result];
}
}
if (!empty($list)) {
return array_merge(...$list);
}
return $list;
}
/**
* Determine if string potentially has a XSS attack. This simple function does not catch all XSS and it is likely to
* return false positives because of it tags all potentially dangerous HTML tags and attributes without looking into
* their content.
*
* @param string $string The string to run XSS detection logic on
* @return boolean|string Type of XSS vector if the given `$string` may contain XSS, false otherwise.
*
* Copies the code from: https://github.com/symphonycms/xssfilter/blob/master/extension.driver.php#L138
*/
public static function detectXss($string)
{
// Skip any null or non string values
if (null === $string || !\is_string($string) || empty($string)) {
return false;
}
// Keep a copy of the original string before cleaning up
$orig = $string;
// URL decode
$string = urldecode($string);
// Convert Hexadecimals
$string = (string)preg_replace_callback('!(&#|\\\)[xX]([0-9a-fA-F]+);?!u', function($m) {
return \chr(hexdec($m[2]));
}, $string);
// Clean up entities
$string = preg_replace('!(&#0+[0-9]+)!u','$1;', $string);
// Decode entities
$string = html_entity_decode($string, ENT_NOQUOTES, 'UTF-8');
// Strip whitespace characters
$string = preg_replace('!\s!u','', $string);
$config = Grav::instance()['config'];
$dangerous_tags = $config->get('security.xss_dangerous_tags');
$dangerous_tags = array_map('preg_quote', array_map("trim", $dangerous_tags));
$enabled_rules = $config->get('security.xss_enabled');
// Set the patterns we'll test against
$patterns = [
// Match any attribute starting with "on" or xmlns
'on_events' => '#(<[^>]+[[a-z\x00-\x20\"\'\/])(\son|\sxmlns)[a-z].*=>?#iUu',
// Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols
'invalid_protocols' => '#((java|live|vb)script|mocha|feed|data):.*?#iUu',
// Match -moz-bindings
'moz_binding' => '#-moz-binding[a-z\x00-\x20]*:#u',
// Match style attributes
'html_inline_styles' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(style=[^>]*(url\:|x\:expression).*)>?#iUu',
// Match potentially dangerous tags
'dangerous_tags' => '#</*(' . implode('|', $dangerous_tags ) . ')[^>]*>?#ui'
];
// Iterate over rules and return label if fail
foreach ((array) $patterns as $name => $regex) {
if ($enabled_rules[$name] === true) {
if (preg_match($regex, $string) || preg_match($regex, $orig)) {
return $name;
}
}
}
return false;
}
}

View File

@@ -16,6 +16,7 @@ use Grav\Common\Config\ConfigFileFinder;
use Grav\Common\Config\Setup;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
class ConfigServiceProvider implements ServiceProviderInterface
@@ -31,7 +32,14 @@ class ConfigServiceProvider implements ServiceProviderInterface
};
$container['config'] = function ($c) {
return static::load($c);
$config = static::load($c);
// After configuration has been loaded, we can disable YAML compatibility if strict mode has been enabled.
if (!$config->get('system.strict_mode.yaml_compat', true)) {
YamlFile::globalSettings(['compat' => false, 'native' => true]);
}
return $config;
};
$container['languages'] = function ($c) {
@@ -65,6 +73,10 @@ class ConfigServiceProvider implements ServiceProviderInterface
return $blueprints->name("master-{$setup->environment}")->load();
}
/**
* @param Container $container
* @return Config
*/
public static function load(Container $container)
{
/** Setup $setup */

View File

@@ -8,6 +8,7 @@
namespace Grav\Common\Service;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
@@ -26,35 +27,33 @@ class PageServiceProvider implements ServiceProviderInterface
/** @var Pages $pages */
$pages = $c['pages'];
/** @var Config $config */
$config = $c['config'];
/** @var Uri $uri */
$uri = $c['uri'];
$path = $uri->path(); // Don't trim to support trailing slash default routes
$path = $path ?: '/';
$path = $uri->path() ?: '/'; // Don't trim to support trailing slash default routes
$page = $pages->dispatch($path);
// Redirection tests
if ($page) {
/** @var Language $language */
$language = $c['language'];
// some debugger override logic
if ($page->debugger() === false) {
$c['debugger']->enabled(false);
}
if ($c['config']->get('system.force_ssl')) {
if (!isset($_SERVER['HTTPS']) || $_SERVER["HTTPS"] != "on") {
$url = "https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
if ($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();
$url = $pages->route($page->route());
if ($uri->params()) {
if ($url == '/') { //Avoid double slash
if ($url === '/') { //Avoid double slash
$url = $uri->params();
} else {
$url .= $uri->params();
@@ -67,18 +66,16 @@ class PageServiceProvider implements ServiceProviderInterface
$url .= '#' . $uri->fragment();
}
/** @var Language $language */
$language = $c['language'];
// Language-specific redirection scenarios
if ($language->enabled()) {
if ($language->isLanguageInUrl() && !$language->isIncludeDefaultLanguage()) {
$c->redirect($url);
}
if (!$language->isLanguageInUrl() && $language->isIncludeDefaultLanguage()) {
$c->redirectLangSafe($url);
}
if ($language->enabled() && ($language->isLanguageInUrl() xor $language->isIncludeDefaultLanguage())) {
$c->redirect($url);
}
// Default route test and redirect
if ($c['config']->get('system.pages.redirect_default_route') && $page->route() != $path) {
$c->redirectLangSafe($url);
if ($config->get('system.pages.redirect_default_route') && $page->route() !== $path) {
$c->redirect($url);
}
}

View File

@@ -29,21 +29,22 @@ class SessionServiceProvider implements ServiceProviderInterface
/** @var Uri $uri */
$uri = $c['uri'];
// Get session parameters.
$session_timeout = (int)$config->get('system.session.timeout', 1800);
$session_path = $config->get('system.session.path');
if (null === $session_path) {
$session_path = '/' . ltrim(Uri::filterPath($uri->rootUrl(false)), '/');
}
$domain = $uri->host();
if ($domain === 'localhost') {
$domain = '';
}
// Get session options.
$secure = (bool)$config->get('system.session.secure', false);
$httponly = (bool)$config->get('system.session.httponly', true);
$enabled = (bool)$config->get('system.session.enabled', false);
$cookie_secure = (bool)$config->get('system.session.secure', false);
$cookie_httponly = (bool)$config->get('system.session.httponly', true);
$cookie_lifetime = (int)$config->get('system.session.timeout', 1800);
$cookie_path = $config->get('system.session.path');
if (null === $cookie_path) {
$cookie_path = '/' . trim(Uri::filterPath($uri->rootUrl(false)), '/');
}
// Session cookie path requires trailing slash.
$cookie_path = rtrim($cookie_path, '/') . '/';
$cookie_domain = $uri->host();
if ($cookie_domain === 'localhost') {
$cookie_domain = '';
}
// Activate admin if we're inside the admin path.
$is_admin = false;
@@ -56,14 +57,14 @@ class SessionServiceProvider implements ServiceProviderInterface
// Check no language, simple language prefix (en) and region specific language prefix (en-US).
$pos = strpos($current_route, $base);
if ($pos === 0 || $pos === 3 || $pos === 6) {
$session_timeout = $config->get('plugins.admin.session.timeout', 1800);
$cookie_lifetime = $config->get('plugins.admin.session.timeout', 1800);
$enabled = $is_admin = true;
}
}
// Fix for HUGE session timeouts.
if ($session_timeout > 99999999999) {
$session_timeout = 9999999999;
if ($cookie_lifetime > 99999999999) {
$cookie_lifetime = 9999999999;
}
$inflector = new Inflector();
@@ -73,10 +74,16 @@ class SessionServiceProvider implements ServiceProviderInterface
}
// Define session service.
$session = new Session($session_timeout, $session_path, $domain);
$session->setName($session_name);
$session->setSecure($secure);
$session->setHttpOnly($httponly);
$options = [
'name' => $session_name,
'cookie_lifetime' => $cookie_lifetime,
'cookie_path' => $cookie_path,
'cookie_domain' => $cookie_domain,
'cookie_secure' => $cookie_secure,
'cookie_httponly' => $cookie_httponly
] + (array) $config->get('system.session.options');
$session = new Session($options);
$session->setAutoStart($enabled);
return $session;
@@ -84,7 +91,7 @@ class SessionServiceProvider implements ServiceProviderInterface
// Define session message service.
$container['messages'] = function ($c) {
if (!isset($c['session']) || !$c['session']->started()) {
if (!isset($c['session']) || !$c['session']->isStarted()) {
/** @var Debugger $debugger */
$debugger = $c['debugger'];
$debugger->addMessage('Inactive session: session messages may disappear', 'warming');

View File

@@ -8,24 +8,20 @@
namespace Grav\Common;
use RocketTheme\Toolbox\Session\Session as BaseSession;
class Session extends BaseSession
class Session extends \Grav\Framework\Session\Session
{
/** @var bool */
protected $autoStart = false;
/**
* @param int $lifetime Defaults to 1800 seconds.
* @param string $path Cookie path.
* @param string $domain Optional, domain for the session
* @throws \RuntimeException
* @return \Grav\Framework\Session\Session
* @deprecated 1.5 Use getInstance() method instead
*/
public function __construct($lifetime, $path, $domain = null)
public static function instance()
{
if (php_sapi_name() !== 'cli') {
parent::__construct($lifetime, $path, $domain);
}
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getInstance() method instead', E_USER_DEPRECATED);
return static::getInstance();
}
/**
@@ -54,25 +50,29 @@ class Session extends BaseSession
}
/**
* @param bool $secure
* @return $this
* Returns attributes.
*
* @return array Attributes
* @deprecated 1.5 Use getAll() method instead
*/
public function setSecure($secure)
public function all()
{
ini_set('session.cookie_secure', (bool)$secure);
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getAll() method instead', E_USER_DEPRECATED);
return $this;
return $this->getAll();
}
/**
* @param bool $httponly
* @return $this
* Checks if the session was started.
*
* @return Boolean
* @deprecated 1.5 Use isStarted() method instead
*/
public function setHttpOnly($httponly)
public function started()
{
ini_set('session.cookie_httponly', (bool)$httponly);
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use isStarted() method instead', E_USER_DEPRECATED);
return $this;
return $this->isStarted();
}
/**

View File

@@ -98,7 +98,7 @@ class Themes extends Iterator
continue;
}
$theme = $directory->getBasename();
$theme = $directory->getFilename();
$result = self::get($theme);
if ($result) {

View File

@@ -12,7 +12,7 @@ class TwigNodeMarkdown extends \Twig_Node implements \Twig_NodeOutputInterface
{
public function __construct(\Twig_Node $body, $lineno, $tag = 'markdown')
{
parent::__construct(array('body' => $body), array(), $lineno, $tag);
parent::__construct(['body' => $body], [], $lineno, $tag);
}
/**
* Compiles the node to PHP.

View File

@@ -8,13 +8,13 @@
namespace Grav\Common\Twig\Node;
class TwigNodeScript extends \Twig_Node implements \Twig_NodeOutputInterface
class TwigNodeScript extends \Twig_Node implements \Twig_NodeCaptureInterface
{
protected $tagName = 'script';
/**
* TwigNodeScript constructor.
* @param \Twig_NodeInterface|null $body
* @param \Twig_Node|null $body
* @param \Twig_Node_Expression|null $file
* @param \Twig_Node_Expression|null $group
* @param \Twig_Node_Expression|null $priority
@@ -23,12 +23,12 @@ class TwigNodeScript extends \Twig_Node implements \Twig_NodeOutputInterface
* @param string|null $tag
*/
public function __construct(
\Twig_NodeInterface $body = null,
\Twig_Node $body = null,
\Twig_Node_Expression $file = null,
\Twig_Node_Expression $group = null,
\Twig_Node_Expression $priority = null,
\Twig_Node_Expression $attributes = null,
$lineno,
$lineno = 0,
$tag = null
)
{

View File

@@ -8,24 +8,24 @@
namespace Grav\Common\Twig\Node;
class TwigNodeStyle extends \Twig_Node implements \Twig_NodeOutputInterface
class TwigNodeStyle extends \Twig_Node implements \Twig_NodeCaptureInterface
{
protected $tagName = 'style';
/**
* TwigNodeAssets constructor.
* @param \Twig_NodeInterface|null $body
* @param \Twig_Node|null $body
* @param \Twig_Node_Expression|null $attributes
* @param int $lineno
* @param null $tag
*/
public function __construct(
\Twig_NodeInterface $body = null,
\Twig_Node $body = null,
\Twig_Node_Expression $file = null,
\Twig_Node_Expression $group = null,
\Twig_Node_Expression $priority = null,
\Twig_Node_Expression $attributes = null,
$lineno,
$lineno = 0,
$tag = null
)
{

View File

@@ -8,9 +8,15 @@
namespace Grav\Common\Twig\Node;
class TwigNodeSwitch extends \Twig_Node implements \Twig_NodeOutputInterface
class TwigNodeSwitch extends \Twig_Node
{
public function __construct(\Twig_NodeInterface $value, \Twig_NodeInterface $cases, \Twig_NodeInterface $default = null, $lineno, $tag = null)
public function __construct(
\Twig_Node $value,
\Twig_Node $cases,
\Twig_Node $default = null,
$lineno = 0,
$tag = null
)
{
parent::__construct(array('value' => $value, 'cases' => $cases, 'default' => $default), array(), $lineno, $tag);
}
@@ -24,20 +30,17 @@ class TwigNodeSwitch extends \Twig_Node implements \Twig_NodeOutputInterface
{
$compiler
->addDebugInfo($this)
->write("switch (")
->write('switch (')
->subcompile($this->getNode('value'))
->raw(") {\n")
->indent();
foreach ($this->getNode('cases') as $case)
{
if (!$case->hasNode('body'))
{
foreach ($this->getNode('cases') as $case) {
if (!$case->hasNode('body')) {
continue;
}
foreach ($case->getNode('values') as $value)
{
foreach ($case->getNode('values') as $value) {
$compiler
->write('case ')
->subcompile($value)
@@ -53,8 +56,7 @@ class TwigNodeSwitch extends \Twig_Node implements \Twig_NodeOutputInterface
->write("}\n");
}
if ($this->hasNode('default') && $this->getNode('default') !== null)
{
if ($this->hasNode('default') && $this->getNode('default') !== null) {
$compiler
->write("default:\n")
->write("{\n")

View File

@@ -10,7 +10,12 @@ namespace Grav\Common\Twig\Node;
class TwigNodeTryCatch extends \Twig_Node
{
public function __construct(\Twig_NodeInterface $try, \Twig_NodeInterface $catch = null, $lineno, $tag = null)
public function __construct(
\Twig_Node $try,
\Twig_Node $catch = null,
$lineno = 0,
$tag = null
)
{
parent::__construct(array('try' => $try, 'catch' => $catch), array(), $lineno, $tag);
}

View File

@@ -27,7 +27,7 @@ class TwigTokenParserScript extends \Twig_TokenParser
*
* @param \Twig_Token $token A Twig_Token instance
*
* @return \Twig_NodeInterface A Twig_NodeInterface instance
* @return \Twig_Node A Twig_Node instance
*/
public function parse(\Twig_Token $token)
{

View File

@@ -26,7 +26,7 @@ class TwigTokenParserStyle extends \Twig_TokenParser
*
* @param \Twig_Token $token A Twig_Token instance
*
* @return \Twig_NodeInterface A Twig_NodeInterface instance
* @return \Twig_Node A Twig_Node instance
*/
public function parse(\Twig_Token $token)
{

View File

@@ -37,8 +37,7 @@ class TwigTokenParserSwitch extends \Twig_TokenParser
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
// There can be some whitespace between the {% switch %} and first {% case %} tag.
while ($stream->getCurrent()->getType() == \Twig_Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) == '')
{
while ($stream->getCurrent()->getType() === \Twig_Token::TEXT_TYPE && trim($stream->getCurrent()->getValue()) === '') {
$stream->next();
}
@@ -47,56 +46,45 @@ class TwigTokenParserSwitch extends \Twig_TokenParser
$expressionParser = $this->parser->getExpressionParser();
$default = null;
$cases = array();
$cases = [];
$end = false;
while (!$end)
{
while (!$end) {
$next = $stream->next();
switch ($next->getValue())
{
switch ($next->getValue()) {
case 'case':
{
$values = array();
$values = [];
while (true)
{
$values[] = $expressionParser->parsePrimaryExpression();
// Multiple allowed values?
if ($stream->test(\Twig_Token::OPERATOR_TYPE, 'or'))
{
$stream->next();
}
else
{
break;
}
while (true) {
$values[] = $expressionParser->parsePrimaryExpression();
// Multiple allowed values?
if ($stream->test(\Twig_Token::OPERATOR_TYPE, 'or')) {
$stream->next();
} else {
break;
}
}
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideIfFork'));
$cases[] = new \Twig_Node([
'values' => new \Twig_Node($values),
'body' => $body
]);
break;
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
$body = $this->parser->subparse(array($this, 'decideIfFork'));
$cases[] = new \Twig_Node(array(
'values' => new \Twig_Node($values),
'body' => $body
));
break;
}
case 'default':
{
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
$default = $this->parser->subparse(array($this, 'decideIfEnd'));
break;
}
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
$default = $this->parser->subparse(array($this, 'decideIfEnd'));
break;
case 'endswitch':
{
$end = true;
break;
}
$end = true;
break;
default:
{
throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1);
}
throw new \Twig_Error_Syntax(sprintf('Unexpected end of template. Twig was looking for the following tags "case", "default", or "endswitch" to close the "switch" block started at line %d)', $lineno), -1);
}
}
@@ -127,7 +115,6 @@ class TwigTokenParserSwitch extends \Twig_TokenParser
return $token->test(array('endswitch'));
}
/**
* {@inheritdoc}
*/

View File

@@ -28,7 +28,7 @@ class TwigTokenParserTryCatch extends \Twig_TokenParser
*
* @param \Twig_Token $token A Twig_Token instance
*
* @return \Twig_NodeInterface A Twig_NodeInterface instance
* @return \Twig_Node A Twig_Node instance
*/
public function parse(\Twig_Token $token)
{

View File

@@ -102,6 +102,28 @@ class Twig
$this->loader = new \Twig_Loader_Filesystem($this->twig_paths);
// Register all other prefixes as namespaces in twig
foreach ($locator->getPaths('theme') as $prefix => $_) {
if ($prefix === '') {
continue;
}
$twig_paths = [];
// handle language templates if available
if ($language->enabled()) {
$lang_templates = $locator->findResource('theme://'.$prefix.'templates/' . ($active_language ? $active_language : $language->getDefault()));
if ($lang_templates) {
$twig_paths[] = $lang_templates;
}
}
$twig_paths = array_merge($twig_paths, $locator->findResources('theme://'.$prefix.'templates'));
$namespace = trim($prefix, '/');
$this->loader->setPaths($twig_paths, $namespace);
}
$this->grav->fireEvent('onTwigLoader');
$this->loaderArray = new \Twig_Loader_Array([]);
@@ -113,8 +135,15 @@ class Twig
$params['cache'] = new \Twig_Cache_Filesystem($cachePath, \Twig_Cache_Filesystem::FORCE_BYTECODE_INVALIDATION);
}
if (!empty($this->autoescape)) {
$params['autoescape'] = $this->autoescape;
if (!$config->get('system.strict_mode.twig_compat', true)) {
// Force autoescape on for all files if in strict mode.
$params['autoescape'] = 'html';
} elseif (!empty($this->autoescape)) {
$params['autoescape'] = $this->autoescape ? 'html' : false;
}
if (empty($params['autoescape'])) {
user_error('Grav 2.0 will have Twig auto-escaping forced on (can be emulated by turning off \'system.strict_mode.twig_compat\' setting in your configuration)', E_USER_DEPRECATED);
}
$this->twig = new TwigEnvironment($loader_chain, $params);
@@ -122,10 +151,10 @@ class Twig
if ($config->get('system.twig.undefined_functions')) {
$this->twig->registerUndefinedFunctionCallback(function ($name) {
if (function_exists($name)) {
return new \Twig_Function_Function($name);
return new \Twig_SimpleFunction($name, $name);
}
return new \Twig_Function_Function(function () {
return new \Twig_SimpleFunction($name, function () {
});
});
}
@@ -133,10 +162,10 @@ class Twig
if ($config->get('system.twig.undefined_filters')) {
$this->twig->registerUndefinedFilterCallback(function ($name) {
if (function_exists($name)) {
return new \Twig_Filter_Function($name);
return new \Twig_SimpleFilter($name, $name);
}
return new \Twig_Filter_Function(function () {
return new \Twig_SimpleFilter($name, function () {
});
});
}
@@ -145,7 +174,7 @@ class Twig
// set default date format if set in config
if ($config->get('system.pages.dateformat.long')) {
$this->twig->getExtension('core')->setDateFormat($config->get('system.pages.dateformat.long'));
$this->twig->getExtension('Twig_Extension_Core')->setDateFormat($config->get('system.pages.dateformat.long'));
}
// enable the debug extension if required
if ($config->get('system.twig.debug')) {
@@ -159,7 +188,7 @@ class Twig
$pages = $this->grav['pages'];
// Set some standard variables for twig
$this->twig_vars = $this->twig_vars + [
$this->twig_vars += [
'config' => $config,
'system' => $config->get('system'),
'theme' => $config->get('theme'),
@@ -238,7 +267,9 @@ class Twig
// Process Modular Twig
if ($item->modularTwig()) {
$twig_vars['content'] = $content;
$template = $item->template() . TEMPLATE_EXT;
$extension = $item->templateFormat();
$extension = $extension ? ".{$extension}.twig" : TEMPLATE_EXT;
$template = $item->template() . $extension;
$output = $content = $local_twig->render($template, $twig_vars);
}
@@ -406,8 +437,14 @@ class Twig
* Overrides the autoescape setting
*
* @param boolean $state
* @deprecated 1.5
*/
public function setAutoescape($state) {
public function setAutoescape($state)
{
if (!$state) {
user_error(__CLASS__ . '::' . __FUNCTION__ . '(false) is deprecated since Grav 1.5', E_USER_DEPRECATED);
}
$this->autoescape = (bool) $state;
}
}

View File

@@ -11,17 +11,19 @@ namespace Grav\Common\Twig;
use Grav\Common\Grav;
use Grav\Common\Page\Collection;
use Grav\Common\Page\Media;
use Grav\Common\Security;
use Grav\Common\Twig\TokenParser\TwigTokenParserScript;
use Grav\Common\Twig\TokenParser\TwigTokenParserStyle;
use Grav\Common\Twig\TokenParser\TwigTokenParserSwitch;
use Grav\Common\Twig\TokenParser\TwigTokenParserTryCatch;
use Grav\Common\Twig\TokenParser\TwigTokenParserMarkdown;
use Grav\Common\User\User;
use Grav\Common\Utils;
use Grav\Common\Yaml;
use Grav\Common\Markdown\Parsedown;
use Grav\Common\Markdown\ParsedownExtra;
use Grav\Common\Helpers\Base32;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Yaml\Yaml;
class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface
{
@@ -63,20 +65,20 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_SimpleFilter('absolute_url', [$this, 'absoluteUrlFilter']),
new \Twig_SimpleFilter('contains', [$this, 'containsFilter']),
new \Twig_SimpleFilter('chunk_split', [$this, 'chunkSplitFilter']),
new \Twig_SimpleFilter('nicenumber', [$this, 'niceNumberFunc']),
new \Twig_SimpleFilter('nicefilesize', [$this, 'niceFilesizeFunc']),
new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFunc']),
new \Twig_SimpleFilter('defined', [$this, 'definedDefaultFilter']),
new \Twig_SimpleFilter('ends_with', [$this, 'endsWithFilter']),
new \Twig_SimpleFilter('fieldName', [$this, 'fieldNameFilter']),
new \Twig_SimpleFilter('ksort', [$this, 'ksortFilter']),
new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction']),
new \Twig_SimpleFilter('markdown', [$this, 'markdownFunction'], ['is_safe' => ['html']]),
new \Twig_SimpleFilter('md5', [$this, 'md5Filter']),
new \Twig_SimpleFilter('base32_encode', [$this, 'base32EncodeFilter']),
new \Twig_SimpleFilter('base32_decode', [$this, 'base32DecodeFilter']),
new \Twig_SimpleFilter('base64_encode', [$this, 'base64EncodeFilter']),
new \Twig_SimpleFilter('base64_decode', [$this, 'base64DecodeFilter']),
new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFilter']),
new \Twig_SimpleFilter('randomize', [$this, 'randomizeFilter']),
new \Twig_SimpleFilter('modulus', [$this, 'modulusFilter']),
new \Twig_SimpleFilter('rtrim', [$this, 'rtrimFilter']),
@@ -87,9 +89,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_SimpleFilter('safe_truncate_html', ['\Grav\Common\Utils', 'safeTruncateHTML']),
new \Twig_SimpleFilter('sort_by_key', [$this, 'sortByKeyFilter']),
new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']),
new \Twig_SimpleFilter('t', [$this, 'translate']),
new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']),
new \Twig_SimpleFilter('ta', [$this, 'translateArray']),
new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils', 'truncate']),
new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils', 'truncateHTML']),
new \Twig_SimpleFilter('json_decode', [$this, 'jsonDecodeFilter']),
@@ -99,6 +98,18 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_SimpleFilter('print_r', 'print_r'),
new \Twig_SimpleFilter('yaml_encode', [$this, 'yamlEncodeFilter']),
new \Twig_SimpleFilter('yaml_decode', [$this, 'yamlDecodeFilter']),
// Translations
new \Twig_SimpleFilter('t', [$this, 'translate']),
new \Twig_SimpleFilter('tl', [$this, 'translateLanguage']),
new \Twig_SimpleFilter('ta', [$this, 'translateArray']),
// Casting values
new \Twig_SimpleFilter('string', [$this, 'stringFilter']),
new \Twig_SimpleFilter('int', [$this, 'intFilter'], ['is_safe' => ['all']]),
new \Twig_SimpleFilter('bool', [$this, 'boolFilter']),
new \Twig_SimpleFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
new \Twig_SimpleFilter('array', [$this, 'arrayFilter']),
];
}
@@ -110,7 +121,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
public function getFunctions()
{
return [
new \Twig_SimpleFunction('array', [$this, 'arrayFunc']),
new \Twig_SimpleFunction('array', [$this, 'arrayFilter']),
new \Twig_SimpleFunction('array_key_value', [$this, 'arrayKeyValueFunc']),
new \Twig_SimpleFunction('array_key_exists', 'array_key_exists'),
new \Twig_SimpleFunction('array_unique', 'array_unique'),
@@ -129,10 +140,8 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_simpleFunction('random_string', [$this, 'randomStringFunc']),
new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']),
new \Twig_SimpleFunction('regex_replace', [$this, 'regexReplace']),
new \Twig_SimpleFunction('regex_filter', [$this, 'regexFilter']),
new \Twig_SimpleFunction('string', [$this, 'stringFunc']),
new \Twig_simpleFunction('t', [$this, 'translate']),
new \Twig_simpleFunction('tl', [$this, 'translateLanguage']),
new \Twig_simpleFunction('ta', [$this, 'translateArray']),
new \Twig_SimpleFunction('url', [$this, 'urlFunc']),
new \Twig_SimpleFunction('json_decode', [$this, 'jsonDecodeFilter']),
new \Twig_SimpleFunction('get_cookie', [$this, 'getCookie']),
@@ -145,7 +154,15 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
new \Twig_SimpleFunction('theme_var', [$this, 'themeVarFunc']),
new \Twig_SimpleFunction('header_var', [$this, 'pageHeaderVarFunc']),
new \Twig_SimpleFunction('read_file', [$this, 'readFileFunc']),
new \Twig_SimpleFunction('nicenumber', [$this, 'niceNumberFunc']),
new \Twig_SimpleFunction('nicefilesize', [$this, 'niceFilesizeFunc']),
new \Twig_SimpleFunction('nicetime', [$this, 'nicetimeFunc']),
new \Twig_SimpleFunction('xss', [$this, 'xssFunc']),
// Translations
new \Twig_simpleFunction('t', [$this, 'translate']),
new \Twig_simpleFunction('tl', [$this, 'translateLanguage']),
new \Twig_simpleFunction('ta', [$this, 'translateArray']),
];
}
@@ -367,25 +384,14 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*
* @param array $input
* @param string $filter
* @param array|int $direction
* @param int $direction
* @param int $sort_flags
*
* @return array
*/
public function sortByKeyFilter($input, $filter, $direction = SORT_ASC)
public function sortByKeyFilter($input, $filter, $direction = SORT_ASC, $sort_flags = SORT_REGULAR)
{
$output = [];
if (!is_array($input) || !$input) {
return $output;
}
foreach ($input as $key => $row) {
$output[$key] = $row[$filter];
}
array_multisort($output, $direction, $input);
return $input;
return Utils::sortArrayByKey($input, $filter, $direction, $sort_flags);
}
/**
@@ -439,7 +445,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*
* @return boolean
*/
public function nicetimeFilter($date, $long_strings = true)
public function nicetimeFunc($date, $long_strings = true)
{
if (empty($date)) {
return $this->grav['language']->translate('NICETIME.NO_DATE_PROVIDED', null, true);
@@ -526,6 +532,27 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
return "$difference $periods[$j] {$tense}";
}
/**
* Allow quick check of a string for XSS Vulnerabilities
*
* @param $string
* @return bool|string|array
*/
public function xssFunc($data)
{
if (is_array($data)) {
$results = Security::detectXssFromArray($data);
} else {
return Security::detectXss($data);
}
$results_parts = array_map(function($value, $key) {
return $key.': \''.$value . '\'';
}, array_values($results), array_keys($results));
return implode(', ', $results_parts);
}
/**
* @param $string
*
@@ -623,6 +650,62 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
return ltrim($value, $chars);
}
/**
* Casts input to string.
*
* @param mixed $input
* @return string
*/
public function stringFilter($input)
{
return (string) $input;
}
/**
* Casts input to int.
*
* @param mixed $input
* @return int
*/
public function intFilter($input)
{
return (int) $input;
}
/**
* Casts input to bool.
*
* @param mixed $input
* @return bool
*/
public function boolFilter($input)
{
return (bool) $input;
}
/**
* Casts input to float.
*
* @param mixed $input
* @return float
*/
public function floatFilter($input)
{
return (float) $input;
}
/**
* Casts input to array.
*
* @param mixed $input
* @return array
*/
public function arrayFilter($input)
{
return (array) $input;
}
/**
* @return mixed
*/
@@ -699,7 +782,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
$template = $env->createTemplate($twig);
return $template->render($context);
;
}
/**
@@ -754,7 +836,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
* Output a Gist
*
* @param string $id
* @param string $file
* @param string|bool $file
*
* @return string
*/
@@ -794,19 +876,6 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
return str_pad($input, (int)$pad_length, $pad_string, $pad_type);
}
/**
* Cast a value to array
*
* @param $value
*
* @return array
*/
public function arrayFunc($value)
{
return (array)$value;
}
/**
* Workaround for twig associative array initialization
* Returns a key => val array
@@ -882,7 +951,10 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
*/
public function authorize($action)
{
if (!$this->grav['user']->authenticated) {
/** @var User $user */
$user = $this->grav['user'];
if (!$user->authenticated || (isset($user->authorized) && !$user->authorized)) {
return false;
}
@@ -891,7 +963,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
$prefix = is_int($key) ? '' : $key . '.';
$perms = $prefix ? (array) $perms : [$perms => true];
foreach ($perms as $action2 => $authenticated) {
if ($this->grav['user']->authorize($prefix . $action2)) {
if ($user->authorize($prefix . $action2)) {
return $authenticated;
}
}
@@ -958,6 +1030,18 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
return preg_replace($pattern, $replace, $subject, $limit);
}
/**
* Twig wrapper for PHP's preg_grep method
*
* @param $array
* @param $regex
* @param int $flags
* @return array
*/
public function regexFilter($array, $regex, $flags = 0) {
return preg_grep($regex, $array, $flags);
}
/**
* redirect browser from twig
*
@@ -967,7 +1051,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
public function redirectFunc($url, $statusCode = 303)
{
header('Location: ' . $url, true, $statusCode);
die();
exit();
}
/**
@@ -1051,7 +1135,7 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
if (file_exists($filepath)) {
return file_get_contents($filepath);
}
}
return false;
}
@@ -1088,6 +1172,43 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
var_dump($var);
}
/**
* Returns a nicer more readable filesize based on bytes
*
* @param $bytes
* @return string
*/
public function niceFilesizeFunc($bytes)
{
if ($bytes >= 1073741824)
{
$bytes = number_format($bytes / 1073741824, 2) . ' GB';
}
elseif ($bytes >= 1048576)
{
$bytes = number_format($bytes / 1048576, 2) . ' MB';
}
elseif ($bytes >= 1024)
{
$bytes = number_format($bytes / 1024, 1) . ' KB';
}
elseif ($bytes > 1)
{
$bytes = $bytes . ' bytes';
}
elseif ($bytes == 1)
{
$bytes = $bytes . ' byte';
}
else
{
$bytes = '0 bytes';
}
return $bytes;
}
/**
* Returns a nicer more readable number
*
@@ -1199,11 +1320,12 @@ class TwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsIn
* Dump/Encode data into YAML format
*
* @param $data
* @param $inline integer number of levels of inline syntax
* @return mixed
*/
public function yamlEncodeFilter($data)
public function yamlEncodeFilter($data, $inline = 10)
{
return Yaml::dump($data, 10);
return Yaml::dump($data, $inline);
}
/**

View File

@@ -11,9 +11,11 @@ namespace Grav\Common;
use Grav\Common\Config\Config;
use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
use Grav\Common\Page\Pages;
use Grav\Framework\Route\RouteFactory;
use Grav\Framework\Uri\UriFactory;
use Grav\Framework\Uri\UriPartsFilter;
use RocketTheme\Toolbox\Event\Event;
class Uri
{
@@ -49,6 +51,8 @@ class Uri
protected $root;
protected $root_path;
protected $uri;
protected $content_type;
protected $post;
/**
* Uri constructor.
@@ -133,7 +137,12 @@ class Uri
$custom_parts = parse_url($custom_base);
$orig_root_path = $this->root_path;
$this->root_path = isset($custom_parts['path']) ? rtrim($custom_parts['path'], '/') : '';
$this->root = isset($custom_parts['scheme']) ? $custom_base : $this->base . $this->root_path;
if (isset($custom_parts['scheme'])) {
$this->base = $custom_parts['scheme'] . '://' . $custom_parts['host'];
$this->root = $custom_base;
} else {
$this->root = $this->base . $this->root_path;
}
$this->uri = Utils::replaceFirstOccurrence($orig_root_path, $this->root_path, $this->uri);
} else {
$this->root = $this->base . $this->root_path;
@@ -146,13 +155,7 @@ class Uri
// remove the setup.php based base if set:
$setup_base = $grav['pages']->base();
if ($setup_base) {
$uri = str_replace($setup_base, '', $uri);
}
// If configured to, redirect trailing slash URI's with a 302 redirect
$redirect = str_replace($this->root, '', rtrim($uri, '/'));
if ($redirect && $uri !== '/' && $redirect !== $this->base() && $config->get('system.pages.redirect_trailing_slash', false) && Utils::endsWith($uri, '/')) {
$grav->redirect($redirect, 302);
$uri = preg_replace('|^' . preg_quote($setup_base, '|') . '|', '', $uri);
}
// process params
@@ -199,9 +202,9 @@ class Uri
}
// Set some Grav stuff
$grav['base_url_absolute'] = $grav['config']->get('system.custom_base_url') ?: $this->rootUrl(true);
$grav['base_url_absolute'] = $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'];
$grav['base_url'] = $config->get('system.absolute_urls') ? $grav['base_url_absolute'] : $grav['base_url_relative'];
RouteFactory::setRoot($this->root_path);
RouteFactory::setLanguage($language->getLanguageURLPrefix());
@@ -369,6 +372,17 @@ class Uri
return $this->extension;
}
public function method()
{
$method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
if ($method === 'POST' && isset($_SERVER['X-HTTP-METHOD-OVERRIDE'])) {
$method = strtoupper($_SERVER['X-HTTP-METHOD-OVERRIDE']);
}
return $method;
}
/**
* Return the scheme of the URI
*
@@ -464,6 +478,21 @@ class Uri
return $this->basename;
}
/**
* Return the full uri
*
* @param bool $include_root
* @return mixed
*/
public function uri($include_root = true)
{
if ($include_root) {
return $this->uri;
}
return str_replace($this->root_path, '', $this->uri);
}
/**
* Return the base of the URI
*
@@ -484,16 +513,10 @@ class Uri
{
$grav = Grav::instance();
// Link processing should prepend language
$language = $grav['language'];
$language_append = '';
if ($language->enabled()) {
$language_append = $language->getLanguageURLPrefix();
}
/** @var Pages $pages */
$pages = $grav['pages'];
$base = $grav['base_url_relative'];
return rtrim($base . $grav['pages']->base(), '/') . $language_append;
return $pages->baseUrl(null, false);
}
/**
@@ -609,10 +632,9 @@ class Uri
}
return $ip;
}
/**
/**
* Returns current Uri.
*
* @return \Grav\Framework\Uri\Uri
@@ -662,18 +684,19 @@ class Uri
*/
public static function buildUrl($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : (isset($parsed_url['host']) ? '//' : '');
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "{$pass}@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$path = !empty($parsed_url['params']) ? rtrim($path, '/') . static::buildParams($parsed_url['params']) : $path;
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . ':' : '';
$authority = isset($parsed_url['host']) ? '//' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "{$pass}@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$path = !empty($parsed_url['params']) ? rtrim($path, '/') . static::buildParams($parsed_url['params']) : $path;
$query = !empty($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "{$scheme}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}";
return "{$scheme}{$authority}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}";
}
/**
@@ -858,7 +881,26 @@ class Uri
public static function parseUrl($url)
{
$grav = Grav::instance();
$parts = parse_url($url);
$encodedUrl = preg_replace_callback(
'%[^:/@?&=#]+%usD',
function ($matches) { return rawurlencode($matches[0]); },
$url
);
$parts = parse_url($encodedUrl);
if (false === $parts) {
return false;
}
foreach($parts as $name => $value) {
$parts[$name] = rawurldecode($value);
}
if (!isset($parts['path'])) {
$parts['path'] = '';
}
list($stripped_path, $params) = static::extractParams($parts['path'], $grav['config']->get('system.param_sep'));
@@ -1094,8 +1136,12 @@ class Uri
protected function createFromEnvironment(array $env)
{
// Build scheme.
if (isset($env['REQUEST_SCHEME'])) {
$this->scheme = $env['REQUEST_SCHEME'];
if (isset($env['HTTP_X_FORWARDED_PROTO'])) {
$this->scheme = $env['HTTP_X_FORWARDED_PROTO'];
} elseif (isset($env['X-FORWARDED-PROTO'])) {
$this->scheme = $env['X-FORWARDED-PROTO'];
} elseif (isset($env['REQUEST_SCHEME'])) {
$this->scheme = $env['REQUEST_SCHEME'];
} else {
$https = isset($env['HTTPS']) ? $env['HTTPS'] : '';
$this->scheme = (empty($https) || strtolower($https) === 'off') ? 'http' : 'https';
@@ -1118,7 +1164,16 @@ class Uri
$this->host = $this->validateHostname($hostname) ? $hostname : 'unknown';
// Build port.
$this->port = isset($env['SERVER_PORT']) ? (int)$env['SERVER_PORT'] : null;
if (isset($env['HTTP_X_FORWARDED_PORT'])) {
$this->port = (int)$env['HTTP_X_FORWARDED_PORT'];
} elseif (isset($env['X-FORWARDED-PORT'])) {
$this->port = (int)$env['X-FORWARDED-PORT'];
} elseif (isset($env['SERVER_PORT'])) {
$this->port = (int)$env['SERVER_PORT'];
} else {
$this->port = null;
}
if ($this->hasStandardPort()) {
$this->port = null;
}
@@ -1185,7 +1240,6 @@ class Uri
if ($this->host) {
$this->host = $this->validateHostname($this->host) ? $this->host : 'unknown';
}
// Filter userinfo, path, query string and fragment.
$this->user = $this->user !== null ? static::filterUserInfo($this->user) : null;
$this->password = $this->password !== null ? static::filterUserInfo($this->password) : null;
@@ -1213,6 +1267,58 @@ class Uri
$this->url = $this->base . $this->uri;
}
/**
* Get's post from either $_POST or JSON response object
* By default returns all data, or can return a single item
*
* @param string $element
* @param string $filter_type
* @return array|mixed|null
*/
public function post($element = null, $filter_type = null)
{
if (!$this->post) {
$content_type = $this->getContentType();
if ($content_type === 'application/json') {
$json = file_get_contents('php://input');
$this->post = json_decode($json, true);
} elseif (!empty($_POST)) {
$this->post = (array)$_POST;
}
$event = new Event(['post' => &$this->post]);
Grav::instance()->fireEvent('onHttpPostFilter', $event);
}
if ($this->post && null !== $element) {
$item = Utils::getDotNotation($this->post, $element);
if ($filter_type) {
$item = filter_var($item, $filter_type);
}
return $item;
}
return $this->post;
}
/**
* Get content type from request
*
* @param bool $short
* @return null|string
*/
private function getContentType($short = true)
{
if (isset($_SERVER['CONTENT_TYPE'])) {
$content_type = $_SERVER['CONTENT_TYPE'];
if ($short) {
return Utils::substrToString($content_type,';');
}
return $content_type;
}
return null;
}
/**
* Get the base URI with port if needed
*
@@ -1234,11 +1340,6 @@ class Uri
$scriptPath = str_replace('\\', '/', $_SERVER['PHP_SELF']);
$rootPath = str_replace(' ', '%20', rtrim(substr($scriptPath, 0, strpos($scriptPath, 'index.php')), '/'));
// check if userdir in the path and workaround PHP bug with PHP_SELF
if (strpos($this->uri, '/~') !== false && strpos($scriptPath, '/~') === false) {
$rootPath = substr($this->uri, 0, strpos($this->uri, '/', 1)) . $rootPath;
}
return $rootPath;
}

View File

@@ -62,9 +62,9 @@ class User extends Data
$files = $account_dir ? array_diff(scandir($account_dir), ['.', '..']) : [];
// Try with username first, you never know!
if (in_array('username', $fields)) {
if (in_array('username', $fields, true)) {
$user = User::load($query);
unset($fields[array_search('username', $fields)]);
unset($fields[array_search('username', $fields, true)]);
} else {
$user = User::load('');
}
@@ -95,11 +95,41 @@ class User extends Data
public static function remove($username)
{
$file_path = Grav::instance()['locator']->findResource('account://' . $username . YAML_EXT);
if ($file_path && unlink($file_path)) {
return true;
return $file_path && unlink($file_path);
}
/**
* @param string $offset
* @return bool
*/
public function offsetExists($offset)
{
$value = parent::offsetExists($offset);
// Handle special case where user was logged in before 'authorized' was added to the user object.
if (false === $value && $offset === 'authorized') {
$value = $this->offsetExists('authenticated');
}
return false;
return $value;
}
/**
* @param string $offset
* @return mixed
*/
public function offsetGet($offset)
{
$value = parent::offsetGet($offset);
// Handle special case where user was logged in before 'authorized' was added to the user object.
if (null === $value && $offset === 'authorized') {
$value = $this->offsetGet('authenticated');
$this->offsetSet($offset, $value);
}
return $value;
}
/**
@@ -236,6 +266,8 @@ class User extends Data
*/
public function authorise($action)
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use authorize() method instead', E_USER_DEPRECATED);
return $this->authorize($action);
}

View File

@@ -45,8 +45,20 @@ abstract class Utils
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
// Get relative path to the resource (or false if not found).
$resource = $locator->findResource($input, false);
$parts = Uri::parseUrl($input);
if ($parts) {
$resource = $locator->findResource("{$parts['scheme']}://{$parts['host']}{$parts['path']}", false);
if (isset($parts['query'])) {
$resource = $resource . '?' . $parts['query'];
}
} else {
// Not a valid URL (can still be a stream).
$resource = $locator->findResource($input, false);
}
} else {
$resource = $input;
}
@@ -262,7 +274,7 @@ abstract class Utils
// is $break present between $limit and the end of the string?
if ($up_to_break && false !== ($breakpoint = mb_strpos($string, $break, $limit))) {
if ($breakpoint < mb_strlen($string) - 1) {
$string = mb_substr($string, 0, $breakpoint) . $break;
$string = mb_substr($string, 0, $breakpoint) . $pad;
}
} else {
$string = mb_substr($string, 0, $limit) . $pad;
@@ -296,10 +308,6 @@ abstract class Utils
*/
public static function truncateHtml($text, $length = 100, $ellipsis = '...')
{
if (mb_strlen($text) <= $length) {
return $text;
}
return Truncator::truncateLetters($text, $length, $ellipsis);
}
@@ -470,6 +478,51 @@ abstract class Utils
return $default;
}
/**
* Return the mimetype based on filename
*
* @param string $filename Filename or path to file
* @param string $default default value
*
* @return string
*/
public static function getMimeByFilename($filename, $default = 'application/octet-stream')
{
return static::getMimeByExtension(pathinfo($filename, PATHINFO_EXTENSION), $default);
}
/**
* Return the mimetype based on existing local file
*
* @param string $filename Path to the file
*
* @return string|bool
*/
public static function getMimeByLocalFile($filename, $default = 'application/octet-stream')
{
$type = false;
// For local files we can detect type by the file content.
if (!stream_is_local($filename) || !file_exists($filename)) {
return false;
}
// Prefer using finfo if it exists.
if (\extension_loaded('fileinfo')) {
$finfo = finfo_open(FILEINFO_SYMLINK | FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $filename);
finfo_close($finfo);
} else {
// Fall back to use getimagesize() if it is available (not recommended, but better than nothing)
$info = @getimagesize($filename);
if ($info) {
$type = $info['mime'];
}
}
return $type ?: static::getMimeByFilename($filename, $default);
}
/**
* Return the mimetype based on filename extension
*
@@ -512,6 +565,33 @@ abstract class Utils
return $default;
}
/**
* Returns true if filename is considered safe.
*
* @param string $filename
* @return bool
*/
public static function checkFilename($filename)
{
$dangerous_extensions = Grav::instance()['config']->get('security.uploads_dangerous_extensions', []);
array_walk($dangerous_extensions, function(&$val) {
$val = '.' . $val;
});
$extension = '.' . pathinfo($filename, PATHINFO_EXTENSION);
return !(
// Empty filenames are not allowed.
!$filename
// Filename should not contain horizontal/vertical tabs, newlines, nils or back/forward slashes.
|| strtr($filename, "\t\v\n\r\0\\/", '_______') !== $filename
// Filename should not start or end with dot or space.
|| trim($filename, '. ') !== $filename
// Filename should not contain .php in it.
|| static::contains($extension, $dangerous_extensions)
);
}
/**
* Normalize path by processing relative `.` and `..` syntax and merging path
*
@@ -688,6 +768,8 @@ abstract class Utils
*/
public static function resolve(array $array, $path, $default = null)
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDotNotation() method instead', E_USER_DEPRECATED);
return static::getDotNotation($array, $path, $default);
}
@@ -709,11 +791,11 @@ abstract class Utils
* with reverse proxy setups.
*
* @param string $action
* @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours)
* @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
*
* @return string the nonce string
*/
private static function generateNonceString($action, $plusOneTick = false)
private static function generateNonceString($action, $previousTick = false)
{
$username = '';
if (isset(Grav::instance()['user'])) {
@@ -724,29 +806,8 @@ abstract class Utils
$token = session_id();
$i = self::nonceTick();
if ($plusOneTick) {
$i++;
}
return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
}
//Added in version 1.0.8 to ensure that existing nonces are not broken.
private static function generateNonceStringOldStyle($action, $plusOneTick = false)
{
if (isset(Grav::instance()['user'])) {
$user = Grav::instance()['user'];
$username = $user->username;
if (isset($_SERVER['REMOTE_ADDR'])) {
$username .= $_SERVER['REMOTE_ADDR'];
}
} else {
$username = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
}
$token = session_id();
$i = self::nonceTick();
if ($plusOneTick) {
$i++;
if ($previousTick) {
$i--;
}
return ($i . '|' . $action . '|' . $username . '|' . $token . '|' . Grav::instance()['config']->get('security.salt'));
@@ -772,33 +833,20 @@ abstract class Utils
* action is the same for 12 hours.
*
* @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage)
* @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours)
* @param bool $previousTick if true, generates the token for the previous tick (the previous 12 hours)
*
* @return string the nonce
*/
public static function getNonce($action, $plusOneTick = false)
public static function getNonce($action, $previousTick = false)
{
// Don't regenerate this again if not needed
if (isset(static::$nonces[$action])) {
return static::$nonces[$action];
if (isset(static::$nonces[$action][$previousTick])) {
return static::$nonces[$action][$previousTick];
}
$nonce = md5(self::generateNonceString($action, $plusOneTick));
static::$nonces[$action] = $nonce;
$nonce = md5(self::generateNonceString($action, $previousTick));
static::$nonces[$action][$previousTick] = $nonce;
return static::$nonces[$action];
}
//Added in version 1.0.8 to ensure that existing nonces are not broken.
public static function getNonceOldStyle($action, $plusOneTick = false)
{
// Don't regenerate this again if not needed
if (isset(static::$nonces[$action])) {
return static::$nonces[$action];
}
$nonce = md5(self::generateNonceStringOldStyle($action, $plusOneTick));
static::$nonces[$action] = $nonce;
return static::$nonces[$action];
return static::$nonces[$action][$previousTick];
}
/**
@@ -822,20 +870,8 @@ abstract class Utils
}
//Nonce generated 12-24 hours ago
$plusOneTick = true;
if ($nonce === self::getNonce($action, $plusOneTick)) {
return true;
}
//Added in version 1.0.8 to ensure that existing nonces are not broken.
//Nonce generated 0-12 hours ago
if ($nonce === self::getNonceOldStyle($action)) {
return true;
}
//Nonce generated 12-24 hours ago
$plusOneTick = true;
if ($nonce === self::getNonceOldStyle($action, $plusOneTick)) {
$previousTick = true;
if ($nonce === self::getNonce($action, $previousTick)) {
return true;
}
@@ -966,6 +1002,32 @@ abstract class Utils
return $ordered + $array;
}
/**
* Sort an array by a key value in the array
*
* @param $array
* @param $array_key
* @param int $direction
* @param int $sort_flags
* @return array
*/
public static function sortArrayByKey($array, $array_key, $direction = SORT_DESC, $sort_flags = SORT_REGULAR )
{
$output = [];
if (!is_array($array) || !$array) {
return $output;
}
foreach ($array as $key => $row) {
$output[$key] = $row[$array_key];
}
array_multisort($output, $direction, $sort_flags, $array);
return $array;
}
/**
* Get's path based on a token
*

View File

@@ -0,0 +1,47 @@
<?php
/**
* @package Grav.Common
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common;
use Grav\Framework\File\Formatter\YamlFormatter;
abstract class Yaml
{
/** @var YamlFormatter */
private static $yaml;
public static function parse($data)
{
if (null === static::$yaml) {
static::init();
}
return static::$yaml->decode($data);
}
public static function dump($data, $inline = null, $indent = null)
{
if (null === static::$yaml) {
static::init();
}
return static::$yaml->encode($data, $inline, $indent);
}
private static function init()
{
$config = [
'inline' => 5,
'indent' => 2,
'native' => true,
'compat' => true
];
static::$yaml = new YamlFormatter($config);
}
}

View File

@@ -120,6 +120,8 @@ class CleanCommand extends Command
'vendor/league/climate/composer.json',
'vendor/matthiasmullie/minify/bin',
'vendor/matthiasmullie/minify/composer.json',
'vendor/matthiasmullie/minify/docker-composer.yml',
'vendor/matthiasmullie/minify/Dockerfile',
'vendor/matthiasmullie/minify/CONTRIBUTING.md',
'vendor/matthiasmullie/path-converter/composer.json',
'vendor/maximebf/debugbar/bower.json',
@@ -151,6 +153,7 @@ class CleanCommand extends Command
'vendor/pimple/pimple/src/Pimple/Tests',
'vendor/psr/container/composer.json',
'vendor/psr/container/.gitignore',
'vendor/psr/simple-cache/composer.json',
'vendor/psr/log/composer.json',
'vendor/psr/log/.gitignore',
'vendor/rockettheme/toolbox/.git',

View File

@@ -9,9 +9,9 @@
namespace Grav\Console\Cli;
use Grav\Console\ConsoleCommand;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Yaml\Yaml;
class InstallCommand extends ConsoleCommand
{
@@ -71,20 +71,23 @@ class InstallCommand extends ConsoleCommand
// Look for dependencies file in ROOT and USER dir
if (file_exists($this->user_path . $dependencies_file)) {
$this->config = Yaml::parse(file_get_contents($this->user_path . $dependencies_file));
$file = YamlFile::instance($this->user_path . $dependencies_file);
} elseif (file_exists($this->destination . $dependencies_file)) {
$this->config = Yaml::parse(file_get_contents($this->destination . $dependencies_file));
$file = YamlFile::instance($this->destination . $dependencies_file);
} else {
$this->output->writeln('<red>ERROR</red> Missing .dependencies file in <cyan>user/</cyan> folder');
if ($this->input->getArgument('destination')) {
$this->output->writeln('<yellow>HINT</yellow> <info>Are you trying to install a plugin or a theme? Make sure you use <cyan>bin/gpm install <something></cyan>, not <cyan>bin/grav install</cyan>. This command is only used to install Grav skeletons.');
$this->output->writeln('<yellow>HINT</yellow> <info>Are you trying to install a plugin or a theme? Make sure you use <cyan>bin/gpm install <something></cyan>, not <cyan>bin/grav install</cyan>. This command is only used to install Grav skeletons.');
} else {
$this->output->writeln('<yellow>HINT</yellow> <info>Are you trying to install Grav? Grav is already installed. You need to run this command only if you download a skeleton from GitHub directly.');
$this->output->writeln('<yellow>HINT</yellow> <info>Are you trying to install Grav? Grav is already installed. You need to run this command only if you download a skeleton from GitHub directly.');
}
return;
}
$this->config = $file->content();
$file->free();
// If yaml config, process
if ($this->config) {
if (!$this->input->getOption('symlink')) {
@@ -153,10 +156,22 @@ class InstallCommand extends ConsoleCommand
exec('cd ' . $this->destination);
foreach ($this->config['links'] as $repo => $data) {
$from = $this->local_config[$data['scm'] . '_repos'] . $data['src'];
$repos = (array) $this->local_config[$data['scm'] . '_repos'];
$from = false;
$to = $this->destination . $data['path'];
if (file_exists($from)) {
foreach ($repos as $repo) {
$path = $repo . $data['src'];
if (file_exists($path)) {
$from = $path;
continue;
}
}
if (!$from) {
$this->output->writeln('<red>source for ' . $data['src'] . ' does not exists, skipping...</red>');
$this->output->writeln('');
} else {
if (!file_exists($to)) {
symlink($from, $to);
$this->output->writeln('<green>SUCCESS</green> symlinked <magenta>' . $data['src'] . '</magenta> -> <cyan>' . $data['path'] . '</cyan>');
@@ -165,11 +180,7 @@ class InstallCommand extends ConsoleCommand
$this->output->writeln('<red>destination: ' . $to . ' already exists, skipping...</red>');
$this->output->writeln('');
}
} else {
$this->output->writeln('<red>source: ' . $from . ' does not exists, skipping...</red>');
$this->output->writeln('');
}
}
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* @package Grav.Console
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Console\Cli;
use Grav\Common\Grav;
use Grav\Common\Security;
use Grav\Console\ConsoleCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Style\SymfonyStyle;
class SecurityCommand extends ConsoleCommand
{
/** @var ProgressBar $progress */
protected $progress;
/**
*
*/
protected function configure()
{
$this
->setName("security")
->setDescription("Capable of running various Security checks")
->setHelp('The <info>security</info> runs various security checks on your Grav site');
$this->source = getcwd();
}
/**
* @return int|null|void
*/
protected function serve()
{
/** @var Grav $grav */
$grav = Grav::instance();
$grav['uri']->init();
$grav['config']->init();
$grav['debugger']->enabled(false);
$grav['streams'];
$grav['plugins']->init();
$grav['themes']->init();
$grav['twig']->init();
$grav['pages']->init();
$this->progress = new ProgressBar($this->output, (count($grav['pages']->routes()) - 1));
$this->progress->setFormat('Scanning <cyan>%current%</cyan> pages [<green>%bar%</green>] <white>%percent:3s%%</white> %elapsed:6s%');
$this->progress->setBarWidth(100);
$io = new SymfonyStyle($this->input, $this->output);
$io->title('Grav Security Check');
$output = Security::detectXssFromPages($grav['pages'], [$this, 'outputProgress']);
$io->newline(2);
if (!empty($output)) {
$counter = 1;
foreach ($output as $route => $results) {
$results_parts = array_map(function($value, $key) {
return $key.': \''.$value . '\'';
}, array_values($results), array_keys($results));
$io->writeln($counter++ .' - <cyan>' . $route . '</cyan> → <red>' . implode(', ', $results_parts) . '</red>');
}
$io->error('Security Scan complete: ' . count($output) . ' potential XSS issues found...');
} else {
$io->success('Security Scan complete: No issues found...');
}
$io->newline(1);
}
/**
* @param $args
*/
public function outputProgress($args)
{
switch ($args['type']) {
case 'count':
$steps = $args['steps'];
$freq = intval($steps > 100 ? round($steps / 100) : $steps);
$this->progress->setMaxSteps($steps);
$this->progress->setRedrawFrequency($freq);
break;
case 'progress':
if (isset($args['complete']) && $args['complete']) {
$this->progress->finish();
} else {
$this->progress->advance();
}
break;
}
}
}

View File

@@ -12,11 +12,11 @@ use Grav\Common\Grav;
use Grav\Common\Composer;
use Grav\Common\GravTrait;
use Grav\Console\Cli\ClearCacheCommand;
use RocketTheme\Toolbox\File\YamlFile;
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
{
@@ -123,7 +123,9 @@ trait ConsoleTrait
$local_config_file = $home_folder . '/.grav/config';
if (file_exists($local_config_file)) {
$this->local_config = Yaml::parse(file_get_contents($local_config_file));
$file = YamlFile::instance($local_config_file);
$this->local_config = $file->content();
$file->free();
return $local_config_file;
}

View File

@@ -182,7 +182,7 @@ class InstallCommand extends ConsoleCommand
//We're done installing dependencies. Install the actual packages
foreach ($this->data as $data) {
foreach ($data as $package_name => $package) {
if (in_array($package_name, array_keys($dependencies))) {
if (array_key_exists($package_name, $dependencies)) {
$this->output->writeln("<green>Package " . $package_name . " already installed as dependency</green>");
} else {
$is_valid_destination = Installer::isValidDestination($this->destination . DS . $package->install_path);
@@ -444,18 +444,21 @@ class InstallCommand extends ConsoleCommand
{
$matches = $this->getGitRegexMatches($package);
foreach ($this->local_config as $path) {
foreach ($this->local_config as $paths) {
if (Utils::endsWith($matches[2], '.git')) {
$repo_dir = preg_replace('/\.git$/', '', $matches[2]);
} else {
$repo_dir = $matches[2];
}
$from = rtrim($path, '/') . '/' . $repo_dir;
if (file_exists($from)) {
return $from;
$paths = (array) $paths;
foreach ($paths as $repo) {
$path = rtrim($repo, '/') . '/' . $repo_dir;
if (file_exists($path)) {
return $path;
}
}
}
return false;

View File

@@ -11,9 +11,9 @@ namespace Grav\Console\Gpm;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Upgrader;
use Grav\Console\ConsoleCommand;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Yaml\Yaml;
class VersionCommand extends ConsoleCommand
{
@@ -84,7 +84,10 @@ class VersionCommand extends ConsoleCommand
}
}
$package_yaml = Yaml::parse(file_get_contents($blueprints_path));
$file = YamlFile::instance($blueprints_path);
$package_yaml = $file->content();
$file->free();
$version = $package_yaml['version'];
if (!$version) {

View File

@@ -16,21 +16,18 @@ use Grav\Framework\Cache\Exception\InvalidArgumentException;
*/
trait CacheTrait
{
/**
* @var string
*/
/** @var string */
private $namespace = '';
/**
* @var int|null
*/
/** @var int|null */
private $defaultLifetime = null;
/**
* @var \stdClass
*/
/** @var \stdClass */
private $miss;
/** @var bool */
private $validation = true;
/**
* Always call from constructor.
*
@@ -45,6 +42,14 @@ trait CacheTrait
$this->miss = new \stdClass;
}
/**
* @param $validation
*/
public function setValidation($validation)
{
$this->validation = (bool) $validation;
}
/**
* @return string
*/
@@ -307,6 +312,10 @@ trait CacheTrait
*/
protected function validateKeys($keys)
{
if (!$this->validation) {
return;
}
foreach ($keys as $key) {
$this->validateKey($key);
}

View File

@@ -24,11 +24,6 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
*/
public function reverse()
{
// TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
if (!method_exists($this, 'createFrom')) {
return new static(array_reverse($this->toArray()));
}
return $this->createFrom(array_reverse($this->toArray()));
}
@@ -42,11 +37,6 @@ class ArrayCollection extends BaseArrayCollection implements CollectionInterface
$keys = $this->getKeys();
shuffle($keys);
// TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
if (!method_exists($this, 'createFrom')) {
return new static(array_replace(array_flip($keys), $this->toArray()));
}
return $this->createFrom(array_replace(array_flip($keys), $this->toArray()));
}

View File

@@ -27,6 +27,7 @@ class ContentBlock implements ContentBlockInterface
protected $tokenTemplate = '@@BLOCK-%s@@';
protected $content = '';
protected $blocks = [];
protected $checksum;
/**
* @param string $id
@@ -40,6 +41,7 @@ class ContentBlock implements ContentBlockInterface
/**
* @param array $serialized
* @return ContentBlockInterface
* @throws \InvalidArgumentException
*/
public static function fromArray(array $serialized)
{
@@ -48,14 +50,14 @@ class ContentBlock implements ContentBlockInterface
$id = isset($serialized['id']) ? $serialized['id'] : null;
if (!$type || !$id || !is_a($type, 'Grav\Framework\ContentBlock\ContentBlockInterface', true)) {
throw new \RuntimeException('Bad data');
throw new \InvalidArgumentException('Bad data');
}
/** @var ContentBlockInterface $instance */
$instance = new $type($id);
$instance->build($serialized);
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e);
throw new \InvalidArgumentException(sprintf('Cannot unserialize Block: %s', $e->getMessage()), $e->getCode(), $e);
}
return $instance;
@@ -104,9 +106,13 @@ class ContentBlock implements ContentBlockInterface
$array = [
'_type' => get_class($this),
'_version' => $this->version,
'id' => $this->id,
'id' => $this->id
];
if ($this->checksum) {
$array['checksum'] = $this->checksum;
}
if ($this->content) {
$array['content'] = $this->content;
}
@@ -158,6 +164,7 @@ class ContentBlock implements ContentBlockInterface
$this->checkVersion($serialized);
$this->id = isset($serialized['id']) ? $serialized['id'] : $this->generateId();
$this->checksum = isset($serialized['checksum']) ? $serialized['checksum'] : null;
if (isset($serialized['content'])) {
$this->setContent($serialized['content']);
@@ -169,6 +176,25 @@ class ContentBlock implements ContentBlockInterface
}
}
/**
* @param string $checksum
* @return $this
*/
public function setChecksum($checksum)
{
$this->checksum = $checksum;
return $this;
}
/**
* @return string
*/
public function getChecksum()
{
return $this->checksum;
}
/**
* @param string $content
* @return $this
@@ -222,7 +248,7 @@ class ContentBlock implements ContentBlockInterface
*/
protected function checkVersion(array $serialized)
{
$version = isset($serialized['_version']) ? (string) $serialized['_version'] : '1';
$version = isset($serialized['_version']) ? (int) $serialized['_version'] : 1;
if ($version !== $this->version) {
throw new \RuntimeException(sprintf('Unsupported version %s', $version));
}

View File

@@ -61,6 +61,17 @@ interface ContentBlockInterface extends \Serializable
*/
public function build(array $serialized);
/**
* @param string $checksum
* @return $this
*/
public function setChecksum($checksum);
/**
* @return string
*/
public function getChecksum();
/**
* @param string $content
* @return $this

View File

@@ -15,6 +15,7 @@ namespace Grav\Framework\ContentBlock;
*/
class HtmlBlock extends ContentBlock implements HtmlBlockInterface
{
protected $version = 1;
protected $frameworks = [];
protected $styles = [];
protected $scripts = [];

View File

@@ -0,0 +1,44 @@
<?php
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
interface FormatterInterface
{
/**
* Get default file extension from current formatter (with dot).
*
* Default file extension is the first defined extension.
*
* @return string File extension (can be empty).
*/
public function getDefaultFileExtension();
/**
* Get file extensions supported by current formatter (with dot).
*
* @return string[]
*/
public function getSupportedFileExtensions();
/**
* Encode data into a string.
*
* @param array $data
* @return string
*/
public function encode($data);
/**
* Decode a string into data.
*
* @param string $data
* @return array
*/
public function decode($data);
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class IniFormatter implements FormatterInterface
{
/** @var array */
private $config;
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.ini'
];
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
*/
public function getFileExtension()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getDefaultFileExtension();
}
/**
* {@inheritdoc}
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
{
$string = '';
foreach ($data as $key => $value) {
$string .= $key . '="' . preg_replace(
['/"/', '/\\\/', "/\t/", "/\n/", "/\r/"],
['\"', '\\\\', '\t', '\n', '\r'],
$value
) . "\"\n";
}
return $string;
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$decoded = @parse_ini_string($data);
if ($decoded === false) {
throw new \RuntimeException('Decoding INI failed');
}
return $decoded;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class JsonFormatter implements FormatterInterface
{
/** @var array */
private $config;
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.json',
'encode_options' => 0,
'decode_assoc' => true
];
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
*/
public function getFileExtension()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getDefaultFileExtension();
}
/**
* {@inheritdoc}
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
{
$encoded = @json_encode($data, $this->config['encode_options']);
if ($encoded === false) {
throw new \RuntimeException('Encoding JSON failed');
}
return $encoded;
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$decoded = @json_decode($data, $this->config['decode_assoc']);
if ($decoded === false) {
throw new \RuntimeException('Decoding JSON failed');
}
return $decoded;
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class MarkdownFormatter implements FormatterInterface
{
/** @var array */
private $config;
/** @var FormatterInterface */
private $headerFormatter;
public function __construct(array $config = [], FormatterInterface $headerFormatter = null)
{
$this->config = $config + [
'file_extension' => '.md',
'header' => 'header',
'body' => 'markdown',
'raw' => 'frontmatter',
'yaml' => ['inline' => 20]
];
$this->headerFormatter = $headerFormatter ?: new YamlFormatter($this->config['yaml']);
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
*/
public function getFileExtension()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getDefaultFileExtension();
}
/**
* {@inheritdoc}
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
{
$headerVar = $this->config['header'];
$bodyVar = $this->config['body'];
$header = isset($data[$headerVar]) ? (array) $data[$headerVar] : [];
$body = isset($data[$bodyVar]) ? (string) $data[$bodyVar] : '';
// Create Markdown file with YAML header.
$encoded = '';
if ($header) {
$encoded = "---\n" . trim($this->headerFormatter->encode($data['header'])) . "\n---\n\n";
}
$encoded .= $body;
// Normalize line endings to Unix style.
$encoded = preg_replace("/(\r\n|\r)/", "\n", $encoded);
return $encoded;
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$headerVar = $this->config['header'];
$bodyVar = $this->config['body'];
$rawVar = $this->config['raw'];
$content = [
$headerVar => [],
$bodyVar => ''
];
$headerRegex = "/^---\n(.+?)\n---\n{0,}(.*)$/uis";
// Normalize line endings to Unix style.
$data = preg_replace("/(\r\n|\r)/", "\n", $data);
// Parse header.
preg_match($headerRegex, ltrim($data), $matches);
if(empty($matches)) {
$content[$bodyVar] = $data;
} else {
// Normalize frontmatter.
$frontmatter = preg_replace("/\n\t/", "\n ", $matches[1]);
if ($rawVar) {
$content[$rawVar] = $frontmatter;
}
$content[$headerVar] = $this->headerFormatter->decode($frontmatter);
$content[$bodyVar] = $matches[2];
}
return $content;
}
}

View File

@@ -0,0 +1,98 @@
<?php
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
class SerializeFormatter implements FormatterInterface
{
/** @var array */
private $config;
/**
* IniFormatter constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.ser'
];
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
*/
public function getFileExtension()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getDefaultFileExtension();
}
/**
* {@inheritdoc}
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data)
{
return serialize($this->preserveLines($data, ["\n", "\r"], ['\\n', '\\r']));
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$decoded = @unserialize($data);
if ($decoded === false) {
throw new \RuntimeException('Decoding serialized data failed');
}
return $this->preserveLines($decoded, ['\\n', '\\r'], ["\n", "\r"]);
}
/**
* Preserve new lines, recursive function.
*
* @param mixed $data
* @param array $search
* @param array $replace
* @return mixed
*/
protected function preserveLines($data, $search, $replace)
{
if (is_string($data)) {
$data = str_replace($search, $replace, $data);
} elseif (is_array($data)) {
foreach ($data as &$value) {
$value = $this->preserveLines($value, $search, $replace);
}
unset($value);
}
return $data;
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* @package Grav\Framework\File\Formatter
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\File\Formatter;
use Symfony\Component\Yaml\Exception\DumpException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml as YamlParser;
use RocketTheme\Toolbox\Compat\Yaml\Yaml as FallbackYamlParser;
class YamlFormatter implements FormatterInterface
{
/** @var array */
private $config;
public function __construct(array $config = [])
{
$this->config = $config + [
'file_extension' => '.yaml',
'inline' => 5,
'indent' => 2,
'native' => true,
'compat' => true
];
}
/**
* @deprecated 1.5 Use $formatter->getDefaultFileExtension() instead.
*/
public function getFileExtension()
{
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use getDefaultFileExtension() method instead', E_USER_DEPRECATED);
return $this->getDefaultFileExtension();
}
/**
* {@inheritdoc}
*/
public function getDefaultFileExtension()
{
$extensions = $this->getSupportedFileExtensions();
return (string) reset($extensions);
}
/**
* {@inheritdoc}
*/
public function getSupportedFileExtensions()
{
return (array) $this->config['file_extension'];
}
/**
* {@inheritdoc}
*/
public function encode($data, $inline = null, $indent = null)
{
try {
return (string) YamlParser::dump(
$data,
$inline ? (int) $inline : $this->config['inline'],
$indent ? (int) $indent : $this->config['indent'],
YamlParser::DUMP_EXCEPTION_ON_INVALID_TYPE
);
} catch (DumpException $e) {
throw new \RuntimeException('Encoding YAML failed: ' . $e->getMessage(), 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
// Try native PECL YAML PHP extension first if available.
if ($this->config['native'] && function_exists('yaml_parse')) {
// Safely decode YAML.
$saved = @ini_get('yaml.decode_php');
@ini_set('yaml.decode_php', 0);
$decoded = @yaml_parse($data);
@ini_set('yaml.decode_php', $saved);
if ($decoded !== false) {
return (array) $decoded;
}
}
try {
return (array) YamlParser::parse($data);
} catch (ParseException $e) {
if ($this->config['compat']) {
return (array) FallbackYamlParser::parse($data);
}
throw new \RuntimeException('Decoding YAML failed: ' . $e->getMessage(), 0, $e);
}
}
}

View File

@@ -32,11 +32,6 @@ trait ObjectCollectionTrait
$list[$key] = is_object($value) ? clone $value : $value;
}
// TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
if (!method_exists($this, 'createFrom')) {
return new static($list);
}
return $this->createFrom($list);
}
@@ -170,12 +165,7 @@ trait ObjectCollectionTrait
{
$collections = [];
foreach ($this->group($property) as $id => $elements) {
// TODO: remove when PHP 5.6 is minimum (with doctrine/collections v1.4).
if (!method_exists($this, 'createFrom')) {
$collection = new static($elements);
} else {
$collection = $this->createFrom($elements);
}
$collection = $this->createFrom($elements);
$collections[$id] = $collection;
}

View File

@@ -15,7 +15,7 @@ namespace Grav\Framework\Object\Base;
*/
trait ObjectTrait
{
static protected $prefix;
/** @var string */
static protected $type;
/**
@@ -23,18 +23,28 @@ trait ObjectTrait
*/
private $_key;
/**
* @return string
*/
protected function getTypePrefix()
{
return '';
}
/**
* @param bool $prefix
* @return string
*/
public function getType($prefix = true)
{
$type = $prefix ? $this->getTypePrefix() : '';
if (static::$type) {
return ($prefix ? static::$prefix : '') . static::$type;
return $type . static::$type;
}
$class = get_class($this);
return ($prefix ? static::$prefix : '') . strtolower(substr($class, strrpos($class, '\\') + 1));
return $type . strtolower(substr($class, strrpos($class, '\\') + 1));
}
/**
@@ -108,7 +118,7 @@ trait ObjectTrait
*/
public function serialize()
{
return serialize($this->jsonSerialize());
return serialize($this->doSerialize());
}
/**
@@ -124,6 +134,14 @@ trait ObjectTrait
$this->doUnserialize($data);
}
/**
* @return array
*/
protected function doSerialize()
{
return $this->jsonSerialize();
}
/**
* @param array $serialized
*/
@@ -159,10 +177,13 @@ trait ObjectTrait
/**
* @param string $key
* @return $this
*/
protected function setKey($key)
{
$this->_key = (string) $key;
return $this;
}
abstract protected function doHasProperty($property);

View File

@@ -0,0 +1,198 @@
<?php
/**
* @package Grav\Framework\Object
*
* @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Framework\Object\Collection;
use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor;
use Doctrine\Common\Collections\Expr\Comparison;
class ObjectExpressionVisitor extends ClosureExpressionVisitor
{
/**
* Accesses the field of a given object.
*
* @param object $object
* @param string $field
*
* @return mixed
*/
public static function getObjectFieldValue($object, $field)
{
$op = $value = null;
$pos = strpos($field, '(');
if (false !== $pos) {
list ($op, $field) = explode('(', $field, 2);
$field = rtrim($field, ')');
}
if (isset($object[$field])) {
$value = $object[$field];
} else {
$accessors = array('', 'get', 'is');
foreach ($accessors as $accessor) {
$accessor .= $field;
if (!method_exists($object, $accessor)) {
continue;
}
$value = $object->{$accessor}();
break;
}
}
if ($op) {
$function = 'filter' . ucfirst(strtolower($op));
if (method_exists(static::class, $function)) {
$value = static::$function($value);
}
}
return $value;
}
public static function filterLower($str)
{
return mb_strtolower($str);
}
public static function filterUpper($str)
{
return mb_strtoupper($str);
}
public static function filterLength($str)
{
return mb_strlen($str);
}
public static function filterLtrim($str)
{
return ltrim($str);
}
public static function filterRtrim($str)
{
return rtrim($str);
}
public static function filterTrim($str)
{
return trim($str);
}
/**
* Helper for sorting arrays of objects based on multiple fields + orientations.
*
* @param string $name
* @param int $orientation
* @param \Closure $next
*
* @return \Closure
*/
public static function sortByField($name, $orientation = 1, \Closure $next = null)
{
if (!$next) {
$next = function() {
return 0;
};
}
return function ($a, $b) use ($name, $next, $orientation) {
$aValue = static::getObjectFieldValue($a, $name);
$bValue = static::getObjectFieldValue($b, $name);
if ($aValue === $bValue) {
return $next($a, $b);
}
return (($aValue > $bValue) ? 1 : -1) * $orientation;
};
}
/**
* {@inheritDoc}
*/
public function walkComparison(Comparison $comparison)
{
$field = $comparison->getField();
$value = $comparison->getValue()->getValue(); // shortcut for walkValue()
switch ($comparison->getOperator()) {
case Comparison::EQ:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) === $value;
};
case Comparison::NEQ:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) !== $value;
};
case Comparison::LT:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) < $value;
};
case Comparison::LTE:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) <= $value;
};
case Comparison::GT:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) > $value;
};
case Comparison::GTE:
return function ($object) use ($field, $value) {
return static::getObjectFieldValue($object, $field) >= $value;
};
case Comparison::IN:
return function ($object) use ($field, $value) {
return \in_array(static::getObjectFieldValue($object, $field), $value, true);
};
case Comparison::NIN:
return function ($object) use ($field, $value) {
return !\in_array(static::getObjectFieldValue($object, $field), $value, true);
};
case Comparison::CONTAINS:
return function ($object) use ($field, $value) {
return false !== strpos(static::getObjectFieldValue($object, $field), $value);
};
case Comparison::MEMBER_OF:
return function ($object) use ($field, $value) {
$fieldValues = static::getObjectFieldValue($object, $field);
if (!is_array($fieldValues)) {
$fieldValues = iterator_to_array($fieldValues);
}
return \in_array($value, $fieldValues, true);
};
case Comparison::STARTS_WITH:
return function ($object) use ($field, $value) {
return 0 === strpos(static::getObjectFieldValue($object, $field), $value);
};
case Comparison::ENDS_WITH:
return function ($object) use ($field, $value) {
return $value === substr(static::getObjectFieldValue($object, $field), -strlen($value));
};
default:
throw new \RuntimeException("Unknown comparison operator: " . $comparison->getOperator());
}
}
}

View File

@@ -8,13 +8,14 @@
namespace Grav\Framework\Object\Interfaces;
use Doctrine\Common\Collections\Selectable;
use Grav\Framework\Collection\CollectionInterface;
/**
* ObjectCollection Interface
* @package Grav\Framework\Collection
*/
interface ObjectCollectionInterface extends CollectionInterface, ObjectInterface
interface ObjectCollectionInterface extends CollectionInterface, Selectable, ObjectInterface
{
/**
* Create a copy from this collection by cloning all objects in the collection.

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