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 .= ''; + + // 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