diff --git a/.dependencies b/.dependencies
index bedd0a5d26..3e82e0680f 100644
--- a/.dependencies
+++ b/.dependencies
@@ -2,15 +2,15 @@ git:
problems:
url: https://github.com/getgrav/grav-plugin-problems
path: user/plugins/problems
- branch: develop
+ branch: master
error:
url: https://github.com/getgrav/grav-plugin-error
path: user/plugins/error
- branch: develop
+ branch: master
antimatter:
url: https://github.com/getgrav/grav-theme-antimatter
path: user/themes/antimatter
- branch: develop
+ branch: master
links:
problems:
src: grav-plugin-problems
diff --git a/.travis.yml b/.travis.yml
index c0b976c1b7..f494198f78 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -53,7 +53,7 @@ script:
for file in ${FILES[@]}; do
NAME=${file##*/};
REPO="$(echo ${NAME} | rev | cut -f 2- -d "-" | rev)";
- if [[ $REPO == 'grav' || $REPO == 'grav-update' ]]; then
+ if [[ $REPO == 'grav' || $REPO == 'grav-admin' || $REPO == 'grav-update' ]]; then
REPO="grav";
fi;
API="$(curl --fail --user "${GH_API_USER}" -s https://api.github.com/repos/${GH_USER}/${REPO}/releases/latest)";
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f4e4c03e9..7018d29c9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,43 @@
+# v0.9.41
+## 09/11/2015
+
+1. [](#new)
+ * New and improved multibyte-safe TruncateHTML function and filter
+ * Added support for custom page date format
+ * Added a `string` Twig filter to render as json_encoded string
+ * Added `authorize` Twig filter
+ * Added support for theme inheritance in the admin
+ * Support for multiple content collections on a page
+ * Added configurable files/folders ignores for pages
+ * Added the ability to set the default PHP locale and override via multi-lang configuration
+ * Added ability to save as YAML via admin
+ * Added check for `mbstring` support
+ * Added new `redirect` header for pages
+1. [](#improved)
+ * Changed dependencies from `develop` to `master`
+ * Updated logging to log everything from `debug` level on (was `warning`)
+ * Added missing `accounts/` folder
+ * Default to performing a 301 redirect for URIs with trailing slashes
+ * Improved Twig error messages
+ * Allow validating of forms from anywhere such as plugins
+ * Added logic so modular pages are by default non-routable
+ * Hide password input in `bin/grav newuser` command
+1. [](#bugfix)
+ * Fixed `Pages.all()` not returning modular pages
+ * Fix for modular template types not getting found
+ * Fix for `markdown_extra:` overriding `markdown:extra:` setting
+ * Fix for multi-site routing
+ * Fix for multi-lang page name error
+ * Fixed a redirect loop in `URI` class
+ * Fixed a potential error when `unsupported_inline_types` is empty
+ * Correctly generate 2x retina image
+ * Typo fixes in page publish/unpublish blueprint
+
# v0.9.40
## 08/31/2015
1. [](#new)
- * Added some new Twig filers: `defined`, `rtrim`, `ltrim`
+ * Added some new Twig filters: `defined`, `rtrim`, `ltrim`
* Admin support for customizable page file name + template override
1. [](#improved)
* Better message for incompatible/unsupported Twig template
diff --git a/composer.lock b/composer.lock
index b45ddfc5ef..f518624221 100644
--- a/composer.lock
+++ b/composer.lock
@@ -8,16 +8,16 @@
"packages": [
{
"name": "doctrine/cache",
- "version": "v1.4.1",
+ "version": "v1.4.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
- "reference": "c9eadeb743ac6199f7eec423cb9426bc518b7b03"
+ "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/cache/zipball/c9eadeb743ac6199f7eec423cb9426bc518b7b03",
- "reference": "c9eadeb743ac6199f7eec423cb9426bc518b7b03",
+ "url": "https://api.github.com/repos/doctrine/cache/zipball/8c434000f420ade76a07c64cbe08ca47e5c101ca",
+ "reference": "8c434000f420ade76a07c64cbe08ca47e5c101ca",
"shasum": ""
},
"require": {
@@ -74,7 +74,7 @@
"cache",
"caching"
],
- "time": "2015-04-15 00:11:59"
+ "time": "2015-08-31 12:36:41"
},
{
"name": "donatj/phpuseragentparser",
@@ -459,16 +459,16 @@
},
{
"name": "monolog/monolog",
- "version": "1.16.0",
+ "version": "1.17.1",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
- "reference": "c0c0b4bee3aabce7182876b0d912ef2595563db7"
+ "reference": "0524c87587ab85bc4c2d6f5b41253ccb930a5422"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c0c0b4bee3aabce7182876b0d912ef2595563db7",
- "reference": "c0c0b4bee3aabce7182876b0d912ef2595563db7",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/0524c87587ab85bc4c2d6f5b41253ccb930a5422",
+ "reference": "0524c87587ab85bc4c2d6f5b41253ccb930a5422",
"shasum": ""
},
"require": {
@@ -485,7 +485,7 @@
"php-console/php-console": "^3.1.3",
"phpunit/phpunit": "~4.5",
"phpunit/phpunit-mock-objects": "2.3.0",
- "raven/raven": "~0.8",
+ "raven/raven": "~0.11",
"ruflin/elastica": ">=0.90 <3.0",
"swiftmailer/swiftmailer": "~5.3",
"videlalvaro/php-amqplib": "~2.4"
@@ -531,7 +531,7 @@
"logging",
"psr-3"
],
- "time": "2015-08-09 17:44:44"
+ "time": "2015-08-31 09:17:37"
},
{
"name": "mrclay/minify",
@@ -713,16 +713,16 @@
},
{
"name": "symfony/console",
- "version": "v2.7.3",
+ "version": "v2.7.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/Console.git",
- "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e"
+ "reference": "9ff9032151186bd66ecee727d728f1319f52d1d8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/Console/zipball/d6cf02fe73634c96677e428f840704bfbcaec29e",
- "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e",
+ "url": "https://api.github.com/repos/symfony/Console/zipball/9ff9032151186bd66ecee727d728f1319f52d1d8",
+ "reference": "9ff9032151186bd66ecee727d728f1319f52d1d8",
"shasum": ""
},
"require": {
@@ -766,20 +766,20 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
- "time": "2015-07-28 15:18:12"
+ "time": "2015-09-03 11:40:38"
},
{
"name": "symfony/event-dispatcher",
- "version": "v2.7.3",
+ "version": "v2.7.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/EventDispatcher.git",
- "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3"
+ "reference": "b58c916f1db03a611b72dd702564f30ad8fe83fa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9310b5f9a87ec2ea75d20fec0b0017c77c66dac3",
- "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3",
+ "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/b58c916f1db03a611b72dd702564f30ad8fe83fa",
+ "reference": "b58c916f1db03a611b72dd702564f30ad8fe83fa",
"shasum": ""
},
"require": {
@@ -824,20 +824,20 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
- "time": "2015-06-18 19:21:56"
+ "time": "2015-08-24 07:13:45"
},
{
"name": "symfony/var-dumper",
- "version": "v2.7.3",
+ "version": "v2.7.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "e8903ebba5eb019f5886ffce739ea9e3b7519579"
+ "reference": "b39221998ff5fc26ba63f96d2b833dfddc233d57"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e8903ebba5eb019f5886ffce739ea9e3b7519579",
- "reference": "e8903ebba5eb019f5886ffce739ea9e3b7519579",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b39221998ff5fc26ba63f96d2b833dfddc233d57",
+ "reference": "b39221998ff5fc26ba63f96d2b833dfddc233d57",
"shasum": ""
},
"require": {
@@ -883,20 +883,20 @@
"debug",
"dump"
],
- "time": "2015-07-28 15:18:12"
+ "time": "2015-08-31 12:28:11"
},
{
"name": "symfony/yaml",
- "version": "v2.7.3",
+ "version": "v2.7.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/Yaml.git",
- "reference": "71340e996171474a53f3d29111d046be4ad8a0ff"
+ "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff",
- "reference": "71340e996171474a53f3d29111d046be4ad8a0ff",
+ "url": "https://api.github.com/repos/symfony/Yaml/zipball/2dc7b06c065df96cc686c66da2705e5e18aef661",
+ "reference": "2dc7b06c065df96cc686c66da2705e5e18aef661",
"shasum": ""
},
"require": {
@@ -932,20 +932,20 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
- "time": "2015-07-28 14:07:07"
+ "time": "2015-08-24 07:13:45"
},
{
"name": "twig/twig",
- "version": "v1.21.1",
+ "version": "v1.21.2",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "ca8d3aa90b6a01c82e07909fe815d6b443e75a23"
+ "reference": "ddce1136beb8db29b9cd7dffa8ab518b978c9db3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/ca8d3aa90b6a01c82e07909fe815d6b443e75a23",
- "reference": "ca8d3aa90b6a01c82e07909fe815d6b443e75a23",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/ddce1136beb8db29b9cd7dffa8ab518b978c9db3",
+ "reference": "ddce1136beb8db29b9cd7dffa8ab518b978c9db3",
"shasum": ""
},
"require": {
@@ -993,7 +993,7 @@
"keywords": [
"templating"
],
- "time": "2015-08-26 08:58:31"
+ "time": "2015-09-09 05:28:51"
}
],
"packages-dev": [],
diff --git a/index.php b/index.php
index 9379b76375..3220f22c51 100644
--- a/index.php
+++ b/index.php
@@ -19,6 +19,12 @@
// Set timezone to default, falls back to system if php.ini not set
date_default_timezone_set(@date_default_timezone_get());
+// Set internal encoding if mbstring loaded
+if (!extension_loaded('mbstring')) {
+ throw new \RuntimeException("'mbstring' extension is not loaded. This is required for Grav to run correctly");
+}
+mb_internal_encoding('UTF-8');
+
// Get the Grav instance
$grav = Grav::instance(
array(
diff --git a/nginx.conf b/nginx.conf
index 7e7147adcd..5ce2521b4d 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,88 +1,87 @@
-worker_processes 1;
+worker_processes 1;
events {
- worker_connections 1024;
+ worker_connections 1024;
}
-
http {
- include mime.types;
- default_type application/octet-stream;
- sendfile on;
- keepalive_timeout 65;
+ include mime.types;
+ default_type application/octet-stream;
+ sendfile on;
+ keepalive_timeout 65;
server {
- listen 80;
- server_name localhost;
+ listen 80;
+ server_name localhost;
- error_page 500 502 503 504 /50x.html;
+ error_page 500 502 503 504 /50x.html;
location = /50x.html {
- root html;
+ root html;
}
- location / {
- root html;
- index index.php;
- if (!-e $request_filename){ rewrite ^(.*)$ /index.php last; }
- }
+ location / {
+ root html;
+ index index.php;
+ if (!-e $request_filename){ rewrite ^(.*)$ /index.php last; }
+ }
- # if you want grav in a sub-directory of your main site
- # (for example, example.com/mygrav) then you need this rewrite:
+ # if you want grav in a sub-directory of your main site
+ # (for example, example.com/mygrav) then you need this rewrite:
location /mygrav {
- index index.php;
+ index index.php;
if (!-e $request_filename){ rewrite ^(.*)$ /mygrav/$2 last; }
try_files $uri $uri/ /index.php?$args;
}
- # if using grav in a sub-directory of your site,
- # prepend the actual path to each location
- # for example: /mygrav/images
- # and: /mygrav/user
- # and: /mygrav/cache
- # and so on
-
- location /images/ {
- # Serve images as static
- }
-
- location /user {
- rewrite ^/user/accounts/(.*)$ /error redirect;
- rewrite ^/user/config/(.*)$ /error redirect;
- rewrite ^/user/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
- }
-
- location /cache {
- rewrite ^/cache/(.*) /error redirect;
- }
-
- location /bin {
- rewrite ^/bin/(.*)$ /error redirect;
- }
+ # if using grav in a sub-directory of your site,
+ # prepend the actual path to each location
+ # for example: /mygrav/images
+ # and: /mygrav/user
+ # and: /mygrav/cache
+ # and so on
+
+ location /images/ {
+ # Serve images as static
+ }
+
+ location /user {
+ rewrite ^/user/accounts/(.*)$ /error redirect;
+ rewrite ^/user/config/(.*)$ /error redirect;
+ rewrite ^/user/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
+ }
+
+ location /cache {
+ rewrite ^/cache/(.*) /error redirect;
+ }
+
+ location /bin {
+ rewrite ^/bin/(.*)$ /error redirect;
+ }
location /backup {
- rewrite ^/backup/(.*) /error redirect;
+ rewrite ^/backup/(.*) /error redirect;
}
- location /system {
- rewrite ^/system/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
- }
+ location /system {
+ rewrite ^/system/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
+ }
- location /vendor {
- rewrite ^/vendor/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
- }
+ location /vendor {
+ rewrite ^/vendor/(.*)\.(txt|md|html|php|yaml|json|twig|sh|bat)$ /error redirect;
+ }
- # Remember to change 127.0.0.1:9000 to the Ip/port
- # you configured php-cgi.exe to run from
+ # Remember to change 127.0.0.1:9000 to the Ip/port
+ # you configured php-cgi.exe to run from
location ~ \.php$ {
- try_files $uri =404;
+ try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
- fastcgi_pass 127.0.0.1:9000;
- fastcgi_index index.php;
- fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
- include fastcgi_params;
+ fastcgi_pass 127.0.0.1:9000;
+ fastcgi_index index.php;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ include fastcgi_params;
}
- }
+ }
}
diff --git a/system/blueprints/config/site.yaml b/system/blueprints/config/site.yaml
index 0586c31a20..485f2c375e 100644
--- a/system/blueprints/config/site.yaml
+++ b/system/blueprints/config/site.yaml
@@ -6,6 +6,7 @@ form:
content:
type: section
title: PLUGIN_ADMIN.DEFAULTS
+ underline: true
fields:
title:
@@ -41,6 +42,7 @@ form:
summary:
type: section
title: PLUGIN_ADMIN.PAGE_SUMMARY
+ underline: true
fields:
summary.enabled:
@@ -83,6 +85,7 @@ form:
metadata:
type: section
title: PLUGIN_ADMIN.METADATA
+ underline: true
fields:
metadata:
@@ -95,6 +98,7 @@ form:
routes:
type: section
title: PLUGIN_ADMIN.REDIRECTS_AND_ROUTES
+ underline: true
fields:
redirects:
diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml
index feb0b039a2..f357f9a02c 100644
--- a/system/blueprints/config/system.yaml
+++ b/system/blueprints/config/system.yaml
@@ -49,6 +49,20 @@ form:
options:
'': 'Default (Server Timezone)'
+ pages.dateformat.default:
+ type: select
+ size: medium
+ selectize:
+ create: true
+ label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT
+ help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP
+ placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER
+ @data-options: '\Grav\Common\Utils::dateFormats'
+ options:
+ "": Auto Guess or Enter Custom
+ validate:
+ type: string
+
pages.dateformat.short:
type: dateformat
size: medium
@@ -139,6 +153,35 @@ form:
validate:
type: bool
+ pages.redirect_trailing_slash:
+ type: toggle
+ label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH
+ help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP
+ highlight: 1
+ options:
+ 1: PLUGIN_ADMIN.YES
+ 0: PLUGIN_ADMIN.NO
+ validate:
+ type: bool
+
+ pages.ignore_files:
+ type: selectize
+ size: large
+ label: PLUGIN_ADMIN.IGNORE_FILES
+ help: PLUGIN_ADMIN.IGNORE_FILES_HELP
+ classes: fancy
+ validate:
+ type: commalist
+
+ pages.ignore_folders:
+ type: selectize
+ size: large
+ label: PLUGIN_ADMIN.IGNORE_FOLDERS
+ help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP
+ classes: fancy
+ validate:
+ type: commalist
+
languages:
type: section
title: PLUGIN_ADMIN.LANGUAGES
@@ -202,7 +245,29 @@ form:
languages.home_redirect.include_route:
type: toggle
label: PLUGIN_ADMIN.HOME_REDIRECT_INCLUDE_ROUTE
- help: PLUGIN_ADMIN.HOME_REDIRECT_INCLUDE_ROUTE
+ help: PLUGIN_ADMIN.HOME_REDIRECT_INCLUDE_ROUTE_HELP
+ highlight: 0
+ options:
+ 1: PLUGIN_ADMIN.YES
+ 0: PLUGIN_ADMIN.NO
+ validate:
+ type: bool
+
+ languages.http_accept_language:
+ type: toggle
+ label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE
+ help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP
+ highlight: 0
+ options:
+ 1: PLUGIN_ADMIN.YES
+ 0: PLUGIN_ADMIN.NO
+ validate:
+ type: bool
+
+ languages.override_locale:
+ type: toggle
+ label: PLUGIN_ADMIN.OVERRIDE_LOCALE
+ help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP
highlight: 0
options:
1: PLUGIN_ADMIN.YES
diff --git a/system/blueprints/pages/default.yaml b/system/blueprints/pages/default.yaml
index 04ee6e753a..5c577dbca4 100644
--- a/system/blueprints/pages/default.yaml
+++ b/system/blueprints/pages/default.yaml
@@ -34,7 +34,7 @@ form:
type: textarea
uploads:
- type: uploads
+ type: pagemedia
label: PLUGIN_ADMIN.PAGE_MEDIA
options:
@@ -68,13 +68,13 @@ form:
toggleable: true
help: PLUGIN_ADMIN.DATE_HELP
- header.published_date:
+ header.publish_date:
type: datetime
label: PLUGIN_ADMIN.PUBLISHED_DATE
toggleable: true
help: PLUGIN_ADMIN.PUBLISHED_DATE_HELP
- header.unpublished_date:
+ header.unpublish_date:
type: datetime
label: PLUGIN_ADMIN.UNPUBLISHED_DATE
toggleable: true
@@ -151,7 +151,7 @@ form:
label: PLUGIN_ADMIN.PAGE_FILE
help: PLUGIN_ADMIN.PAGE_FILE_HELP
default: default
- @data-options: '\Grav\Common\Page\Pages::types'
+ @data-options: '\Grav\Common\Page\Pages::pageTypes'
header.body_classes:
type: text
@@ -194,6 +194,12 @@ form:
message: PLUGIN_ADMIN.SLUG_VALIDATE_MESSAGE
rule: slug
+ header.redirect:
+ type: text
+ label: PLUGIN_ADMIN.REDIRECT
+ toggleable: true
+ help: PLUGIN_ADMIN.REDIRECT_HELP
+
header.process:
type: checkboxes
label: PLUGIN_ADMIN.PROCESS
diff --git a/system/blueprints/pages/modular_raw.yaml b/system/blueprints/pages/modular_raw.yaml
index f3c308beac..bf40a733cf 100644
--- a/system/blueprints/pages/modular_raw.yaml
+++ b/system/blueprints/pages/modular_raw.yaml
@@ -28,7 +28,7 @@ form:
label: PLUGIN_ADMIN.CONTENT
uploads:
- type: uploads
+ type: pagemedia
label: PLUGIN_ADMIN.PAGE_MEDIA
diff --git a/system/blueprints/pages/raw.yaml b/system/blueprints/pages/raw.yaml
index 55d06e72dd..25116eadd7 100644
--- a/system/blueprints/pages/raw.yaml
+++ b/system/blueprints/pages/raw.yaml
@@ -21,13 +21,14 @@ form:
frontmatter:
type: frontmatter
label: PLUGIN_ADMIN.FRONTMATTER
+ autofocus: true
content:
type: markdown
label: PLUGIN_ADMIN.CONTENT
uploads:
- type: uploads
+ type: pagemedia
label: PLUGIN_ADMIN.PAGE_MEDIA
options:
diff --git a/system/config/system.yaml b/system/config/system.yaml
index 77edb1d937..efaddf136f 100644
--- a/system/config/system.yaml
+++ b/system/config/system.yaml
@@ -1,5 +1,6 @@
absolute_urls: false # Absolute or relative URLs for `base_url`
timezone: '' # Valid values: http://php.net/manual/en/timezones.php
+default_locale: # Default locale (defaults to system)
param_sep: ':' # Parameter separator, use ';' for Apache on windows
languages:
@@ -10,7 +11,8 @@ languages:
home_redirect:
include_lang: true # Include language in home redirect (/en)
include_route: false # Include route in home redirect (/blog)
-
+ http_accept_language: false # Attempt to set the language based on http_accept_language header in the browser
+ override_locale: false # Override the default or system locale with language specific one
home:
alias: '/home' # Default path for home, ie /
@@ -23,6 +25,7 @@ pages:
list:
count: 20 # Default item count per page
dateformat:
+ default: # The default date format Grav expects in the `date: ` field
short: 'jS M Y' # Short date format
long: 'F jS \a\t g:ia' # Long date format
publish_dates: true # automatically publish/unpublish based on dates
@@ -46,6 +49,9 @@ pages:
etag: false # Set the etag header tag
vary_accept_encoding: false # Add `Vary: Accept-Encoding` header
redirect_default_route: false # Automatically redirect to a page's default route
+ redirect_trailing_slash: true # Handle automatically or 301 redirect a trailing / URL
+ ignore_files: [.DS_Store] # Files to ignore in Pages
+ ignore_folders: [.git, .idea] # Folders to ignore in Pages
cache:
enabled: true # Set to true to enable caching
diff --git a/system/defines.php b/system/defines.php
index 8773bbed3f..cf14265543 100644
--- a/system/defines.php
+++ b/system/defines.php
@@ -2,7 +2,7 @@
// Some standard defines
define('GRAV', true);
-define('GRAV_VERSION', '0.9.40');
+define('GRAV_VERSION', '0.9.41');
define('DS', '/');
// Directories and Paths
diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php
index 30db908f71..bc9db3a73b 100644
--- a/system/src/Grav/Common/Data/Validation.php
+++ b/system/src/Grav/Common/Data/Validation.php
@@ -1,5 +1,8 @@
translate($name) . '""';
if (method_exists(__CLASS__, $method)) {
$success = self::$method($value, $validate, $field);
@@ -69,6 +77,16 @@ public static function filter($value, array $field)
return null;
}
+ // if this is a YAML field, simply parse it and return the value
+ if (isset($field['yaml']) && $field['yaml'] == true) {
+ try {
+ $yaml = new Parser();
+ return $yaml->parse($value);
+ } catch (ParseException $e) {
+ throw new \RuntimeException($e->getMessage());
+ }
+ }
+
// Validate type with fallback type text.
$type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type'];
$method = 'filter'.strtr($type, '-', '_');
@@ -288,6 +306,17 @@ protected static function filterNumber($value, array $params, array $field)
return (int) $value;
}
+ protected static function filterDateTime($value, array $params, array $field)
+ {
+ $format = self::getGrav()['config']->get('system.pages.dateformat.default');
+ if ($format) {
+ $converted = new \DateTime($value);
+ return $converted->format($format);
+ }
+ return $value;
+ }
+
+
/**
* HTML5 input: range
*
diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php
index 3ca03a0412..8a93c583c3 100644
--- a/system/src/Grav/Common/Filesystem/Folder.php
+++ b/system/src/Grav/Common/Filesystem/Folder.php
@@ -38,7 +38,7 @@ public static function lastModifiedFolder($path)
* Recursively find the last modified time under given path by file.
*
* @param string $path
- * @param string $extensions
+ * @param string $extensions which files to search for specifically
*
* @return int
*/
diff --git a/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
index f2d63f72e3..aa9fd4a843 100644
--- a/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
+++ b/system/src/Grav/Common/Filesystem/RecursiveFolderFilterIterator.php
@@ -1,11 +1,31 @@
get('system.pages.ignore_folders');
+ }
+ }
+
public function accept()
{
- // only accept directories
- return $this->current()->isDir();
+
+ /** @var $current \SplFileInfo */
+ $current = $this->current();
+
+ if ($current->isDir() && !in_array($current->getFilename(), $this::$folder_ignores)) {
+ return true;
+ }
+ return false;
}
}
diff --git a/system/src/Grav/Common/GPM/Remote/Grav.php b/system/src/Grav/Common/GPM/Remote/Grav.php
index d0a62a25f5..f8148a5266 100644
--- a/system/src/Grav/Common/GPM/Remote/Grav.php
+++ b/system/src/Grav/Common/GPM/Remote/Grav.php
@@ -27,7 +27,7 @@ public function __construct($refresh = false, $callback = null)
$this->version = isset($this->data['version']) ? $this->data['version'] : '-';
$this->date = isset($this->data['date']) ? $this->data['date'] : '-';
- foreach ($this->data['assets'] as $slug => $data) {
+ if (isset($this->data['assets'])) foreach ($this->data['assets'] as $slug => $data) {
$this->items[$slug] = new Package($data);
}
}
diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php
index e9aceffefd..2cb5a94372 100644
--- a/system/src/Grav/Common/Grav.php
+++ b/system/src/Grav/Common/Grav.php
@@ -192,6 +192,13 @@ public function process()
date_default_timezone_set($this['config']->get('system.timezone'));
}
+ // Initialize Locale if set and configured
+ if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
+ setlocale(LC_ALL, $this['language']->getLanguage());
+ } elseif ($this['config']->get('system.default_locale')) {
+ setlocale(LC_ALL, $this['config']->get('system.default_locale'));
+ }
+
$debugger->startTimer('streams', 'Streams');
$this['streams'];
$debugger->stopTimer('streams');
@@ -455,7 +462,7 @@ protected function fallbackUrl($page, $path)
if ($extension) {
$download = true;
- if (in_array(ltrim($extension, '.'), $this['config']->get('system.media.unsupported_inline_types'))) {
+ if (in_array(ltrim($extension, '.'), $this['config']->get('system.media.unsupported_inline_types', []))) {
$download = false;
}
Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
diff --git a/system/src/Grav/Common/Helpers/Truncator.php b/system/src/Grav/Common/Helpers/Truncator.php
new file mode 100644
index 0000000000..996cfdedd6
--- /dev/null
+++ b/system/src/Grav/Common/Helpers/Truncator.php
@@ -0,0 +1,164 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class InvalidHtmlException extends \Exception {
+}
+
+class Truncator {
+ public static $default_options = array(
+ 'ellipsis' => '…',
+ 'break' => ' ',
+ 'length_in_chars' => false,
+ 'word_safe' => false,
+ );
+ // These tags are allowed to have an ellipsis inside
+ public static $ellipsable_tags = array(
+ 'p', 'ol', 'ul', 'li',
+ 'div', 'header', 'article', 'nav',
+ 'section', 'footer', 'aside',
+ 'dd', 'dt', 'dl',
+ );
+ public static $self_closing_tags = array(
+ 'br', 'hr', 'img',
+ );
+ /**
+ * Truncate given HTML string to specified length.
+ * If length_in_chars is false it's trimmed by number
+ * of words, otherwise by number of characters.
+ *
+ * @param string $html
+ * @param integer $length
+ * @param string|array $opts
+ * @return string
+ */
+ public static function truncate($html, $length, $opts=array()) {
+ if (is_string($opts)) $opts = array('ellipsis' => $opts);
+ $opts = array_merge(static::$default_options, $opts);
+ // wrap the html in case it consists of adjacent nodes like
foo
bar
+ $html = mb_convert_encoding("".$html."
", 'HTML-ENTITIES', 'UTF-8');
+
+ $root_node = null;
+ // Parse using HTML5Lib if it's available.
+ if (class_exists('HTML5Lib\\Parser')) {
+ try {
+ $doc = \HTML5Lib\Parser::parse($html);
+ $root_node = $doc->documentElement->lastChild->lastChild;
+ }
+ catch (\Exception $e) {
+ ;
+ }
+ }
+ if ($root_node === null) {
+ // HTML5Lib not available so we'll have to use DOMDocument
+ // We'll only be able to parse HTML5 if it's valid XML
+ $doc = new DOMDocument('4.01', 'utf-8');
+ $doc->formatOutput = false;
+ $doc->preserveWhitespace = true;
+ // loadHTML will fail with HTML5 tags (article, nav, etc)
+ // so we need to suppress errors and if it fails to parse we
+ // retry with the XML parser instead
+ $prev_use_errors = libxml_use_internal_errors(true);
+ if ($doc->loadHTML($html)) {
+ $root_node = $doc->documentElement->lastChild->lastChild;
+ }
+ else if ($doc->loadXML($html)) {
+ $root_node = $doc->documentElement;
+ }
+ else {
+ libxml_use_internal_errors($prev_use_errors);
+ throw new InvalidHtmlException;
+ }
+ libxml_use_internal_errors($prev_use_errors);
+ }
+ list($text, $_, $opts) = static::_truncate_node($doc, $root_node, $length, $opts);
+ $text = mb_substr(mb_substr($text, 0, -6), 5);
+
+ return $text;
+ }
+ protected static function _truncate_node($doc, $node, $length, $opts) {
+ if ($length === 0 && !static::ellipsable($node)) {
+ return array('', 1, $opts);
+ }
+ list($inner, $remaining, $opts) = static::_inner_truncate($doc, $node, $length, $opts);
+ if (0 === mb_strlen($inner)) {
+ return array(in_array(mb_strtolower($node->nodeName), static::$self_closing_tags) ? $doc->saveXML($node) : "", $length - $remaining, $opts);
+ }
+ while($node->firstChild) {
+ $node->removeChild($node->firstChild);
+ }
+ $newNode = $doc->createDocumentFragment();
+ $newNode->appendXml($inner);
+ $node->appendChild($newNode);
+ return array($doc->saveXML($node), $length - $remaining, $opts);
+ }
+ protected static function _inner_truncate($doc, $node, $length, $opts) {
+ $inner = '';
+ $remaining = $length;
+ foreach($node->childNodes as $childNode) {
+ if ($childNode->nodeType === XML_ELEMENT_NODE) {
+ list($txt, $nb, $opts) = static::_truncate_node($doc, $childNode, $remaining, $opts);
+ }
+ else if ($childNode->nodeType === XML_TEXT_NODE) {
+ list($txt, $nb, $opts) = static::_truncate_text($doc, $childNode, $remaining, $opts);
+ } else {
+ $txt = '';
+ $nb = 0;
+ }
+ $remaining -= $nb;
+ $inner .= $txt;
+ if ($remaining < 0) {
+ if (static::ellipsable($node)) {
+ $inner = preg_replace('/(?:[\s\pP]+|(?:&(?:[a-z]+|#[0-9]+);?))*$/', '', $inner).$opts['ellipsis'];
+ $opts['ellipsis'] = '';
+ $opts['was_truncated'] = true;
+ }
+ break;
+ }
+ }
+ return array($inner, $remaining, $opts);
+ }
+ protected static function _truncate_text($doc, $node, $length, $opts) {
+ $string = $node->textContent;
+
+ if ($opts['length_in_chars']) {
+ $count = mb_strlen($string);
+ if ($count <= $length && $length > 0) {
+ return array($string, $count, $opts);
+ }
+ if ($opts['word_safe']) {
+ if (false !== ($breakpoint = mb_strpos($string, $opts['break'], $length))) {
+ if ($breakpoint < mb_strlen($string) - 1) {
+ $string = mb_substr($string, 0, $breakpoint) . $opts['break'];
+ }
+ }
+ return array($string, $count, $opts);
+ }
+ return array(mb_substr($node->textContent, 0, $length), $count, $opts);
+ }
+ else {
+ preg_match_all('/\s*\S+/', $string, $words);
+ $words = $words[0];
+ $count = count($words);
+ if ($count <= $length && $length > 0) {
+ return array($xhtml, $count, $opts);
+ }
+ return array(implode('', array_slice($words, 0, $length)), $count, $opts);
+ }
+ }
+ protected static function ellipsable($node) {
+ return ($node instanceof DOMDocument)
+ || in_array(mb_strtolower($node->nodeName), static::$ellipsable_tags)
+ ;
+ }
+}
\ No newline at end of file
diff --git a/system/src/Grav/Common/Page/Collection.php b/system/src/Grav/Common/Page/Collection.php
index 029788f2fa..f1f3a2b9cf 100644
--- a/system/src/Grav/Common/Page/Collection.php
+++ b/system/src/Grav/Common/Page/Collection.php
@@ -3,6 +3,7 @@
use Grav\Common\Grav;
use Grav\Common\Iterator;
+use Grav\Common\Utils;
/**
* Collection of Pages.
@@ -225,8 +226,8 @@ public function currentPosition($path)
*/
public function dateRange($startDate, $endDate = false, $field = false)
{
- $start = strtotime($startDate);
- $end = $endDate ? strtotime($endDate) : strtotime("now +1000 years");
+ $start = Utils::date2timestamp($startDate);
+ $end = $endDate ? Utils::date2timestamp($endDate) : strtotime("now +1000 years");
$date_range = [];
@@ -395,4 +396,47 @@ public function nonRoutable()
$this->items = $routable;
return $this;
}
+
+ /**
+ * Creates new collection with only pages of the specified type
+ *
+ * @return Collection The collection
+ */
+ public function ofType($type)
+ {
+ $items = [];
+
+ foreach ($this->items as $path => $slug) {
+ $page = $this->pages->get($path);
+ if ($page->template() == $type) {
+ $items[$path] = $slug;
+ }
+ }
+
+ $this->items = $items;
+ return $this;
+ }
+
+ /**
+ * Creates new collection with only pages of one of the specified types
+ *
+ * @return Collection The collection
+ */
+ public function ofOneOfTheseTypes($types)
+ {
+ $items = [];
+
+ foreach ($this->items as $path => $slug) {
+ $page = $this->pages->get($path);
+ if (in_array($page->template(), $types)) {
+ $items[$path] = $slug;
+ }
+ }
+
+ $this->items = $items;
+ return $this;
+ }
+
+
+
}
diff --git a/system/src/Grav/Common/Page/Media.php b/system/src/Grav/Common/Page/Media.php
index 1b7ac2f0bc..415f3e19bf 100644
--- a/system/src/Grav/Common/Page/Media.php
+++ b/system/src/Grav/Common/Page/Media.php
@@ -88,7 +88,7 @@ public function __construct($path)
$altMedium = $altMedium['file'];
- $medium = MediumFactory::scaledFromMedium($altMedium, $ratio, 1);
+ $medium = MediumFactory::scaledFromMedium($altMedium, $ratio, 1)['file'];
}
if (!$medium) {
@@ -116,7 +116,7 @@ public function __construct($path)
continue;
}
- $types['alternative'][$i] = MediumFactory::scaledFromMedium($alternatives[$max], $max, $i);
+ $types['alternative'][$i] = MediumFactory::scaledFromMedium($alternatives[$max]['file'], $max, $i);
}
foreach ($types['alternative'] as $ratio => $altMedium) {
diff --git a/system/src/Grav/Common/Page/Medium/MediumFactory.php b/system/src/Grav/Common/Page/Medium/MediumFactory.php
index ffa6b8f50f..e15524e773 100644
--- a/system/src/Grav/Common/Page/Medium/MediumFactory.php
+++ b/system/src/Grav/Common/Page/Medium/MediumFactory.php
@@ -140,6 +140,6 @@ public static function scaledFromMedium($medium, $from, $to)
$medium = self::fromFile($file);
$medium->set('size', $size);
- return $medium;
+ return ['file' => $medium, 'size' => $size];
}
}
diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php
index 5f88af978e..71507d1aa4 100644
--- a/system/src/Grav/Common/Page/Page.php
+++ b/system/src/Grav/Common/Page/Page.php
@@ -42,6 +42,7 @@ class Page
protected $path;
protected $extension;
+ protected $id;
protected $parent;
protected $template;
protected $expires;
@@ -56,7 +57,7 @@ class Page
protected $routes;
protected $routable;
protected $modified;
- protected $id;
+ protected $redirect;
protected $items;
protected $header;
protected $frontmatter;
@@ -101,7 +102,7 @@ public function __construct()
/** @var Config $config */
$config = self::getGrav()['config'];
- $this->routable = true;
+
$this->taxonomy = array();
$this->process = $config->get('system.pages.process');
$this->published = true;
@@ -127,6 +128,14 @@ public function init(\SplFileInfo $file, $extension = null)
$this->setPublishState();
$this->published();
+ // some routable logic
+ if (empty($this->routable) && $this->modular()) {
+ $this->routable = false;
+ } else {
+ $this->routable = true;
+ }
+
+ // some extension logic
if (empty($extension)) {
$this->extension('.'.$file->getExtension());
} else {
@@ -295,6 +304,9 @@ public function header($var = null)
if (isset($this->header->visible)) {
$this->visible = (bool) $this->header->visible;
}
+ if (isset($this->header->redirect)) {
+ $this->redirect = trim($this->header->redirect);
+ }
if (isset($this->header->order_dir)) {
$this->order_dir = trim($this->header->order_dir);
}
@@ -305,7 +317,7 @@ public function header($var = null)
$this->order_manual = (array)$this->header->order_manual;
}
if (isset($this->header->date)) {
- $this->date = strtotime($this->header->date);
+ $this->date($this->header->date);
}
if (isset($this->header->markdown_extra)) {
$this->markdown_extra = (bool)$this->header->markdown_extra;
@@ -327,10 +339,10 @@ public function header($var = null)
$this->published = (bool) $this->header->published;
}
if (isset($this->header->publish_date)) {
- $this->publish_date = strtotime($this->header->publish_date);
+ $this->publishDate($this->header->publish_date);
}
if (isset($this->header->unpublish_date)) {
- $this->unpublish_date = strtotime($this->header->unpublish_date);
+ $this->unpublishDate($this->header->unpublish_date);
}
if (isset($this->header->expires)) {
$this->expires = intval($this->header->expires);
@@ -541,7 +553,7 @@ protected function processMarkdown()
}
// pages.markdown_extra is deprecated, but still check it...
- if (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null) {
+ if (!isset($defaults['extra']) && (isset($this->markdown_extra) || $config->get('system.pages.markdown_extra') !== null)) {
$defaults['extra'] = $this->markdown_extra ?: $config->get('system.pages.markdown_extra');
}
@@ -624,7 +636,8 @@ public function value($name, $default = null)
return preg_replace($regex, '', $this->folder);
}
if ($name == 'name') {
- $name_val = str_replace('.md', '', $this->name());
+ $language = $this->language() ? '.' . $this->language() : '';
+ $name_val = str_replace($language .'.md', '', $this->name());
if ($this->modular()) {
return 'modular/' . $name_val;
}
@@ -771,7 +784,7 @@ public function blueprints()
$blueprint = $pages->blueprints($this->blueprintName());
$fields = $blueprint->fields();
- $edit_mode = self::getGrav()['admin'] ? self::getGrav()['config']->get('plugins.admin.edit_mode') : null;
+ $edit_mode = isset(self::getGrav()['admin']) ? self::getGrav()['config']->get('plugins.admin.edit_mode') : null;
// override if you only want 'normal' mode
if (empty($fields) && ($edit_mode == 'auto' || $edit_mode == 'normal')) {
@@ -1051,7 +1064,7 @@ public function published($var = null)
public function publishDate($var = null)
{
if ($var !== null) {
- $this->publish_date = strtotime($var);
+ $this->publish_date = Utils::date2timestamp($var);
}
if ($this->publish_date === null) {
@@ -1070,7 +1083,7 @@ public function publishDate($var = null)
public function unpublishDate($var = null)
{
if ($var !== null) {
- $this->unpublish_date = strtotime($var);
+ $this->unpublish_date = Utils::date2timestamp($var);
}
return $this->unpublish_date;
@@ -1089,6 +1102,7 @@ public function routable($var = null)
if ($var !== null) {
$this->routable = (bool) $var;
}
+
return $this->routable && $this->published();
}
@@ -1383,6 +1397,20 @@ public function modified($var = null)
return $this->modified;
}
+ /**
+ * Gets the redirect set in the header.
+ *
+ * @param string $var redirect url
+ * @return array
+ */
+ public function redirect($var = null)
+ {
+ if ($var !== null) {
+ $this->redirect = $var;
+ }
+ return $this->redirect;
+ }
+
/**
* Gets and sets the option to show the etag header for the page.
*
@@ -1487,7 +1515,7 @@ public function folder($var = null)
public function date($var = null)
{
if ($var !== null) {
- $this->date = strtotime($var);
+ $this->date = Utils::date2timestamp($var);
}
if (!$this->date) {
@@ -1838,6 +1866,7 @@ public function collection($params = 'content', $pagination = true)
$collection->setParams(['taxonomies' => [$taxonomy => $items]]);
foreach ($collection as $page) {
+ // Don't filter modular pages
if ($page->modular()) {
continue;
}
@@ -1905,7 +1934,11 @@ protected function evaluate($value)
$cmd = (string) key($value);
$params = (array) current($value);
} else {
- return $value;
+ $result = [];
+ foreach($value as $key => $val) {
+ $result = $result + $this->evaluate([$key=>$val])->toArray();
+ }
+ return new Collection($result);
}
// We only evaluate commands which start with @
@@ -1952,7 +1985,7 @@ protected function evaluate($value)
if (!empty($parts)) {
$params = [implode('.', $parts) => $params];
}
- $results = $taxonomy_map->findTaxonomy($params);
+ $results = $taxonomy_map->findTaxonomy($params)->published();
break;
}
diff --git a/system/src/Grav/Common/Page/Pages.php b/system/src/Grav/Common/Page/Pages.php
index 27fe4c3ce5..3c857c0d1b 100644
--- a/system/src/Grav/Common/Page/Pages.php
+++ b/system/src/Grav/Common/Page/Pages.php
@@ -10,6 +10,7 @@
use Grav\Common\Data\Blueprint;
use Grav\Common\Data\Blueprints;
use Grav\Common\Filesystem\Folder;
+use Grav\Plugin\Admin;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Whoops\Exception\ErrorException;
@@ -62,6 +63,9 @@ class Pages
*/
protected $last_modified;
+ protected $ignore_files;
+ protected $ignore_folders;
+
/**
* @var Types
*/
@@ -101,6 +105,10 @@ public function base($path = null)
*/
public function init()
{
+ $config = $this->grav['config'];
+ $this->ignore_files = $config->get('system.pages.ignore_files');
+ $this->ignore_folders = $config->get('system.pages.ignore_folders');
+
$this->buildPages();
}
@@ -263,8 +271,17 @@ public function dispatch($url, $all = false)
// Fetch page if there's a defined route to it.
$page = isset($this->routes[$url]) ? $this->get($this->routes[$url]) : null;
+ // Are we in the admin? this is important!
+ $not_admin = !isset($this->grav['admin']);
+
// If the page cannot be reached, look into site wide redirects, routes + wildcards
- if (!$all && (!$page || !$page->routable())) {
+ if (!$all && $not_admin && (!$page || ($page && (!$page->routable() || $page->redirect())))) {
+
+ // If the page is a simple redirect, just do it.
+ if ($page && $page->redirect()) {
+ $this->grav->redirectLangSafe($page->redirect());
+ }
+
/** @var Config $config */
$config = $this->grav['config'];
@@ -282,7 +299,7 @@ public function dispatch($url, $all = false)
$this->grav->redirectLangSafe($found);
}
} catch (ErrorException $e) {
- $this->grav['log']->error('site.redirects: '. $pattern . '-> ' . $e->getMessage());
+ $this->grav['log']->error('site.redirects: ' . $pattern . '-> ' . $e->getMessage());
}
}
@@ -352,9 +369,11 @@ public function blueprints($type)
public function all(Page $current = null)
{
$all = new Collection();
+
+ /** @var Page $current */
$current = $current ?: $this->root();
- if ($current->routable()) {
+ if (!$current->root()) {
$all[$current->path()] = [ 'slug' => $current->slug() ];
}
@@ -403,10 +422,11 @@ public function getList(Page $current = null, $level = 0)
*/
public static function getTypes()
{
+ $locator = Grav::instance()['locator'];
if (!self::$types) {
self::$types = new Types();
- file_exists('theme://blueprints/') && self::$types->scanBlueprints('theme://blueprints/');
- file_exists('theme://templates/') && self::$types->scanTemplates('theme://templates/');
+ file_exists('theme://blueprints/') && self::$types->scanBlueprints($locator->findResources('theme://blueprints/'));
+ file_exists('theme://templates/') && self::$types->scanTemplates($locator->findResources('theme://templates/'));
$event = new Event();
$event->types = self::$types;
@@ -440,6 +460,26 @@ public static function modularTypes()
return $types->modularSelect();
}
+ /**
+ * Get template types based on page type (standard or modular)
+ *
+ * @return array
+ */
+ public static function pageTypes()
+ {
+ /** @var Admin $admin */
+ $admin = Grav::instance()['admin'];
+
+ /** @var Page $page */
+ $page = $admin->getPage($admin->route);
+
+ if ($page && $page->modular()) {
+ return static::modularTypes();
+ }
+
+ return static::types();
+ }
+
/**
* Get available parents.
*
@@ -664,10 +704,10 @@ protected function recurse($directory, Page &$parent = null)
if ($file->isFile()) {
// Update the last modified if it's newer than already found
- if ($file->getBasename() !== '.DS_Store' && ($modified = $file->getMTime()) > $last_modified) {
+ if (!in_array($file->getBasename(), $this->ignore_files) && ($modified = $file->getMTime()) > $last_modified) {
$last_modified = $modified;
}
- } elseif ($file->isDir()) {
+ } elseif ($file->isDir() && !in_array($file->getFilename(), $this->ignore_folders)) {
if (!$page->path()) {
$page->path($file->getPath());
}
diff --git a/system/src/Grav/Common/Page/Types.php b/system/src/Grav/Common/Page/Types.php
index 7138359d97..6ef1191406 100644
--- a/system/src/Grav/Common/Page/Types.php
+++ b/system/src/Grav/Common/Page/Types.php
@@ -28,12 +28,12 @@ public function register($type, $blueprint = null)
}
}
- public function scanBlueprints($path)
+ public function scanBlueprints($paths)
{
- $this->items = $this->findBlueprints($path) + $this->items;
+ $this->items = $this->findBlueprints($paths) + $this->items;
}
- public function scanTemplates($path)
+ public function scanTemplates($paths)
{
$options = [
'compare' => 'Filename',
@@ -52,12 +52,15 @@ public function scanTemplates($path)
// register default by default
$this->register('default');
- foreach (Folder::all($path, $options) as $type) {
- $this->register($type);
- }
- if (file_exists($path . 'modular/')) {
- foreach (Folder::all($path . 'modular/', $options) as $type) {
- $this->register('modular/' . $type);
+ foreach ((array) $paths as $path) {
+ foreach (Folder::all($path, $options) as $type) {
+ $this->register($type);
+ }
+ $modular_path = rtrim($path, '/') . '/modular';
+ if (file_exists($modular_path)) {
+ foreach (Folder::all($modular_path, $options) as $type) {
+ $this->register('modular/' . $type);
+ }
}
}
}
@@ -88,7 +91,7 @@ public function modularSelect()
return $list;
}
- private function findBlueprints($path)
+ private function findBlueprints($paths)
{
$options = [
'compare' => 'Filename',
@@ -100,6 +103,8 @@ private function findBlueprints($path)
'value' => 'PathName',
];
- return Folder::all($path, $options);
+ foreach ((array) $paths as $path) {
+ return Folder::all($path, $options);
+ }
}
}
diff --git a/system/src/Grav/Common/Service/LoggerServiceProvider.php b/system/src/Grav/Common/Service/LoggerServiceProvider.php
index 8e2884b539..edaf921789 100644
--- a/system/src/Grav/Common/Service/LoggerServiceProvider.php
+++ b/system/src/Grav/Common/Service/LoggerServiceProvider.php
@@ -13,7 +13,7 @@ public function register(Container $container)
$log = new Logger('grav');
$log_file = LOG_DIR.'grav.log';
- $log->pushHandler(new StreamHandler($log_file, Logger::WARNING));
+ $log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));
$container['log'] = $log;
}
diff --git a/system/src/Grav/Common/Twig/Twig.php b/system/src/Grav/Common/Twig/Twig.php
index b2615ffc97..6ecdb077f5 100644
--- a/system/src/Grav/Common/Twig/Twig.php
+++ b/system/src/Grav/Common/Twig/Twig.php
@@ -5,6 +5,7 @@
use Grav\Common\Config\Config;
use Grav\Common\Page\Page;
use Grav\Common\Inflector;
+use Grav\Common\Utils;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
@@ -327,8 +328,13 @@ public function processSite($format = null)
$output = $this->twig->render($template, $twig_vars);
} catch (\Twig_Error_Loader $e) {
// If loader error, and not .html.twig, try it as fallback
- $inflector = new Inflector();
- $error_msg = 'The template file for this page: "' . $page->template().'.html'.TWIG_EXT.'" is not provided by the theme: "'. $inflector->titleize($config->get('system.pages.theme')) .'"';
+ if (Utils::contains($e->getMessage(), $template)) {
+ $inflector = new Inflector();
+ $error_msg = 'The template file for this page: "' . $template.'" is not provided by the theme: "'. $inflector->titleize($config->get('system.pages.theme')) .'"';
+ } else {
+ $error_msg = $e->getMessage();
+ }
+ // Try html version of this template if initial template was NOT html
if ($ext != '.html'.TWIG_EXT) {
try {
$output = $this->twig->render($page->template().'.html'.TWIG_EXT, $twig_vars);
diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php
index c003c5c30e..6daca96241 100644
--- a/system/src/Grav/Common/Twig/TwigExtension.php
+++ b/system/src/Grav/Common/Twig/TwigExtension.php
@@ -45,25 +45,28 @@ public function getName()
public function getFilters()
{
return [
- new \Twig_SimpleFilter('fieldName', [$this,'fieldNameFilter']),
- new \Twig_SimpleFilter('safe_email', [$this,'safeEmailFilter']),
- new \Twig_SimpleFilter('randomize', [$this,'randomizeFilter']),
- new \Twig_SimpleFilter('truncate', [$this,'truncateFilter']),
new \Twig_SimpleFilter('*ize', [$this,'inflectorFilter']),
- new \Twig_SimpleFilter('md5', [$this,'md5Filter']),
- new \Twig_SimpleFilter('sort_by_key', [$this,'sortByKeyFilter']),
- new \Twig_SimpleFilter('ksort', [$this,'ksortFilter']),
+ new \Twig_SimpleFilter('absolute_url', [$this, 'absoluteUrlFilter']),
new \Twig_SimpleFilter('contains', [$this, 'containsFilter']),
- new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFilter']),
new \Twig_SimpleFilter('defined', [$this, 'definedDefaultFilter']),
- new \Twig_SimpleFilter('absolute_url', [$this, 'absoluteUrlFilter']),
+ 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, 'markdownFilter']),
- new \Twig_SimpleFilter('starts_with', [$this, 'startsWithFilter']),
+ new \Twig_SimpleFilter('md5', [$this,'md5Filter']),
+ new \Twig_SimpleFilter('nicetime', [$this, 'nicetimeFilter']),
+ new \Twig_SimpleFilter('randomize', [$this,'randomizeFilter']),
new \Twig_SimpleFilter('rtrim', [$this, 'rtrimFilter']),
- new \Twig_SimpleFilter('ltrim', [$this, 'ltrimFilter']),
- new \Twig_SimpleFilter('ends_with', [$this, 'endsWithFilter']),
+ new \Twig_SimpleFilter('safe_email', [$this,'safeEmailFilter']),
+ new \Twig_SimpleFilter('safe_truncate', ['\Grav\Common\Utils','safeTruncate']),
+ 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('ta', [$this, 'translateArray'])
+ new \Twig_SimpleFilter('ta', [$this, 'translateArray']),
+ new \Twig_SimpleFilter('truncate', ['\Grav\Common\Utils','truncate']),
+ new \Twig_SimpleFilter('truncate_html', ['\Grav\Common\Utils','truncateHTML']),
];
}
@@ -75,15 +78,17 @@ public function getFilters()
public function getFunctions()
{
return [
- new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']),
- new \Twig_SimpleFunction('url', [$this, 'urlFunc']),
- new \Twig_SimpleFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
+ new \Twig_SimpleFunction('array', [$this, 'arrayFunc']),
+ new \Twig_simpleFunction('authorize', [$this, 'authorize']),
new \Twig_SimpleFunction('debug', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
+ new \Twig_SimpleFunction('dump', [$this, 'dump'], ['needs_context' => true, 'needs_environment' => true]),
new \Twig_SimpleFunction('gist', [$this, 'gistFunc']),
+ new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']),
new \Twig_simpleFunction('random_string', [$this, 'randomStringFunc']),
- new \Twig_SimpleFunction('array', [$this, 'arrayFunc']),
+ new \Twig_SimpleFunction('string', [$this, 'stringFunc']),
new \Twig_simpleFunction('t', [$this, 'translate']),
- new \Twig_simpleFunction('ta', [$this, 'translateArray'])
+ new \Twig_simpleFunction('ta', [$this, 'translateArray']),
+ new \Twig_SimpleFunction('url', [$this, 'urlFunc']),
];
}
@@ -116,33 +121,6 @@ public function safeEmailFilter($str)
return $email;
}
- /**
- * Truncate content by a limit.
- *
- * @param string $string
- * @param int $limit Max number of characters.
- * @param string $break Break point.
- * @param string $pad Appended padding to the end of the string.
- * @return string
- */
- public function truncateFilter($string, $limit = 150, $break = ".", $pad = "…")
- {
- // return with no change if string is shorter than $limit
- if (strlen($string) <= $limit) {
- return $string;
- }
-
- // is $break present between $limit and the end of the string?
- if (false !== ($breakpoint = strpos($string, $break, $limit))) {
- if ($breakpoint < strlen($string) - 1) {
- $string = substr($string, 0, $breakpoint) . $pad;
- }
- }
-
- return $string;
- }
-
-
/**
* Returns array in a random order.
*
@@ -394,7 +372,7 @@ public function ltrimFilter($value, $chars = null)
public function translate()
{
- return $this->grav['language']->translate(func_get_args());
+ return $this->grav['language']->translate(func_get_args());
}
public function translateArray($key, $index, $lang = null)
@@ -506,13 +484,65 @@ public function randomStringFunc($count = 5)
return Utils::generateRandomString($count);
}
+ /**
+ * Cast a value to array
+ *
+ * @param $value
+ *
+ * @return array
+ */
public function arrayFunc($value)
{
return (array) $value;
}
+ /**
+ * Returns a string from a value. If the value is array, return it json encoded
+ *
+ * @param $value
+ *
+ * @return string
+ */
+ public function stringFunc($value)
+ {
+ if (is_array($value)) { //format the array as a string
+ return json_encode($value);
+ } else {
+ return $value;
+ }
+ }
+
+ /**
+ * Translate a string
+ *
+ * @return string
+ */
public function translateFunc()
{
return $this->grav['language']->translate(func_get_args());
}
+
+ /**
+ * Authorize an action. Returns true if the user is logged in and has the right to execute $action.
+ *
+ * @param string $action
+ *
+ * @return bool
+ */
+ public function authorize($action)
+ {
+ if (!$this->grav['user']->authenticated) {
+ return false;
+ }
+
+ $action = (array)$action;
+
+ foreach ($action as $a) {
+ if ($this->grav['user']->authorize($a)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php
index 7f8275d45a..563f4232b5 100644
--- a/system/src/Grav/Common/Uri.php
+++ b/system/src/Grav/Common/Uri.php
@@ -31,7 +31,6 @@ class Uri
*/
public function __construct()
{
-
$base = 'http://';
$name = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost';
$port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
@@ -85,10 +84,20 @@ public function init()
$this->params = [];
$this->query = [];
-
// get any params and remove them
$uri = str_replace($this->root, '', $this->url);
+ // 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 301 redirect
+ if ($config->get('system.pages.redirect_trailing_slash', false) && $uri != '/' && Utils::endsWith($uri, '/')) {
+ $grav->redirect(rtrim($uri, '/'), 301);
+ }
+
// process params
$uri = $this->processParams($uri, $config->get('system.param_sep'));
@@ -105,6 +114,7 @@ public function init()
}
}
+
// split the URL and params
$bits = parse_url($uri);
diff --git a/system/src/Grav/Common/User/User.php b/system/src/Grav/Common/User/User.php
index 7fdc24ca49..1982bddd2c 100644
--- a/system/src/Grav/Common/User/User.php
+++ b/system/src/Grav/Common/User/User.php
@@ -117,7 +117,7 @@ public function save()
* @param string $action
* @return bool
*/
- public function authorise($action)
+ public function authorize($action)
{
if (empty($this->items)) {
return false;
@@ -125,4 +125,17 @@ public function authorise($action)
return $this->get("access.{$action}") === true;
}
+
+ /**
+ * Checks user authorization to the action.
+ * Ensures backwards compatibility
+ *
+ * @param string $action
+ * @deprecated use authorize()
+ * @return bool
+ */
+ public function authorise($action)
+ {
+ $this->authorize($action);
+ }
}
diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php
index d7421c9b63..67b776b863 100644
--- a/system/src/Grav/Common/Utils.php
+++ b/system/src/Grav/Common/Utils.php
@@ -1,6 +1,9 @@
'd-m-Y H:i (e.g. '.$now->format('d-m-Y H:i').')',
+ 'Y-m-d H:i' => 'Y-m-d H:i (e.g. '.$now->format('Y-m-d H:i').')',
+ 'm/d/Y h:i a' => 'm/d/Y h:i (e.g. '.$now->format('m/d/Y h:i a').')',
+ 'H:i d-m-Y' => 'H:i d-m-Y (e.g. '.$now->format('H:i d-m-Y').')',
+ 'h:i a m/d/Y' => 'h:i a m/d/Y (e.g. '.$now->format('h:i a m/d/Y').')',
+ ];
+ $default_format = self::getGrav()['config']->get('system.pages.dateformat.default');
+ if ($default_format) {
+ $date_formats = array_merge([$default_format => $default_format.' (e.g. '.$now->format($default_format).')'], $date_formats);
+ }
+ return $date_formats;
+ }
+
/**
- * Truncate HTML by text length.
- *
- * @param string $text
- * @param int $length
- * @param string $ending
- * @param bool $exact
- * @param bool $considerHtml
+ * Truncate text by number of characters but can cut off words.
*
+ * @param string $string
+ * @param int $limit Max number of characters.
+ * @param bool $up_to_break truncate up to breakpoint after char count
+ * @param string $break Break point.
+ * @param string $pad Appended padding to the end of the string.
* @return string
*/
- public static function truncateHtml($text, $length = 100, $ending = '...', $exact = false, $considerHtml = true)
+ public static function truncate($string, $limit = 150, $up_to_break = false, $break = " ", $pad = "…")
{
- $open_tags = array();
- if ($considerHtml) {
- // if the plain text is shorter than the maximum length, return the whole text
- if (strlen(preg_replace('/<.*?>/', '', $text)) <= $length) {
- return $text;
- }
- // splits all html-tags to scannable lines
- preg_match_all('/(<.+?>)?([^<>]*)/s', $text, $lines, PREG_SET_ORDER);
- $total_length = strlen($ending);
- $truncate = '';
- foreach ($lines as $line_matchings) {
- // if there is any html-tag in this line, handle it and add it (uncounted) to the output
- if (!empty($line_matchings[1])) {
- // if it's an "empty element" with or without xhtml-conform closing slash
- if (preg_match('/^<(\s*.+?\/\s*|\s*(img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param)(\s.+?)?)>$/is',
- $line_matchings[1])) {
- // do nothing
- // if tag is a closing tag
- } else {
- if (preg_match('/^<\s*\/([^\s]+?)\s*>$/s', $line_matchings[1], $tag_matchings)) {
- // delete tag from $open_tags list
- $pos = array_search($tag_matchings[1], $open_tags);
- if ($pos !== false) {
- unset($open_tags[$pos]);
- }
- // if tag is an opening tag
- } else {
- if (preg_match('/^<\s*([^\s>!]+).*?>$/s', $line_matchings[1], $tag_matchings)) {
- // add tag to the beginning of $open_tags list
- array_unshift($open_tags, strtolower($tag_matchings[1]));
- }
- }
- }
- // add html-tag to $truncate'd text
- $truncate .= $line_matchings[1];
- }
- // calculate the length of the plain text part of the line; handle entities as one character
- $content_length = strlen(preg_replace('/&[0-9a-z]{2,8};|[0-9]{1,7};|[0-9a-f]{1,6};/i', ' ',
- $line_matchings[2]));
- if ($total_length + $content_length > $length) {
- // the number of characters which are left
- $left = $length - $total_length;
- $entities_length = 0;
- // search for html entities
- if (preg_match_all('/&[0-9a-z]{2,8};|[0-9]{1,7};|[0-9a-f]{1,6};/i', $line_matchings[2], $entities,
- PREG_OFFSET_CAPTURE)) {
- // calculate the real length of all entities in the legal range
- foreach ($entities[0] as $entity) {
- if ($entity[1] + 1 - $entities_length <= $left) {
- $left--;
- $entities_length += strlen($entity[0]);
- } else {
- // no more characters left
- break;
- }
- }
- }
- $truncate .= substr($line_matchings[2], 0, $left + $entities_length);
- // maximum length is reached, so get off the loop
- break;
- } else {
- $truncate .= $line_matchings[2];
- $total_length += $content_length;
- }
- // if the maximum length is reached, get off the loop
- if ($total_length >= $length) {
- break;
- }
- }
- } else {
- if (strlen($text) <= $length) {
- return $text;
- } else {
- $truncate = substr($text, 0, $length - strlen($ending));
- }
- }
- // if the words shouldn't be cut in the middle...
- if (!$exact) {
- // ...search the last occurrence of a space...
- $spacepos = strrpos($truncate, ' ');
- if (isset($spacepos)) {
- // ...and cut the text in this position
- $truncate = substr($truncate, 0, $spacepos);
- }
+ // return with no change if string is shorter than $limit
+ if (mb_strlen($string) <= $limit) {
+ return $string;
}
- // add the defined ending to the text
- $truncate .= $ending;
- if ($considerHtml) {
- // close all unclosed html-tags
- foreach ($open_tags as $tag) {
- $truncate .= '' . $tag . '>';
+
+ // 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;
}
+ } else {
+ $string = mb_substr($string, 0, $limit) . $pad;
}
- return $truncate;
+ return $string;
+ }
+
+ /**
+ * Truncate text by number of characters in a "word-safe" manor.
+ *
+ * @param $string
+ * @param int $limit
+ * @return string
+ */
+ public static function safeTruncate($string, $limit = 150)
+ {
+ return static::truncate($string, $limit, true);
+ }
+
+
+ /**
+ * Truncate HTML by number of characters. not "word-safe"!
+ *
+ * @param string $text
+ * @param int $length
+ *
+ * @return string
+ */
+ public static function truncateHtml($text, $length = 100)
+ {
+ return Truncator::truncate($text, $length, array('length_in_chars' => true));
+ }
+
+ /**
+ * Truncate HTML by number of characters. not "word-safe"!
+ *
+ * @param string $text
+ * @param int $length
+ *
+ * @return string
+ */
+ public static function safeTruncateHtml($text, $length = 100)
+ {
+ return Truncator::truncate($text, $length, array('length_in_chars' => true, 'word_safe' => true));
}
/**
@@ -449,6 +428,24 @@ public static function pathPrefixedByLangCode($string)
return false;
}
+ public static function date2timestamp($date)
+ {
+ $config = self::getGrav()['config'];
+ $default_dateformat = $config->get('system.pages.dateformat.default');
+ // try to use DateTime and default format
+ if ($default_dateformat) {
+ $datetime = DateTime::createFromFormat($default_dateformat, $date);
+ } else {
+ $datetime = new DateTime($date);
+ }
+
+ // fallback to strtotime if DateTime approach failed
+ if ($datetime !== false) {
+ return $datetime->getTimestamp();
+ } else {
+ return strtotime($date);
+ }
+ }
}
diff --git a/system/src/Grav/Console/Cli/NewUserCommand.php b/system/src/Grav/Console/Cli/NewUserCommand.php
index 2e3da7db86..82be0d8028 100644
--- a/system/src/Grav/Console/Cli/NewUserCommand.php
+++ b/system/src/Grav/Console/Cli/NewUserCommand.php
@@ -8,6 +8,7 @@
use Grav\Console\ConsoleTrait;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -66,14 +67,19 @@ protected function execute(InputInterface $input, OutputInterface $output)
$username = $helper->ask($this->input, $this->output, $question);
// Get password and validate
- $question = new Question('Enter a password: ');
- $question->setValidator(function ($value) {
- if (!preg_match('/(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/', $value)) {
+ $password = $this->askForPassword($helper, 'Enter a password: ', function ($password1) use ($helper) {
+ if (!preg_match('/(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/', $password1)) {
throw new RuntimeException('Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters');
}
- return $value;
+ // Since input is hidden when prompting for passwords, the user is asked to repeat the password
+ return $this->askForPassword($helper, 'Repeat the password: ', function ($password2) use ($password1) {
+ if (strcmp($password2, $password1)) {
+ throw new RuntimeException('Passwords did not match.');
+ }
+ return $password2;
+ });
});
- $data['password'] = $helper->ask($this->input, $this->output, $question);
+ $data['password'] = $password;
// Get email and validate
$question = new Question('Enter an email: ');
@@ -133,4 +139,22 @@ protected function execute(InputInterface $input, OutputInterface $output)
$this->output->writeln('');
$this->output->writeln('Success! User '. $username .' created.');
}
+
+ /**
+ * Get password and validate.
+ *
+ * @param Helper $helper
+ * @param string $question
+ * @param callable $validator
+ *
+ * @return string
+ */
+ protected function askForPassword(Helper $helper, $question, callable $validator)
+ {
+ $question = new Question($question);
+ $question->setValidator($validator);
+ $question->setHidden(true);
+ $question->setHiddenFallback(true);
+ return $helper->ask($this->input, $this->output, $question);
+ }
}
diff --git a/user/accounts/.gitkeep b/user/accounts/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2