diff --git a/CHANGELOG.md b/CHANGELOG.md index 617449b80..813d9e6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ 1. [](#bugfix) * Fixed a fatal error if you have a collection with missing or invalid `@page: /route` + * Fixed gzip compression making it to work correctly with all servers and browsers # v1.0.0-rc.3 ## 10/29/2015 diff --git a/system/blueprints/config/system.yaml b/system/blueprints/config/system.yaml index e79da93e3..ce1169ea4 100644 --- a/system/blueprints/config/system.yaml +++ b/system/blueprints/config/system.yaml @@ -73,7 +73,7 @@ form: options: "F jS \\a\\t g:ia": Date1 "l jS \\of F g:i A": Date2 - "D, m M Y G:i:s": Date3 + "D, d M Y G:i:s": Date3 "d-m-y G:i": Date4 "jS M Y": Date5 @@ -86,7 +86,7 @@ form: options: "F jS \\a\\t g:ia": Date1 "l jS \\of F g:i A": Date2 - "D, m M Y G:i:s": Date3 + "D, d M Y G:i:s": Date3 "d-m-y G:i": Date4 "jS M Y": Date5 diff --git a/system/config/system.yaml b/system/config/system.yaml index 43b1dbba5..a53951e7c 100644 --- a/system/config/system.yaml +++ b/system/config/system.yaml @@ -1,113 +1,114 @@ -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 -wrapped_site: false # For themes/plugins to know if Grav is wrapped by another platform +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 +wrapped_site: false # For themes/plugins to know if Grav is wrapped by another platform languages: - supported: [] # List of languages supported. eg: [en, fr, de] - include_default_lang: true # Include the default lang prefix in all URLs - translations: true # Enable translations by default - translations_fallback: true # Fallback through supported translations if active lang doesn't exist - session_store_active: false # Store active language in session - 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 + supported: [] # List of languages supported. eg: [en, fr, de] + include_default_lang: true # Include the default lang prefix in all URLs + translations: true # Enable translations by default + translations_fallback: true # Fallback through supported translations if active lang doesn't exist + session_store_active: false # Store active language in session + 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 / + alias: '/home' # Default path for home, ie / pages: - theme: antimatter # Default theme (defaults to "antimatter" theme) + theme: antimatter # Default theme (defaults to "antimatter" theme) order: - by: default # Order pages by "default", "alpha" or "date" - dir: asc # Default ordering direction, "asc" or "desc" + by: default # Order pages by "default", "alpha" or "date" + dir: asc # Default ordering direction, "asc" or "desc" list: - count: 20 # Default item count per page + 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 + 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 process: - markdown: true # Process Markdown - twig: false # Process Twig + markdown: true # Process Markdown + twig: false # Process Twig events: - page: true # Enable page level events - twig: true # Enable twig level events + page: true # Enable page level events + twig: true # Enable twig level events markdown: - extra: false # Enable support for Markdown Extra support (GFM by default) - auto_line_breaks: false # Enable automatic line breaks - auto_url_links: false # Enable automatic HTML links - escape_markup: false # Escape markup tags into entities - special_chars: # List of special characters to automatically convert to entities + extra: false # Enable support for Markdown Extra support (GFM by default) + auto_line_breaks: false # Enable automatic line breaks + auto_url_links: false # Enable automatic HTML links + escape_markup: false # Escape markup tags into entities + special_chars: # List of special characters to automatically convert to entities '>': 'gt' '<': 'lt' - types: [txt,xml,html,json,rss,atom] # list of valid page types - expires: 604800 # Page expires time in seconds (604800 seconds = 7 days) - last_modified: false # Set the last modified date header based on file modifcation timestamp - 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_default_code: 301 # Default code to use for redirects - 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 - ignore_hidden: true # Ignore all Hidden files and folders - url_taxonomy_filters: true # Enable auto-magic URL-based taxonomy filters for page collections - fallback_types: [png,jpg,jpeg,gif] # Allowed types of files found if accessed via Page route + types: [txt,xml,html,htm,json,rss,atom] # list of valid page types + append_url_extension: '' # Append page's extension in Page urls (e.g. '.html' results in /path/page.html) + expires: 604800 # Page expires time in seconds (604800 seconds = 7 days) + last_modified: false # Set the last modified date header based on file modifcation timestamp + 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_default_code: 301 # Default code to use for redirects + 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 + ignore_hidden: true # Ignore all Hidden files and folders + url_taxonomy_filters: true # Enable auto-magic URL-based taxonomy filters for page collections + fallback_types: [png,jpg,jpeg,gif] # Allowed types of files found if accessed via Page route cache: - enabled: true # Set to true to enable caching + enabled: true # Set to true to enable caching check: - method: file # Method to check for updates in pages: file|folder|none - driver: auto # One of: auto|file|apc|xcache|memcache|wincache - prefix: 'g' # Cache prefix string (prevents cache conflicts) - lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite) - gzip: false # GZip compress the page output + method: file # Method to check for updates in pages: file|folder|none + driver: auto # One of: auto|file|apc|xcache|memcache|wincache + prefix: 'g' # Cache prefix string (prevents cache conflicts) + lifetime: 604800 # Lifetime of cached data in seconds (0 = infinite) + gzip: false # GZip compress the page output twig: - cache: true # Set to true to enable twig caching - debug: false # Enable Twig debug - auto_reload: true # Refresh cache on changes - autoescape: false # Autoescape Twig vars - undefined_functions: true # Allow undefined functions - undefined_filters: true # Allow undefined filters - umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775 + cache: true # Set to true to enable twig caching + debug: false # Enable Twig debug + auto_reload: true # Refresh cache on changes + autoescape: false # Autoescape Twig vars + undefined_functions: true # Allow undefined functions + undefined_filters: true # Allow undefined filters + umask_fix: false # By default Twig creates cached files as 755, fix switches this to 775 -assets: # Configuration for Assets Manager (JS, CSS) - css_pipeline: false # The CSS pipeline is the unification of multiple CSS resources into one file - css_minify: true # Minify the CSS during pipelining - css_minify_windows: false # Minify Override for Windows platforms. False by default due to ThreadStackSize - css_rewrite: true # Rewrite any CSS relative URLs during pipelining - js_pipeline: false # The JS pipeline is the unification of multiple JS resources into one file - js_minify: true # Minify the JS during pipelining - enable_asset_timestamp: false # Enable asset timestamps +assets: # Configuration for Assets Manager (JS, CSS) + css_pipeline: false # The CSS pipeline is the unification of multiple CSS resources into one file + css_minify: true # Minify the CSS during pipelining + css_minify_windows: false # Minify Override for Windows platforms. False by default due to ThreadStackSize + css_rewrite: true # Rewrite any CSS relative URLs during pipelining + js_pipeline: false # The JS pipeline is the unification of multiple JS resources into one file + js_minify: true # Minify the JS during pipelining + enable_asset_timestamp: false # Enable asset timestamps collections: jquery: system://assets/jquery/jquery-2.1.4.min.js errors: - display: false # Display full backtrace-style error page - log: true # Log errors to /logs folder + display: false # Display full backtrace-style error page + log: true # Log errors to /logs folder debugger: - enabled: false # Enable Grav debugger and following settings + enabled: false # Enable Grav debugger and following settings shutdown: - close_connection: true # Close the connection before calling onShutdown(). false for debugging + close_connection: true # Close the connection before calling onShutdown(). false for debugging images: - default_image_quality: 85 # Default image quality to use when resampling images (85%) - cache_all: false # Cache all image by default - debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example + default_image_quality: 85 # Default image quality to use when resampling images (85%) + cache_all: false # Cache all image by default + debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example media: - enable_media_timestamp: false # Enable media timetsamps - upload_limit: 0 # Set maximum upload size in bytes (0 is unlimited) - unsupported_inline_types: [] # Array of unsupported media file types to try to display inline + enable_media_timestamp: false # Enable media timetsamps + upload_limit: 0 # Set maximum upload size in bytes (0 is unlimited) + unsupported_inline_types: [] # Array of unsupported media file types to try to display inline session: - enabled: true # Enable Session support - timeout: 1800 # Timeout in seconds - name: grav-site # Name prefix of the session cookie + enabled: true # Enable Session support + timeout: 1800 # Timeout in seconds + name: grav-site # Name prefix of the session cookie security: default_hash: $2y$10$kwsyMVwM8/7j0K/6LHT.g.Fs49xOCTp2b8hh/S5.dPJuJcJB6T.UK diff --git a/system/languages/en.yaml b/system/languages/en.yaml index 5ae01dbde..a1eab6f19 100644 --- a/system/languages/en.yaml +++ b/system/languages/en.yaml @@ -92,3 +92,6 @@ NICETIME: MO_PLURAL: mos YR_PLURAL: yrs DEC_PLURAL: decs +FORM: + VALIDATION_FAIL: Validation failed: + INVALID_INPUT: Invalid input in diff --git a/system/src/Grav/Common/Data/Blueprint.php b/system/src/Grav/Common/Data/Blueprint.php index 6ebe8ae19..6f0c71e50 100644 --- a/system/src/Grav/Common/Data/Blueprint.php +++ b/system/src/Grav/Common/Data/Blueprint.php @@ -79,7 +79,8 @@ class Blueprint implements \ArrayAccess, ExportInterface $this->validateArray($data, $this->nested); } catch (\RuntimeException $e) { $language = self::getGrav()['language']; - throw new \RuntimeException(sprintf('Validation failed: %s', $language->translate($e->getMessage()))); + $message = sprintf($language->translate('FORM.VALIDATION_FAIL', null, true) . ' %s', $e->getMessage()); + throw new \RuntimeException($message); } } @@ -452,7 +453,8 @@ class Blueprint implements \ArrayAccess, ExportInterface if (isset($field['validate']['required']) && $field['validate']['required'] === true && empty($data[$name])) { - throw new \RuntimeException("Missing required field: {$field['label']}"); + $value = isset($field['label']) ? $field['label'] : $field['name']; + throw new \RuntimeException("Missing required field: {$value}"); } } } diff --git a/system/src/Grav/Common/Data/Validation.php b/system/src/Grav/Common/Data/Validation.php index 653386fe9..c232467ba 100644 --- a/system/src/Grav/Common/Data/Validation.php +++ b/system/src/Grav/Common/Data/Validation.php @@ -38,7 +38,7 @@ class Validation $type = (string) isset($field['validate']['type']) ? $field['validate']['type'] : $field['type']; $method = 'type'.strtr($type, '-', '_'); $name = ucfirst(isset($field['label']) ? $field['label'] : $field['name']); - $message = (string) isset($field['validate']['message']) ? $field['validate']['message'] : 'Invalid input in "' . $language->translate($name) . '""'; + $message = (string) isset($field['validate']['message']) ? $field['validate']['message'] : $language->translate('FORM.INVALID_INPUT', null, true) . ' "' . $language->translate($name) . '"'; if (method_exists(__CLASS__, $method)) { $success = self::$method($value, $validate, $field); diff --git a/system/src/Grav/Common/Filesystem/Folder.php b/system/src/Grav/Common/Filesystem/Folder.php index 6026d8f04..af1b93352 100644 --- a/system/src/Grav/Common/Filesystem/Folder.php +++ b/system/src/Grav/Common/Filesystem/Folder.php @@ -64,8 +64,8 @@ abstract class Folder /** * Get relative path between target and base path. If path isn't relative, return full path. * - * @param string $path - * @param string $base + * @param string $path + * @param mixed|string $base * @return string */ public static function getRelativePath($path, $base = GRAV_ROOT) diff --git a/system/src/Grav/Common/GPM/PackageInterface.php b/system/src/Grav/Common/GPM/PackageInterface.php deleted file mode 100644 index ad856c08f..000000000 --- a/system/src/Grav/Common/GPM/PackageInterface.php +++ /dev/null @@ -1,58 +0,0 @@ -get('system.cache.gzip')) { - ob_start('ob_gzhandler'); + // Enable zip/deflate with a fallback in case of if browser does not support compressing. + if(!ob_start("ob_gzhandler")) { + ob_start(); + } } // Initialize the timezone @@ -416,22 +419,25 @@ class Grav extends Container public function shutdown() { if ($this['config']->get('system.debugger.shutdown.close_connection')) { - //stop user abort + // Prevent user abort. if (function_exists('ignore_user_abort')) { @ignore_user_abort(true); } - // close the session + // Close the session. if (isset($this['session'])) { $this['session']->close(); } - // flush buffer if gzip buffer was started if ($this['config']->get('system.cache.gzip')) { - ob_end_flush(); // gzhandler buffer + // Flush gzhandler buffer if gzip was enabled. + ob_end_flush(); + } else { + // Otherwise prevent server from compressing the output. + header('Content-Encoding: none'); } - // get lengh and close the connection + // Get length and close the connection. header('Content-Length: ' . ob_get_length()); header("Connection: close"); @@ -440,7 +446,7 @@ class Grav extends Container @ob_flush(); flush(); - // fix for fastcgi close connection issue + // Fix for fastcgi close connection issue. if (function_exists('fastcgi_finish_request')) { @fastcgi_finish_request(); } diff --git a/system/src/Grav/Common/Page/Page.php b/system/src/Grav/Common/Page/Page.php index f2c6ea2d9..0132c6b2e 100644 --- a/system/src/Grav/Common/Page/Page.php +++ b/system/src/Grav/Common/Page/Page.php @@ -41,6 +41,7 @@ class Page protected $folder; protected $path; protected $extension; + protected $url_extension; protected $id; protected $parent; @@ -128,6 +129,7 @@ class Page $this->modularTwig($this->slug[0] == '_'); $this->setPublishState(); $this->published(); + $this->urlExtension(); // some extension logic if (empty($extension)) { @@ -136,6 +138,7 @@ class Page $this->extension($extension); } + // extract page language from page extension $language = trim(basename($this->extension(), 'md'), '.') ?: null; $this->language($language); @@ -349,7 +352,6 @@ class Page if (isset($this->header->last_modified)) { $this->last_modified = (bool) $this->header->last_modified; } - } return $this->header; @@ -957,6 +959,17 @@ class Page return $this->extension; } + public function urlExtension() + { + // if not set in the page get the value from system config + if (empty($this->url_extension)) { + $this->url_extension = trim(isset($this->header->append_url_extension) ? $this->header->append_url_extension : self::getGrav()['config']->get('system.pages.append_url_extension', false)); + } + + return $this->url_extension; + + } + /** * Gets and sets the expires field. If not set will return the default * @@ -1262,7 +1275,7 @@ class Page $rootUrl = $uri->rootUrl($include_host) . $pages->base(); - $url = $rootUrl.'/'.trim($route, '/'); + $url = $rootUrl.'/'.trim($route, '/') . $this->urlExtension(); // trim trailing / if not root if ($url !== '/') { diff --git a/system/src/Grav/Common/Twig/TwigExtension.php b/system/src/Grav/Common/Twig/TwigExtension.php index 2da411210..aa8c4a3ee 100644 --- a/system/src/Grav/Common/Twig/TwigExtension.php +++ b/system/src/Grav/Common/Twig/TwigExtension.php @@ -94,14 +94,17 @@ class TwigExtension extends \Twig_Extension 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('evaluate', [$this, 'evaluateFunc']), new \Twig_SimpleFunction('gist', [$this, 'gistFunc']), + new \Twig_SimpleFunction('nonce_field', [$this, 'nonceFieldFunc']), new \Twig_simpleFunction('random_string', [$this, 'randomStringFunc']), new \Twig_SimpleFunction('repeat', [$this, 'repeatFunc']), new \Twig_SimpleFunction('string', [$this, 'stringFunc']), new \Twig_simpleFunction('t', [$this, 'translate']), new \Twig_simpleFunction('ta', [$this, 'translateArray']), new \Twig_SimpleFunction('url', [$this, 'urlFunc']), - new \Twig_SimpleFunction('evaluate', [$this, 'evaluateFunc']), + + ]; } @@ -595,4 +598,22 @@ class TwigExtension extends \Twig_Extension return false; } + + /** + * Used to add a nonce to a form. Call {{ nonce_field('action') }} specifying a string representing the action. + * + * For maximum protection, ensure that the string representing the action is as specific as possible. + * + * @todo evaluate if adding referrer or not + * + * @param string action the action + * @param string nonceParamName a custom nonce param name + * + * @return string the nonce input field + */ + public function nonceFieldFunc($action, $nonceParamName = 'nonce') + { + $string = ''; + return $string; + } } diff --git a/system/src/Grav/Common/Uri.php b/system/src/Grav/Common/Uri.php index 64a9c4ad4..98b45fcac 100644 --- a/system/src/Grav/Common/Uri.php +++ b/system/src/Grav/Common/Uri.php @@ -139,6 +139,7 @@ class Uri $valid_page_types = implode('|', $config->get('system.pages.types')); + // Strip the file extension for valid page types if (preg_match("/\.(".$valid_page_types.")$/", $parts['basename'])) { $uri = rtrim(str_replace(DIRECTORY_SEPARATOR, DS, $parts['dirname']), DS). '/' .$parts['filename']; } @@ -571,4 +572,21 @@ class Uri return $normalized_url; } } + + /** + * Adds the nonce to a URL for a specific action + * + * @param string $url the url + * @param string $action the action + * @param string $nonceParamName the param name to use + * + * @return string the url with the nonce + */ + public static function addNonce($url, $action, $nonceParamName = 'nonce') + { + $nonce = Utils::getNonce($action); + $nonce = str_replace('/', 'SLASH', $nonce); + $urlWithNonce = $url . '/' . $nonceParamName . Grav::instance()['config']->get('system.param_sep', ':') . $nonce; + return $urlWithNonce; + } } diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 8c55669a2..4935ee53a 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -384,7 +384,109 @@ abstract class Utils * * @return boolean */ - public static function isPositive($value) { + public static function isPositive($value) + { return in_array($value, [true, 1, '1', 'yes', 'on', 'true'], true); } + + + /** + * Generates a nonce string to be hashed. Called by self::getNonce() + * + * @param string $action + * @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours) + * + * @return string the nonce string + */ + private static function generateNonceString($action, $plusOneTick = false) + { + if (isset(self::getGrav()['user'])) { + $user = self::getGrav()['user']; + $username = $user->username; + } else { + $username = false; + } + + if (!$username) { + $username = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; + } + + $token = session_id(); + $i = self::nonceTick(); + + if ($plusOneTick) { + $i++; + } + + return ( $i . '|' . $action . '|' . $username . '|' . $token ); + } + + /** + * Get the time-dependent variable for nonce creation. + * + * @todo now a tick lasts a day. Once the day is passed, the nonce is not valid any more. Find a better way + * to ensure nonces issued near the end of the day do not expire in that small amount of time + * + * @return int the time part of the nonce. Changes once every 24 hours + */ + private static function nonceTick() + { + $secondsInHalfADay = 60 * 60 * 12; + return (int)ceil(time() / ( $secondsInHalfADay )); + } + + /** + * Get hash of given string + * + * @param string $data string to hash + * + * @return string hashed value of $data, cut to 10 characters + */ + private static function hash($data) + { + $hash = password_hash($data, PASSWORD_DEFAULT); + return $hash; + } + + /** + * Creates a hashed nonce tied to the passed action. Tied to the current user and time. The nonce for a given + * action is the same for 12 hours. + * + * @param string $action the action the nonce is tied to (e.g. save-user-admin or move-page-homepage) + * @param bool $plusOneTick if true, generates the token for the next tick (the next 12 hours) + * + * @return string the nonce + */ + public static function getNonce($action, $plusOneTick = false) + { + $nonce = self::hash(self::generateNonceString($action, $plusOneTick)); + return $nonce; + } + + /** + * Verify the passed nonce for the give action + * + * @param string $nonce the nonce to verify + * @param string $action the action to verify the nonce to + * + * @return boolean verified or not + */ + public static function verifyNonce($nonce, $action) + { + $nonce = str_replace('SLASH', '/', $nonce); + + //Nonce generated 0-12 hours ago + if (password_verify(self::generateNonceString($action), $nonce)) { + return true; + } + + //Nonce generated 12-24 hours ago + $plusOneTick = true; + if (password_verify(self::generateNonceString($action, $plusOneTick), $nonce)) { + return true; + } + + //Invalid nonce + return false; + } }